Compare commits

..

3 Commits

Author SHA1 Message Date
Pierre Donias
9894f679de Minor fixes 2023-11-16 11:16:59 +01:00
Pierre Donias
c61019d402 camelCase, colors and workflow fixes 2023-11-16 10:44:22 +01:00
Pierre Donias
7bd9ef7167 chore(lite): release script
New script to:
- build XO Lite
- deploy it on xen-orchestra.com
- generate a tarball of built files
- create a GitHub release with the tarball
2023-11-16 09:59:55 +01:00
1776 changed files with 29410 additions and 100194 deletions

View File

@@ -15,10 +15,9 @@ module.exports = {
overrides: [
{
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js', '**/scripts/**.{,c,m}js'],
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js'],
rules: {
'n/no-process-exit': 'off',
'n/shebang': 'off',
'no-console': 'off',
},
},
@@ -39,12 +38,6 @@ module.exports = {
version: '>=16',
},
],
'n/no-unsupported-features/es-builtins': [
'error',
{
version: '>=16',
},
],
'n/no-unsupported-features/es-syntax': [
'error',
{
@@ -53,218 +46,8 @@ module.exports = {
],
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/**/*.{vue,ts}'],
parser: 'vue-eslint-parser',
parserOptions: {
sourceType: 'module',
parser: '@typescript-eslint/parser',
},
plugins: ['import'],
extends: [
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-essential',
],
settings: {
'import/resolver': {
typescript: true,
'eslint-import-resolver-custom-alias': {
alias: {
'@core': '../web-core/lib',
'@': './src',
},
extensions: ['.ts'],
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'],
},
},
},
globals: {
XO_LITE_VERSION: true,
XO_LITE_GIT_HEAD: true,
},
rules: {
'import/order': [
'error',
{
groups: ['builtin', 'object', 'internal', ['type', 'external']],
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
pathGroups: [
{
pattern: '@/**',
group: 'internal',
},
{
pattern: '@core/**',
group: 'internal',
},
],
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'no-void': ['error', { allowAsStatement: true }],
'n/no-missing-import': 'off', // using 'import' plugin instead, to support TS aliases
'no-redeclare': 'off', // automatically checked by the TypeScript compiler
'no-dupe-class-members': 'off', // automatically checked by the TypeScript compiler
'no-undef': 'off', // automatically checked by the TypeScript compiler
'no-use-before-define': 'off', // replaced with the rule below
'@typescript-eslint/no-use-before-define': ['error', { functions: false }],
'@typescript-eslint/no-explicit-any': 'off',
'vue/multi-word-component-names': 'off',
// Vue 3 - Strongly Recommended
'vue/attribute-hyphenation': 'error',
'vue/component-definition-name-casing': 'error',
'vue/first-attribute-linebreak': 'error',
'vue/html-closing-bracket-newline': 'off', // Conflicts with Prettier
'vue/html-closing-bracket-spacing': 'error',
'vue/html-end-tags': 'error',
'vue/html-indent': 'off', // Conflicts with Prettier
'vue/html-quotes': 'error',
'vue/html-self-closing': ['error', { html: { void: 'always', normal: 'always', component: 'always' } }],
'vue/max-attributes-per-line': 'off', // Conflicts with Prettier
'vue/multiline-html-element-content-newline': 'error',
'vue/mustache-interpolation-spacing': 'error',
'vue/no-multi-spaces': 'error',
'vue/no-spaces-around-equal-signs-in-attribute': 'error',
'vue/no-template-shadow': 'error',
'vue/prop-name-casing': 'error',
'vue/require-default-prop': 'off', // https://github.com/vuejs/eslint-plugin-vue/issues/2051
'vue/require-explicit-emits': 'error',
'vue/require-prop-types': 'error',
'vue/singleline-html-element-content-newline': 'off',
'vue/v-bind-style': 'error',
'vue/v-on-event-hyphenation': ['error', 'always', { autofix: true }],
'vue/v-on-style': 'error',
'vue/v-slot-style': 'error',
// Vue 3 - Recommended
'vue/attributes-order': 'error',
'vue/no-lone-template': 'error',
'vue/no-multiple-slot-args': 'error',
'vue/no-v-html': 'error',
'vue/this-in-template': 'error',
// Vue 3 - Uncategorized
'vue/block-lang': ['error', { script: { lang: 'ts' }, style: { lang: 'postcss' } }],
'vue/block-order': ['error', { order: ['template', 'script', 'style'] }],
'vue/block-tag-newline': ['error', { singleline: 'consistent', multiline: 'always', maxEmptyLines: 0 }],
'vue/component-api-style': ['error', ['script-setup']],
'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-emits-declaration': ['error', 'type-literal'],
'vue/define-macros-order': [
'error',
{
order: ['defineOptions', 'defineProps', 'defineEmits', 'defineModel', 'defineSlots'],
defineExposeLast: true,
},
],
'vue/define-props-declaration': ['error', 'type-based'],
'vue/enforce-style-attribute': ['error', { allow: ['scoped', 'module'] }],
'vue/html-button-has-type': 'error',
'vue/html-comment-content-newline': ['error', { singleline: 'never', multiline: 'always' }],
'vue/html-comment-content-spacing': ['error', 'always'],
'vue/no-duplicate-attr-inheritance': 'error',
'vue/no-empty-component-block': 'error',
'vue/no-multiple-objects-in-class': 'error',
'vue/no-ref-object-reactivity-loss': 'error',
'vue/no-required-prop-with-default': 'error',
'vue/no-static-inline-styles': 'error',
'vue/no-template-target-blank': 'error',
'vue/no-undef-components': ['error', { ignorePatterns: ['RouterLink', 'RouterView', 'I18nT'] }],
'vue/no-undef-properties': 'error',
'vue/no-unused-refs': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/no-useless-mustaches': 'error',
'vue/no-useless-v-bind': 'error',
'vue/no-v-text': 'error',
'vue/padding-line-between-blocks': 'error',
'vue/prefer-define-options': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/prefer-true-attribute-shorthand': 'error',
'vue/require-macro-variable-name': 'error',
'vue/valid-define-options': 'error',
},
},
// Specific rules for XO i18n-related files
{
files: [
'@xen-orchestra/lite/**/*.{vue,ts,json}',
'@xen-orchestra/web/**/*.{vue,ts,json}',
'@xen-orchestra/web-core/**/*.{vue,ts,json}',
],
extends: ['plugin:@intlify/vue-i18n/base'],
settings: {
'vue-i18n': {
localeDir: [
'@xen-orchestra/lite/src/locales/*.json',
'@xen-orchestra/web/src/locales/*.json',
'@xen-orchestra/web-core/lib/locales/*.json',
],
messageSyntaxVersion: '^9.9.0',
},
},
rules: {
strict: 'off',
'no-irregular-whitespace': 'off',
'@intlify/vue-i18n/no-deprecated-i18n-component': 'error',
'@intlify/vue-i18n/no-deprecated-i18n-place-attr': 'error',
'@intlify/vue-i18n/no-deprecated-i18n-places-prop': 'error',
'@intlify/vue-i18n/no-deprecated-modulo-syntax': 'error',
'@intlify/vue-i18n/no-deprecated-tc': 'error',
'@intlify/vue-i18n/no-html-messages': 'error',
'@intlify/vue-i18n/no-i18n-t-path-prop': 'error',
'@intlify/vue-i18n/no-missing-keys': 'error',
'@intlify/vue-i18n/no-raw-text': [
'error',
{
ignoreText: ['Xen Orchestra Lite', 'Xen Orchestra', 'XO Lite', 'XCP-ng', 'XO 5', '404', '⚠️'],
},
],
'@intlify/vue-i18n/no-v-html': 'error',
'@intlify/vue-i18n/valid-message-syntax': 'error',
'@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
'@intlify/vue-i18n/no-missing-keys-in-other-locales': ['error', { ignoreLocales: ['de', 'fa', 'cs'] }],
},
},
// Specific rules for XO dev-related files
{
files: [
'@xen-orchestra/lite/src/stories/**/*',
'@xen-orchestra/lite/src/components/component-story/**/*',
'@xen-orchestra/web/src/pages/dev/**/*',
'@xen-orchestra/lite/src/views/story/**/*',
],
rules: {
'@intlify/vue-i18n/no-raw-text': 'off',
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/src/pages/**/*.vue'],
parserOptions: {
sourceType: 'module',
},
rules: {
'vue/multi-word-component-names': 'off',
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/typed-router.d.ts'],
parserOptions: {
sourceType: 'module',
},
rules: {
'eslint-comments/disable-enable-pair': 'off',
'eslint-comments/no-unlimited-disable': 'off',
},
},
],
ignorePatterns: ['@vates/fatfs/'],
parserOptions: {
ecmaVersion: 13,
sourceType: 'script',
@@ -285,11 +68,6 @@ module.exports = {
'no-console': ['error', { allow: ['warn', 'error'] }],
// this rule can prevent race condition bugs like parallel `a += await foo()`
//
// as it has a lots of false positive, it is only enabled as a warning for now
'require-atomic-updates': ['warn', { allowProperties: true }],
strict: 'error',
},
}

48
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,48 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
assignees: ''
---
1. ⚠️ **If you don't follow this template, the issue will be closed**.
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
Are you using XOA or XO from the sources?
If XOA:
- which release channel? (`stable` vs `latest`)
- please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
If XO from the sources:
- Provide **your commit number**. If it's older than a week, we won't investigate
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please provide the following information):**
- Node: [e.g. 16.12.1]
- hypervisor: [e.g. XCP-ng 8.2.0]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,119 +0,0 @@
name: Bug Report
description: Create a report to help us improve
labels: ['type: bug :bug:', 'status: triaging :triangular_flag_on_post:']
body:
- type: markdown
attributes:
value: |
1. ⚠️ **If you don't follow this template, the issue will be closed**.
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
- type: markdown
attributes:
value: '## Are you using XOA or XO from the sources?'
- type: dropdown
id: xo-origin
attributes:
label: Are you using XOA or XO from the sources?
options:
- XOA
- XO from the sources
- both
validations:
required: false
- type: markdown
attributes:
value: '### If XOA:'
- type: dropdown
id: xoa-channel
attributes:
label: Which release channel?
description: please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
options:
- stable
- latest
- both
validations:
required: false
- type: markdown
attributes:
value: '### If XO from the sources:'
- type: markdown
attributes:
value: |
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
- type: input
id: xo-sources-commit-number
attributes:
label: Provide your commit number
description: If it's older than a week, we won't investigate
placeholder: e.g. 579f0
validations:
required: false
- type: markdown
attributes:
value: '## Bug description:'
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
validations:
required: true
- type: textarea
id: error-message
attributes:
label: Error message
render: Text
validations:
required: false
- type: textarea
id: steps
attributes:
label: To reproduce
description: 'Steps to reproduce the behavior:'
value: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: false
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem
validations:
required: false
- type: markdown
attributes:
value: '## Environment (please provide the following information):'
- type: input
id: node-version
attributes:
label: Node
placeholder: e.g. 16.12.1
validations:
required: true
- type: input
id: hypervisor-version
attributes:
label: Hypervisor
placeholder: e.g. XCP-ng 8.2.0
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here
validations:
required: false

View File

@@ -5,46 +5,28 @@ jobs:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install packages
run: |
sudo apt-get update
sudo apt-get install -y curl qemu-utils python3-vmdkstream git libxml2-utils libfuse2 nbdkit
# https://turbo.build/repo/docs/guides/ci-vendors/github-actions#caching-with-github-actionscache
- name: Cache Turbo
uses: actions/cache@v4
# https://github.com/actions/cache
uses: actions/cache@v3
with:
path: '.turbo'
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
# https://github.com/actions/setup-node
path: '**/node_modules/.cache/turbo'
key: ${{ runner.os }}-turbo-cache
- name: Setup Node environment
uses: actions/setup-node@v4
# https://github.com/actions/setup-node
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: '18'
cache: 'yarn'
- name: Install project dependencies
run: yarn
- name: Ensure packages are sorted in CHANGELOG.unreleased.md
run: ./scripts/gen-deps-list.js --check-order > /dev/null
- name: Ensure yarn.lock is up-to-date
run: git diff --exit-code yarn.lock
- name: Build the project
run: yarn build
- name: Unit tests
run: yarn test-unit
- name: Lint tests
run: yarn test-lint
- name: Integration tests
run: sudo yarn test-integration

4
.gitignore vendored
View File

@@ -30,12 +30,8 @@ pnpm-debug.log.*
yarn-error.log
yarn-error.log.*
.env
*.tsbuildinfo
# code coverage
.nyc_output/
coverage/
.turbo/
# https://node-tap.org/dot-tap-folder/
.tap/

View File

@@ -1,6 +0,0 @@
# Ignore Handlebars files because partials are not currently supported
# https://github.com/prettier/prettier/issues/11834
**/*.hbs
# Generated files
/@xen-orchestra/web/typed-router.d.ts

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it, beforeEach } = require('node:test')
const { describe, it, beforeEach } = require('test')
const assert = require('assert').strict
const { spy } = require('sinon')

View File

@@ -30,9 +30,11 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"sinon": "^18.0.0"
"sinon": "^16.0.0",
"tap": "^16.3.0",
"test": "^3.2.1"
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const assert = require('assert')
const { coalesceCalls } = require('./')

View File

@@ -31,6 +31,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const assert = require('node:assert').strict
const { compose } = require('./')

View File

@@ -20,6 +20,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
}
}

View File

@@ -62,42 +62,6 @@ decorateClass(Foo, {
})
```
### `decorateObject(object, map)`
Decorates an object the same way `decorateClass()` decorates a class:
```js
import { decorateObject } from '@vates/decorate-with'
const object = {
get bar() {
// body
},
set bar(value) {
// body
},
baz() {
// body
},
}
decorateObject(object, {
// getter and/or setter
bar: {
// without arguments
get: lodash.memoize,
// with arguments
set: [lodash.debounce, 150],
},
// method (with or without arguments)
baz: lodash.curry,
})
```
### `perInstance(fn, ...args)`
Helper to decorate the method by instance instead of for the whole class.

View File

@@ -80,42 +80,6 @@ decorateClass(Foo, {
})
```
### `decorateObject(object, map)`
Decorates an object the same way `decorateClass()` decorates a class:
```js
import { decorateObject } from '@vates/decorate-with'
const object = {
get bar() {
// body
},
set bar(value) {
// body
},
baz() {
// body
},
}
decorateObject(object, {
// getter and/or setter
bar: {
// without arguments
get: lodash.memoize,
// with arguments
set: [lodash.debounce, 150],
},
// method (with or without arguments)
baz: lodash.curry,
})
```
### `perInstance(fn, ...args)`
Helper to decorate the method by instance instead of for the whole class.

View File

@@ -14,13 +14,10 @@ function applyDecorator(decorator, value) {
}
exports.decorateClass = exports.decorateMethodsWith = function decorateClass(klass, map) {
return decorateObject(klass.prototype, map)
}
function decorateObject(object, map) {
const { prototype } = klass
for (const name of Object.keys(map)) {
const decorator = map[name]
const descriptor = getOwnPropertyDescriptor(object, name)
const descriptor = getOwnPropertyDescriptor(prototype, name)
if (typeof decorator === 'function' || Array.isArray(decorator)) {
descriptor.value = applyDecorator(decorator, descriptor.value)
} else {
@@ -33,11 +30,10 @@ function decorateObject(object, map) {
}
}
defineProperty(object, name, descriptor)
defineProperty(prototype, name, descriptor)
}
return object
return klass
}
exports.decorateObject = decorateObject
exports.perInstance = function perInstance(fn, decorator, ...args) {
const map = new WeakMap()

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert')
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const { decorateClass, decorateWith, decorateMethodsWith, perInstance } = require('./')

View File

@@ -20,12 +20,15 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.1.0",
"version": "2.0.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('node:assert/strict')
const test = require('node:test')
const test = require('test')
const diff = require('./index.js')

View File

@@ -28,6 +28,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"test": "^3.3.0"
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const { useFakeTimers, spy, assert } = require('sinon')
const { createDebounceResource } = require('./debounceResource')

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const { spy, assert } = require('sinon')
const { deduped } = require('./deduped')

View File

@@ -14,21 +14,22 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.6",
"version": "0.1.4",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@vates/multi-key-map": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.6.0",
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^18.0.0"
"sinon": "^16.0.0",
"test": "^3.2.1"
}
}

View File

@@ -20,9 +20,6 @@ function assertListeners(t, event, listeners) {
}
t.beforeEach(function (t) {
// work around https://github.com/tapjs/tapjs/issues/998
t.context = {}
t.context.ee = new EventEmitter()
t.context.em = new EventListenersManager(t.context.ee)
})

View File

@@ -38,9 +38,9 @@
"version": "1.0.1",
"scripts": {
"postversion": "npm publish --access public",
"test": "tap --allow-incomplete-coverage"
"test": "tap --branches=72"
},
"devDependencies": {
"tap": "^18.7.0"
"tap": "^16.2.0"
}
}

View File

@@ -1,3 +0,0 @@
.DS_Store
.vscode/
node_modules/

View File

@@ -1,127 +0,0 @@
# @vates/fatfs
This is a fork of the original library [natevw/fatfs](https://github.com/natevw/fatfs).
## Why?
This fork was created to add a missing feature in the original library: the ability to create labels (createLabel).
The original library did not support this functionality, which was required to make Cloudbase-Init work on windows VMs.
See: https://github.com/natevw/fatfs/issues/30, https://github.com/natevw/fatfs/pull/31
A standalone FAT16/FAT32 implementation that takes in a block-access interface and exposes something quite similar to `require('fs')` (i.e. the node.js built-in [Filesystem API](http://nodejs.org/api/fs.html)).
## Installation
`npm install fatfs`
## Example
```js
var fatfs = require('fatfs'),
fs = fatfs.createFileSystem(exampleDriver) // see below
fs.stat('autoexec.bat', function (e, stats) {
if (e) console.error(e)
else console.log(stats)
})
// TODO: open a file and write to it or something…
```
## API
- `fs = fatfs.createFileSystem(vol, [opts], [cb])` — Simply pass in a block driver (see below) mapped to a FAT partition somewhere, and get back the API documented [here](http://nodejs.org/api/fs.html). An options dictionary can be provided, details are in the next section. You may also optionally provide a callback `cb(err)` which will be automatically registered for the on `'ready'` or `'error'` event.
- `'ready'` event — fired on `fs` when initial volume information has been determined and the API is ready to use. It is safe to call other `fs` methods before this fires **only if** you are sure the first sector will be readable and represents a valid FAT volume.
- `'error'` event — fired if initialization fails for whatever reason.
### Filesystem options
The `opts` dictionary you pass to `fatfs.createFileSystem` can contain any of the following options:
- `ro` — Enables readonly mode if `true`. It defaults to `false`, but if your volume driver does not provide a `writeSectors` method it will be overriden to `true`.
- `noatime` — The FAT filesystem can track the last access time (just a date, actually) but this means every read operation would also incur some write overhead. Defaults to `true`, meaning by default access times will **not** be updated on reads. Set this to `false` to track access times.
- `modmode` — chooses how `fs.chmod` (and the mode field from `fs.stat`–family calls) should map FAT attributes to POSIX permissions. Set to the number `0111` to map the readonly flag to the user's write bit being unset, and the archive/system/hidden flags to the user/group/other executable bits respectively. Set to the number `07000` to map the readonly flag to _all_ write bits being unset, and the archive/system/hidden flags to the sticky/setgid/setuid bits respectively. Set to `null` for readonly mapping. Defaults to `0111`.
- `umask` — any bits _set_ in this octal number will be _unset_ in the 'mode' field from `fs.stat`–family calls. It does not affect anything else. Defaults to `process.umask()`, or `0022` if that is unavailable.
- `uid` — This value will be returned as the 'uid' stat field. It does not affect anything else. Defaults to `process.getuid()`, or `0` if that is unavailable.
- `gid` — This value will be returned as the 'gid' stat field. It does not affect anything else. Defaults to `process.getgid()`, or `0` if that is unavailable.
(Note that these are similar to the options you could use with a POSIX `mount` operation.)
And that's it! The [rest of the API](http://nodejs.org/api/fs.html) (`fs.readdir`, `fs.open`, `fs.createReadStream`, `fs.appendFile`, etc.) is as documented by the node.js project.
Well, sort of…
## Caveats
### Temporary
- **BETA** **BETA** **BETA**. Seriously, this is a _brand new_, _from scratch_, _completely unproven_ filesystem implementation. It does not have full automated test coverage, and it has not been manually tested very much either. Please please please **make sure you have a backup** of any important drive/image/card you unleash this upon.
- A few methods are not quite implemented, either: `fs.rename`, `fs.unlink` and `fs.rmdir`, as well as `fs.watchFile`/`fs.unwatchFile` and `fs.watch`. These are Coming Soon™.
- There are several internal housekeeping items (redundant FAT tables, extra FAT32 information, etc.) that are not done. These do not seem to affect interop, but you may see warnings when repairing a filesystem written by this module.
- Oh, and not to scare you, but if an IO error happens while writing, the library usually just bails — bubbling an error up to your callback as if it were a hot potato. Although some attempt has been made to do separate writes in the safest order (e.g. allocating an additional file cluster, then appending data into it, and then finally updating the file's size), but this behavior has not been thoroughly audited for all operations. There's certainly no attempt to retry/cleanup/rollback if a multi-step change runs into trouble partway through.
### As-planned
Some of the differences between `fatfs` and the node.js `fs` module are "by design" for architectural simplicity and/or due to underlying FAT limitations.
- There are no `fs.*Sync` methods. (The volume driver is async, not to mention that supporting a separate \*Sync codepath would be an enormous duplication of effort of dubious value.)
- This module does [almost] no read/write caching. This should be done in your volume driver, but see notes below.
- You'll need multiple `createFileSystem` instances for multiple volumes; paths are relative to each, and don't share a namespace.
- The FAT filesystem has no concept of symlinks, and hardlinks are not really an intentional feature. You will get an ENOSYS-like error when trying to create either type of link.
## "Volume driver" API
To use 'fatfs', you must provide a driver object with the following properties/methods:
- `driver.sectorSize` — number of bytes per sector on this device
- `driver.numSectors` — count of sectors available on this media
- `driver.readSectors(i, dest, cb)` — Fill `dest` with data starting at the `i`th sector and notify `cb(e)` when finished. You may assume `dest.length` is a multiple of `driver.sectorSize`.
- `driver.writeSectors(i, data, cb)` — (optional) Write `data` starting at the `i`th sector and notify `cb(e)` when finished. You may assume `data.length` is a multiple of `driver.sectorSize`.
If you do not provide a `writeSectors` method, then `fatfs` will work in readonly mode. Pretty simple, eh? And the 'fatfs' module makes a good effort to check the parameters passed to your driver methods!
**TBD:** to facilitate proper cache handling, this module might add an optional `driver.flush(cb)` method at some point in the future.
Here's an example taken from code used to run this module's own tests:
```js
// NOTE: this assumes image at `path` has no partition table.
// If it did, you'd need to translate positions, natch…
var fs = require('fs')
exports.createDriverSync = function (path, opts) {
opts || (opts = {})
var secSize = 512,
ro = opts.readOnly || false,
fd = fs.openSync(path, ro ? 'r' : 'r+'),
s = fs.fstatSync(fd)
return {
sectorSize: secSize,
numSectors: s.size / secSize,
readSectors: function (i, dest, cb) {
if (dest.length % secSize) throw Error('Unexpected buffer length!')
fs.read(fd, dest, 0, dest.length, i * secSize, function (e, n, d) {
cb(e, d)
})
},
writeSectors: ro
? null
: function (i, data, cb) {
if (data.length % secSize) throw Error('Unexpected buffer length!')
fs.write(fd, data, 0, data.length, i * secSize, function (e) {
cb(e)
})
},
}
}
```
## License
© 2014 Nathan Vander Wilt.
Funding for this work was provided by Technical Machine, Inc.
Reuse under your choice of:
- [BSD-2-Clause](http://opensource.org/licenses/BSD-2-Clause)
- [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html)

View File

@@ -1,60 +0,0 @@
var _ = require("./helpers.js");
exports.wrapDriver = function (volume, opts) {
opts = _.extend({
maxSectors: 2048
}, opts);
var cache = {},
advice = 'NORMAL',
secSize = volume.sectorSize;
function _freezeBuffer(b) {
var f = _.allocBuffer(b.length);
b.copy(f);
return f;
}
function addToCache(i, data) {
if (advice === 'SEQUENTIAL' || advice === 'NOREUSE') return;
data = _freezeBuffer(data);
cache[i] = data;
//if (data.length > secSize) addToCache(i+1, data.slice(secSize));
while (data.length > secSize) {
data = data.slice(secSize);
cache[++i] = data;
}
// simple highest-sectors-lose eviction policy for now
Object.keys(cache).sort().slice(opts.maxSectors).forEach(function (x) {
delete cache[x];
});
_.log(_.log.DBG, "Cache now contains:", Object.keys(cache).join(','));
}
return {
sectorSize: volume.sectorSize,
numSectors: volume.numSectors,
advice: function (val) {
if (!arguments.length) return advice;
else advice = val;
if (advice === 'SEQUENTIAL' || advice === 'NOREUSE') cache = {};
return this;
},
readSectors: function (i, dest, cb) {
// TODO: handle having partial parts of dest!
if (i in cache && dest.length === secSize) {
cache[i].copy(dest);
setImmediate(cb);
} else volume.readSectors(i, dest, function (e) {
if (e) cb(e);
else addToCache(i, dest), cb();
});
},
writeSectors: (!volume.writeSectors) ? null : function (i, data, cb) {
volume.writeSectors(i, data, function (e) {
if (e) cb(e);
else addToCache(i, data), cb();
});
}
};
};

View File

@@ -1,287 +0,0 @@
var S = require("./structs.js"),
_ = require("./helpers.js");
function _baseChain(vol) {
var chain = {};
chain.sectorSize = vol._sectorSize;
function posFromOffset(off) {
var secSize = chain.sectorSize,
offset = off % secSize,
sector = (off - offset) / secSize;
return {sector:sector, offset:offset};
}
var sectorCache = vol._makeCache();
Object.defineProperty(chain, 'cacheAdvice', {
enumerable: true,
get: function () { return sectorCache.advice(); },
set: function (v) { sectorCache.advice(v); }
});
chain._vol_readSectors = vol._readSectors.bind(vol, sectorCache);
chain._vol_writeSectors = vol._writeSectors.bind(vol, sectorCache);
// cb(error, bytesRead, buffer)
chain.readFromPosition = function (targetPos, buffer, cb) {
if (typeof targetPos === 'number') targetPos = posFromOffset(targetPos);
if (typeof buffer === 'number') buffer = _.allocBuffer(buffer);
/* NOTE: to keep our contract with the volume driver, we need to read on _full_ sector boundaries!
So we divide the read into [up to] three parts: {preface, main, trailer}
This is kind of unfortunate, but in practice should often still be reasonably efficient. */
if (targetPos.offset) chain.readSectors(targetPos.sector, _.allocBuffer(chain.sectorSize), function (e,d) {
if (e || !d) cb(e, 0, buffer);
else { // copy preface into `buffer`
var dBeg = targetPos.offset,
dEnd = dBeg + buffer.length;
d.copy(buffer, 0, dBeg, dEnd);
if (dEnd > d.length) readMain();
else cb(null, buffer.length, buffer);
}
}); else readMain();
function readMain() {
var prefaceLen = targetPos.offset && (chain.sectorSize - targetPos.offset),
trailerLen = (buffer.length - prefaceLen) % chain.sectorSize,
mainSector = (prefaceLen) ? targetPos.sector + 1 : targetPos.sector,
mainBuffer = (trailerLen) ? buffer.slice(prefaceLen, -trailerLen) : buffer.slice(prefaceLen);
if (mainBuffer.length) chain.readSectors(mainSector, mainBuffer, function (e,d) {
if (e || !d) cb(e, prefaceLen, buffer);
else if (!trailerLen) cb(null, buffer.length, buffer);
else readTrailer();
}); else readTrailer();
function readTrailer() {
var trailerSector = mainSector + (mainBuffer.length / chain.sectorSize);
chain.readSectors(trailerSector, _.allocBuffer(chain.sectorSize), function (e,d) {
if (e || !d) cb(e, buffer.length-trailerLen, buffer);
else {
d.copy(buffer, buffer.length-trailerLen, 0, trailerLen);
cb(null, buffer.length, buffer);
}
});
}
}
};
// cb(error)
chain.writeToPosition = function (targetPos, data, cb) {
_.log(_.log.DBG, "WRITING", data.length, "bytes at", targetPos, "in", this.toJSON(), data);
if (typeof targetPos === 'number') targetPos = posFromOffset(targetPos);
var prefaceBuffer = (targetPos.offset) ? data.slice(0, chain.sectorSize-targetPos.offset) : null;
if (prefaceBuffer) _modifySector(targetPos.sector, targetPos.offset, prefaceBuffer, function (e) {
if (e) cb(e);
else if (prefaceBuffer.length < data.length) writeMain();
else cb();
}); else writeMain();
function writeMain() {
var prefaceLen = (prefaceBuffer) ? prefaceBuffer.length : 0,
trailerLen = (data.length - prefaceLen) % chain.sectorSize,
mainSector = (prefaceLen) ? targetPos.sector + 1 : targetPos.sector,
mainBuffer = (trailerLen) ? data.slice(prefaceLen, -trailerLen) : data.slice(prefaceLen);
if (mainBuffer.length) chain.writeSectors(mainSector, mainBuffer, function (e) {
if (e) cb(e);
else if (!trailerLen) cb();
else writeTrailer();
}); else writeTrailer();
function writeTrailer() {
var trailerSector = mainSector + (mainBuffer.length / chain.sectorSize),
trailerBuffer = data.slice(data.length-trailerLen); // WORKAROUND: https://github.com/tessel/runtime/issues/721
_modifySector(trailerSector, 0, trailerBuffer, cb);
}
}
function _modifySector(sec, off, data, cb) {
chain.readSectors(sec, _.allocBuffer(chain.sectorSize), function (e, orig) {
if (e) return cb(e);
orig || (orig = _.allocBuffer(chain.sectorSize, 0));
data.copy(orig, off);
chain.writeSectors(sec, orig, cb);
});
}
};
return chain;
};
exports.clusterChain = function (vol, firstCluster, _parent) {
var chain = _baseChain(vol),
cache = [firstCluster];
chain.firstCluster = firstCluster;
function _cacheIsComplete() {
return cache[cache.length-1] === 'eof';
}
function extendCacheToInclude(i, cb) { // NOTE: may `cb()` before returning!
if (i < cache.length) cb(null, cache[i]);
else if (_cacheIsComplete()) cb(null, 'eof');
else vol.fetchFromFAT(cache[cache.length-1], function (e,d) {
if (e) cb(e);
else if (typeof d === 'string' && d !== 'eof') cb(S.err.IO());
else {
cache.push(d);
extendCacheToInclude(i, cb);
}
});
}
function expandChainToLength(clusterCount, cb) {
if (!_cacheIsComplete()) throw Error("Must be called only when cache is complete!");
else cache.pop(); // remove 'eof' entry until finished
function addCluster(clustersNeeded, lastCluster) {
if (!clustersNeeded) cache.push('eof'), cb();
else vol.allocateInFAT(lastCluster, function (e, newCluster) {
if (e) cb(e);
else vol.storeToFAT(lastCluster, newCluster, function (e) {
if (e) return cb(e);
cache.push(newCluster);
addCluster(clustersNeeded-1, newCluster);
});
});
}
addCluster(clusterCount - cache.length, cache[cache.length - 1]);
}
function shrinkChainToLength(clusterCount, cb) {
if (!_cacheIsComplete()) throw Error("Must be called only when cache is complete!");
else cache.pop(); // remove 'eof' entry until finished
function removeClusters(count, cb) {
if (!count) cache.push('eof'), cb();
else vol.storeToFAT(cache.pop(), 'free', function (e) {
if (e) cb(e);
else removeClusters(count - 1, cb);
});
}
// NOTE: for now, we don't remove the firstCluster ourselves; we should though!
if (clusterCount) removeClusters(cache.length - clusterCount, cb);
else removeClusters(cache.length - 1, cb);
}
// [{firstSector,numSectors},{firstSector,numSectors},…]
function determineSectorGroups(sectorIdx, numSectors, alloc, cb) {
var sectorOffset = sectorIdx % vol._sectorsPerCluster,
clusterIdx = (sectorIdx - sectorOffset) / vol._sectorsPerCluster,
numClusters = Math.ceil((numSectors + sectorOffset) / vol._sectorsPerCluster),
chainLength = clusterIdx + numClusters;
extendCacheToInclude(chainLength-1, function (e,c) {
if (e) cb(e);
else if (c === 'eof' && alloc) expandChainToLength(chainLength, function (e) {
if (e) cb(e);
else _determineSectorGroups();
});
else _determineSectorGroups();
});
function _determineSectorGroups() {
// …now we have a complete cache
var groups = [],
_group = null;
for (var i = clusterIdx; i < chainLength; ++i) {
var c = (i < cache.length) ? cache[i] : 'eof';
if (c === 'eof') break;
else if (_group && c !== _group._nextCluster) {
groups.push(_group);
_group = null;
}
if (!_group) _group = {
_nextCluster: c+1,
firstSector: vol._firstSectorOfCluster(c) + sectorOffset,
numSectors: vol._sectorsPerCluster - sectorOffset
}; else {
_group._nextCluster += 1;
_group.numSectors += vol._sectorsPerCluster;
}
sectorOffset = 0; // only first group is offset
}
if (_group) groups.push(_group);
cb(null, groups, i === chainLength);
}
}
chain.readSectors = function (i, dest, cb) {
var groupOffset = 0, groupsPending;
determineSectorGroups(i, dest.length / chain.sectorSize, false, function (e, groups, complete) {
if (e) cb(e);
else if (!complete) groupsPending = -1, _pastEOF(cb);
else if ((groupsPending = groups.length)) groups.forEach(function (group) {
var groupLength = group.numSectors * chain.sectorSize,
groupBuffer = dest.slice(groupOffset, groupOffset += groupLength);
chain._vol_readSectors(group.firstSector, groupBuffer, function (e,d) {
if (e && groupsPending !== -1) groupsPending = -1, cb(e);
else if (--groupsPending === 0) cb(null, dest);
});
});
else cb(null, dest); // 0-length destination case
});
};
// TODO: does this handle NOSPC condition?
chain.writeSectors = function (i, data, cb) {
var groupOffset = 0, groupsPending;
determineSectorGroups(i, data.length / chain.sectorSize, true, function (e, groups) {
if (e) cb(e);
else if ((groupsPending = groups.length)) groups.forEach(function (group) {
var groupLength = group.numSectors * chain.sectorSize,
groupBuffer = data.slice(groupOffset, groupOffset += groupLength);
chain._vol_writeSectors(group.firstSector, groupBuffer, function (e) {
if (e && groupsPending !== -1) groupsPending = -1, cb(e);
else if (--groupsPending === 0) cb();
});
});
else cb(); // 0-length data case
});
};
chain.truncate = function (numSectors, cb) {
extendCacheToInclude(Infinity, function (e,c) {
if (e) return cb(e);
var currentLength = cache.length-1,
clustersNeeded = Math.ceil(numSectors / vol._sectorsPerCluster);
if (clustersNeeded < currentLength) shrinkChainToLength(clustersNeeded, cb);
else if (clustersNeeded > currentLength) expandChainToLength(clustersNeeded, cb);
else cb();
});
};
chain.toJSON = function () {
return {firstCluster:firstCluster};
};
return chain;
};
exports.sectorChain = function (vol, firstSector, numSectors) {
var chain = _baseChain(vol);
chain.firstSector = firstSector;
chain.numSectors = numSectors;
chain.readSectors = function (i, dest, cb) {
if (i < numSectors) chain._vol_readSectors(firstSector+i, dest, cb);
else _pastEOF(cb);
};
chain.writeSectors = function (i, data, cb) {
if (i < numSectors) chain._vol_writeSectors(firstSector+i, data, cb);
else _.delayedCall(cb, S.err.NOSPC());
};
chain.truncate = function (i, cb) {
_.delayedCall(cb, S.err.INVAL());
};
chain.toJSON = function () {
return {firstSector:firstSector, numSectors:numSectors};
};
return chain;
};
// NOTE: used with mixed feelings, broken out to mark uses
function _pastEOF(cb) { _.delayedCall(cb, null, null); }

View File

@@ -1,372 +0,0 @@
var S = require("./structs.js"),
_ = require("./helpers.js");
var dir = exports;
dir.iterator = function (dirChain, opts) {
opts || (opts = {});
var cache = {buffer:null, n: null};
function getSectorBuffer(n, cb) {
if (cache.n === n) cb(null, cache.buffer);
else cache.n = cache.buffer = null, dirChain.readSectors(n, _.allocBuffer(dirChain.sectorSize), function (e,d) {
if (e) cb(e);
else if (!d) return cb(null, null);
else {
cache.n = n;
cache.buffer = d;
getSectorBuffer(n, cb);
}
});
}
var secIdx = 0,
off = {bytes:0},
long = null;
function getNextEntry(cb) {
if (off.bytes >= dirChain.sectorSize) {
secIdx += 1;
off.bytes -= dirChain.sectorSize;
}
var entryPos = {chain:dirChain, sector:secIdx, offset:off.bytes};
getSectorBuffer(secIdx, function (e, buf) {
if (e) return cb(S.err.IO());
else if (!buf) return cb(null, null, entryPos);
var entryIdx = off.bytes,
signalByte = buf[entryIdx];
if (signalByte === S.entryDoneFlag) return cb(null, null, entryPos);
else if (signalByte === S.entryFreeFlag) {
off.bytes += S.dirEntry.size;
long = null;
if (opts.includeFree) return cb(null, {_free:true,_pos:entryPos}, entryPos);
else return getNextEntry(cb); // usually just skip these
}
var attrByte = buf[entryIdx+S.dirEntry.fields.Attr.offset],
entryType = (attrByte === S.longDirFlag) ? S.longDirEntry : S.dirEntry_simple;
var entry = entryType.valueFromBytes(buf, off);
entry._pos = entryPos;
_.log(_.log.DBG, "entry:", entry, secIdx, entryIdx);
if (entryType === S.longDirEntry) {
var firstEntry;
if (entry.Ord & S.lastLongFlag) {
firstEntry = true;
entry.Ord &= ~S.lastLongFlag;
long = {
name: -1,
sum: entry.Chksum,
_rem: entry.Ord-1,
_arr: []
}
}
if (firstEntry || long && entry.Chksum === long.sum && entry.Ord === long._rem--) {
var S_lde_f = S.longDirEntry.fields,
namepart = entry.Name1;
if (entry.Name1.length === S_lde_f.Name1.size/2) {
namepart += entry.Name2;
if (entry.Name2.length === S_lde_f.Name2.size/2) {
namepart += entry.Name3;
}
}
long._arr.push(namepart);
if (!long._rem) {
long.name = long._arr.reverse().join('');
delete long._arr;
delete long._rem;
}
} else long = null;
} else if ((attrByte & 0x08) === 0) { // NOTE: checks `!entry.Attr.volume_id`
var bestName = null;
if (long && long.name) {
var pos = entryIdx + S.dirEntry.fields['Name'].offset,
sum = _.checksumName(buf, pos);
if (sum === long.sum) bestName = long.name;
}
if (!bestName) {
if (signalByte === S.entryIsE5Flag) entry.Name.filename = '\u00E5'+entry.Name.filename.slice(1);
var nam = entry.Name.filename.replace(/ +$/, ''),
ext = entry.Name.extension.replace(/ +$/, '');
// TODO: lowercase bits http://en.wikipedia.org/wiki/8.3_filename#Compatibility
// via NTRes, bits 0x08 and 0x10 http://www.fdos.org/kernel/fatplus.txt.1
bestName = (ext) ? nam+'.'+ext : nam;
}
entry._name = bestName;
// OPTIMIZATION: avoid processing any fields for non-matching entries
// TODO: we could make this automatic via getters, but…?
var _entryBuffer = buf.slice(off.bytes-S.dirEntry.size, off.bytes);
entry._full = function () {
var _entry = S.dirEntry.valueFromBytes(_entryBuffer);
_.extend(entry, _entry);
entry._size = entry.FileSize;
entry._firstCluster = (entry.FstClusHI << 16) + entry.FstClusLO;
return entry;
};
long = null;
return cb(null, entry, entryPos);
} else long = null;
getNextEntry(cb);
});
}
function iter(cb) {
getNextEntry(cb);
return iter; // TODO: previous value can't be re-used, so why make caller re-assign?
}
return iter;
};
function _updateEntry(vol, entry, newStats) {
if ('size' in newStats) entry._size = entry.FileSize = newStats.size;
if ('_touch' in newStats) newStats.archive = newStats.atime = newStats.mtime = true;
if ('archive' in newStats) entry.Attr.archive = true; // TODO: also via newStats.mode?
var _now;
function applyDate(d, prefix, timeToo, tenthToo) {
if (d === true) d = _now || (_now = new Date());
entry[prefix+'Date'] = {year:d.getFullYear()-1980, month:d.getMonth()+1, day:d.getDate()};
if (timeToo) {
entry[prefix+'Time'] = {hours:d.getHours(), minutes:d.getMinutes(), seconds_2:d.getSeconds()>>>1};
if (tenthToo) {
var msec = (d.getSeconds() % 2)*1000 + d.getMilliseconds();
entry[prefix+'TimeTenth'] = Math.floor(msec / 100);
}
}
}
if ('ctime' in newStats) applyDate(newStats.ctime, 'Crt', true, true);
if ('mtime' in newStats) applyDate(newStats.mtime, 'Wrt', true);
if ('atime' in newStats) applyDate(newStats.atime, 'LstAcc');
if ('mode' in newStats) {
entry.Attr.directory = (newStats.mode & S._I.FDIR) ? true : false;
entry.Attr.volume_id = (newStats.mode & S._I.FREG) ? false : true;
if (vol.opts.modmode === 0111) {
entry.Attr.archive = (newStats.mode & S._I.XUSR) ? true : false;
entry.Attr.system = (newStats.mode & S._I.XGRP) ? true : false;
entry.Attr.hidden = (newStats.mode & S._I.XOTH) ? true : false;
entry.Attr.readonly = (newStats.mode & S._I.WUSR) ? false : true;
} else if (vol.opts.modmode === 07000) {
entry.Attr.archive = (newStats.mode & S._I.SVTX) ? true : false;
entry.Attr.system = (newStats.mode & S._I.SGID) ? true : false;
entry.Attr.hidden = (newStats.mode & S._I.SUID) ? true : false;
entry.Attr.readonly = (
newStats.mode & S._I.WUSR ||
newStats.mode & S._I.WGRP ||
newStats.mode & S._I.WOTH
) ? false : true;
}
}
if ('firstCluster' in newStats) {
entry.FstClusLO = newStats.firstCluster & 0xFFFF;
entry.FstClusHI = newStats.firstCluster >>> 16;
entry._firstCluster = newStats.firstCluster;
}
return entry;
}
dir.makeStat = function (vol, entry) {
var stats = {}; // TODO: return an actual `instanceof fs.Stat` somehow?
stats.isFile = function () {
return (!entry.Attr.volume_id && !entry.Attr.directory);
};
stats.isDirectory = function () {
return entry.Attr.directory;
};
stats.isBlockDevice = function () { return false; }
stats.isCharacterDevice = function () { return false; }
stats.isSymbolicLink = function () { return false; }
stats.isFIFO = function () { return false; }
stats.isSocket = function () { return false; }
stats.size = entry.FileSize;
stats.blksize = vol._sectorsPerCluster*vol._sectorSize;
stats.blocks = Math.ceil(stats.size / stats.blksize) || 1;
stats.nlink = 1;
stats.mode = S._I.RUSR | S._I.RGRP | S._I.ROTH;
if (!entry.Attr.readonly) stats.mode |= S._I.WUSR | S._I.WGRP | S._I.WOTH;
if (entry.Attr.directory) stats.mode |= S._I.FDIR;
else if (!entry.Attr.volume_id) stats.mode |= S._I.FREG;
// NOTE: discussion at https://github.com/natevw/fatfs/issues/7
if (vol.opts.modmode === 0111) {
// expose using executable bits, like Samba
if (entry.Attr.archive) stats.mode |= S._I.XUSR;
if (entry.Attr.system) stats.mode |= S._I.XGRP;
if (entry.Attr.hidden) stats.mode |= S._I.XOTH;
} else if (vol.opts.modmode === 07000) {
// expose using setXid/sticky bits, like MKS
if (entry.Attr.archive) stats.mode |= S._I.SVTX;
if (entry.Attr.system) stats.mode |= S._I.SGID;
if (entry.Attr.hidden) stats.mode |= S._I.SUID;
}
stats.mode &= ~vol.opts.umask;
stats.uid = vol.opts.uid;
stats.gid = vol.opts.gid;
function extractDate(prefix) {
var date = entry[prefix+'Date'],
time = entry[prefix+'Time'] || {hours:0, minutes:0, seconds_2:0},
secs = time.seconds_2 * 2,
sect = entry[prefix+'TimeTenth'] || 0;
if (sect > 100) {
secs += 1;
sect -= 100;
}
return new Date(date.year+1980, date.month-1, date.day, time.hours, time.minutes, secs, sect*100);
}
stats.atime = extractDate('LstAcc');
stats.mtime = extractDate('Wrt');
stats.ctime = extractDate('Crt');
entry = { // keep immutable copy (with only the fields we need)
Attr: _.extend({},entry.Attr),
};
return stats;
};
dir.init = function (vol, dirInfo, cb) {
var dirChain = dirInfo.chain,
isRootDir = ('numSectors' in dirChain), // HACK: all others would be a clusterChain
initialCluster = _.allocBuffer(dirChain.sectorSize*vol._sectorsPerCluster),
entriesOffset = {bytes:0};
initialCluster.fill(0);
function writeEntry(name, clusterNum) {
while (name.length < 8) name += " ";
S.dirEntry.bytesFromValue(_updateEntry(vol, {
Name: {filename:name, extension:" "},
Attr: {directory:true}
}, {firstCluster:clusterNum, _touch:true,ctime:true}), initialCluster, entriesOffset);
}
if (!isRootDir) {
writeEntry(".", dirChain.firstCluster);
writeEntry("..", dirInfo.parent.chain.firstCluster);
};
dirChain.writeToPosition(0, initialCluster, cb);
};
dir.addFile = function (vol, dirChain, entryInfo, opts, cb) {
if (typeof opts === 'function') {
cb = opts;
opts = {};
}
var name = entryInfo.name,
entries = [], mainEntry;
entries.push(mainEntry = {
Name: _.shortname(name),
Attr: {directory:opts.dir||false},
_name: name
});
if (1 || mainEntry.Name._lossy) { // HACK: always write long names until `._lossy` is more useful!
var workaroundTessel427 = ('\uFFFF'.length !== 1);
if (workaroundTessel427) throw Error("Your JS runtime does not have proper Unicode string support. (If Tessel, is your firmware up-to-date?)");
// name entries should be 0x0000-terminated and 0xFFFF-filled
var S_lde_f = S.longDirEntry.fields,
ENTRY_CHUNK_LEN = (S_lde_f.Name1.size + S_lde_f.Name2.size + S_lde_f.Name3.size)/2,
partialLen = name.length % ENTRY_CHUNK_LEN,
paddingNeeded = partialLen && (ENTRY_CHUNK_LEN - partialLen);
if (paddingNeeded--) name += '\u0000';
while (paddingNeeded-- > 0) name += '\uFFFF';
// now fill in as many entries as it takes
var off = 0,
ord = 1;
while (off < name.length) entries.push({
Ord: ord++,
Name1: name.slice(off, off+=S_lde_f.Name1.size/2),
Attr_raw: S.longDirFlag,
Chksum: null,
Name2: name.slice(off, off+=S_lde_f.Name2.size/2),
Name3: name.slice(off, off+=S_lde_f.Name3.size/2)
});
entries[entries.length - 1].Ord |= S.lastLongFlag;
}
if (entryInfo.tail) {
var name = mainEntry.Name.filename,
suffix = '~'+entryInfo.tail,
endIdx = name.indexOf(' '),
sufIdx = (~endIdx) ? Math.min(endIdx, name.length-suffix.length) : name.length-suffix.length;
if (sufIdx < 0) return cb(S.err.NAMETOOLONG()); // TODO: would EXIST be more correct?
mainEntry.Name.filename = name.slice(0,sufIdx)+suffix+name.slice(sufIdx+suffix.length);
_.log(_.log.DBG, "Shortname amended to:", mainEntry.Name);
}
vol.allocateInFAT(dirChain.firstCluster || 2, function (e,fileCluster) {
if (e) return cb(e);
var nameBuf = S.dirEntry.fields['Name'].bytesFromValue(mainEntry.Name),
nameSum = _.checksumName(nameBuf);
// TODO: finalize initial properties… (via `opts.mode` instead?)
_updateEntry(vol, mainEntry, {firstCluster:fileCluster, size:0, ctime:true,_touch:true});
mainEntry._pos = _.adjustedPos(vol, entryInfo.target, S.dirEntry.size*(entries.length-1));
entries.slice(1).forEach(function (entry) {
entry.Chksum = nameSum;
});
entries.reverse();
if (entryInfo.lastEntry) entries.push({});
var entriesData = _.allocBuffer(S.dirEntry.size*entries.length),
dataOffset = {bytes:0};
entries.forEach(function (entry) {
var entryType = ('Ord' in entry) ? S.longDirEntry : S.dirEntry;
entryType.bytesFromValue(entry, entriesData, dataOffset);
});
_.log(_.log.DBG, "Writing", entriesData.length, "byte directory entry", mainEntry, "into", dirChain.toJSON(), "at", entryInfo.target);
dirChain.writeToPosition(entryInfo.target, entriesData, function (e) {
// TODO: if we get error, what/should we clean up?
if (e) cb(e);
else cb(null, mainEntry, vol.chainForCluster(fileCluster, dirChain));
});
});
};
dir.findInDirectory = function (vol, dirChain, name, opts, cb) {
var matchName = name.toUpperCase(),
tailName = (opts.prepareForCreate) ? _.shortname(name) : null,
maxTail = 0;
function processNext(next) {
next = next(function (e, d, entryPos) {
if (e) cb(e);
else if (!d) cb(S.err.NOENT(), {tail:maxTail, target:entryPos, lastEntry:true});
else if (d._free) processNext(next); // TODO: look for long enough reusable run
else if (d._name.toUpperCase() === matchName) return cb(null, d._full());
else if (!opts.prepareForCreate) processNext(next);
else {
var dNum = 1,
dName = d.Name.filename,
dTail = dName.match(/(.*)~(\d+)/);
if (dTail) {
dNum = +dTail[2];
dName = dTail[1];
}
if (tailName.extension === d.Name.extension &&
tailName.filename.indexOf(dName) === 0)
{
maxTail = Math.max(dNum+1, maxTail);
}
processNext(next);
}
});
}
processNext(dir.iterator(dirChain, {includeFree:(0 && opts.prepareForCreate)}));
};
dir.updateEntry = function (vol, entry, newStats, cb) {
if (!entry._pos || !entry._pos.chain) throw Error("Entry source unknown!");
var entryPos = entry._pos,
newEntry = _updateEntry(vol, entry, newStats),
data = S.dirEntry.bytesFromValue(newEntry);
_.log(_.log.DBG, "UPDATING ENTRY", newStats, newEntry, entryPos, data);
// TODO: if write fails, then entry becomes corrupt!
entryPos.chain.writeToPosition(entryPos, data, cb);
};

View File

@@ -1,206 +0,0 @@
var S = require("./structs.js"),
_xok = require('xok');
// ponyfills for older node.js
exports.allocBuffer = Buffer.alloc || function (len, val) {
var b = Buffer(len);
if (arguments.length > 1) b.fill(val);
return b;
};
exports.bufferFrom = Buffer.from || function (arg0, arg1) {
return (arguments.length > 1) ? Buffer(arg0, arg1) : Buffer(arg0);
};
// flag for WORKAROUND: https://github.com/tessel/beta/issues/380
exports.workaroundTessel380 = !Buffer.from && function () {
var b = Buffer([0]),
s = b.slice(0);
return ((s[0] = 0xFF) !== b[0]);
}();
// WORKAROUND: https://github.com/tessel/beta/issues/433
var oldslice;
if (!Buffer.alloc && Buffer(5).slice(10).length < 0) oldslice = Buffer.prototype.slice, Buffer.prototype.slice = function (s, e) {
if (s > this.length) s = this.length;
// ~WORKAROUND: https://github.com/tessel/beta/issues/434
return (arguments.length > 1) ? oldslice.call(this, s, e) : oldslice.call(this, s);
}
exports.absoluteSteps = function (path) {
var steps = [];
path.split('/').forEach(function (str) {
// NOTE: these should actually be fine, just wasteful…
if (str === '..') steps.pop();
else if (str && str !== '.') steps.push(str);
});
return steps.map(exports.longname);
};
exports.absolutePath = function (path) {
return '/'+exports.absoluteSteps(path).join('/');
};
exports.parseFlags = function (flags) {
// read, write, append, create, truncate, exclusive
var info, _dir; // NOTE: there might be more clever ways to "parse", but…
if (flags[0] === '\\') {
// internal flag used internally to `fs.open` directories without `S.err.ISDIR()`
flags = flags.slice(1);
_dir = true;
}
switch (flags) {
case 'r': info = {read:true, write:false, create:false}; break;
case 'r+': info = {read:true, write:true, create:false}; break;
case 'rs': info = {read:true, write:false, create:false, sync:true}; break;
case 'rs+': info = {read:true, write:true, create:false, sync:true}; break;
case 'w': info = {read:false, write:true, create:true, truncate:true}; break;
case 'wx': info = {read:false, write:true, create:true, exclusive:true}; break;
case 'w+': info = {read:true, write:true, create:true, truncate:true}; break;
case 'wx+': info = {read:true, write:true, create:true, exclusive:true}; break;
case 'a': info = {read:false, write:true, create:true, append:true}; break;
case 'ax': info = {read:false, write:true, create:true, append:true, exclusive:true}; break;
case 'a+': info = {read:true, write:true, create:true, append:true}; break;
case 'ax+': info = {read:true, write:true, create:true, append:true, exclusive:true}; break;
default: throw Error("Uknown mode: "+flags); // TODO: throw as `S.err.INVAL`
}
if (info.sync) throw Error("Mode not implemented."); // TODO: what would this require of us?
if (_dir) info._openDir = true;
return info;
};
// TODO: these are great candidates for special test coverage!
var _snInvalid = /[^A-Z0-9$%'-_@~`!(){}^#&.]/g; // NOTE: '.' is not valid but we split it away
exports.shortname = function (name) {
var lossy = false;
// TODO: support preservation of case for otherwise non-lossy name!
name = name.toUpperCase().replace(/ /g, '').replace(/^\.+/, '');
name = name.replace(_snInvalid, function () {
lossy = true;
return '_';
});
var parts = name.split('.'),
basis3 = parts.pop(),
basis8 = parts.join('');
if (!parts.length) {
basis8 = basis3;
basis3 = ' ';
}
if (basis8.length > 8) {
basis8 = basis8.slice(0,8);
// NOTE: technically, spec's "lossy conversion" flag is NOT set by excess length.
// But since lossy conversion and truncated names both need a numeric tail…
lossy = true;
} else while (basis8.length < 8) basis8 += ' ';
if (basis3.length > 3) {
basis3 = basis3.slice(0,3);
lossy = true;
} else while (basis3.length < 3) basis3 += ' ';
return {filename:basis8, extension:basis3, _lossy:lossy};
return {basis:[basis8,basis3], lossy:lossy};
};
//shortname("autoexec.bat") => {basis:['AUTOEXEC','BAT'],lossy:false}
//shortname("autoexecutable.batch") => {basis:['AUTOEXEC','BAT'],lossy:true}
// TODO: OS X stores `shortname("._.Trashes")` as ['~1', 'TRA'] — should we?
var _lnInvalid = /[^a-zA-Z0-9$%'-_@~`!(){}^#&.+,;=[\] ]/g;
exports.longname = function (name) {
name = name.trim().replace(/\.+$/, '').replace(_lnInvalid, function (c) {
if (c.length > 1) throw Error("Internal problem: unexpected match length!");
if (c.charCodeAt(0) > 127) return c;
else throw Error("Invalid character "+JSON.stringify(c)+" in name.");
lossy = true;
return '_';
});
if (name.length > 255) throw Error("Name is too long.");
return name;
};
function nameChkSum(sum, c) {
return ((sum & 1) ? 0x80 : 0) + (sum >>> 1) + c & 0xFF;
}
// WORKAROUND: https://github.com/tessel/beta/issues/335
function reduceBuffer(buf, start, end, fn, res) {
// NOTE: does not handle missing `res` like Array.prototype.reduce would
for (var i = start; i < end; ++i) {
res = fn(res, buf[i]);
}
return res;
}
exports.checksumName = function (buf,off) {
off || (off = 0);
var len = S.dirEntry.fields['Name'].size;
return reduceBuffer(buf, off, off+len, nameChkSum, 0);
};
/* comparing C rounding trick from FAT spec with Math.ceil
function tryBoth(d) {
var a = ((D.RootEntCnt * 32) + (D.BytsPerSec - 1)) / D.BytsPerSec >>> 0,
b = Math.ceil((D.RootEntCnt * 32) / D.BytsPerSec);
if (b !== a) console.log("try", b, a, (b === a) ? '' : '*');
return (b === a);
}
// BytsPerSec — "may take on only the following values: 512, 1024, 2048 or 4096"
[512, 1024, 2048, 4096].forEach(function (bps) {
// RootEntCnt — "a count that when multiplied by 32 results in an even multiple of BPB_BytsPerSec"
for (var evenMultiplier = 0; evenMultiplier < 1024*1024*16; evenMultiplier += 2) {
var rec = (bps * evenMultiplier) / 32;
tryBoth({RootEntCnt:rec, BytsPerSec:bps});
}
});
*/
exports.fmtHex = function (n, ff) {
return (1+ff+n).toString(16).slice(1);
};
exports.delayedCall = function (fn) {
if (!fn) throw Error("No function provided!"); // debug aid
var ctx = this,
args = Array.prototype.slice.call(arguments, 1);
setImmediate(function () {
fn.apply(ctx, args);
});
};
exports.adjustedPos = function (vol, pos, bytes) {
var _pos = {
chain: pos.chain,
sector: pos.sector,
offset: pos.offset + bytes
}, secSize = vol._sectorSize;
while (_pos.offset >= secSize) {
_pos.sector += 1;
_pos.offset -= secSize;
}
return _pos;
};
exports.extend = _xok;
var _prevDbg = Date.now(),
_thresh = 50;
function log(level) {
if (level < log.level) return;
var now = Date.now(),
diff = now - _prevDbg;
arguments[0] = ((diff < _thresh) ? " " : '') + diff.toFixed(0) + "ms";
console.log.apply(console, arguments);
_prevDbg = now;
}
log.DBG = -4;
log.INFO = -3;
log.WARN = -2;
log.ERR = -1;
log.level = log.WARN;
exports.log = log;

View File

@@ -1,25 +0,0 @@
var fs = require('fs');
exports.createDriverSync = function (path, opts) {
opts || (opts = {});
var secSize = 512,
ro = opts.readOnly || false,
fd = fs.openSync(path, (ro) ? 'r' : 'r+'),
s = fs.fstatSync(fd);
return {
sectorSize: secSize,
numSectors: s.size / secSize,
readSectors: function (i, dest, cb) {
fs.read(fd, dest, 0, dest.length, i*secSize, function (e,n,d) {
cb(e,d);
});
},
writeSectors: (ro) ? null : function (i, data, cb) {
fs.write(fd, data, 0, data.length, i*secSize, function (e) {
cb(e);
});
}
};
};

View File

@@ -1,771 +0,0 @@
var events = require('events'),
streams = require('stream'),
fifolock = require('fifolock'),
S = require('./structs.js'),
_ = require('./helpers.js')
//_.log.level = _.log.DBG;
exports.createFileSystem = function (volume, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = null
}
opts = _.extend(
{
// c.f. https://www.kernel.org/doc/Documentation/filesystems/vfat.txt
ro: false,
noatime: true,
modmode: 0111, // or `07000`
umask: 'umask' in process ? process.umask() : 0022,
uid: 'getuid' in process ? process.getuid() : 0,
gid: 'getgid' in process ? process.getgid() : 0,
},
opts
)
if (!volume.writeSectors) opts.ro = true
if (opts.ro) opts.noatime = true // natch
var fs = new events.EventEmitter(),
vol = null,
dir = require('./dir.js'),
c = require('./chains.js'),
q = fifolock()
var GROUP =
_.log.level < _.log.INFO
? q.TRANSACTION_WRAPPER.bind({
postAcquire: function (proceed) {
_.log(_.log.DBG, '=== Starting GROUP ===')
proceed()
},
preRelease: function (finish) {
_.log(_.log.DBG, '=== Finishing GROUP ===')
finish()
},
})
: q.TRANSACTION_WRAPPER
q.acquire(function (unlock) {
// because of this, callers can start before 'ready'
var d = _.allocBuffer(volume.sectorSize)
volume.readSectors(0, d, function (e) {
if (e) fs.emit('error', e)
else {
try {
init(d)
} catch (e) {
fs.emit('error', e)
unlock()
return
}
fs.emit('ready')
}
unlock()
})
})
if (cb) fs.on('error', cb).on('ready', cb.bind(null, null))
function init(bootSector) {
vol = require('./vol.js').init(volume, opts, bootSector)
fs._dirIterator = dir.iterator.bind(dir)
var entryInfoByPath = {},
baseEntry = {
_refs: 0,
_record: function () {
entryInfoByPath[this.path] = this
if (this.parent) this.parent.retain()
return this
},
retain: function () {
if (!this._refs) this._record()
this._refs += 1
return this
},
release: function () {
this._refs -= 1
if (!this._refs) this._rescind()
},
_rescind: function () {
if (this.parent) this.parent.release()
delete entryInfoByPath[this.path]
},
}
fs._createSharedEntry = function (path, entry, chain, parent) {
return _.extend(Object.create(baseEntry), {
_refs: 0, // WORKAROUND: https://github.com/tessel/beta/issues/455
path: path,
entry: entry,
chain: chain,
parent: parent,
}).retain()
}
fs._createSharedEntry('/', { Attr: { directory: true } }, vol.rootDirectoryChain)
fs._sharedEntryForSteps = function (steps, opts, cb) {
// NOTE: may `cb` before returning!
var path = steps.join('/') || '/',
name = steps.pop(), // n.b.
info = entryInfoByPath[path]
if (info) cb(null, info.retain())
else
fs._sharedEntryForSteps(steps, {}, function (e, parentInfo) {
// n.b. `steps` don't include `name`
if (e) cb(e)
else if (!parentInfo.entry.Attr.directory) cb(S.err.NOTDIR())
else
dir.findInDirectory(vol, parentInfo.chain, name, opts, function (e, entry) {
if (e && !opts.prepareForCreate) cb(e)
else if (e) cb(e, { missingChild: _.extend(entry, { name: name }), parent: parentInfo })
else cb(null, fs._createSharedEntry(path, entry, vol.chainForCluster(entry._firstCluster), parentInfo))
})
})
}
fs._updateEntry = dir.updateEntry.bind(dir, vol)
fs._makeStat = dir.makeStat.bind(dir, vol)
fs._addFile = dir.addFile.bind(dir, vol)
fs._initDir = dir.init.bind(dir, vol)
}
/**** ---- CORE API ---- ****/
// NOTE: we really don't share namespace, but avoid first three anyway…
var fileDescriptors = [null, null, null]
fs.open = function (path, flags, mode, cb, _n_) {
if (typeof mode === 'function') {
_n_ = cb
cb = mode
mode = 0666
}
cb = GROUP(
cb,
function () {
var _fd = { flags: null, entry: null, chain: null, pos: 0 },
f = _.parseFlags(flags)
if (vol.opts.ro && (f.write || f.create || f.truncate)) return _.delayedCall(cb, S.err.ROFS())
else _fd.flags = f
fs._sharedEntryForSteps(_.absoluteSteps(path), { prepareForCreate: f.create }, function (e, info) {
if (e && !(e.code === 'NOENT' && f.create && info)) cb(e)
else if (e)
fs._addFile(info.parent.chain, info.missingChild, { dir: f._openDir }, function (e, newEntry, newChain) {
if (e) cb(e)
else finish(fs._createSharedEntry(_.absolutePath(path), newEntry, newChain, info.parent))
})
else if (info && f.exclusive) cb(S.err.EXIST())
else if (info.entry.Attr.directory && !f._openDir) cb(S.err.ISDIR())
else if (f.write && info.entry.Attr.readonly) cb(S.err.ACCES())
else finish(info)
function finish(fileInfo) {
var fd = fileDescriptors.push(_fd) - 1
_fd.info = fileInfo
_fd.entry = fileInfo.entry
_fd.chain = fileInfo.chain
if (f.append) _fd.pos = _fd.entry._size
if (f._openDir) _fd.chain.cacheAdvice = 'WILLNEED'
if (f.truncate && _fd.entry._size)
fs.ftruncate(
fd,
0,
function (e) {
cb(e, fd)
},
'_nested_'
)
else _.delayedCall(cb, null, fd) // (delay in case fs._sharedEntryForSteps all cached!)
}
})
},
_n_ === '_nested_'
)
}
fs.fstat = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb, null, fs._makeStat(_fd.entry))
},
_n_ === '_nested_'
)
}
fs.futimes = function (fd, atime, mtime, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
// NOTE: ctime would get touched on POSIX; but we map that to create time!
else fs._updateEntry(_fd.entry, { atime: atime || true, mtime: mtime || true }, cb)
},
_n_ === '_nested_'
)
}
fs.fchmod = function (fd, mode, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else {
mode &= S._I._chmoddable
if (_fd.entry.Attr.directory) mode |= S._I.FDIR
else if (!_fd.entry.Attr.volume_id) mode |= S._I.FREG
fs._updateEntry(_fd.entry, { mode: mode }, cb)
}
},
_n_ === '_nested_'
)
}
fs.read = function (fd, buf, off, len, pos, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) return _.delayedCall(cb, S.err.BADF())
var _pos = pos === null ? _fd.pos : pos,
_len = Math.min(len, _fd.entry._size - _pos),
_buf = buf.slice(off, off + _len)
_fd.chain.readFromPosition(_pos, _buf, function (e, bytes, slice) {
if (_.workaroundTessel380) _buf.copy(buf, off) // WORKAROUND: https://github.com/tessel/beta/issues/380
_fd.pos = _pos + bytes
if (e || vol.opts.noatime) finish(e)
else fs._updateEntry(_fd.entry, { atime: true }, finish)
function finish(e) {
cb(e, bytes, buf)
}
})
},
_n_ === '_nested_'
)
}
fs._readdir = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) return _.delayedCall(cb, S.err.BADF())
var entryNames = [],
getNextEntry = fs._dirIterator(_fd.chain)
function processNext() {
getNextEntry(function (e, d) {
if (e) cb(e)
else if (!d && !entryNames.length)
cb(null, entryNames) // WORKAROUND: https://github.com/tessel/beta/issues/435
else if (!d)
cb(null, entryNames.sort()) // NOTE: sort not required, but… [simplifies tests for starters!]
else {
if (d._name !== '.' && d._name !== '..') entryNames.push(d._name)
processNext()
}
})
}
processNext()
},
_n_ === '_nested_'
)
}
fs._mkdir = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else fs._initDir(_fd.info, cb)
},
_n_ === '_nested_'
)
}
fs.write = function (fd, buf, off, len, pos, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) return _.delayedCall(cb, S.err.BADF())
var _pos = pos === null || _fd.flags.append ? _fd.pos : pos,
_buf = buf.slice(off, off + len)
if (_pos > _fd.entry._size) {
// TODO: handle huge jumps by zeroing clusters individually?
var padLen = _pos - _fd.entry._size,
padBuf = _.allocBuffer(padLen + _buf.length)
padBuf.fill(0x00, 0, padLen)
_buf.copy(padBuf, padLen)
_pos = _fd.entry._size
_buf = padBuf
}
_fd.chain.writeToPosition(_pos, _buf, function (e) {
_fd.pos = _pos + len
var newSize = Math.max(_fd.entry._size, _fd.pos),
newInfo = { size: newSize, _touch: true }
fs._updateEntry(_fd.entry, newInfo, function (ee) {
cb(e || ee, len, buf)
})
})
},
_n_ === '_nested_'
)
}
fs.ftruncate = function (fd, len, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) return _.delayedCall(cb, S.err.BADF())
var newStats = { size: len, _touch: true }
// NOTE: we order operations for best state in case of only partial success
if (len === _fd.entry._size) _.delayedCall(cb)
else if (len < _fd.entry._size)
fs._updateEntry(_fd.entry, newStats, function (e) {
if (e) cb(e)
else _fd.chain.truncate(Math.ceil(len / _fd.chain.sectorSize), cb)
})
// TODO: handle huge file expansions without as much memory pressure
else
_fd.chain.writeToPosition(_fd.entry._size, _.allocBuffer(len - _fd.entry._size, 0x00), function (e) {
if (e) cb(e)
else fs._updateEntry(_fd.entry, newStats, cb)
})
},
_n_ === '_nested_'
)
}
// 'NORMAL', 'SEQUENTIAL', 'RANDOM', 'WILLNEED', 'DONTNEED', 'NOREUSE'
fs._fadviseSync = function (fd, off, len, advice) {
if (off !== 0 || len !== 0) throw Error('Cache advise can currently be given only for whole file!')
var _fd = fileDescriptors[fd]
if (!_fd) throw S.err.BADF()
else _fd.chain.cacheAdvice = advice
}
fs.fsync = function (fd, cb) {
// NOTE: we'll need to flush write cache here once we have one…
var _fd = fileDescriptors[fd]
if (!_fd) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb)
}
fs.close = function (fd, cb) {
var _fd = fileDescriptors[fd]
if (!_fd) _.delayedCall(cb, S.err.BADF())
else setTimeout(_fd.info.release.bind(_fd.info), 500), _.delayedCall(cb, (fileDescriptors[fd] = null))
}
/* STREAM WRAPPERS */
var workaroundTessel436
try {
new require('stream').Readable({ encoding: 'utf8' })
new streams.Readable({ encoding: 'utf8' })
} catch (e) {
workaroundTessel436 = true
}
function _createStream(StreamType, path, opts) {
// [NOT REALLY A] WORKAROUND: https://github.com/tessel/beta/issues/436
if (workaroundTessel436 && 'encoding' in opts) {
console.warn('Tessel does not currently support encoding option for Readable streams, discarding!')
delete opts.encoding
}
var fd = opts.fd !== null ? opts.fd : '_opening_',
pos = opts.start,
stream = new StreamType(opts)
if (fd === '_opening_')
fs.open(path, opts.flags, opts.mode, function (e, _fd) {
if (e) {
fd = '_open_error_'
stream.emit('error', e)
} else {
fd = _fd
fs._fadviseSync(fd, 0, 0, 'SEQUENTIAL')
stream.emit('open', fd)
}
})
function autoClose(tombstone) {
// NOTE: assumes caller will clear `fd`
if (opts.autoClose)
fs.close(fd, function (e) {
if (e) stream.emit('error', e)
else stream.emit('close')
})
fd = tombstone
}
if (StreamType === streams.Readable) {
stream._read = function (n) {
var buf
// TODO: optimize to fetch at least a full sector regardless of `n`…
n = Math.min(n, opts.end - pos)
if (fd === '_opening_')
stream.once('open', function () {
stream._read(n)
})
else if (pos > opts.end) stream.push(null)
else if (n > 0)
(buf = _.allocBuffer(n)),
fs.read(fd, buf, 0, n, pos, function (e, n, d) {
if (e) {
autoClose('_read_error_')
stream.emit('error', e)
} else stream.push(n ? d.slice(0, n) : null)
}),
(pos += n)
else stream.push(null)
}
stream.once('end', function () {
autoClose('_ended_')
})
} else if (StreamType === streams.Writable) {
stream.bytesWritten = 0
stream._write = function (data, _enc, cb) {
if (fd === '_opening_')
stream.once('open', function () {
stream._write(data, null, cb)
})
else
fs.write(fd, data, 0, data.length, pos, function (e, n) {
if (e) {
autoClose('_write_error_')
cb(e)
} else {
stream.bytesWritten += n
cb()
}
}),
(pos += data.length)
}
stream.once('finish', function () {
autoClose('_finished_')
})
}
return stream
}
fs.createReadStream = function (path, opts) {
return _createStream(
streams.Readable,
path,
_.extend(
{
start: 0,
end: Infinity,
flags: 'r',
mode: 0666,
encoding: null,
fd: null, // ??? see https://github.com/joyent/node/issues/7708
autoClose: true,
},
opts
)
)
}
fs.createWriteStream = function (path, opts) {
return _createStream(
streams.Writable,
path,
_.extend(
{
start: 0,
flags: 'w',
mode: 0666,
//encoding: null, // see https://github.com/joyent/node/issues/7710
fd: null, // ??? see https://github.com/joyent/node/issues/7708
autoClose: true,
},
opts,
{ decodeStrings: true, objectMode: false }
)
)
}
/* PATH WRAPPERS (albeit the only public interface for some folder operations) */
function _fdOperation(path, opts, fn, cb) {
cb = GROUP(cb, function () {
opts.advice || (opts.advice = 'NORMAL')
fs.open(
path,
opts.flag,
function (e, fd) {
if (e) cb(e)
else
fs._fadviseSync(fd, 0, 0, opts.advice),
fn(fd, function () {
var ctx = this,
args = arguments
fs.close(
fd,
function (closeErr) {
cb.apply(ctx, args)
},
'_nested_'
)
})
},
'_nested_'
)
})
}
fs.stat = fs.lstat = function (path, cb) {
_fdOperation(
path,
{ flag: 'r' },
function (fd, cb) {
fs.fstat(fd, cb, '_nested_')
},
cb
)
}
fs.exists = function (path, cb) {
fs.stat(path, function (err) {
cb(err ? false : true)
})
}
fs.readFile = function (path, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'r')
opts.advice || (opts.advice = 'NOREUSE')
_fdOperation(
path,
opts,
function (fd, cb) {
fs.fstat(
fd,
function (e, stat) {
if (e) return cb(e)
else {
var buffer = _.allocBuffer(stat.size)
fs.read(
fd,
buffer,
0,
buffer.length,
null,
function (e) {
if (e) cb(e)
else cb(null, opts.encoding ? buffer.toString(opts.encoding) : buffer)
},
'_nested_'
)
}
},
'_nested_'
)
},
cb
)
}
fs.writeFile = function (path, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'w')
opts.advice || (opts.advice = 'NOREUSE')
_fdOperation(
path,
opts,
function (fd, cb) {
if (typeof data === 'string') data = _.bufferFrom(data, opts.encoding || 'utf8')
fs.write(
fd,
data,
0,
data.length,
null,
function (e) {
cb(e)
},
'_nested_'
)
},
cb
)
}
fs.appendFile = function (path, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'a')
fs.writeFile(path, data, opts, cb)
}
fs.truncate = function (path, len, cb) {
_fdOperation(
path,
{ flag: 'r+' },
function (fd, cb) {
fs.ftruncate(fd, len, cb, '_nested_')
},
cb
)
}
fs.readdir = function (path, cb) {
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
fs._readdir(fd, cb, '_nested_')
},
cb
)
}
fs.mkdir = function (path, mode, cb) {
if (typeof mode === 'function') {
cb = mode
mode = 0777
}
_fdOperation(
path,
{ flag: '\\wx' },
function (fd, cb) {
fs._mkdir(fd, cb, '_nested_')
},
cb
)
}
fs.utimes = function (path, atime, mtime, cb) {
_fdOperation(
path,
{ flag: 'r+' },
function (fd, cb) {
fs.futimes(fd, atime, mtime, cb, '_nested_')
},
cb
)
}
fs.chmod = fs.lchmod = function (path, mode, cb) {
_fdOperation(
path,
{ flag: '\\r+' },
function (fd, cb) {
fs.fchmod(fd, mode, cb, '_nested_')
},
cb
)
}
fs.chown = fs.lchown = function (path, uid, gid, cb) {
_fdOperation(
path,
{ flag: '\\r+' },
function (fd, cb) {
fs.fchown(fd, uid, gid, cb, '_nested_')
},
cb
)
}
/* STUBS */
fs.link = function (src, dst, cb) {
// NOTE: theoretically we _could_ do hard links [with untracked `stat.nlink` count…]
_.delayedCall(cb, S.err.NOSYS())
}
fs.symlink = function (src, dst, type, cb) {
if (typeof type === 'function') {
cb = type
type = null
}
_.delayedCall(cb, S.err.NOSYS())
}
fs.readlink = function (path, cb) {
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
// the named file is *never* a symbolic link…
// NOTE: we still use _fdOperation for catching e.g. NOENT/NOTDIR errors…
cb(S.err.INVAL())
},
cb
)
}
fs.realpath = function (path, cache, cb) {
if (typeof cache === 'function') {
cb = cache
cache = null
}
if (cache)
_.delayedCall(cb, S.err.NOSYS()) // TODO: what would be involved here?
else
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
cb(null, _.absolutePath(path))
},
cb
)
}
fs.fchown = function (fd, uid, gid, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb, S.err.NOSYS())
},
_n_ === '_nested_'
)
}
// See https://github.com/natevw/fatfs/pull/31/files
fs.createLabel = function (name, cb) {
fs.open(name, 'a', (e, _fd) => {
if (e) {
cb(e)
} else {
let fd = fileDescriptors[_fd]
fd.entry.Attr.volume_id = true
fd.entry.FstClusLO = 0
fd.entry.FstClusHI = 0
fs._updateEntry(fd.entry, {}, cb)
}
})
}
return fs
}

View File

@@ -1,36 +0,0 @@
#! /bin/sh
# NOTE: this only works on OS X
# TODO: Linux could use mkfs stuff without attach?
# ./make_sample.sh /tmp/fat16.img FAT16
FILE=$1
: ${FILE:?must be provided as first argument}
TYPE=$2
: ${TYPE:?must be provided as second argument}
SIZE=$3 # in MB
case "$TYPE" in
"FAT12") : ${SIZE:=1} # <128
;;
"FAT16") : ${SIZE:=5} # >4
;;
"FAT32") : ${SIZE:=33} # >32
;;
*) : ${SIZE:=1} # e.g. ExFAT
esac
echo Making $TYPE of $SIZE MB at $FILE
if [[ "$TYPE" = FAT* ]]
then
TYPE="MS-DOS $TYPE"
fi
dd if=/dev/zero of=$FILE bs=1048576 count=$SIZE
DEV=`hdiutil attach $FILE -nomount`
diskutil eraseVolume "$TYPE" "FATFS TEST" $DEV
hdiutil detach $DEV

View File

@@ -1,32 +0,0 @@
{
"private": false,
"name": "@vates/fatfs",
"version": "0.11.0",
"description": "fs implementation on top of raw FAT16/FAT32 block source",
"main": "index.js",
"scripts": {
"postversion": "npm publish --access public"
},
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra"
},
"keywords": [
"filesystem",
"fs",
"fat",
"fat32",
"fat16"
],
"author": "Vates SAS",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/vatesfr/xen-orchestra/issues"
},
"homepage": "https://github.com/vatesfr/xen-orchestra",
"dependencies": {
"fifolock": "^1.0.0",
"struct-fu": "^1.2.1",
"xok": "^1.0.0"
}
}

View File

@@ -1,255 +0,0 @@
// see http://staff.washington.edu/dittrich/misc/fatgen103.pdf
// and http://www.cse.scu.edu/~tschwarz/COEN252_09/Lectures/FAT.html
var _ = require('struct-fu'),
__ = require("./helpers.js");
var bootBase = _.struct([
_.byte('jmpBoot', 3),
_.char('OEMName', 8),
_.uint16le('BytsPerSec'),
_.uint8('SecPerClus'),
_.uint16le('ResvdSecCnt'), // Rsvd in table, but Resvd in calcs…
_.uint8('NumFATs'),
_.uint16le('RootEntCnt'),
_.uint16le('TotSec16'),
_.uint8('Media'),
_.uint16le('FATSz16'),
_.uint16le('SecPerTrk'),
_.uint16le('NumHeads'),
_.uint32le('HiddSec'),
_.uint32le('TotSec32')
]);
var bootInfo = _.struct([
_.uint8('DrvNum'),
_.uint8('Reserved1'),
_.uint8('BootSig'),
_.uint32le('VolID'),
_.char('VolLab', 11),
_.char('FilSysType', 8)
]);
exports.boot16 = _.struct([
bootBase,
bootInfo
]);
exports.boot32 = _.struct([
bootBase,
_.uint32le('FATSz32'),
_.struct('ExtFlags', [
_.ubit('NumActiveFAT', 4),
_.ubit('_reserved1', 3),
_.bool('MirroredFAT'),
_.ubit('_reserved2', 8)
].reverse()),
_.struct('FSVer', [
_.uint8('Major'),
_.uint8('Minor')
]),
_.uint32le('RootClus'),
_.uint16le('FSInfo'),
_.uint16le('BkBootSec'),
_.byte('Reserved', 12),
bootInfo
]);
var _time = _.struct([
_.ubit('hours',5),
_.ubit('minutes',6),
_.ubit('seconds_2',5)
]), time = {
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var _buf = __.bufferFrom([buf[off.bytes+1], buf[off.bytes+0]]),
val = _time.valueFromBytes(_buf);
off.bytes += this.size;
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = {hours:0, minutes:0, seconds_2:0});
buf || (buf = __.allocBuffer(this.size));
off || (off = {bytes:0});
var _buf = _time.bytesFromValue(val);
buf[off.bytes+1] = _buf[0];
buf[off.bytes+0] = _buf[1];
off.bytes += this.size;
return buf;
},
size: _time.size
};
var _date = _.struct([
_.ubit('year',7),
_.ubit('month',4),
_.ubit('day',5)
]), date = {
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var _buf = __.bufferFrom([buf[off.bytes+1], buf[off.bytes+0]]),
val = _date.valueFromBytes(_buf);
off.bytes += this.size;
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = {year:0, month:0, day:0});
buf || (buf = __.allocBuffer(this.size));
off || (off = {bytes:0});
var _buf = _date.bytesFromValue(val);
buf[off.bytes+1] = _buf[0];
buf[off.bytes+0] = _buf[1];
off.bytes += this.size;
return buf;
},
size: _date.size
};
exports.dirEntry = _.struct([
_.struct('Name', [
_.char('filename',8),
_.char('extension',3)
]),
_.struct('Attr', [
_.bool('readonly'),
_.bool('hidden'),
_.bool('system'),
_.bool('volume_id'),
_.bool('directory'),
_.bool('archive'),
_.ubit('reserved', 2)
].reverse()),
_.byte('NTRes', 1),
_.uint8('CrtTimeTenth'),
_.struct('CrtTime', [time]),
_.struct('CrtDate', [date]),
_.struct('LstAccDate', [date]),
_.uint16le('FstClusHI'),
_.struct('WrtTime', [time]),
_.struct('WrtDate', [date]),
_.uint16le('FstClusLO'),
_.uint32le('FileSize')
]);
exports.entryDoneFlag = 0x00;
exports.entryFreeFlag = 0xE5;
exports.entryIsE5Flag = 0x05;
exports.dirEntry_simple = _.struct([
_.struct('Name', [
_.char('filename',8),
_.char('extension',3)
]),
_.padTo(exports.dirEntry.size)
/*
_.uint8('Attr_raw'),
_.byte('NTRes', 1),
_.byte('Crt_raw', 1+2+2),
_.byte('Lst_raw', 2),
_.uint16le('FstClusHI'),
_.byte('Wrt_raw', 2+2),
_.uint16le('FstClusLO'),
_.uint32le('FileSize')
*/
]);
exports.lastLongFlag = 0x40;
exports.longDirFlag = 0x0F;
exports.longDirEntry = _.struct([
_.uint8('Ord'),
_.char16le('Name1', 10),
_.uint8('Attr_raw'),
_.uint8('Type'),
_.uint8('Chksum'),
_.char16le('Name2', 12),
_.uint16le('FstClusLO'),
_.char16le('Name3', 4)
]);
if (exports.longDirEntry.size !== exports.dirEntry.size) throw Error("Structs ain't right!");
exports.fatField = {
'fat12': _.struct('Status', [
_.ubit('field0bc', 8),
_.ubit('field1c', 4),
_.ubit('field0a', 4),
_.ubit('field1ab', 8),
]),
'fat16': _.uint16le('Status'),
'fat32': _.uint32le('Status') // more properly this 4 bits reserved + uint28le
};
exports.fatPrefix = {
'fat12': 0xF00,
'fat16': 0xFF00,
'fat32': 0x0FFFFF00
};
exports.fatStat = {
free: 0x00,
_undef: 0x01,
rsvMin: 0xF0,
bad: 0xF7,
eofMin: 0xF8,
eof: 0xFF
};
exports._I = {
RUSR: 0400,
WUSR: 0200,
XUSR: 0100,
RGRP: 0040,
WGRP: 0020,
XGRP: 0010,
ROTH: 0004,
WOTH: 0002,
XOTH: 0001,
SUID: 04000,
SGID: 02000,
SVTX: 01000,
FDIR: 040000,
FREG: 0100000,
};
exports._I.RWXU = exports._I.RUSR | exports._I.WUSR | exports._I.XUSR;
exports._I.RWXG = exports._I.RGRP | exports._I.WGRP | exports._I.XGRP;
exports._I.RWXO = exports._I.ROTH | exports._I.WOTH | exports._I.XOTH;
exports._I._sss = exports._I.SUID | exports._I.SGID | exports._I.SVTX;
exports._I._chmoddable = exports._I.RWXU | exports._I.RWXG | exports._I.RWXO | exports._I._sss;
var _errors = {
IO: "Input/output error",
NOENT: "No such file or directory",
INVAL: "Invalid argument",
EXIST: "File exists",
NAMETOOLONG: "Filename too long",
NOSPC: "No space left on device",
NOSYS: "Function not supported",
ROFS: "ROFLCopter file system",
NOTDIR: "Not a directory",
BADF: "Bad file descriptor",
EXIST: "File exists",
ISDIR: "Is a directory",
ACCES: "Permission denied",
NOSYS: "Function not implemented",
_TODO: "Not implemented yet!"
};
exports.err = {};
Object.keys(_errors).forEach(function (sym) {
var msg = _errors[sym];
exports.err[sym] = function () {
var e = new Error(msg);
e.code = sym;
return e;
};
});

View File

@@ -1,338 +0,0 @@
// can be called from CLI with image type, absolute path to an image, or, required as a module
var _ = require("./helpers.js");
var type = process.argv[2];
if (module.parent) exports.startTests = startTests;
else if (!type) throw "Usage: node test [FAT12|FAT16|FAT32|ExFAT|…|/path/to/image]";
else if (type[0] === '/') testWithImage(type);
else {
var uniq = Math.random().toString(36).slice(2),
IMG = require('os').tmpdir()+"fatfs-test-"+uniq+".img";
require('child_process').exec("./make_sample.sh "+JSON.stringify(IMG)+" "+JSON.stringify(type), function (e,out,err) {
if (e) throw e;
console.warn(err.toString());
//console.log(out.toString());
testWithImage(IMG);
//console.log("open", IMG);
//return;
require('fs').unlink(IMG, function (e) {
if (e) console.warn("Error cleaning up test image", e);
});
});
}
function testWithImage(imagePath) {
var vol = require("./img_volume.js").createDriverSync(imagePath);
startTests(vol);
}
function startTests(vol, waitTime) {
var fatfs = require("./"),
fs = fatfs.createFileSystem(vol, {umask:0020, uid:99, gid:42});
waitTime || (waitTime = 0.5e3);
[
'mkdir','readdir',
//'rename','unlink','rmdir',
'close','open','fsync',
'ftruncate','truncate',
'write','read','readFile','writeFile', 'appendFile',
'chown','lchown','fchown',
'chmod','lchmod', 'fchmod',
'utimes','futimes',
'stat','lstat','fstat','exists',
'link','symlink','readlink','realpath',
//'watchFile','unwatchFile','watch'
].forEach(function (method) { assert(method in fs, "fs."+method+" has implementation."); });
var BASE_DIR = "/fat_test-"+Math.random().toFixed(20).slice(2),
FILENAME = "Simple File.txt",
TEXTDATA = "Hello world!";
var isReady = false;
fs.on('ready', function () {
assert(isReady = true, "Driver is ready.");
}).on('error', function (e) {
assert(e, "If fs driver fires 'error' event, it should include error object.");
assert(false, "…but driver should not error when initializing in our case.");
});
setTimeout(function () {
assert(isReady, "Driver fired ready event in timely fashion.");
}, waitTime);
fs.readdir("/", function (e,files) {
assert(isReady, "Method completed after 'ready' event.");
assert(!e, "No error reading root directory.");
assert(Array.isArray(files), "Got a list of files: "+files);
});
fs.mkdir(BASE_DIR, function (e) {
assert(!e, "No error from fs.mkdir");
fs.readdir(BASE_DIR, function (e,arr) {
assert(!e, "No error from fs.readdir");
assert(arr.length === 0 , "No files in BASE_DIR yet.");
});
var file = [BASE_DIR,FILENAME].join('/');
fs.writeFile(file, TEXTDATA, function (e) {
assert(!e, "No error from fs.writeFile");
startStreamTests();
fs.realpath(file, function (e,path) {
assert(!e, "No error from basic fs.realpath call.");
assert(path === file, "We already had the real path.");
});
fs.realpath([BASE_DIR,".","garbage",".","..",FILENAME].join('/'), function (e,path) {
assert(!e, "No error from fluffy fs.realpath call.");
assert(path === file, "Fixed fluffy path matches normal one.");
});
fs.realpath([BASE_DIR,"non","existent","path"].join('/'), function (e) {
assert(e, "Expected error calling fs.realpath on non-existent file.");
});
fs.readdir(BASE_DIR, function (e, arr) {
assert(!e, "Still no error from fs.readdir");
assert(arr.length === 2, "Test directory contains two files."); // (ours + startStreamTests's)
assert(arr[0] === FILENAME, "Filename is correct.");
fs.stat(file, function (e,d) {
assert(!e, "No error from fs.stat");
assert(d.isFile() === true, "Result is a file…");
assert(d.isDirectory() === false, "…and not a directory.");
assert(d.size === Buffer.byteLength(TEXTDATA), "Size matches length of content written.");
});
fs.exists(file, function (bool) {
assert(bool === true, "File exists.");
});
fs.readFile(file, {encoding:'utf8'}, function (e, d) {
assert(!e, "No error from fs.readFile");
assert(d === TEXTDATA, "Data matches what was written.");
});
// now, overwrite the same file and make sure that goes well too
fs.writeFile(file, _.bufferFrom([0x42]), function (e) {
assert(!e, "Still no error from fs.writeFile");
fs.readdir(BASE_DIR, function (e, arr) {
assert(!e, "No error from fs.readdir");
assert(arr.length === 2, "Test directory still contains two files.");
assert(arr[0] === FILENAME, "Filename still correct.");
fs.stat(file, function (e,d) {
assert(!e, "Still no error from fs.stat");
assert(d.isFile() === true, "Result is still a file…");
assert(d.isDirectory() === false, "…and not a directory.");
assert(d.size === 1, "Size matches length of now-truncated content.");
});
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile");
assert(Buffer.isBuffer(d), "Result without encoding is a buffer.");
assert(d.length === 1, "Buffer is correct size.");
assert(d[0] === 0x42, "Buffer content is correct.");
fs.truncate(file, 1025, function (e) {
assert(!e, "No error from fs.truncate (extending)");
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile after extension");
assert(d.length === 1025, "Read after extension is correct size.");
assert(d[0] === 0x42, "First byte is still correct.");
var allZeroes = true;
for (var i = 1, len = d.length; i < len; ++i) if (d[i] !== 0) allZeroes = false;
assert(allZeroes, "Extended portion of file is zero-filled.");
fs.truncate(file, 3, function (e) {
assert(!e, "No error from fs.truncate (shortening)");
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile after shortening.");
assert(d.length === 3, "Read after shortening is correct size.");
assert(d[0] === 0x42, "First byte is still correct.");
assert(d[1] === 0x00, "Second byte is still correct.");
assert(d[2] === 0x00, "Third byte is still correct.");
proceedWithMoreTests();
});
});
});
});
});
});
});
});
});
function proceedWithMoreTests() {
var fd;
fs.open(file, 'r', function (e, _fd) {
assert(!e, "No error from fs.open.");
fd = _fd;
});
var was = "\u0042\u0000\u0000",
str = "abc";
fs.appendFile(file, str, function (e) {
assert(!e, "No error from fs.appendFile.");
assert(fd, "File descriptor opened before appendFile called.");
var buf = _.allocBuffer(str.length);
fs.read(fd, buf, 0, buf.length, was.length, function (e,n,d) {
assert(!e, "No error from fs.read after append.");
assert(n === str.length, "All appended data was readable.");
assert(d === buf, "Buffer returned from fs.read matched what was passed in.");
assert(d.toString() === str, "Correct data found where expected.");
});
});
fs.readFile(file, {encoding:'ascii'}, function (e,d) {
assert(!e, "No error from fs.appendFile.");
assert(d.length === 6, "Read is correct size after append.");
assert(d === was+str, "Read string matches what was written and then appended.");
fs.open(file, 'a', function (e, fd2) {
assert(!e, "No error from second open of file.");
var str2 = "zyx",
buf2 = _.allocBuffer(str2.length+2);
buf2.write(str2, 1);
fs.write(fd2, buf2, 1, buf2.length-2, was.length, function (e,n,d) {
assert(!e, "No error from appending fs.write.");
assert(n === buf2.length-2, "Wrote proper amount from buffer.");
assert(d === buf2, "Returned original buffer.");
buf2.fill(0);
buf2[0] = 0xFF;
fs.read(fd, buf2, 1, buf2.length-1, was.length, function (e,n,d) {
assert(!e, "No error from twice-appended read.");
assert(n === buf2.length-1, "Read proper amount into buffer.");
assert(buf2[0] === 0xFF, "Read left first byte in buffer properly alone.");
assert(d.slice(1).toString() === (str+str2).slice(0, buf2.length-1), "Data was appended, not written at position.");
});
});
});
});
var F = [BASE_DIR,"Manually inspect from time to time, please!.txt"].join('/'),
S = 512,
N = 16,
b = _.allocBuffer(S*N);
for (var i = 0; i < N; ++i) b.slice(S*i, S*i+S).fill(i.toString(16).charCodeAt(0));
fs.writeFile(F, b, function (e) {
assert(!e, "No error from fs.writeFile with counting blocks.");
fs.readFile(F, function (e,d) {
assert(!e, "No error from fs.readFile with counting blocks.");
assert(d.length === b.length, "Readback is correct size");
var matched = true;
for (var i = 0; i < S*N; ++i) if (b[i] !== d[i]) matched = false;
assert(matched, "Readback matches write byte-for-byte");
});
});
fs.stat(F, function (e,d) {
assert(!e, "No error from fs.stat on counting blocks file.");
assert(d.atime instanceof Date && !isNaN(d.atime.getTime()), "Access time is a valid date.");
assert(d.mtime instanceof Date && !isNaN(d.mtime.getTime()), "Modify time is a valid date.");
assert(d.ctime instanceof Date && !isNaN(d.ctime.getTime()), "Change^WCreate time is a valid date.");
var tf = d.ctime.getTime(),
ct = Date.now();
assert(tf - 2*waitTime < ct && ct < tf + 2*waitTime, "Create time is within ± two `waitTime`s of now.");
fs.utimes(F, new Date(2009, 7-1, 2), null, function (e) {
assert(!e, "No error from fs.utimes.");
fs.stat(F, function (e,d2) {
assert(!e, "No fs.stat error after touching timestamps.");
assert(+d.ctime === +d2.ctime, "Create time not changed by fs.utimes");
assert(d2.atime.toString().indexOf("Jul 02 2009 00:00:00") === 4, "Access time set correctly");
var tf = d2.mtime.getTime(),
ct = Date.now();
assert(tf-2e3 < ct && ct < tf+2e3+waitTime, "Modify time is within ± a few seconds of now.");
// NOTE: due to serialization, this can check results of the `fs.chmod` below, too!
assert(!(d2.mode & 0100), "Archive bit is now unset.");
assert(!(d2.mode & 0222), "Writable perms are unset.");
assert(d2.mode & 0100000, "Regular file bit is set.");
});
});
assert(d.uid === 99, "Desired UID applied.");
assert(d.gid === 42, "Desired GID applied.");
assert(d.mode & 0100, "Archive bit is set.");
assert(d.mode & 0200, "Writable perm is set for user.");
assert((d.mode & 0022) === 0002, "Writable perm is only masked out for group.");
fs.chmod(F, 0422, function (e) {
assert(!e, "No error from fs.chmod.");
});
});
fs.chown(F, 99, 256, function (e) {
assert(e && e.code === 'NOSYS', "Expected error from fs.fchown.");
});
}
function startStreamTests() {
var file2 = [BASE_DIR,FILENAME+"2"].join('/'),
outStream = fs.createWriteStream(file2);
var outStreamOpened = false;
outStream.on('open', function (fd) {
outStreamOpened = true;
assert(typeof fd === 'number', "Got file descriptor on fs.createWriteStream open.");
}).on('error', function (e) {
assert(e, "If fs.createWriteStream fires 'error' event, it should include error object.");
assert(false, "But, fs.createWriteStream should not error during these tests.");
});
setTimeout(function () {
assert(outStreamOpened, "outStream fired 'open' event in a timely fashion.");
}, waitTime);
var TEXT_MOD = TEXTDATA.toLowerCase()+"\n",
NUM_REPS = (waitTime <= 1e3) ? 1024 : 16;
outStream.write(TEXT_MOD, 'utf16le');
outStream.write("Ο καλύτερος χρόνος να φυτευτεί ένα \ud83c\udf31 είναι δέκα έτη πριν.", 'utf16le');
outStream.write("La vez del segundo mejor ahora está.\n", 'utf16le');
for (var i = 0; i < NUM_REPS; ++i) outStream.write("123456789\n", 'ascii');
outStream.write("JavaScript how do they work\n", 'utf16le');
outStream.write("The end, almost.\n", 'utf16le');
outStream.end(TEXTDATA, 'utf16le');
var outStreamFinished = false;
outStream.on('finish', function () {
outStreamFinished = true;
var inStream = fs.createReadStream(file2, {start:NUM_REPS*10, encoding:'utf16le', autoClose:false}),
gotData = false, gotEOF = false, inStreamFD = null;
inStream.on('open', function (fd) {
assert(typeof fd === 'number', "Got file descriptor on fs.createReadStream open.");
inStreamFD = fd;
});
inStream.on('data', function (d) {
gotData = true;
assert(typeof d === 'string', "Data returned as string.");
assert(d.slice(d.length-TEXTDATA.length) === TEXTDATA, "End of file matches what was written.");
});
inStream.on('end', function () {
gotEOF = true;
var len = Buffer.byteLength(TEXT_MOD, 'utf16le'),
buf = _.allocBuffer(len);
fs.fsync(inStreamFD, function (e) {
assert(!e, "No error from proper fsync.");
});
fs.fsync('garbage', function (e) {
assert(e, "Expected error from garbage fsync.");
});
fs.read(inStreamFD, buf, 0, len, 0, function (e,n,d) {
assert(!e, "No error reading from beginning of inStream's file descriptor.");
assert(n === len, "Read complete buffer at beginning of inStream's fd.");
assert(d.toString('utf16le') === TEXT_MOD, "Data matches at beginning of inStream's fd.");
fs.close(inStreamFD, function (e) {
assert(!e, "No error closing inStream's fd.");
});
});
});
setTimeout(function () {
assert(gotData, "inStream fired 'data' event in a timely fashion.");
setTimeout(function () {
assert(gotEOF, "inStream fired 'eof' event in a timely fashion.");
}, waitTime);
}, waitTime);
});
setTimeout(function () {
assert(outStreamFinished, "outStream fired 'finish' event in a timely fashion.");
}, 2*waitTime);
}
});
}
function assert(b,msg) { if (!msg) console.warn("no msg", Error().stack); if (!b) throw Error("Assertion failure. "+msg); else console.log(msg); }

View File

@@ -1,165 +0,0 @@
var S = require("./structs.js"),
c = require("./chains.js"),
$ = require("./cache.js"),
_ = require("./helpers.js");
exports.init = function (volume, opts, bootSector) {
if (bootSector[510] !== 0x55 || bootSector[511] !== 0xAA) throw Error("Invalid volume signature!");
var isFAT16 = bootSector.readUInt16LE(S.boot16.fields['FATSz16'].offset),
bootStruct = (isFAT16) ? S.boot16 : S.boot32,
BS = bootStruct.valueFromBytes(bootSector);
_.log(_.log.DBG, "Boot sector info:", BS);
bootSector = null; // allow GC
if (!BS.BytsPerSec) throw Error("This looks like an ExFAT volume! (unsupported)");
else if (BS.BytsPerSec !== volume.sectorSize) throw Error("Sector size mismatch with FAT table.");
var FATSz = (isFAT16) ? BS.FATSz16 : BS.FATSz32,
rootDirSectors = Math.ceil((BS.RootEntCnt * 32) / BS.BytsPerSec),
firstDataSector = BS.ResvdSecCnt + (BS.NumFATs * FATSz) + rootDirSectors,
totSec = (BS.TotSec16) ? BS.TotSec16 : BS.TotSec32,
dataSec = totSec - firstDataSector,
countofClusters = Math.floor(dataSec / BS.SecPerClus);
// avoid corrupting sectors from other partitions or whatnot
if (totSec > volume.numSectors) throw Error("Volume size mismatch!");
var fatType;
if (countofClusters < 4085) {
fatType = 'fat12';
} else if (countofClusters < 65525) {
fatType = 'fat16';
} else {
fatType = 'fat32';
}
_.log(_.log.DBG, "rootDirSectors", rootDirSectors, "firstDataSector", firstDataSector, "countofClusters", countofClusters, "=>", fatType);
var vol = {};
vol.opts = opts;
vol._sectorSize = BS.BytsPerSec;
vol._sectorsPerCluster = BS.SecPerClus;
vol._firstSectorOfCluster = function (n) {
return firstDataSector + (n-2)*vol._sectorsPerCluster;
};
vol._makeCache = function () {
return $.wrapDriver(volume);
};
vol._readSectors = function (cache, secNum, dest, cb) {
if (typeof dest === 'function') {
cb = dest;
dest = _.allocBuffer(vol._sectorSize);
}
_.log(_.log.DBG, "vol._readSectors", secNum, dest.length);
if (secNum < volume.numSectors) cache.readSectors(secNum, dest, function (e) { cb(e, dest); });
else throw Error("Invalid sector number!");
};
vol._writeSectors = function (cache, secNum, data, cb) {
_.log(_.log.DBG, "vol._writeSectors", secNum, data.length);
// NOTE: these are internal assertions, public API will get proper `S.err`s
if (data.length % volume.sectorSize) throw Error("Buffer length not a multiple of sector size");
else if (opts.ro) throw Error("Read-only filesystem");
else if (secNum < volume.numSectors) cache.writeSectors(secNum, data, cb);
else throw Error("Invalid sector number!");
};
function fatInfoForCluster(n) {
var entryStruct = S.fatField[fatType],
FATOffset = (fatType === 'fat12') ? Math.floor(n/2) * entryStruct.size : n * entryStruct.size,
SecNum = BS.ResvdSecCnt + Math.floor(FATOffset / BS.BytsPerSec);
EntOffset = FATOffset % BS.BytsPerSec;
return {sector:SecNum-BS.ResvdSecCnt, offset:EntOffset, struct:entryStruct};
}
// TODO: all this FAT manipulation is crazy inefficient! needs read caching *and* write caching
// …the best place for cache might be in `volume` handler, though. add a `flush` method to that spec?
// TODO: how should we handle redundant FATs? mirror every write? just ignore completely? copy-on-eject?
var fatChain = c.sectorChain(vol, BS.ResvdSecCnt, FATSz);
fatChain.cacheAdvice = 'RANDOM';
vol.fetchFromFAT = function (clusterNum, cb) {
var info = fatInfoForCluster(clusterNum);
fatChain.readFromPosition(info, info.struct.size, function (e,n,d) {
if (e) return cb(e);
var status = info.struct.valueFromBytes(d), prefix;
if (fatType === 'fat12') {
if (clusterNum % 2) {
status = (status.field1ab << 4) + status.field1c;
} else {
status = (status.field0a << 8) + status.field0bc;
}
}
else if (fatType === 'fat32') {
status &= 0x0FFFFFFF;
}
var prefix = S.fatPrefix[fatType];
if (status === S.fatStat.free) cb(null, 'free');
else if (status === S.fatStat._undef) cb(null, '-invalid-');
else if (status > prefix+S.fatStat.eofMin) cb(null, 'eof');
else if (status === prefix+S.fatStat.bad) cb(null, 'bad');
else if (status > prefix+S.fatStat.rsvMin) cb(null, 'reserved');
else cb(null, status);
});
};
vol.storeToFAT = function (clusterNum, status, cb) {
if (typeof status === 'string') {
status = S.fatStat[status];
status += S.fatPrefix[fatType];
}
var info = fatInfoForCluster(clusterNum);
// TODO: technically fat32 needs to *preserve* the high 4 bits
if (fatType === 'fat12') fatChain.readFromPosition(info, info.struct.size, function (e,n,d) {
var value = info.struct.valueFromBytes(d);
if (clusterNum % 2) {
value.field1ab = status >>> 4;
value.field1c = status & 0x0F;
} else {
value.field0a = status >>> 8;
value.field0bc = status & 0xFF;
}
var entry = info.struct.bytesFromValue(value);
fatChain.writeToPosition(info, entry, cb);
}); else {
var entry = info.struct.bytesFromValue(status);
fatChain.writeToPosition(info, entry, cb);
}
};
vol.allocateInFAT = function (hint, cb) {
if (typeof hint === 'function') {
cb = hint;
hint = 2; // TODO: cache a better starting point?
}
function searchForFreeCluster(num, cb) {
if (num < countofClusters) vol.fetchFromFAT(num, function (e, status) {
if (e) cb(e);
else if (status === 'free') cb(null, num);
else searchForFreeCluster(num+1, cb);
}); else cb(S.err.NOSPC()); // TODO: try searching backwards from hint…
}
searchForFreeCluster(hint, function (e, clusterNum) {
if (e) cb(e);
else vol.storeToFAT(clusterNum, 'eof', cb.bind(null,null,clusterNum));
});
};
vol.rootDirectoryChain = (isFAT16) ?
c.sectorChain(vol, firstDataSector - rootDirSectors, rootDirSectors) :
c.clusterChain(vol, BS.RootClus);
vol.rootDirectoryChain.cacheAdvice = 'WILLNEED';
vol.chainForCluster = c.clusterChain.bind(c, vol);
vol.chainFromJSON = function (d) {
return ('numSectors' in d) ?
c.sectorChain(vol, d.firstSector, d.numSectors) :
c.clusterChain(vol, d.firstCluster);
};
return vol;
}

View File

@@ -1,28 +0,0 @@
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder :
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`

View File

@@ -1,59 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/fuse-vhd
[![Package Version](https://badgen.net/npm/v/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd) ![License](https://badgen.net/npm/license/@vates/fuse-vhd) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/fuse-vhd)](https://bundlephobia.com/result?p=@vates/fuse-vhd) [![Node compatibility](https://badgen.net/npm/node/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/fuse-vhd):
```sh
npm install --save @vates/fuse-vhd
```
## Usage
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder :
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env node
import Disposable from 'promise-toolbox/Disposable'
import { getSyncedHandler } from '@xen-orchestra/fs'
import { mount } from './index.mjs'
async function* main([remoteUrl, vhdPathInRemote, mountPoint]) {
if (mountPoint === undefined) {
throw new TypeError('missing arg: cli <remoteUrl> <vhdPathInRemote> <mountPoint>')
}
const handler = yield getSyncedHandler({ url: remoteUrl })
const mounted = await mount(handler, vhdPathInRemote, mountPoint)
let disposePromise
process.on('SIGINT', async () => {
// ensure single dispose
if (!disposePromise) {
disposePromise = mounted.dispose()
}
await disposePromise
process.exit()
})
}
Disposable.wrap(main)(process.argv.slice(2))

View File

@@ -1,11 +1,8 @@
import LRU from 'lru-cache'
import Fuse from 'fuse-native'
import { createLogger } from '@xen-orchestra/log'
import { VhdSynthetic } from 'vhd-lib'
import { Disposable, fromCallback } from 'promise-toolbox'
const { warn } = createLogger('vates:fuse-vhd')
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
const stat = st => ({
mtime: st.mtime || new Date(),
@@ -55,16 +52,13 @@ export const mount = Disposable.factory(async function* mount(handler, diskPath,
},
read(path, fd, buf, len, pos, cb) {
if (path === '/vhd0') {
return vhd.readRawData(pos, len, cache, buf).then(cb, error => {
warn('read error', { path, len, pos, error })
cb(Fuse.EIO)
})
return vhd.readRawData(pos, len, cache, buf).then(cb)
}
throw new Error(`read file ${path} not exists`)
},
})
return new Disposable(
() => fromCallback(cb => fuse.unmount(cb)),
fromCallback(cb => fuse.mount(cb))
() => fromCallback(() => fuse.unmount()),
fromCallback(() => fuse.mount())
)
})

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "2.1.2",
"version": "2.0.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
@@ -19,15 +19,10 @@
},
"main": "./index.mjs",
"dependencies": {
"@xen-orchestra/fs": "^4.3.0",
"@xen-orchestra/log": "^0.7.1",
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.11.2"
},
"bin": {
"xo-fuse-vhd": "./cli.mjs"
"vhd-lib": "^4.6.1"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -1,18 +0,0 @@
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
The hash is encoded using [Base64 URL](https://en.wikipedia.org/wiki/Base64#URL_applications) to make it easy to use in URLs and filenames.
```js
import { jsonHash } from '@vates/json-hash'
console.log(jsonHash('foo'))
// → "siEyldVkkW-JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs"
// order of properties is ignored
console.log(jsonHash({ foo: 0, bar: 1 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
console.log(jsonHash({ bar: 1, foo: 0 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,51 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/json-hash
[![Package Version](https://badgen.net/npm/v/@vates/json-hash)](https://npmjs.org/package/@vates/json-hash) ![License](https://badgen.net/npm/license/@vates/json-hash) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/json-hash)](https://bundlephobia.com/result?p=@vates/json-hash) [![Node compatibility](https://badgen.net/npm/node/@vates/json-hash)](https://npmjs.org/package/@vates/json-hash)
> Compute a stable hash from a JSON-ifiable value
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/json-hash):
```sh
npm install --save @vates/json-hash
```
## Usage
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
The hash is encoded using [Base64 URL](https://en.wikipedia.org/wiki/Base64#URL_applications) to make it easy to use in URLs and filenames.
```js
import { jsonHash } from '@vates/json-hash'
console.log(jsonHash('foo'))
// → "siEyldVkkW-JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs"
// order of properties is ignored
console.log(jsonHash({ foo: 0, bar: 1 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
console.log(jsonHash({ bar: 1, foo: 0 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,38 +0,0 @@
'use strict'
const { createHash } = require('node:crypto')
function updateJsonHash(value, hash) {
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
hash.update('[')
for (const item of value) {
updateJsonHash(item, hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update(']')
} else {
hash.update('{')
for (const key of Object.keys(value).sort()) {
updateJsonHash(key, hash)
hash.update(':')
updateJsonHash(value[key], hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update('}')
}
} else {
hash.update(JSON.stringify(value))
}
}
exports.jsonHash = function jsonHash(value) {
// this hash does not need to be secure, it just needs to be fast and with low collisions
const hash = createHash('sha256')
updateJsonHash(value, hash)
return hash.digest('base64url')
}

View File

@@ -1,21 +0,0 @@
'use strict'
const { jsonHash } = require('@vates/json-hash')
const assert = require('node:assert/strict')
const test = require('node:test')
const cases = [
[null, 'dCNOmK_nSY-12vHzasLXiswzlGT5UHA7jAGYkvmCuQs'],
[42, 'c0dctApWjo2ooEXO0RATfhWfiQrE2og7axfcZRs6gEk'],
['foo', 'siEyldVkkW-JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs'],
[[], 'T1PNoYwrqgwDVLtfmj7L5e0Sq02OEbqHPC8RFhICuUU'],
[{}, 'RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o'],
[{ a: 1, b: 2 }, 'LMjVxyj2vtMZ3W1cZ2cJrQij5zn7FBOlPiyBB38OmFE'],
[{ b: 2, a: 1 }, 'LMjVxyj2vtMZ3W1cZ2cJrQij5zn7FBOlPiyBB38OmFE'],
]
for (const [value, hash] of cases) {
test(JSON.stringify(value), function () {
assert.equal(jsonHash(value), hash)
})
}

View File

@@ -1,30 +0,0 @@
{
"private": false,
"name": "@vates/json-hash",
"description": "Compute a stable hash from a JSON-ifiable value",
"keywords": [
"hash",
"json",
"stable"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/json-hash",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/json-hash",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.2.0",
"engines": {
"node": ">=14.18.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
}
}

View File

@@ -17,14 +17,4 @@ map.get(['foo', 'bar']) // 2
map.get(['bar', 'foo']) // 3
map.get([OBJ]) // 4
map.get([{}]) // undefined
map.delete([])
for (const [key, value] of map.entries() {
console.log(key, value)
}
for (const value of map.values()) {
console.log(value)
}
```

View File

@@ -35,16 +35,6 @@ map.get(['foo', 'bar']) // 2
map.get(['bar', 'foo']) // 3
map.get([OBJ]) // 4
map.get([{}]) // undefined
map.delete([])
for (const [key, value] of map.entries() {
console.log(key, value)
}
for (const value of map.values()) {
console.log(value)
}
```
## Contributions

View File

@@ -36,31 +36,14 @@ function del(node, i, keys) {
return node
}
function* entries(node, key) {
if (node !== undefined) {
if (node instanceof Node) {
const { value } = node
if (value !== undefined) {
yield [key, node.value]
}
for (const [childKey, child] of node.children.entries()) {
yield* entries(child, key.concat(childKey))
}
} else {
yield [key, node]
}
}
}
function get(node, i, keys) {
return i === keys.length
? node instanceof Node
? node.value
: node
: node instanceof Node
? get(node.children.get(keys[i]), i + 1, keys)
: undefined
? get(node.children.get(keys[i]), i + 1, keys)
: undefined
}
function set(node, i, keys, value) {
@@ -86,22 +69,6 @@ function set(node, i, keys, value) {
return node
}
function* values(node) {
if (node !== undefined) {
if (node instanceof Node) {
const { value } = node
if (value !== undefined) {
yield node.value
}
for (const child of node.children.values()) {
yield* values(child)
}
} else {
yield node
}
}
}
exports.MultiKeyMap = class MultiKeyMap {
constructor() {
// each node is either a value or a Node if it contains children
@@ -112,10 +79,6 @@ exports.MultiKeyMap = class MultiKeyMap {
this._root = del(this._root, 0, keys)
}
entries() {
return entries(this._root, [])
}
get(keys) {
return get(this._root, 0, keys)
}
@@ -123,8 +86,4 @@ exports.MultiKeyMap = class MultiKeyMap {
set(keys, value) {
this._root = set(this._root, 0, keys, value)
}
values() {
return values(this._root)
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const assert = require('node:assert')
const { MultiKeyMap } = require('./')
@@ -19,7 +19,7 @@ describe('MultiKeyMap', () => {
// reverse composite key
['bar', 'foo'],
]
const values = keys.map(() => Math.random())
const values = keys.map(() => ({}))
// set all values first to make sure they are all stored and not only the
// last one
@@ -27,12 +27,6 @@ describe('MultiKeyMap', () => {
map.set(key, values[i])
})
assert.deepEqual(
Array.from(map.entries()),
keys.map((key, i) => [key, values[i]])
)
assert.deepEqual(Array.from(map.values()), values)
keys.forEach((key, i) => {
// copy the key to make sure the array itself is not the key
assert.strictEqual(map.get(key.slice()), values[i])

View File

@@ -18,12 +18,15 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.2.0",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
}
}

View File

@@ -1,9 +1,10 @@
import assert from 'node:assert'
import { Socket } from 'node:net'
import { connect } from 'node:tls'
import { fromCallback, pRetry, pDelay, pTimeout, pFromCallback } from 'promise-toolbox'
import { fromCallback, pRetry, pDelay, pTimeout } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import { createLogger } from '@xen-orchestra/log'
import {
INIT_PASSWD,
NBD_CMD_READ,
@@ -20,6 +21,7 @@ import {
OPTS_MAGIC,
NBD_CMD_DISC,
} from './constants.mjs'
const { warn } = createLogger('vates:nbd-client')
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
@@ -34,10 +36,10 @@ export default class NbdClient {
#exportSize
#waitBeforeReconnect
#readAhead
#readBlockRetries
#reconnectRetry
#connectTimeout
#messageTimeout
// AFAIK, there is no guaranty the server answers in the same order as the queries
// so we handle a backlog of command waiting for response and handle concurrency manually
@@ -50,23 +52,17 @@ export default class NbdClient {
#reconnectingPromise
constructor(
{ address, port = NBD_DEFAULT_PORT, exportname, cert },
{
connectTimeout = 6e4,
messageTimeout = 6e4,
waitBeforeReconnect = 1e3,
readBlockRetries = 5,
reconnectRetry = 5,
} = {}
{ connectTimeout = 6e4, waitBeforeReconnect = 1e3, readAhead = 10, readBlockRetries = 5, reconnectRetry = 5 } = {}
) {
this.#serverAddress = address
this.#serverPort = port
this.#exportName = exportname
this.#serverCert = cert
this.#waitBeforeReconnect = waitBeforeReconnect
this.#readAhead = readAhead
this.#readBlockRetries = readBlockRetries
this.#reconnectRetry = reconnectRetry
this.#connectTimeout = connectTimeout
this.#messageTimeout = messageTimeout
}
get exportSize() {
@@ -119,27 +115,13 @@ export default class NbdClient {
if (!this.#connected) {
return
}
this.#connected = false
const socket = this.#serverSocket
const queryId = this.#nextCommandQueryId
this.#nextCommandQueryId++
const buffer = Buffer.alloc(28)
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
buffer.writeBigUInt64BE(queryId, 8)
buffer.writeBigUInt64BE(0n, 16)
buffer.writeInt32BE(0, 24)
const promise = pFromCallback(cb => {
socket.end(buffer, 'utf8', cb)
})
try {
await pTimeout.call(promise, this.#messageTimeout)
} catch (error) {
socket.destroy()
}
await this.#write(buffer)
await this.#serverSocket.destroy()
this.#serverSocket = undefined
this.#connected = false
}
@@ -213,13 +195,11 @@ export default class NbdClient {
}
#read(length) {
const promise = readChunkStrict(this.#serverSocket, length)
return pTimeout.call(promise, this.#messageTimeout)
return readChunkStrict(this.#serverSocket, length)
}
#write(buffer) {
const promise = fromCallback.call(this.#serverSocket, 'write', buffer)
return pTimeout.call(promise, this.#messageTimeout)
return fromCallback.call(this.#serverSocket, 'write', buffer)
}
async #readInt32() {
@@ -252,20 +232,19 @@ export default class NbdClient {
}
try {
this.#waitingForResponse = true
const buffer = await this.#read(16)
const magic = buffer.readInt32BE(0)
const magic = await this.#readInt32()
if (magic !== NBD_REPLY_MAGIC) {
throw new Error(`magic number for block answer is wrong : ${magic} ${NBD_REPLY_MAGIC}`)
}
const error = buffer.readInt32BE(4)
const error = await this.#readInt32()
if (error !== 0) {
// @todo use error code from constants.mjs
throw new Error(`GOT ERROR CODE : ${error}`)
}
const blockQueryId = buffer.readBigUInt64BE(8)
const blockQueryId = await this.#readInt64()
const query = this.#commandQueryBacklog.get(blockQueryId)
if (!query) {
throw new Error(` no query associated with id ${blockQueryId}`)
@@ -286,7 +265,7 @@ export default class NbdClient {
}
}
async #readBlock(index, size) {
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
// we don't want to add anything in backlog while reconnecting
if (this.#reconnectingPromise) {
await this.#reconnectingPromise
@@ -302,13 +281,7 @@ export default class NbdClient {
buffer.writeInt16BE(NBD_CMD_READ, 6) // we want to read a data block
buffer.writeBigUInt64BE(queryId, 8)
// byte offset in the raw disk
const offset = BigInt(index) * BigInt(size)
const remaining = this.#exportSize - offset
if (remaining < BigInt(size)) {
size = Number(remaining)
}
buffer.writeBigUInt64BE(offset, 16)
buffer.writeBigUInt64BE(BigInt(index) * BigInt(size), 16)
buffer.writeInt32BE(size, 24)
return new Promise((resolve, reject) => {
@@ -334,13 +307,45 @@ export default class NbdClient {
})
}
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
return pRetry(() => this.#readBlock(index, size), {
tries: this.#readBlockRetries,
onRetry: async err => {
warn('will retry reading block ', index, err)
await this.reconnect()
},
})
async *readBlocks(indexGenerator) {
// default : read all blocks
if (indexGenerator === undefined) {
const exportSize = this.#exportSize
const chunkSize = 2 * 1024 * 1024
indexGenerator = function* () {
const nbBlocks = Math.ceil(Number(exportSize / BigInt(chunkSize)))
for (let index = 0; BigInt(index) < nbBlocks; index++) {
yield { index, size: chunkSize }
}
}
}
const readAhead = []
const readAheadMaxLength = this.#readAhead
const makeReadBlockPromise = (index, size) => {
const promise = pRetry(() => this.readBlock(index, size), {
tries: this.#readBlockRetries,
onRetry: async err => {
warn('will retry reading block ', index, err)
await this.reconnect()
},
})
// error is handled during unshift
promise.catch(() => {})
return promise
}
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
for (const { index, size } of indexGenerator()) {
// stack readAheadMaxLength promises before starting to handle the results
if (readAhead.length === readAheadMaxLength) {
// any error will stop reading blocks
yield readAhead.shift()
}
readAhead.push(makeReadBlockPromise(index, size))
}
while (readAhead.length > 0) {
yield readAhead.shift()
}
}
}

View File

@@ -1,115 +0,0 @@
import { asyncEach } from '@vates/async-each'
import { NBD_DEFAULT_BLOCK_SIZE } from './constants.mjs'
import NbdClient from './index.mjs'
import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('vates:nbd-client:multi')
export default class MultiNbdClient {
#clients = []
#nbdConcurrency
#options
#readAhead
#settings
get exportSize() {
return this.#clients[0].exportSize
}
constructor(settings, { nbdConcurrency = 8, readAhead = 16, ...options } = {}) {
this.#readAhead = readAhead
this.#options = options
this.#nbdConcurrency = nbdConcurrency
if (!Array.isArray(settings)) {
settings = [settings]
}
this.#settings = settings
}
/**
*
* open nbdConcurrency connections to NBD servers
* it must obtain at least one connection to succeed
* it tries to spread connections on multiple host
*/
async connect() {
const candidates = [...this.#settings]
const baseOptions = this.#options
const _connect = async () => {
if (candidates.length === 0) {
return
}
// a little bit of randomization to spread the load
const nbdInfo = candidates[Math.floor(Math.random() * candidates.length)]
const client = new NbdClient(nbdInfo, {
...baseOptions,
readAhead: Math.ceil(this.#readAhead / this.#nbdConcurrency),
})
try {
await client.connect()
this.#clients.push(client)
} catch (err) {
client.disconnect().catch(() => {})
// do not hammer unreachable hosts, once failed, remove from the list
const candidateIndex = candidates.findIndex(({ address }) => address === nbdInfo.address)
if (candidateIndex >= 0) {
// this candidate may have already been deleted by another parallel promise
candidates.splice(candidateIndex, 1)
}
warn(`can't connect to one nbd client`, { err })
// retry with another candidate (if available)
return _connect()
}
}
// don't connect in parallel since this can lead to race condition
// on distributed systems ( like the NBD server of the XAPI)
for (let i = 0; i < this.#nbdConcurrency; i++) {
await _connect()
}
if (this.#clients.length === 0) {
throw new Error(`Fail to connect to any Nbd client`)
}
if (this.#clients.length < this.#nbdConcurrency) {
warn(
`incomplete connection by multi Nbd, only ${this.#clients.length} over ${this.#nbdConcurrency} expected clients`
)
}
}
async disconnect() {
await asyncEach(this.#clients, client => client.disconnect(), {
stopOnError: false,
})
}
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
const clientId = index % this.#clients.length
return this.#clients[clientId].readBlock(index, size)
}
async *readBlocks(indexGenerator) {
// default : read all blocks
const readAhead = []
const makeReadBlockPromise = (index, size) => {
const promise = this.readBlock(index, size)
// error is handled during unshift
promise.catch(() => {})
return promise
}
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
for (const { index, size } of indexGenerator()) {
// stack readAheadMaxLength promises before starting to handle the results
if (readAhead.length === this.#readAhead) {
// any error will stop reading blocks
yield readAhead.shift()
}
readAhead.push(makeReadBlockPromise(index, size))
}
while (readAhead.length > 0) {
yield readAhead.shift()
}
}
}

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "3.1.2",
"version": "2.0.0",
"engines": {
"node": ">=14.0"
},
@@ -22,16 +22,16 @@
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^4.6.0"
"xen-api": "^1.3.6"
},
"devDependencies": {
"tap": "^18.7.0",
"tap": "^16.3.0",
"tmp": "^0.2.1"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap --allow-incomplete-coverage"
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.mjs"
}
}

View File

@@ -1,3 +1,4 @@
import NbdClient from '../index.mjs'
import { spawn, exec } from 'node:child_process'
import fs from 'node:fs/promises'
import { test } from 'tap'
@@ -6,10 +7,8 @@ import { pFromCallback } from 'promise-toolbox'
import { Socket } from 'node:net'
import { NBD_DEFAULT_PORT } from '../constants.mjs'
import assert from 'node:assert'
import MultiNbdClient from '../multi.mjs'
const CHUNK_SIZE = 1024 * 1024 // non default size
const FILE_SIZE = 1024 * 1024 * 9.5 // non aligned file size
const FILE_SIZE = 10 * 1024 * 1024
async function createTempFile(size) {
const tmpPath = await pFromCallback(cb => tmp.file(cb))
@@ -78,14 +77,15 @@ async function killNbdKit() {
)
}
test('it works secured network', async tap => {
test('it works with unsecured network', async tap => {
const path = await createTempFile(FILE_SIZE)
let nbdServer = await spawnNbdKit(path)
const connectionSettings = {
address: '127.0.0.1',
exportname: 'MY_SECRET_EXPORT',
cert: `-----BEGIN CERTIFICATE-----
const client = new NbdClient(
{
address: '127.0.0.1',
exportname: 'MY_SECRET_EXPORT',
cert: `-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUeHpQ0IeD6BmP2zgsv3LV3J4BI/EwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MTcxMzU1MzBaFw0yNDA1
@@ -107,21 +107,15 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
57ABhfYpMlZ4aHjuN1bL
-----END CERTIFICATE-----
`,
}
const invalid = {
address: '500.500.500.500',
port: 0,
exportname: 'nop',
}
// it should work even with some broken servers
const nbdInfos = [connectionSettings, connectionSettings, invalid]
const client = new MultiNbdClient(nbdInfos, {
nbdConcurrency: 4,
readAhead: 2,
})
},
{
readAhead: 2,
}
)
await client.connect()
tap.equal(client.exportSize, BigInt(FILE_SIZE))
const CHUNK_SIZE = 1024 * 1024 // non default size
const indexes = []
for (let i = 0; i < FILE_SIZE / CHUNK_SIZE; i++) {
indexes.push(i)
@@ -133,9 +127,9 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
})
let i = 0
for await (const block of nbdIterator) {
let blockOk = block.length === Math.min(CHUNK_SIZE, FILE_SIZE - CHUNK_SIZE * i)
let blockOk = true
let firstFail
for (let j = 0; j < block.length; j += 4) {
for (let j = 0; j < CHUNK_SIZE; j += 4) {
const wanted = i * CHUNK_SIZE + j
const found = block.readUInt32BE(j)
blockOk = blockOk && found === wanted
@@ -143,7 +137,7 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
firstFail = j
}
}
tap.ok(blockOk, `check block ${i} content ${block.length}`)
tap.ok(blockOk, `check block ${i} content`)
i++
// flaky server is flaky
@@ -153,6 +147,17 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
nbdServer = await spawnNbdKit(path)
}
}
// we can reuse the conneciton to read other blocks
// default iterator
const nbdIteratorWithDefaultBlockIterator = client.readBlocks()
let nb = 0
for await (const block of nbdIteratorWithDefaultBlockIterator) {
nb++
tap.equal(block.length, 2 * 1024 * 1024)
}
tap.equal(nb, 5)
assert.rejects(() => client.readBlock(100, CHUNK_SIZE))
await client.disconnect()
@@ -161,18 +166,3 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
nbdServer.kill()
await fs.unlink(path)
})
test('fails if server does not answer', async () => {
const client = new MultiNbdClient(
{
address: '500.500.500.500',
port: 0,
exportname: 'nop',
},
{
nbdConcurrency: 1,
readAhead: 2,
}
)
await assert.rejects(client.connect())
})

View File

@@ -17,20 +17,7 @@ import https from 'node:https'
import util from 'util'
import soap from 'soap'
import Cookie from 'soap-cookie' // required for session persistence
import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('xo:node-vsphere-soap:client')
class VmwareError extends Error {
constructor(rawError) {
super(rawError.body)
const matches = rawError.body?.match(/<faultstring[^>]*>(.*)<\/faultstring>/im)
this.message = matches?.[1] ?? rawError.message
this.name = 'VmwareError'
this.stack = rawError.stack
// not putting the cause since the error failed to be stringified correctly down the road
warn(rawError)
}
}
// Client class
// inherits from EventEmitter
// possible events: connect, error, ready
@@ -44,20 +31,14 @@ export function Client(vCenterHostname, username, password, sslVerify) {
EventEmitter.call(this)
// sslVerify argument handling
// don't use any proxy to connect to the esxi
if (sslVerify) {
this.clientopts = {
request: axios.create({
proxy: false,
}),
}
this.clientopts = {}
} else {
this.clientopts = {
request: axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
proxy: false,
}),
}
}
@@ -153,9 +134,8 @@ Client.prototype._connect = function () {
self.clientopts,
function (err, client) {
if (err) {
const vErr = new VmwareError(err)
self.emit('error', vErr)
throw vErr
self.emit('error', err)
throw err
}
self.client = client // save client for later use
@@ -164,7 +144,6 @@ Client.prototype._connect = function () {
.runCommand('RetrieveServiceContent', { _this: 'ServiceInstance' })
.once('result', function (result, raw, soapHeader) {
if (!result.returnval) {
// this is a domain/server error return it as is
self.status = 'disconnected'
self.emit('error', raw)
return
@@ -223,7 +202,7 @@ Client.prototype._close = function () {
function _soapErrorHandler(self, emitter, command, args, err) {
err = err || { body: 'general error' }
if (err.body?.match(/session is not authenticated/)) {
if (err.body.match(/session is not authenticated/)) {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
@@ -235,19 +214,16 @@ function _soapErrorHandler(self, emitter, command, args, err) {
emitter.emit('result', result, raw, soapHeader)
})
.once('error', function (err) {
const vErr = new VmwareError(err)
emitter.emit('error', vErr)
throw vErr
emitter.emit('error', err.body)
throw err
})
} else {
const vErr = new VmwareError(err)
emitter.emit('error', vErr)
throw vErr
emitter.emit('error', err.body)
throw err
}
} else {
const vErr = new VmwareError(err)
emitter.emit('error', vErr)
throw vErr
emitter.emit('error', err.body)
throw err
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/node-vsphere-soap",
"version": "2.1.1",
"version": "2.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"author": "reedog117",
@@ -11,11 +11,13 @@
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/log": "^0.7.1",
"axios": "^1.4.0",
"soap": "^1.0.0",
"soap-cookie": "^0.10.1"
},
"devDependencies": {
"test": "^3.3.0"
},
"keywords": [
"vsphere",
"vcenter",

View File

@@ -5,7 +5,7 @@
*/
import assert from 'assert'
import { describe, it } from 'node:test'
import { describe, it } from 'test'
import * as vc from '../lib/client.mjs'

View File

@@ -1,46 +0,0 @@
### `replace(value, replacement)`
> Recursively replace sensitive entries with a replacement value.
```js
import { replace } from '@vates/obfuscate'
replace({ username: 'foo', password: 'bar' }, 'obfuscated')
// → { username: 'foo', password: 'obfuscated'}
```
### `obfuscate(value)`
> Recursively replace sensitive entries with a special tag.
```js
import { obfuscate } from '@vates/obfuscate'
obfuscate({ username: 'foo', password: 'bar' })
// → {
// username: 'foo',
// password: 'obfuscated-q3oi6d9X8uenGvdLnHk2',
// }
```
### `merge(newValue, oldValue)`
> Replace entries obfuscated by `obfuscate()` in the new value by entries from the old one.
>
> This is especially useful to give an obfuscated value to a client, let them modify it, and then merge modified and unmodified values.
```js
import { obfuscate, merge } from '@vates/obfuscate'
const original = { username: 'foo', password: 'bar', token: 'baz' }
// compute an obfuscated value to send to a client
const obfuscated = obfuscate(original)
// let the client update any entries
obfuscated.token = 'qux'
// merge the updated value with the original one
merge(obfuscated, original)
// → { username: 'foo', password: 'bar', token: 'qux' }
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,77 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/obfuscate
[![Package Version](https://badgen.net/npm/v/@vates/obfuscate)](https://npmjs.org/package/@vates/obfuscate) ![License](https://badgen.net/npm/license/@vates/obfuscate) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/obfuscate)](https://bundlephobia.com/result?p=@vates/obfuscate) [![Node compatibility](https://badgen.net/npm/node/@vates/obfuscate)](https://npmjs.org/package/@vates/obfuscate)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/obfuscate):
```sh
npm install --save @vates/obfuscate
```
## Usage
### `replace(value, replacement)`
> Recursively replace sensitive entries with a replacement value.
```js
import { replace } from '@vates/obfuscate'
replace({ username: 'foo', password: 'bar' }, 'obfuscated')
// → { username: 'foo', password: 'obfuscated'}
```
### `obfuscate(value)`
> Recursively replace sensitive entries with a special tag.
```js
import { obfuscate } from '@vates/obfuscate'
obfuscate({ username: 'foo', password: 'bar' })
// → {
// username: 'foo',
// password: 'obfuscated-q3oi6d9X8uenGvdLnHk2',
// }
```
### `merge(newValue, oldValue)`
> Replace entries obfuscated by `obfuscate()` in the new value by entries from the old one.
>
> This is especially useful to give an obfuscated value to a client, let them modify it, and then merge modified and unmodified values.
```js
import { obfuscate, merge } from '@vates/obfuscate'
const original = { username: 'foo', password: 'bar', token: 'baz' }
// compute an obfuscated value to send to a client
const obfuscated = obfuscate(original)
// let the client update any entries
obfuscated.token = 'qux'
// merge the updated value with the original one
merge(obfuscated, original)
// → { username: 'foo', password: 'bar', token: 'qux' }
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,23 +0,0 @@
{
"private": false,
"name": "@vates/obfuscate",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/obfuscate",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/obfuscate",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -9,15 +9,6 @@ function assert(name, value) {
}
}
const WHITESPACES = /\s+/g
function cleanToken(otp) {
// remove whitespaces
//
// some OTP clients add whitespaces for better readability (e.g. `XXX XXX`)
// and users might be tempted to write them
return otp && otp.replace(WHITESPACES, '')
}
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
function generateUri(protocol, label, params) {
assert('label', typeof label === 'string')
@@ -73,7 +64,7 @@ export function generateHotpUri({ counter, digits, issuer, label, secret }) {
}
export async function verifyHotp(token, opts) {
return cleanToken(token) === (await generateHotp(opts))
return token === (await generateHotp(opts))
}
function totpCounter(period = 30, timestamp = Math.floor(Date.now() / 1e3)) {
@@ -94,7 +85,6 @@ export async function verifyTotp(token, { period, timestamp, window = 1, ...opts
const counter = totpCounter(period, timestamp)
const end = counter + window
opts.counter = counter - window
token = cleanToken(token)
while (opts.counter <= end) {
if (token === (await generateHotp(opts))) {
return true

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'node:assert'
import test from 'node:test'
import { describe, it } from 'tap/mocha'
import {
generateHotp,
@@ -11,8 +11,6 @@ import {
verifyTotp,
} from './index.mjs'
const { describe, it } = test
describe('generateSecret', function () {
it('generates a string of 32 chars', async function () {
const secret = generateSecret()

View File

@@ -22,7 +22,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.1.0",
"version": "1.0.0",
"engines": {
"node": ">=15"
},
@@ -31,6 +31,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "tap"
},
"devDependencies": {
"tap": "^16.3.0"
}
}

View File

@@ -6,11 +6,9 @@ exports.parseDuration = value => {
if (typeof value === 'number') {
return value
}
if (typeof value === 'string' && value !== '') {
const duration = ms(value)
if (duration !== undefined) {
return duration
}
const duration = ms(value)
if (duration === undefined) {
throw new TypeError(`not a valid duration: ${value}`)
}
throw new TypeError(`not a valid duration: ${value}`)
return duration
}

View File

@@ -1,22 +0,0 @@
import { describe, it } from 'node:test'
import { parseDuration } from '@vates/parse-duration'
import assert from 'node:assert/strict'
describe('parseDuration()', () => {
it('should parse string', () => {
const input = '2 days'
const expected = 172800000
assert.strictEqual(parseDuration(input), expected)
})
it('should return its input if already a number', () => {
const input = 172800000
assert.strictEqual(parseDuration(input), input)
})
for (const input of [undefined, '', 'invalid duration']) {
it('should throw an error for ' + input, () => {
assert.throws(() => parseDuration(input), { message: `not a valid duration: ${input}` })
})
}
})

View File

@@ -27,7 +27,6 @@
"ms": "^2.1.2"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"postversion": "npm publish --access public"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert/strict')
const { describe, it } = require('node:test')
const { describe, it } = require('tap').mocha
const { every, not, some } = require('./')

View File

@@ -32,6 +32,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "tap"
},
"devDependencies": {
"tap": "^16.0.1"
}
}

View File

@@ -22,41 +22,41 @@ const readChunk = (stream, size) =>
stream.errored != null
? Promise.reject(stream.errored)
: stream.closed || stream.readableEnded
? Promise.resolve(null)
: new Promise((resolve, reject) => {
if (size !== undefined) {
assert(size > 0)
? Promise.resolve(null)
: new Promise((resolve, reject) => {
if (size !== undefined) {
assert(size > 0)
// per Node documentation:
// > The size argument must be less than or equal to 1 GiB.
assert(size < 1073741824)
}
// per Node documentation:
// > The size argument must be less than or equal to 1 GiB.
assert(size < 1073741824)
}
function onEnd() {
resolve(null)
function onEnd() {
resolve(null)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read(size)
if (data !== null) {
resolve(data)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read(size)
if (data !== null) {
resolve(data)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
exports.readChunk = readChunk
/**
@@ -111,42 +111,42 @@ async function skip(stream, size) {
return stream.errored != null
? Promise.reject(stream.errored)
: size === 0 || stream.closed || stream.readableEnded
? Promise.resolve(0)
: new Promise((resolve, reject) => {
let left = size
function onEnd() {
resolve(size - left)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read()
left -= data === null ? 0 : data.length
if (left > 0) {
// continue to read
} else {
// if more than wanted has been read, push back the rest
if (left < 0) {
stream.unshift(data.slice(left))
}
resolve(size)
removeListeners()
? Promise.resolve(0)
: new Promise((resolve, reject) => {
let left = size
function onEnd() {
resolve(size - left)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read()
left -= data === null ? 0 : data.length
if (left > 0) {
// continue to read
} else {
// if more than wanted has been read, push back the rest
if (left < 0) {
stream.unshift(data.slice(left))
}
resolve(size)
removeListeners()
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
}
exports.skip = skip

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const assert = require('node:assert').strict
const { Readable } = require('stream')

View File

@@ -25,12 +25,15 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"devDependencies": {
"test": "^3.2.1"
},
"dependencies": {
"isutf8": "^4.0.0"
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const assert = require('node:assert').strict
const { Readable } = require('stream')

View File

@@ -31,6 +31,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"devDependencies": {
"test": "^3.3.0"
}
}

View File

@@ -49,15 +49,6 @@ const result = await task.runInside(fn)
// if fn rejects, the task will be marked as failed
// if fn resolves, the task will be marked as succeeded
const result = await task.run(fn)
// manually starts the task
task.start()
// manually finishes the task in success state
task.success(result)
// manually finishes the task in failure state
task.failure(error)
```
Inside a task:
@@ -81,27 +72,6 @@ Task.warning(message, data)
Task.set(property, value)
```
### `Task.run([opts], fn, ...args)`
This is a convenient shortcut to `new Task(opts).run(() => fn(...args))`.
```js
// options can be passed
await Task.run({ properties: { name: 'foo' } }, fn)
// arguments can be passed
await Task.run(fn, 'foo', 'bar')
// context (this) is forwarded
await Task.run.call(object, object.method)
// as a convenience, you can pass a method name directly
await Task.run.call(object, 'method')
// everything together
await Task.run.call(object, 'method', 'foo', 'bar')
```
### `combineEvents`
Create a consolidated log from individual events.
@@ -153,7 +123,7 @@ const onProgress = makeOnProgress({
onTaskUpdate(taskLog) {},
})
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:

View File

@@ -65,15 +65,6 @@ const result = await task.runInside(fn)
// if fn rejects, the task will be marked as failed
// if fn resolves, the task will be marked as succeeded
const result = await task.run(fn)
// manually starts the task
task.start()
// manually finishes the task in success state
task.success(result)
// manually finishes the task in failure state
task.failure(error)
```
Inside a task:
@@ -97,27 +88,6 @@ Task.warning(message, data)
Task.set(property, value)
```
### `Task.run([opts], fn, ...args)`
This is a convenient shortcut to `new Task(opts).run(() => fn(...args))`.
```js
// options can be passed
await Task.run({ properties: { name: 'foo' } }, fn)
// arguments can be passed
await Task.run(fn, 'foo', 'bar')
// context (this) is forwarded
await Task.run.call(object, object.method)
// as a convenience, you can pass a method name directly
await Task.run.call(object, 'method')
// everything together
await Task.run.call(object, 'method', 'foo', 'bar')
```
### `combineEvents`
Create a consolidated log from individual events.
@@ -141,7 +111,7 @@ const onProgress = makeOnProgress({
// current status of the task as described in the previous section
taskLog.status
// undefined or a dictionary of properties attached to the task
// undefined or a dictionnary of properties attached to the task
taskLog.properties
// timestamp at which the abortion was requested, undefined otherwise
@@ -169,7 +139,7 @@ const onProgress = makeOnProgress({
onTaskUpdate(taskLog) {},
})
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:

View File

@@ -44,8 +44,6 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
} else if (type === 'property') {
;(taskLog.properties ?? (taskLog.properties = { __proto__: null }))[event.name] = event.value
} else if (type === 'end') {
taskLogs.delete(id)
taskLog.end = event.timestamp
taskLog.result = event.result
taskLog.status = event.status

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('node:assert').strict
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const { makeOnProgress } = require('./combineEvents.js')
const { Task } = require('./index.js')

View File

@@ -3,10 +3,6 @@
const assert = require('node:assert').strict
const { AsyncLocalStorage } = require('node:async_hooks')
function alreadyEnded() {
throw new Error('task has already ended')
}
// define a read-only, non-enumerable, non-configurable property
function define(object, property, value) {
Object.defineProperty(object, property, { value })
@@ -25,7 +21,7 @@ const asyncStorage = global[asyncStorageKey] ?? (global[asyncStorageKey] = new A
const getTask = () => asyncStorage.getStore()
class Task {
exports.Task = class Task {
static get abortSignal() {
const task = getTask()
if (task !== undefined) {
@@ -33,25 +29,29 @@ class Task {
}
}
static run(...args) {
let opts = args[0]
let fn
if (typeof opts === 'object') {
args.shift()
fn = args[0]
} else {
fn = opts
opts = undefined
static info(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('info', { data, message })
}
args.shift()
}
const thisArg = this !== Task ? this : undefined
static run(opts, fn) {
return new this(opts).run(fn)
}
if (typeof fn !== 'function') {
fn = this[fn]
static set(name, value) {
const task = getTask()
if (task !== undefined) {
task.#emit('property', { name, value })
}
}
return new Task(opts).run(() => fn.apply(thisArg, args))
static warning(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('warning', { data, message })
}
}
static wrap(opts, fn) {
@@ -110,7 +110,9 @@ class Task {
this.#emit('abortionRequested', { reason: signal.reason })
if (!this.#running) {
this.#end(FAILURE, signal.reason)
const status = FAILURE
this.#status = status
this.#emit('end', { result: signal.reason, status })
}
}
})
@@ -121,41 +123,25 @@ class Task {
}
#emit(type, data) {
assert.equal(this.#startData, undefined, 'task has not started yet')
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
#end(status, result) {
assert.equal(this.#status, PENDING)
assert.equal(this.#running, false)
this.#emit('end', { status, result })
this.#onProgress = alreadyEnded
this.#status = status
}
failure(error) {
this.#end(FAILURE, error)
}
info(message, data) {
this.#emit('info', { data, message })
}
#maybeStart() {
if (this.#startData !== undefined) {
this.start()
const startData = this.#startData
if (startData !== undefined) {
this.#startData = undefined
this.#emit('start', startData)
}
}
async run(fn) {
const result = await this.runInside(fn)
if (this.status === PENDING) {
this.#end(SUCCESS, result)
this.#status = SUCCESS
this.#emit('end', { status: SUCCESS, result })
}
return result
}
@@ -169,40 +155,18 @@ class Task {
this.#maybeStart()
try {
const result = await asyncStorage.run(this, fn, this)
const result = await asyncStorage.run(this, fn)
this.#running = false
return result
} catch (result) {
this.#running = false
this.#end(FAILURE, result)
const status = FAILURE
this.#status = status
this.#emit('end', { status, result })
throw result
}
}
set(name, value) {
assert.equal(this.status, PENDING)
this.#emit('property', { name, value })
}
start() {
const startData = this.#startData
assert.notEqual(startData, undefined, 'task has already started')
this.#startData = undefined
this.#emit('start', startData)
}
success(result) {
this.#end(SUCCESS, result)
}
warning(message, data) {
assert.equal(this.status, PENDING)
this.#emit('warning', { data, message })
}
wrap(fn) {
const task = this
return function taskRun() {
@@ -217,15 +181,3 @@ class Task {
}
}
}
exports.Task = Task
// setup static aliases
for (const name of ['info', 'set', 'warning']) {
const method = Task.prototype[name]
Task[name] = function () {
const task = getTask()
if (task !== undefined) {
return method.apply(task, arguments)
}
}
}

View File

@@ -1,9 +1,8 @@
'use strict'
const assert = require('node:assert').strict
const { describe, it } = require('node:test')
const { describe, it } = require('test')
const { makeOnProgress } = require('./combineEvents.js')
const { Task } = require('./index.js')
const noop = Function.prototype
@@ -145,43 +144,6 @@ describe('Task', function () {
})
})
describe('#failure()', function () {
const error = new Error()
it('throws if the task has not started yet', function () {
const task = createTask()
assert.throws(() => task.failure(error), { message: /^task has not started yet\b/ })
})
it('throws if the task is running', function () {
const task = createTask()
task.runInside(noop)
assert.throws(() => task.failure(error), { code: 'ERR_ASSERTION' })
})
it('throws if the task has already finished', function () {
const task = createTask()
task.start()
task.success()
assert.throws(() => task.failure(error), { code: 'ERR_ASSERTION' })
})
it('finishes the task and mark it as failed', function () {
const task = createTask()
task.start()
task.failure(error)
assertEvent(task, {
status: 'failure',
result: error,
type: 'end',
})
})
})
describe('.info()', function () {
it('does nothing when run outside a task', function () {
Task.info('foo')
@@ -200,44 +162,6 @@ describe('Task', function () {
})
})
describe('.run()', function () {
function fn(...args) {
Task.set('baz', 'qux')
return [this, ...args]
}
const obj = { fn }
it('accepts optional options', async function () {
assert.deepEqual(
await Task.run(
{
properties: { foo: 'bar' },
onProgress: makeOnProgress({
onRootTaskEnd(log) {
assert.deepEqual(log.properties, { __proto__: null, foo: 'bar', baz: 'qux' })
},
}),
},
fn
),
[undefined]
)
})
it('accepts optional arguments', async function () {
assert.deepEqual(await Task.run(fn, 'foo', 'bar'), [undefined, 'foo', 'bar'])
})
it('accepts optional context', async function () {
assert.deepEqual(await Task.run.call('foo', fn), ['foo'])
})
it('accepts a method name instead of a function', async function () {
assert.deepEqual(await Task.run.call(obj, 'fn'), [obj])
})
})
describe('.set()', function () {
it('does nothing when run outside a task', function () {
Task.set('progress', 10)
@@ -256,84 +180,6 @@ describe('Task', function () {
})
})
describe('#set()', function () {
const name = 'progress'
const value = 10
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task.set(name, value), { message: /^task has not started yet\b/ })
})
it(`emits an property message`, async function () {
const task = createTask()
await task.run(async () => {
await Task.run(() => {
task.set(name, value)
assertEvent(task, {
name,
type: 'property',
value,
})
})
})
})
})
describe('#start', function () {
it('starts the task', function () {
const task = createTask()
task.start()
assertEvent(task, { type: 'start' })
})
it('throws when the task has already started', function () {
const task = createTask()
task.start()
assert.throws(() => task.start(), { message: 'task has already started' })
})
})
describe('#success()', function () {
const result = {}
it('throws if the task has not started yet', function () {
const task = createTask()
assert.throws(() => task.success(result), { message: /^task has not started yet\b/ })
})
it('throws if the task is running', function () {
const task = createTask()
task.runInside(noop)
assert.throws(() => task.success(result), { code: 'ERR_ASSERTION' })
})
it('throws if the task has already finished', function () {
const task = createTask()
task.start()
task.success()
assert.throws(() => task.success(result), { code: 'ERR_ASSERTION' })
})
it('finishes the task and mark it as successful', function () {
const task = createTask()
task.start()
task.success(result)
assertEvent(task, {
status: 'success',
result,
type: 'end',
})
})
})
describe('.warning()', function () {
it('does nothing when run outside a task', function () {
Task.warning('foo')
@@ -352,30 +198,6 @@ describe('Task', function () {
})
})
for (const type of ['info', 'warning']) {
describe(`#${type}()`, function () {
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task[type]('foo'), { message: /^task has not started yet\b/ })
})
it(`emits an ${type} message`, async function () {
const task = createTask()
await task.run(async () => {
await Task.run(() => {
task[type]('foo')
assertEvent(task, {
data: undefined,
message: 'foo',
type,
})
})
})
})
})
}
describe('#id', function () {
it('can be set', function () {
const task = createTask()

View File

@@ -13,13 +13,16 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.6.0",
"version": "0.2.0",
"engines": {
"node": ">=14"
},
"devDependencies": {
"test": "^3.3.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
"test": "node--test"
},
"exports": {
".": "./index.js",

View File

@@ -1,129 +0,0 @@
### Formatting
```js
import { xmlRpcFormatter } from '@vates/xml-rpc/formatter'
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// → {
// name: 'methodCall',
// children: [
// {
// name: 'methodName',
// children: [ 'examples.getStateName' ]
// },
// {
// name: 'params',
// children: [
// {
// name: 'param',
// children: [ { name: 'value', children: [ '40' ] } ]
// }
// ]
// }
// ]
// }
// There is a dedicated method to format a JS value
xmlRpcFormatter.format_js(new Date(900684535000))
// → {
// name: 'dateTime.iso8601',
// children: [ '1998-07-17T14:08:55.000Z' ]
// }
```
Formatting XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { formatXml } from '@vates/xml/format'
// it can now be passed to the XML-RPC parser
const tree = xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// avoid any indentation or new lines as it will confuse many XML-RPC parsers.
const xml = formatXml(tree, { indent: 0 })
```
### Parsing
```js
import { xmlRpcParser } from '@vates/xml-rpc/parser'
xmlRpcParser.parse({
name: 'methodResponse',
children: [
{
name: 'params',
children: [
{
name: 'param',
children: [
{
name: 'value',
children: [
{
name: 'string',
children: ['South Dakota'],
},
],
},
],
},
],
},
],
})
// { params: [ 'South Dakota' ] }
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcParser.parse_array({
name: 'array',
children: [
{
name: 'data',
children: [
{
name: 'value',
children: [{ name: 'boolean', children: ['1'] }],
},
{
name: 'value',
children: [{ name: 'int', children: ['42'] }],
},
],
},
],
})
// [ true, 42 ]
```
Parsing XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { parseXml } from '@vates/xml/parse'
const xml = `
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
`
// do not normalize or trim whitespaces as it will break string values
const tree = parseXml(xml, { normalize: false, trim: false })
// it can now be passed to the XML-RPC parser
xmlRpcParser.parse(tree)
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,162 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/xml-rpc
[![Package Version](https://badgen.net/npm/v/@vates/xml-rpc)](https://npmjs.org/package/@vates/xml-rpc) ![License](https://badgen.net/npm/license/@vates/xml-rpc) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/xml-rpc)](https://bundlephobia.com/result?p=@vates/xml-rpc) [![Node compatibility](https://badgen.net/npm/node/@vates/xml-rpc)](https://npmjs.org/package/@vates/xml-rpc)
> Parse and format XML-RPC messages, do not handle transport or XML
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/xml-rpc):
```sh
npm install --save @vates/xml-rpc
```
## Usage
### Formatting
```js
import { xmlRpcFormatter } from '@vates/xml-rpc/formatter'
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// → {
// name: 'methodCall',
// children: [
// {
// name: 'methodName',
// children: [ 'examples.getStateName' ]
// },
// {
// name: 'params',
// children: [
// {
// name: 'param',
// children: [ { name: 'value', children: [ '40' ] } ]
// }
// ]
// }
// ]
// }
// There is a dedicated method to format a JS value
xmlRpcFormatter.format_js(new Date(900684535000))
// → {
// name: 'dateTime.iso8601',
// children: [ '1998-07-17T14:08:55.000Z' ]
// }
```
Formatting XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { formatXml } from '@vates/xml/format'
// it can now be passed to the XML-RPC parser
const tree = xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// avoid any indentation or new lines as it will confuse many XML-RPC parsers.
const xml = formatXml(tree, { indent: 0 })
```
### Parsing
```js
import { xmlRpcParser } from '@vates/xml-rpc/parser'
xmlRpcParser.parse({
name: 'methodResponse',
children: [
{
name: 'params',
children: [
{
name: 'param',
children: [
{
name: 'value',
children: [
{
name: 'string',
children: ['South Dakota'],
},
],
},
],
},
],
},
],
})
// { params: [ 'South Dakota' ] }
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcParser.parse_array({
name: 'array',
children: [
{
name: 'data',
children: [
{
name: 'value',
children: [{ name: 'boolean', children: ['1'] }],
},
{
name: 'value',
children: [{ name: 'int', children: ['42'] }],
},
],
},
],
})
// [ true, 42 ]
```
Parsing XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { parseXml } from '@vates/xml/parse'
const xml = `
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
`
// do not normalize or trim whitespaces as it will break string values
const tree = parseXml(xml, { normalize: false, trim: false })
// it can now be passed to the XML-RPC parser
xmlRpcParser.parse(tree)
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,104 +0,0 @@
'use strict'
function node(name, attributes, children) {
if (Array.isArray(attributes)) {
children = attributes
attributes = undefined
}
return { name, attributes, children }
}
class XmlRpcFormatter {
format_js(value) {
const type = typeof value
if (type === 'object') {
return value === null
? this.format_nil()
: value instanceof Date
? this['format_dateTime.iso8601'](value)
: Array.isArray(value)
? this.format_array(value)
: Buffer.isBuffer(value)
? this.format_base64(value)
: this.format_struct(value)
}
return this['format_' + type](value)
}
format_array(values) {
return node('array', [node('data', values.map(this.format_value, this))])
}
format_base64(buf) {
return node('base64', [buf.toString('base64')])
}
format_boolean(bool) {
return node('boolean', [bool ? '1' : '0'])
}
'format_dateTime.iso8601'(date) {
return node('dateTime.iso8601', [date.toISOString()])
}
format_double(str) {
return node('double', [String(str)])
}
format_fault(val) {
return node('fault', [this.format_value(val)])
}
format_i4(str) {
return node('i4', [String(str)])
}
format_int(str) {
return node('int', [String(str)])
}
format_member([name, value]) {
return node('member', [node('name', [name]), this.format_value(value)])
}
format_methodCall({ methodName, params }) {
return node('methodCall', [node('methodName', [methodName]), this.format_params(params)])
}
format_methodResponse({ fault, params }) {
return node('methodResponse', [fault !== undefined ? this.format_fault(fault) : this.format_params(params)])
}
format_nil() {
return node('nil')
}
format_number(num) {
return Number.isInteger(num) ? this.format_int(num) : this.format_double(num)
}
format_param(value) {
return node('param', [this.format_value(value)])
}
format_params(params) {
return node('params', params.map(this.format_param, this))
}
format_string(str) {
return str
}
format_struct(struct) {
return node('struct', Object.entries(struct).map(this.format_member, this))
}
format_value(value) {
return node('value', [this.format_js(value)])
}
}
exports.Formatter = XmlRpcFormatter
exports.xmlRpcFormatter = new XmlRpcFormatter()

View File

@@ -1,32 +0,0 @@
{
"private": false,
"name": "@vates/xml-rpc",
"description": "Parse and format XML-RPC messages, do not handle transport or XML",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/xml-rpc",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/xml-rpc",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.0.0",
"engines": {
"node": ">=12"
},
"devDependencies": {
"@vates/xml": "^2.0.0"
},
"exports": {
"./formatter": "./formatter.js",
"./parser": "./parser.js"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
}
}

View File

@@ -1,133 +0,0 @@
'use strict'
function toValue(node) {
const obj = { __proto__: null }
for (const child of node.children) {
if (typeof child === 'string') {
return child
}
const { name } = child
if (Object.hasOwn(obj, name)) {
const prev = obj[name]
if (Array.isArray(obj, name)) {
prev.push(child)
} else {
obj[name] = [prev, child]
}
} else {
obj[name] = child
}
}
return obj
}
// https://en.wikipedia.org/wiki/XML-RPC#Data_types
class XmlRpcParser {
parse(node) {
return this['parse_' + node.name](node)
}
parse_array({ children: [data] }) {
return data.children.map(this.parse_value, this)
}
parse_base64({ children: [str] }) {
return Buffer.from(str, 'base64')
}
parse_boolean({ children: [str] }) {
return Boolean(Number(str))
}
'parse_dateTime.iso8601'({ children: [str] }) {
const date = new Date(str)
// date successfully parsed
if (!Number.isNaN(+date)) {
return date
}
// XML-RPC dates may use YYYYMMDD instead of YYYY-MM-DD which ECMAScript
// does not parse
return new Date(str.slice(0, 4) + '-' + str.slice(4, 6) + '-' + str.slice(6))
}
parse_double({ children: [str] }) {
return Number(str)
}
parse_fault({ children: [value] }) {
return this.parse_value(value)
}
parse_i4({ children: [str] }) {
return Number(str)
}
parse_int({ children: [str] }) {
return Number(str)
}
parse_member(member) {
member = toValue(member)
return [member.name.children[0], this.parse_value(member.value)]
}
parse_methodCall(methodCall) {
methodCall = toValue(methodCall)
return {
methodName: methodCall.methodName.children[0],
params: this.parse_params(methodCall.params),
}
}
parse_methodResponse(methodResponse) {
methodResponse = toValue(methodResponse)
const { fault } = methodResponse
if (fault !== undefined) {
return { fault: this.parse_fault(fault) }
}
return { params: this.parse_params(methodResponse.params) }
}
parse_nil() {
return null
}
parse_param(param) {
return this.parse_value(param.children[0])
}
parse_params(params) {
return params.children.map(this.parse_param, this)
}
parse_string({ children: [str] }) {
return str
}
parse_struct(struct) {
return Object.fromEntries(struct.children.map(this.parse_member, this))
}
parse_value({ children }) {
if (children.length === 0) {
return ''
}
const child = children[0]
// can be a plain string
if (typeof child === 'string') {
return child
}
return this.parse(child)
}
}
exports.XmlRpcParser = XmlRpcParser
exports.xmlRpcParser = new XmlRpcParser()

View File

@@ -1,19 +0,0 @@
'use strict'
const { formatXml } = require('@vates/xml/format')
const { join } = require('node:path')
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { xmlRpcFormatter } = require('@vates/xml-rpc/formatter')
const assert = require('node:assert/strict')
const test = require('node:test')
const methodCall = require('./methodCall.js')
test(function () {
const xmlTree = parseXml(readFileSync(join(__dirname, 'methodCall.xml')))
const xmlRpc = xmlRpcFormatter.format_methodCall(methodCall)
assert.deepEqual(parseXml(formatXml(xmlRpc)), xmlTree)
})

View File

@@ -1,16 +0,0 @@
'use strict'
module.exports = {
methodName: 'examples.getStateName',
params: [
[1404, 'Something here'],
Buffer.from([121, 111, 117, 32, 99, 97, 110, 39, 116, 32, 114, 101, 97, 100, 32, 116, 104, 105, 115, 33]),
true,
new Date('1998-07-17T14:08:55.000Z'),
-12.53,
42,
'Hello world!',
{ foo: 1, bar: 2 },
null,
],
}

View File

@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value>
<array>
<data>
<value>
<int>1404</int>
</value>
<value>
Something here
</value>
</data>
</array>
</value>
</param>
<param>
<value>
<base64>eW91IGNhbid0IHJlYWQgdGhpcyE=</base64>
</value>
</param>
<param>
<value>
<boolean>1</boolean>
</value>
</param>
<param>
<value>
<dateTime.iso8601>1998-07-17T14:08:55.000Z</dateTime.iso8601>
</value>
</param>
<param>
<value>
<double>-12.53</double>
</value>
</param>
<param>
<value>
<int>42</int>
</value>
</param>
<param>
<value>
Hello world!
</value>
</param>
<param>
<value>
<struct>
<member>
<name>foo</name>
<value>
<int>1</int>
</value>
</member>
<member>
<name>bar</name>
<value>
<int>2</int>
</value>
</member>
</struct>
</value>
</param>
<param>
<value>
<nil />
</value>
</param>
</params>
</methodCall>

View File

@@ -1,32 +0,0 @@
'use strict'
const { join } = require('node:path')
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { xmlRpcParser } = require('@vates/xml-rpc/parser')
const assert = require('node:assert/strict')
const test = require('node:test')
const methodCall = require('./methodCall.js')
test('parser.parse()', function () {
const xml = String(readFileSync(join(__dirname, 'methodCall.xml')))
const xmlRpc = parseXml(xml)
assert.deepEqual(xmlRpcParser.parse(xmlRpc), methodCall)
})
test('datetime without dashes nor milliseconds', function () {
assert.deepEqual(
xmlRpcParser.parse({ name: 'dateTime.iso8601', children: ['19980717T14:08:55Z'] }),
new Date('1998-07-17T14:08:55Z')
)
})
test('i4 support', function () {
assert.equal(xmlRpcParser.parse({ name: 'i4', children: ['42'] }), 42)
})
test('string support', function () {
assert.equal(xmlRpcParser.parse({ name: 'string', children: ['foo'] }), 'foo')
})

View File

@@ -1,56 +0,0 @@
### `parseXml(xml, [opts])`
> Based on [`sax`](https://www.npmjs.com/package/sax)
`opts` is an optional object which can contain the following options:
- `normalize = true`: if true, then turn any whitespace into a single space
- `strict = true`: whether or not to be a jerk
- `trim = true`: whether or not to trim text nodes
```js
import { parseXml } from '@xen-orchestra/xml/parse'
const tree = parseXml(`<?xml version="1.0" encoding="UTF-8"?>
<tag1 attr1="value1" attr2="value2">
Text &amp; entities
<tag2 />
<tag2 />
<ns:tag3 />
</tag1>
`)
// → {
// name: 'tag1',
// attributes: { attr1: 'value1', attr2: 'value2' },
// children: [
// 'Text & entities',
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'ns:tag3', attributes: {}, children: [] }
// ]
// }
```
### `formatXml(tree, [opts])`
`opts` is an optional object which can contain the following options:
- `includeDeclaration = true`: whether to include an XML declaration
- `indent = 2`: string or number of spaces to use to indent; if an empty string or `0`, no indent or new lines will be used
```js
import { formatXml } from '@xen-orchestra/xml/format'
formatXml({
name: 'tag1',
attributes: { attr1: 'value1', attr2: 'value2' },
children: ['Text & entities', { name: 'tag2' }, { name: 'tag2' }, { name: 'ns:tag3' }],
})
// → <?xml version="1.0" encoding="UTF-8"?>
// <tag1 attr1="value1" attr2="value2">
// Text &amp; entities
// <tag2 />
// <tag2 />
// <ns:tag3 />
// </tag1>
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,89 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/xml
[![Package Version](https://badgen.net/npm/v/@vates/xml)](https://npmjs.org/package/@vates/xml) ![License](https://badgen.net/npm/license/@vates/xml) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/xml)](https://bundlephobia.com/result?p=@vates/xml) [![Node compatibility](https://badgen.net/npm/node/@vates/xml)](https://npmjs.org/package/@vates/xml)
> Simple XML formatting/parsing
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/xml):
```sh
npm install --save @vates/xml
```
## Usage
### `parseXml(xml, [opts])`
> Based on [`sax`](https://www.npmjs.com/package/sax)
`opts` is an optional object which can contain the following options:
- `normalize = true`: if true, then turn any whitespace into a single space
- `strict = true`: whether or not to be a jerk
- `trim = true`: whether or not to trim text nodes
```js
import { parseXml } from '@xen-orchestra/xml/parse'
const tree = parseXml(`<?xml version="1.0" encoding="UTF-8"?>
<tag1 attr1="value1" attr2="value2">
Text &amp; entities
<tag2 />
<tag2 />
<ns:tag3 />
</tag1>
`)
// → {
// name: 'tag1',
// attributes: { attr1: 'value1', attr2: 'value2' },
// children: [
// 'Text & entities',
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'ns:tag3', attributes: {}, children: [] }
// ]
// }
```
### `formatXml(tree, [opts])`
`opts` is an optional object which can contain the following options:
- `includeDeclaration = true`: whether to include an XML declaration
- `indent = 2`: string or number of spaces to use to indent; if an empty string or `0`, no indent or new lines will be used
```js
import { formatXml } from '@xen-orchestra/xml/format'
formatXml({
name: 'tag1',
attributes: { attr1: 'value1', attr2: 'value2' },
children: ['Text & entities', { name: 'tag2' }, { name: 'tag2' }, { name: 'ns:tag3' }],
})
// → <?xml version="1.0" encoding="UTF-8"?>
// <tag1 attr1="value1" attr2="value2">
// Text &amp; entities
// <tag2 />
// <tag2 />
// <ns:tag3 />
// </tag1>
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env node
'use strict'
const { inspect } = require('node:util')
const { formatXml } = require('@vates/xml/format')
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
function log(val) {
process.stdout.write(inspect(val, false, null, true))
process.stdout.write('\n')
}
function main([inputPath = 0]) {
const input = readFileSync(inputPath)
// attempt to parse from JSON
let data
try {
data = JSON.parse(input)
} catch (error) {
// fallback to XML
log(parseXml(input))
return
}
process.stdout.write(formatXml(data))
}
main(process.argv.slice(2))

View File

@@ -1,64 +0,0 @@
'use strict'
const ENTITIES = {
'"': '&quot;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&apos;',
}
const RE = new RegExp(Object.keys(ENTITIES).join('|'), 'g')
function replace(match) {
return ENTITIES[match]
}
function escape(str) {
return str.replace(RE, replace)
}
function formatNode(node, depth) {
const indent = this.indent.repeat(depth)
if (typeof node === 'object') {
const line = [indent, '<', node.name]
const { attributes } = node
if (attributes !== undefined) {
for (const name of Object.keys(attributes)) {
line.push(' ', name, '="', escape(attributes[name]), '"')
}
}
const { children } = node
if (children === undefined || children.length === 0) {
line.push(' />')
this.lines.push(line.join(''))
} else {
line.push('>')
this.lines.push(line.join(''))
for (const child of children) {
formatNode.call(this, child, depth + 1)
}
this.lines.push(indent + '</' + node.name + '>')
}
} else {
// string (or scalar)
this.lines.push(indent + escape(String(node)))
}
}
exports.formatXml = function formatXml(tree, { includeDeclaration = true, indent = 2 } = {}) {
const lines = []
if (includeDeclaration) {
lines.push('<?xml version="1.0" encoding="UTF-8"?>')
}
const ctx = {
lines,
indent: typeof indent === 'number' ? ' '.repeat(indent) : indent,
}
formatNode.call(ctx, tree, 0)
return lines.join(indent !== 0 && indent !== '' ? '\n' : '')
}

View File

@@ -1,32 +0,0 @@
{
"private": false,
"name": "@vates/xml",
"description": "Simple XML formatting/parsing",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/xml",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/xml",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.0.0",
"engines": {
"node": ">=14"
},
"dependencies": {
"sax": "^1.3.0"
},
"exports": {
"./format": "./format.js",
"./parse": "./parse.js"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
}
}

Some files were not shown because too many files have changed in this diff Show More