mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-13 18:51:03 -06:00
Compare commits
2 Commits
web-stack/
...
florent-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6d7e87fe5 | ||
|
|
00f02c795f |
@@ -1 +0,0 @@
|
||||
{ "extends": ["@commitlint/config-conventional"] }
|
||||
261
.eslintrc.js
261
.eslintrc.js
@@ -1,7 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
extends: ['plugin:eslint-comments/recommended', 'plugin:n/recommended', 'standard', 'standard-jsx', 'prettier'],
|
||||
extends: ['plugin:eslint-comments/recommended', 'standard', 'standard-jsx', 'prettier'],
|
||||
globals: {
|
||||
__DEV__: true,
|
||||
$Dict: true,
|
||||
@@ -15,261 +13,13 @@ 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',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.mjs'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.{integ,spec,test}.{,c,m}js'],
|
||||
rules: {
|
||||
'n/no-unpublished-require': 'off',
|
||||
'n/no-unpublished-import': 'off',
|
||||
'n/no-unsupported-features/node-builtins': [
|
||||
'error',
|
||||
{
|
||||
version: '>=16',
|
||||
},
|
||||
],
|
||||
'n/no-unsupported-features/es-builtins': [
|
||||
'error',
|
||||
{
|
||||
version: '>=16',
|
||||
},
|
||||
],
|
||||
'n/no-unsupported-features/es-syntax': [
|
||||
'error',
|
||||
{
|
||||
version: '>=16',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
|
||||
rules: {
|
||||
// disabled because XAPI objects are using camel case
|
||||
camelcase: ['off'],
|
||||
@@ -284,12 +34,5 @@ module.exports = {
|
||||
'lines-between-class-members': 'off',
|
||||
|
||||
'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',
|
||||
},
|
||||
}
|
||||
|
||||
16
.flowconfig
Normal file
16
.flowconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
[ignore]
|
||||
<PROJECT_ROOT>/node_modules/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[lints]
|
||||
|
||||
[options]
|
||||
esproposal.decorators=ignore
|
||||
esproposal.optional_chaining=enable
|
||||
include_warnings=true
|
||||
module.use_strict=true
|
||||
|
||||
[strict]
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- Node: [e.g. 16.12.1]
|
||||
- xo-server: [e.g. 5.82.3]
|
||||
- xo-web: [e.g. 5.87.0]
|
||||
- hypervisor: [e.g. XCP-ng 8.2.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
119
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
119
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,6 +4,7 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
50
.github/workflows/ci.yml
vendored
50
.github/workflows/ci.yml
vendored
@@ -1,50 +0,0 @@
|
||||
name: Continous Integration
|
||||
on: push
|
||||
jobs:
|
||||
CI:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
with:
|
||||
path: '.turbo'
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
# https://github.com/actions/setup-node
|
||||
- name: Setup Node environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
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
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/_book/
|
||||
/coverage/
|
||||
/node_modules/
|
||||
/lerna-debug.log
|
||||
/lerna-debug.log.*
|
||||
@@ -10,6 +11,10 @@
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
/@xen-orchestra/proxy/src/app/mixins/index.mjs
|
||||
|
||||
/packages/vhd-cli/src/commands/index.js
|
||||
|
||||
/packages/xen-api/examples/node_modules/
|
||||
/packages/xen-api/plot.dat
|
||||
|
||||
@@ -30,12 +35,3 @@ 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/
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Only check commit message if commit on master or first commit on another
|
||||
# branch to avoid bothering fix commits after reviews
|
||||
#
|
||||
# FIXME: does not properly run with git commit --amend
|
||||
if [ "$(git rev-parse --abbrev-ref HEAD)" = master ] || [ "$(git rev-list --count master..)" -eq 0 ]
|
||||
then
|
||||
npx --no -- commitlint --edit "$1"
|
||||
fi
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -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
|
||||
@@ -1,11 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
jsxSingleQuote: true,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
|
||||
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
|
||||
//
|
||||
|
||||
23
.travis.yml
Normal file
23
.travis.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 14
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- qemu-utils
|
||||
- blktap-utils
|
||||
- vmdk-stream-converter
|
||||
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
script:
|
||||
- yarn run travis-tests
|
||||
@@ -1,35 +0,0 @@
|
||||
### `asyncEach(iterable, iteratee, [opts])`
|
||||
|
||||
Executes `iteratee` in order for each value yielded by `iterable`.
|
||||
|
||||
Returns a promise wich rejects as soon as a call to `iteratee` throws or a promise returned by it rejects, and which resolves when all promises returned by `iteratee` have resolved.
|
||||
|
||||
`iterable` must be an iterable or async iterable.
|
||||
|
||||
`iteratee` is called with the same `this` value as `asyncEach`, and with the following arguments:
|
||||
|
||||
- `value`: the value yielded by `iterable`
|
||||
- `index`: the 0-based index for this value
|
||||
- `iterable`: the iterable itself
|
||||
|
||||
`opts` is an object that can contains the following options:
|
||||
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
|
||||
- `signal`: an abort signal to stop the iteration
|
||||
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
|
||||
```js
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
|
||||
const contents = []
|
||||
await asyncEach(
|
||||
['foo.txt', 'bar.txt', 'baz.txt'],
|
||||
async function (filename, i) {
|
||||
contents[i] = await readFile(filename)
|
||||
},
|
||||
{
|
||||
// reads two files at a time
|
||||
concurrency: 2,
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,68 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/async-each
|
||||
|
||||
[](https://npmjs.org/package/@vates/async-each)  [](https://bundlephobia.com/result?p=@vates/async-each) [](https://npmjs.org/package/@vates/async-each)
|
||||
|
||||
> Run async fn for each item in (async) iterable
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/async-each):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/async-each
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### `asyncEach(iterable, iteratee, [opts])`
|
||||
|
||||
Executes `iteratee` in order for each value yielded by `iterable`.
|
||||
|
||||
Returns a promise wich rejects as soon as a call to `iteratee` throws or a promise returned by it rejects, and which resolves when all promises returned by `iteratee` have resolved.
|
||||
|
||||
`iterable` must be an iterable or async iterable.
|
||||
|
||||
`iteratee` is called with the same `this` value as `asyncEach`, and with the following arguments:
|
||||
|
||||
- `value`: the value yielded by `iterable`
|
||||
- `index`: the 0-based index for this value
|
||||
- `iterable`: the iterable itself
|
||||
|
||||
`opts` is an object that can contains the following options:
|
||||
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
|
||||
- `signal`: an abort signal to stop the iteration
|
||||
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
|
||||
```js
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
|
||||
const contents = []
|
||||
await asyncEach(
|
||||
['foo.txt', 'bar.txt', 'baz.txt'],
|
||||
async function (filename, i) {
|
||||
contents[i] = await readFile(filename)
|
||||
},
|
||||
{
|
||||
// reads two files at a time
|
||||
concurrency: 2,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -1,108 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
class AggregateError extends Error {
|
||||
constructor(errors, message) {
|
||||
super(message)
|
||||
this.errors = errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Item
|
||||
* @param {Iterable<Item>} iterable
|
||||
* @param {(item: Item, index: number, iterable: Iterable<Item>) => Promise<void>} iteratee
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, signal, stopOnError = true } = {}) {
|
||||
if (concurrency === 0) {
|
||||
concurrency = Infinity
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const it = (iterable[Symbol.iterator] || iterable[Symbol.asyncIterator]).call(iterable)
|
||||
const errors = []
|
||||
let running = 0
|
||||
let index = 0
|
||||
|
||||
let onAbort
|
||||
if (signal !== undefined) {
|
||||
onAbort = () => {
|
||||
onRejectedWrapper(new Error('asyncEach aborted'))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort)
|
||||
}
|
||||
|
||||
const clean = () => {
|
||||
onFulfilled = onRejected = noop
|
||||
if (onAbort !== undefined) {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
}
|
||||
|
||||
resolve = (resolve =>
|
||||
function resolveAndClean(value) {
|
||||
resolve(value)
|
||||
clean()
|
||||
})(resolve)
|
||||
reject = (reject =>
|
||||
function rejectAndClean(reason) {
|
||||
reject(reason)
|
||||
clean()
|
||||
})(reject)
|
||||
|
||||
let onFulfilled = value => {
|
||||
--running
|
||||
next()
|
||||
}
|
||||
const onFulfilledWrapper = value => onFulfilled(value)
|
||||
|
||||
let onRejected = stopOnError
|
||||
? reject
|
||||
: error => {
|
||||
--running
|
||||
errors.push(error)
|
||||
next()
|
||||
}
|
||||
const onRejectedWrapper = reason => onRejected(reason)
|
||||
|
||||
let nextIsRunning = false
|
||||
let next = async () => {
|
||||
if (nextIsRunning) {
|
||||
return
|
||||
}
|
||||
nextIsRunning = true
|
||||
if (running < concurrency) {
|
||||
const cursor = await it.next()
|
||||
if (cursor.done) {
|
||||
next = () => {
|
||||
if (running === 0) {
|
||||
if (errors.length === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new AggregateError(errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
++running
|
||||
try {
|
||||
const result = iteratee.call(this, cursor.value, index++, iterable)
|
||||
let then
|
||||
if (result != null && typeof result === 'object' && typeof (then = result.then) === 'function') {
|
||||
then.call(result, onFulfilledWrapper, onRejectedWrapper)
|
||||
} else {
|
||||
onFulfilled(result)
|
||||
}
|
||||
} catch (error) {
|
||||
onRejected(error)
|
||||
}
|
||||
}
|
||||
nextIsRunning = false
|
||||
return next()
|
||||
}
|
||||
nextIsRunning = false
|
||||
}
|
||||
next()
|
||||
})
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it, beforeEach } = require('node:test')
|
||||
const assert = require('assert').strict
|
||||
const { spy } = require('sinon')
|
||||
|
||||
const { asyncEach } = require('./')
|
||||
|
||||
const randomDelay = (max = 10) =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, Math.floor(Math.random() * max + 1))
|
||||
})
|
||||
|
||||
const rejectionOf = p =>
|
||||
new Promise((resolve, reject) => {
|
||||
p.then(reject, resolve)
|
||||
})
|
||||
|
||||
describe('asyncEach', () => {
|
||||
const thisArg = 'qux'
|
||||
const values = ['foo', 'bar', 'baz']
|
||||
|
||||
Object.entries({
|
||||
'sync iterable': () => values,
|
||||
'async iterable': async function* () {
|
||||
for (const value of values) {
|
||||
await randomDelay()
|
||||
yield value
|
||||
}
|
||||
},
|
||||
}).forEach(([what, getIterable]) =>
|
||||
describe('with ' + what, () => {
|
||||
let iterable
|
||||
beforeEach(() => {
|
||||
iterable = getIterable()
|
||||
})
|
||||
|
||||
it('works', async () => {
|
||||
const iteratee = spy(async () => {})
|
||||
|
||||
await asyncEach.call(thisArg, iterable, iteratee, { concurrency: 1 })
|
||||
|
||||
assert.deepStrictEqual(
|
||||
iteratee.thisValues,
|
||||
Array.from(values, () => thisArg)
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
iteratee.args,
|
||||
Array.from(values, (value, index) => [value, index, iterable])
|
||||
)
|
||||
})
|
||||
;[1, 2, 4].forEach(concurrency => {
|
||||
it('respects a concurrency of ' + concurrency, async () => {
|
||||
let running = 0
|
||||
|
||||
await asyncEach(
|
||||
values,
|
||||
async () => {
|
||||
++running
|
||||
assert.deepStrictEqual(running <= concurrency, true)
|
||||
await randomDelay()
|
||||
--running
|
||||
},
|
||||
{ concurrency }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('stops on first error when stopOnError is true', async () => {
|
||||
const tracker = new assert.CallTracker()
|
||||
|
||||
const error = new Error()
|
||||
const iteratee = tracker.calls((_, i) => {
|
||||
if (i === 1) {
|
||||
throw error
|
||||
}
|
||||
}, 2)
|
||||
assert.deepStrictEqual(
|
||||
await rejectionOf(asyncEach(iterable, iteratee, { concurrency: 1, stopOnError: true })),
|
||||
error
|
||||
)
|
||||
|
||||
tracker.verify()
|
||||
})
|
||||
|
||||
it('rejects AggregateError when stopOnError is false', async () => {
|
||||
const errors = []
|
||||
const iteratee = spy(() => {
|
||||
const error = new Error()
|
||||
errors.push(error)
|
||||
throw error
|
||||
})
|
||||
|
||||
const error = await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: false }))
|
||||
assert.deepStrictEqual(error.errors, errors)
|
||||
assert.deepStrictEqual(
|
||||
iteratee.args,
|
||||
Array.from(values, (value, index) => [value, index, iterable])
|
||||
)
|
||||
})
|
||||
|
||||
it('can be interrupted with an AbortSignal', async () => {
|
||||
const tracker = new assert.CallTracker()
|
||||
|
||||
const ac = new AbortController()
|
||||
const iteratee = tracker.calls((_, i) => {
|
||||
if (i === 1) {
|
||||
ac.abort()
|
||||
}
|
||||
}, 2)
|
||||
await assert.rejects(asyncEach(iterable, iteratee, { concurrency: 1, signal: ac.signal }), {
|
||||
message: 'asyncEach aborted',
|
||||
})
|
||||
|
||||
tracker.verify()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/async-each",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/async-each",
|
||||
"description": "Run async fn for each item in (async) iterable",
|
||||
"keywords": [
|
||||
"array",
|
||||
"async",
|
||||
"collection",
|
||||
"each",
|
||||
"for",
|
||||
"foreach",
|
||||
"iterable",
|
||||
"iterator"
|
||||
],
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/async-each",
|
||||
"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": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node --test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sinon": "^18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
Node does not cache queries to `dns.lookup`, which can lead application doing a lot of connections to have perf issues and to saturate Node threads pool.
|
||||
|
||||
This library attempts to mitigate these problems by providing a version of this function with a version short cache, applied on both errors and results.
|
||||
|
||||
> Limitation: `verbatim: false` option is not supported.
|
||||
|
||||
It has exactly the same API as the native method and can be used directly:
|
||||
|
||||
```js
|
||||
import { createCachedLookup } from '@vates/cached-dns.lookup'
|
||||
|
||||
const lookup = createCachedLookup()
|
||||
|
||||
lookup('example.net', { all: true, family: 0 }, (error, result) => {
|
||||
if (error != null) {
|
||||
return console.warn(error)
|
||||
}
|
||||
console.log(result)
|
||||
})
|
||||
```
|
||||
|
||||
Or it can be used to replace the native implementation and speed up the whole app:
|
||||
|
||||
```js
|
||||
// assign our cached implementation to dns.lookup
|
||||
const restore = createCachedLookup().patchGlobal()
|
||||
|
||||
// to restore the previous implementation
|
||||
restore()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,63 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/cached-dns.lookup
|
||||
|
||||
[](https://npmjs.org/package/@vates/cached-dns.lookup)  [](https://bundlephobia.com/result?p=@vates/cached-dns.lookup) [](https://npmjs.org/package/@vates/cached-dns.lookup)
|
||||
|
||||
> Cached implementation of dns.lookup
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/cached-dns.lookup):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/cached-dns.lookup
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Node does not cache queries to `dns.lookup`, which can lead application doing a lot of connections to have perf issues and to saturate Node threads pool.
|
||||
|
||||
This library attempts to mitigate these problems by providing a version of this function with a version short cache, applied on both errors and results.
|
||||
|
||||
> Limitation: `verbatim: false` option is not supported.
|
||||
|
||||
It has exactly the same API as the native method and can be used directly:
|
||||
|
||||
```js
|
||||
import { createCachedLookup } from '@vates/cached-dns.lookup'
|
||||
|
||||
const lookup = createCachedLookup()
|
||||
|
||||
lookup('example.net', { all: true, family: 0 }, (error, result) => {
|
||||
if (error != null) {
|
||||
return console.warn(error)
|
||||
}
|
||||
console.log(result)
|
||||
})
|
||||
```
|
||||
|
||||
Or it can be used to replace the native implementation and speed up the whole app:
|
||||
|
||||
```js
|
||||
// assign our cached implementation to dns.lookup
|
||||
const restore = createCachedLookup().patchGlobal()
|
||||
|
||||
// to restore the previous implementation
|
||||
restore()
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -1,72 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const dns = require('dns')
|
||||
const LRU = require('lru-cache')
|
||||
|
||||
function reportResults(all, results, callback) {
|
||||
if (all) {
|
||||
callback(null, results)
|
||||
} else {
|
||||
const first = results[0]
|
||||
callback(null, first.address, first.family)
|
||||
}
|
||||
}
|
||||
|
||||
exports.createCachedLookup = function createCachedLookup({ lookup = dns.lookup } = {}) {
|
||||
const cache = new LRU({
|
||||
max: 500,
|
||||
|
||||
// 1 minute: long enough to be effective, short enough so there is no need to bother with DNS TTLs
|
||||
ttl: 60e3,
|
||||
})
|
||||
|
||||
function cachedLookup(hostname, options, callback) {
|
||||
let all = false
|
||||
let family = 0
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
} else if (typeof options === 'number') {
|
||||
family = options
|
||||
} else if (options != null) {
|
||||
assert.notStrictEqual(options.verbatim, false, 'not supported by this implementation')
|
||||
;({ all = all, family = family } = options)
|
||||
}
|
||||
|
||||
// cache by family option because there will be an error if there is no
|
||||
// entries for the requestion family so we cannot easily cache all families
|
||||
// and filter on reporting back
|
||||
const key = hostname + '/' + family
|
||||
|
||||
const result = cache.get(key)
|
||||
if (result !== undefined) {
|
||||
setImmediate(reportResults, all, result, callback)
|
||||
} else {
|
||||
lookup(hostname, { all: true, family, verbatim: true }, function onLookup(error, results) {
|
||||
// errors are not cached because this will delay recovery after DNS/network issues
|
||||
//
|
||||
// there are no reliable way to detect if the error is real or simply
|
||||
// that there are no results for the requested hostname
|
||||
//
|
||||
// there should be much fewer errors than success, therefore it should
|
||||
// not be a big deal to not cache them
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
cache.set(key, results)
|
||||
reportResults(all, results, callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
cachedLookup.patchGlobal = function patchGlobal() {
|
||||
const previous = dns.lookup
|
||||
dns.lookup = cachedLookup
|
||||
return function restoreGlobal() {
|
||||
assert.strictEqual(dns.lookup, cachedLookup)
|
||||
dns.lookup = previous
|
||||
}
|
||||
}
|
||||
|
||||
return cachedLookup
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": "^7.0.4"
|
||||
},
|
||||
"private": false,
|
||||
"name": "@vates/cached-dns.lookup",
|
||||
"description": "Cached implementation of dns.lookup",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"dns",
|
||||
"lookup"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/cached-dns.lookup",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/cached-dns.lookup",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/coalesce-calls
|
||||
```
|
||||
> npm install --save @vates/coalesce-calls
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
exports.coalesceCalls = function (fn) {
|
||||
let promise
|
||||
const clean = () => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const assert = require('assert')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { coalesceCalls } = require('./')
|
||||
|
||||
@@ -24,13 +21,13 @@ describe('coalesceCalls', () => {
|
||||
const promise2 = fn(defer2.promise)
|
||||
|
||||
defer1.resolve('foo')
|
||||
assert.strictEqual(await promise1, 'foo')
|
||||
assert.strictEqual(await promise2, 'foo')
|
||||
expect(await promise1).toBe('foo')
|
||||
expect(await promise2).toBe('foo')
|
||||
|
||||
const defer3 = pDefer()
|
||||
const promise3 = fn(defer3.promise)
|
||||
|
||||
defer3.resolve('bar')
|
||||
assert.strictEqual(await promise3, 'bar')
|
||||
expect(await promise3).toBe('bar')
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,6 @@
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node --test"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/compose):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/compose
|
||||
```
|
||||
> npm install --save @vates/compose
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -65,23 +65,6 @@ const f = compose(
|
||||
)
|
||||
```
|
||||
|
||||
Functions can receive extra parameters:
|
||||
|
||||
```js
|
||||
const isIn = (value, min, max) => min <= value && value <= max
|
||||
|
||||
// Only compatible when `fns` is passed as an array!
|
||||
const f = compose([
|
||||
[add, 2],
|
||||
[isIn, 3, 10],
|
||||
])
|
||||
|
||||
console.log(f(1))
|
||||
// → true
|
||||
```
|
||||
|
||||
> Note: if the first function is defined with extra parameters, it will only receive the first value passed to the composed function, instead of all the parameters.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
@@ -46,20 +46,3 @@ const f = compose(
|
||||
[add2, mul3]
|
||||
)
|
||||
```
|
||||
|
||||
Functions can receive extra parameters:
|
||||
|
||||
```js
|
||||
const isIn = (value, min, max) => min <= value && value <= max
|
||||
|
||||
// Only compatible when `fns` is passed as an array!
|
||||
const f = compose([
|
||||
[add, 2],
|
||||
[isIn, 3, 10],
|
||||
])
|
||||
|
||||
console.log(f(1))
|
||||
// → true
|
||||
```
|
||||
|
||||
> Note: if the first function is defined with extra parameters, it will only receive the first value passed to the composed function, instead of all the parameters.
|
||||
@@ -4,13 +4,11 @@ const defaultOpts = { async: false, right: false }
|
||||
|
||||
exports.compose = function compose(opts, fns) {
|
||||
if (Array.isArray(opts)) {
|
||||
fns = opts.slice() // don't mutate passed array
|
||||
fns = opts
|
||||
opts = defaultOpts
|
||||
} else if (typeof opts === 'object') {
|
||||
opts = Object.assign({}, defaultOpts, opts)
|
||||
if (Array.isArray(fns)) {
|
||||
fns = fns.slice() // don't mutate passed array
|
||||
} else {
|
||||
if (!Array.isArray(fns)) {
|
||||
fns = Array.prototype.slice.call(arguments, 1)
|
||||
}
|
||||
} else {
|
||||
@@ -22,24 +20,6 @@ exports.compose = function compose(opts, fns) {
|
||||
if (n === 0) {
|
||||
throw new TypeError('at least one function must be passed')
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const entry = fns[i]
|
||||
if (Array.isArray(entry)) {
|
||||
const fn = entry[0]
|
||||
const args = entry.slice()
|
||||
args[0] = undefined
|
||||
fns[i] = function composeWithArgs(value) {
|
||||
args[0] = value
|
||||
try {
|
||||
return fn.apply(this, args)
|
||||
} finally {
|
||||
args[0] = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (n === 1) {
|
||||
return fns[0]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const assert = require('node:assert').strict
|
||||
/* eslint-env jest */
|
||||
|
||||
const { compose } = require('./')
|
||||
|
||||
@@ -10,42 +7,43 @@ const mul3 = x => x * 3
|
||||
|
||||
describe('compose()', () => {
|
||||
it('throws when no functions is passed', () => {
|
||||
assert.throws(() => compose(), TypeError)
|
||||
assert.throws(() => compose([]), TypeError)
|
||||
expect(() => compose()).toThrow(TypeError)
|
||||
expect(() => compose([])).toThrow(TypeError)
|
||||
})
|
||||
|
||||
it('applies from left to right', () => {
|
||||
assert.strictEqual(compose(add2, mul3)(5), 21)
|
||||
expect(compose(add2, mul3)(5)).toBe(21)
|
||||
})
|
||||
|
||||
it('accepts functions in an array', () => {
|
||||
assert.strictEqual(compose([add2, mul3])(5), 21)
|
||||
expect(compose([add2, mul3])(5)).toBe(21)
|
||||
})
|
||||
|
||||
it('can apply from right to left', () => {
|
||||
assert.strictEqual(compose({ right: true }, add2, mul3)(5), 17)
|
||||
expect(compose({ right: true }, add2, mul3)(5)).toBe(17)
|
||||
})
|
||||
|
||||
it('accepts options with functions in an array', () => {
|
||||
assert.strictEqual(compose({ right: true }, [add2, mul3])(5), 17)
|
||||
expect(compose({ right: true }, [add2, mul3])(5)).toBe(17)
|
||||
})
|
||||
|
||||
it('can compose async functions', async () => {
|
||||
assert.strictEqual(
|
||||
expect(
|
||||
await compose(
|
||||
{ async: true },
|
||||
async x => x + 2,
|
||||
async x => x * 3
|
||||
)(5),
|
||||
21
|
||||
)
|
||||
)(5)
|
||||
).toBe(21)
|
||||
})
|
||||
|
||||
it('forwards all args to first function', () => {
|
||||
expect.assertions(1)
|
||||
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
compose(
|
||||
(...args) => {
|
||||
assert.deepEqual(args, expectedArgs)
|
||||
expect(args).toEqual(expectedArgs)
|
||||
},
|
||||
// add a second function to avoid the one function special case
|
||||
Function.prototype
|
||||
@@ -53,13 +51,15 @@ describe('compose()', () => {
|
||||
})
|
||||
|
||||
it('forwards context to all functions', () => {
|
||||
expect.assertions(2)
|
||||
|
||||
const expectedThis = {}
|
||||
compose(
|
||||
function () {
|
||||
assert.strictEqual(this, expectedThis)
|
||||
expect(this).toBe(expectedThis)
|
||||
},
|
||||
function () {
|
||||
assert.strictEqual(this, expectedThis)
|
||||
expect(this).toBe(expectedThis)
|
||||
}
|
||||
).call(expectedThis)
|
||||
})
|
||||
@@ -14,12 +14,11 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "2.1.0",
|
||||
"version": "2.0.0",
|
||||
"engines": {
|
||||
"node": ">=7.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node --test"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
### `decorateWith(fn, ...args)`
|
||||
|
||||
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
|
||||
|
||||
```js
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(lodash.debounce, 150)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateClass(class, map)`
|
||||
|
||||
Decorates a number of accessors and methods directly, without using the decorator syntax:
|
||||
|
||||
```js
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
get bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
set bar(value) {
|
||||
// body
|
||||
}
|
||||
|
||||
baz() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
|
||||
decorateClass(Foo, {
|
||||
// getter and/or setter
|
||||
bar: {
|
||||
// without arguments
|
||||
get: lodash.memoize,
|
||||
|
||||
// with arguments
|
||||
set: [lodash.debounce, 150],
|
||||
},
|
||||
|
||||
// method (with or without arguments)
|
||||
baz: lodash.curry,
|
||||
})
|
||||
```
|
||||
|
||||
The decorated class is returned, so you can export it directly.
|
||||
|
||||
To apply multiple transforms to an accessor/method, you can either call `decorateClass` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
|
||||
|
||||
```js
|
||||
decorateClass(Foo, {
|
||||
baz: compose([
|
||||
[lodash.debounce, 150]
|
||||
lodash.curry,
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
This is often necessary for caching or deduplicating calls.
|
||||
|
||||
```js
|
||||
import { perInstance } from '@vates/decorateWith'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(perInstance, lodash.memoize)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because it's a normal function, it can also be used with `decorateClass`, with `compose` or even by itself.
|
||||
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
> Deprecated alias for [`decorateClass(class, map)`](#decorateclassclass-map).
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/decorate-with
|
||||
```
|
||||
> npm install --save @vates/decorate-with
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -31,19 +31,15 @@ class Foo {
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateClass(class, map)`
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
Decorates a number of accessors and methods directly, without using the decorator syntax:
|
||||
Decorates a number of methods directly, without using the decorator syntax:
|
||||
|
||||
```js
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
get bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
set bar(value) {
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
@@ -52,93 +48,17 @@ class Foo {
|
||||
}
|
||||
}
|
||||
|
||||
decorateClass(Foo, {
|
||||
// getter and/or setter
|
||||
bar: {
|
||||
// without arguments
|
||||
get: lodash.memoize,
|
||||
decorateMethodsWith(Foo, {
|
||||
// without arguments
|
||||
bar: lodash.curry,
|
||||
|
||||
// with arguments
|
||||
set: [lodash.debounce, 150],
|
||||
},
|
||||
|
||||
// method (with or without arguments)
|
||||
baz: lodash.curry,
|
||||
// with arguments
|
||||
baz: [lodash.debounce, 150],
|
||||
})
|
||||
```
|
||||
|
||||
The decorated class is returned, so you can export it directly.
|
||||
|
||||
To apply multiple transforms to an accessor/method, you can either call `decorateClass` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
|
||||
|
||||
```js
|
||||
decorateClass(Foo, {
|
||||
baz: compose([
|
||||
[lodash.debounce, 150]
|
||||
lodash.curry,
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
This is often necessary for caching or deduplicating calls.
|
||||
|
||||
```js
|
||||
import { perInstance } from '@vates/decorateWith'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(perInstance, lodash.memoize)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because it's a normal function, it can also be used with `decorateClass`, with `compose` or even by itself.
|
||||
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
> Deprecated alias for [`decorateClass(class, map)`](#decorateclassclass-map).
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
42
@vates/decorate-with/USAGE.md
Normal file
42
@vates/decorate-with/USAGE.md
Normal file
@@ -0,0 +1,42 @@
|
||||
### `decorateWith(fn, ...args)`
|
||||
|
||||
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
|
||||
|
||||
```js
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(lodash.debounce, 150)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
Decorates a number of methods directly, without using the decorator syntax:
|
||||
|
||||
```js
|
||||
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
baz() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
|
||||
decorateMethodsWith(Foo, {
|
||||
// without arguments
|
||||
bar: lodash.curry,
|
||||
|
||||
// with arguments
|
||||
baz: [lodash.debounce, 150],
|
||||
})
|
||||
```
|
||||
|
||||
The decorated class is returned, so you can export it directly.
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
exports.decorateWith = function decorateWith(fn, ...args) {
|
||||
return (target, name, descriptor) => ({
|
||||
...descriptor,
|
||||
@@ -9,44 +7,15 @@ exports.decorateWith = function decorateWith(fn, ...args) {
|
||||
|
||||
const { getOwnPropertyDescriptor, defineProperty } = Object
|
||||
|
||||
function applyDecorator(decorator, value) {
|
||||
return typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1))
|
||||
}
|
||||
|
||||
exports.decorateClass = exports.decorateMethodsWith = function decorateClass(klass, map) {
|
||||
return decorateObject(klass.prototype, map)
|
||||
}
|
||||
|
||||
function decorateObject(object, map) {
|
||||
exports.decorateMethodsWith = function decorateMethodsWith(klass, map) {
|
||||
const { prototype } = klass
|
||||
for (const name of Object.keys(map)) {
|
||||
const descriptor = getOwnPropertyDescriptor(prototype, name)
|
||||
const { value } = descriptor
|
||||
|
||||
const decorator = map[name]
|
||||
const descriptor = getOwnPropertyDescriptor(object, name)
|
||||
if (typeof decorator === 'function' || Array.isArray(decorator)) {
|
||||
descriptor.value = applyDecorator(decorator, descriptor.value)
|
||||
} else {
|
||||
const { get, set } = decorator
|
||||
if (get !== undefined) {
|
||||
descriptor.get = applyDecorator(get, descriptor.get)
|
||||
}
|
||||
if (set !== undefined) {
|
||||
descriptor.set = applyDecorator(set, descriptor.set)
|
||||
}
|
||||
}
|
||||
|
||||
defineProperty(object, name, descriptor)
|
||||
}
|
||||
return object
|
||||
}
|
||||
exports.decorateObject = decorateObject
|
||||
|
||||
exports.perInstance = function perInstance(fn, decorator, ...args) {
|
||||
const map = new WeakMap()
|
||||
return function () {
|
||||
let decorated = map.get(this)
|
||||
if (decorated === undefined) {
|
||||
decorated = decorator(fn, ...args)
|
||||
map.set(this, decorated)
|
||||
}
|
||||
return decorated.apply(this, arguments)
|
||||
descriptor.value = typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1))
|
||||
defineProperty(prototype, name, descriptor)
|
||||
}
|
||||
return klass
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { describe, it } = require('node:test')
|
||||
|
||||
const { decorateClass, decorateWith, decorateMethodsWith, perInstance } = require('./')
|
||||
|
||||
const identity = _ => _
|
||||
|
||||
describe('decorateWith', () => {
|
||||
it('works', () => {
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
const expectedFn = Function.prototype
|
||||
const newFn = () => {}
|
||||
|
||||
const decorator = decorateWith(
|
||||
function wrapper(fn, ...args) {
|
||||
assert.deepStrictEqual(fn, expectedFn)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
|
||||
return newFn
|
||||
},
|
||||
...expectedArgs
|
||||
)
|
||||
|
||||
const descriptor = {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: expectedFn,
|
||||
writable: true,
|
||||
}
|
||||
assert.deepStrictEqual(decorator({}, 'foo', descriptor), {
|
||||
...descriptor,
|
||||
value: newFn,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('decorateClass', () => {
|
||||
it('works', () => {
|
||||
class C {
|
||||
foo() {}
|
||||
bar() {}
|
||||
get baz() {}
|
||||
// eslint-disable-next-line accessor-pairs
|
||||
set qux(_) {}
|
||||
}
|
||||
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
const P = C.prototype
|
||||
|
||||
const descriptors = Object.getOwnPropertyDescriptors(P)
|
||||
|
||||
const newFoo = () => {}
|
||||
const newBar = () => {}
|
||||
const newGetBaz = () => {}
|
||||
const newSetQux = _ => {}
|
||||
|
||||
decorateClass(C, {
|
||||
foo(fn) {
|
||||
assert.strictEqual(arguments.length, 1)
|
||||
assert.strictEqual(fn, P.foo)
|
||||
return newFoo
|
||||
},
|
||||
bar: [
|
||||
function (fn, ...args) {
|
||||
assert.strictEqual(fn, P.bar)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
return newBar
|
||||
},
|
||||
...expectedArgs,
|
||||
],
|
||||
baz: {
|
||||
get(fn) {
|
||||
assert.strictEqual(arguments.length, 1)
|
||||
assert.strictEqual(fn, descriptors.baz.get)
|
||||
return newGetBaz
|
||||
},
|
||||
},
|
||||
qux: {
|
||||
set: [
|
||||
function (fn, ...args) {
|
||||
assert.strictEqual(fn, descriptors.qux.set)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
return newSetQux
|
||||
},
|
||||
...expectedArgs,
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const newDescriptors = Object.getOwnPropertyDescriptors(P)
|
||||
assert.deepStrictEqual(newDescriptors.foo, { ...descriptors.foo, value: newFoo })
|
||||
assert.deepStrictEqual(newDescriptors.bar, { ...descriptors.bar, value: newBar })
|
||||
assert.deepStrictEqual(newDescriptors.baz, { ...descriptors.baz, get: newGetBaz })
|
||||
assert.deepStrictEqual(newDescriptors.qux, { ...descriptors.qux, set: newSetQux })
|
||||
})
|
||||
|
||||
it('throws if using an accessor decorator for a method', function () {
|
||||
assert.throws(() =>
|
||||
decorateClass(
|
||||
class {
|
||||
foo() {}
|
||||
},
|
||||
{ foo: { get: identity, set: identity } }
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('throws if using a method decorator for an accessor', function () {
|
||||
assert.throws(() =>
|
||||
decorateClass(
|
||||
class {
|
||||
get foo() {}
|
||||
},
|
||||
{ foo: identity }
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('decorateMethodsWith is an alias of decorateClass', function () {
|
||||
assert.strictEqual(decorateMethodsWith, decorateClass)
|
||||
})
|
||||
|
||||
describe('perInstance', () => {
|
||||
it('works', () => {
|
||||
let calls = 0
|
||||
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
const expectedFn = Function.prototype
|
||||
function wrapper(fn, ...args) {
|
||||
assert.strictEqual(fn, expectedFn)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
const i = ++calls
|
||||
return () => i
|
||||
}
|
||||
|
||||
const wrapped = perInstance(expectedFn, wrapper, ...expectedArgs)
|
||||
|
||||
// decorator is not called before decorated called
|
||||
assert.strictEqual(calls, 0)
|
||||
|
||||
const o1 = {}
|
||||
const o2 = {}
|
||||
|
||||
assert.strictEqual(wrapped.call(o1), 1)
|
||||
|
||||
// the same decorated function is returned for the same instance
|
||||
assert.strictEqual(wrapped.call(o1), 1)
|
||||
|
||||
// a new decorated function is returned for another instance
|
||||
assert.strictEqual(wrapped.call(o2), 2)
|
||||
})
|
||||
})
|
||||
@@ -20,12 +20,11 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "2.1.0",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node --test"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
```js
|
||||
import diff from '@vates/diff'
|
||||
|
||||
diff('foo bar baz', 'Foo qux')
|
||||
// → [ 0, 'F', 4, 'qux', 7, '' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains `F`
|
||||
// - at position 4, it contains `qux`
|
||||
// - at position 7, it ends
|
||||
|
||||
diff('Foo qux', 'foo bar baz')
|
||||
// → [ 0, 'f', 4, 'bar', 7, ' baz' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains f`
|
||||
// - at position 4, it contains `bar`
|
||||
// - at position 7, it contains `baz`
|
||||
|
||||
// works with all collections that supports
|
||||
// - `.length`
|
||||
// - `collection[index]`
|
||||
// - `.slice(start, end)`
|
||||
//
|
||||
// which includes:
|
||||
// - arrays
|
||||
// - strings
|
||||
// - `Buffer`
|
||||
// - `TypedArray`
|
||||
diff([0, 1, 2], [3, 4])
|
||||
// → [ 0, [ 3, 4 ], 2, [] ]
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,65 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/diff
|
||||
|
||||
[](https://npmjs.org/package/@vates/diff)  [](https://bundlephobia.com/result?p=@vates/diff) [](https://npmjs.org/package/@vates/diff)
|
||||
|
||||
> Computes differences between two arrays, buffers or strings
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/diff):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/diff
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import diff from '@vates/diff'
|
||||
|
||||
diff('foo bar baz', 'Foo qux')
|
||||
// → [ 0, 'F', 4, 'qux', 7, '' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains `F`
|
||||
// - at position 4, it contains `qux`
|
||||
// - at position 7, it ends
|
||||
|
||||
diff('Foo qux', 'foo bar baz')
|
||||
// → [ 0, 'f', 4, 'bar', 7, ' baz' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains f`
|
||||
// - at position 4, it contains `bar`
|
||||
// - at position 7, it contains `baz`
|
||||
|
||||
// works with all collections that supports
|
||||
// - `.length`
|
||||
// - `collection[index]`
|
||||
// - `.slice(start, end)`
|
||||
//
|
||||
// which includes:
|
||||
// - arrays
|
||||
// - strings
|
||||
// - `Buffer`
|
||||
// - `TypedArray`
|
||||
diff([0, 1, 2], [3, 4])
|
||||
// → [ 0, [ 3, 4 ], 2, [] ]
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -1,37 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Compare two data arrays, buffers or strings and invoke the provided callback function for each difference.
|
||||
*
|
||||
* @template {Array|Buffer|string} T
|
||||
* @param {Array|Buffer|string} data1 - The first data array or buffer to compare.
|
||||
* @param {T} data2 - The second data array or buffer to compare.
|
||||
* @param {(index: number, diff: T) => void} [cb] - The callback function to invoke for each difference. If not provided, an array of differences will be returned.
|
||||
* @returns {Array<number|T>|undefined} - An array of differences if no callback is provided, otherwise undefined.
|
||||
*/
|
||||
module.exports = function diff(data1, data2, cb) {
|
||||
let result
|
||||
if (cb === undefined) {
|
||||
result = []
|
||||
cb = result.push.bind(result)
|
||||
}
|
||||
|
||||
const n1 = data1.length
|
||||
const n2 = data2.length
|
||||
const n = Math.min(n1, n2)
|
||||
for (let i = 0; i < n; ++i) {
|
||||
if (data1[i] !== data2[i]) {
|
||||
let j = i + 1
|
||||
while (j < n && data1[j] !== data2[j]) {
|
||||
++j
|
||||
}
|
||||
cb(i, data2.slice(i, j))
|
||||
i = j
|
||||
}
|
||||
}
|
||||
if (n1 !== n2) {
|
||||
cb(n, n1 < n2 ? data2.slice(n) : data2.slice(0, 0))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert/strict')
|
||||
const test = require('node:test')
|
||||
|
||||
const diff = require('./index.js')
|
||||
|
||||
test('data of equal length', function () {
|
||||
const data1 = 'foo bar baz'
|
||||
const data2 = 'baz bar foo'
|
||||
assert.deepEqual(diff(data1, data2), [0, 'baz', 8, 'foo'])
|
||||
})
|
||||
|
||||
test('data1 is longer', function () {
|
||||
const data1 = 'foo bar'
|
||||
const data2 = 'foo'
|
||||
assert.deepEqual(diff(data1, data2), [3, ''])
|
||||
})
|
||||
|
||||
test('data2 is longer', function () {
|
||||
const data1 = 'foo'
|
||||
const data2 = 'foo bar'
|
||||
assert.deepEqual(diff(data1, data2), [3, ' bar'])
|
||||
})
|
||||
|
||||
test('with arrays', function () {
|
||||
const data1 = 'foo bar baz'.split('')
|
||||
const data2 = 'baz bar foo'.split('')
|
||||
assert.deepEqual(diff(data1, data2), [0, 'baz'.split(''), 8, 'foo'.split('')])
|
||||
})
|
||||
|
||||
test('with buffers', function () {
|
||||
const data1 = Buffer.from('foo bar baz')
|
||||
const data2 = Buffer.from('baz bar foo')
|
||||
assert.deepEqual(diff(data1, data2), [0, Buffer.from('baz'), 8, Buffer.from('foo')])
|
||||
})
|
||||
|
||||
test('cb param', function () {
|
||||
const data1 = 'foo bar baz'
|
||||
const data2 = 'baz bar foo'
|
||||
|
||||
const calls = []
|
||||
const cb = (...args) => calls.push(args)
|
||||
|
||||
diff(data1, data2, cb)
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
[0, 'baz'],
|
||||
[8, 'foo'],
|
||||
])
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/diff",
|
||||
"description": "Computes differences between two arrays, buffers or strings",
|
||||
"keywords": [
|
||||
"array",
|
||||
"binary",
|
||||
"buffer",
|
||||
"diff",
|
||||
"differences",
|
||||
"string"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/diff",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/diff",
|
||||
"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",
|
||||
"test": "node --test"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/disposable):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/disposable
|
||||
```
|
||||
> npm install --save @vates/disposable
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const { useFakeTimers, spy, assert } = require('sinon')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { createDebounceResource } = require('./debounceResource')
|
||||
|
||||
const clock = useFakeTimers()
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe('debounceResource()', () => {
|
||||
it('calls the resource disposer after 10 seconds', async () => {
|
||||
const debounceResource = createDebounceResource()
|
||||
const delay = 10e3
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
|
||||
const resource = await debounceResource(
|
||||
Promise.resolve({
|
||||
@@ -23,10 +20,10 @@ describe('debounceResource()', () => {
|
||||
|
||||
resource.dispose()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toBeCalled()
|
||||
|
||||
clock.tick(delay)
|
||||
jest.advanceTimersByTime(delay)
|
||||
|
||||
assert.called(dispose)
|
||||
expect(dispose).toBeCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
const ensureArray = require('ensure-array')
|
||||
const { MultiKeyMap } = require('@vates/multi-key-map')
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const { spy, assert } = require('sinon')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { deduped } = require('./deduped')
|
||||
|
||||
describe('deduped()', () => {
|
||||
it('calls the resource function only once', async () => {
|
||||
const value = {}
|
||||
const getResource = spy(async () => ({
|
||||
const getResource = jest.fn(async () => ({
|
||||
value,
|
||||
dispose: Function.prototype,
|
||||
}))
|
||||
@@ -18,13 +15,13 @@ describe('deduped()', () => {
|
||||
const { value: v1 } = await dedupedGetResource()
|
||||
const { value: v2 } = await dedupedGetResource()
|
||||
|
||||
assert.calledOnce(getResource)
|
||||
assert.match(v1, value)
|
||||
assert.match(v2, value)
|
||||
expect(getResource).toHaveBeenCalledTimes(1)
|
||||
expect(v1).toBe(value)
|
||||
expect(v2).toBe(value)
|
||||
})
|
||||
|
||||
it('only disposes the source disposable when its all copies dispose', async () => {
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
const getResource = async () => ({
|
||||
value: '',
|
||||
dispose,
|
||||
@@ -37,35 +34,35 @@ describe('deduped()', () => {
|
||||
|
||||
d1()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
|
||||
d2()
|
||||
|
||||
assert.calledOnce(dispose)
|
||||
expect(dispose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('works with sync factory', () => {
|
||||
const value = {}
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
const dedupedGetResource = deduped(() => ({ value, dispose }))
|
||||
|
||||
const d1 = dedupedGetResource()
|
||||
assert.match(d1.value, value)
|
||||
expect(d1.value).toBe(value)
|
||||
|
||||
const d2 = dedupedGetResource()
|
||||
assert.match(d2.value, value)
|
||||
expect(d2.value).toBe(value)
|
||||
|
||||
d1.dispose()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
|
||||
d2.dispose()
|
||||
|
||||
assert.calledOnce(dispose)
|
||||
expect(dispose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('no race condition on dispose before async acquisition', async () => {
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
const dedupedGetResource = deduped(async () => ({ value: 42, dispose }))
|
||||
|
||||
const d1 = await dedupedGetResource()
|
||||
@@ -74,6 +71,6 @@ describe('deduped()', () => {
|
||||
|
||||
d1.dispose()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -14,21 +14,17 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node --test"
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"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.3.0",
|
||||
"ensure-array": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sinon": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
> This library is compatible with Node's `EventEmitter` and web browsers' `EventTarget` APIs.
|
||||
|
||||
### API
|
||||
|
||||
```js
|
||||
import { EventListenersManager } from '@vates/event-listeners-manager'
|
||||
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
// adding listeners
|
||||
events.add('foo', onFoo).add('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// removing a specific listener
|
||||
events.remove('foo', onFoo)
|
||||
|
||||
// removing all listeners for a specific event
|
||||
events.removeAll('foo')
|
||||
|
||||
// removing all listeners
|
||||
events.removeAll()
|
||||
```
|
||||
|
||||
### Typical use case
|
||||
|
||||
> Removing all listeners when no longer necessary.
|
||||
|
||||
Manually:
|
||||
|
||||
```js
|
||||
const onFoo = () => {}
|
||||
const onBar = () => {}
|
||||
const onBaz = () => {}
|
||||
emitter.on('foo', onFoo).on('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
emitter.off('foo', onFoo).off('bar', onBar).off('baz', onBaz)
|
||||
```
|
||||
|
||||
With this library:
|
||||
|
||||
```js
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
events.add('foo', () => {})).add('bar', () => {})).add('baz', () => {}))
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
events.removeAll()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,81 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/event-listeners-manager
|
||||
|
||||
[](https://npmjs.org/package/@vates/event-listeners-manager)  [](https://bundlephobia.com/result?p=@vates/event-listeners-manager) [](https://npmjs.org/package/@vates/event-listeners-manager)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/event-listeners-manager):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/event-listeners-manager
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> This library is compatible with Node's `EventEmitter` and web browsers' `EventTarget` APIs.
|
||||
|
||||
### API
|
||||
|
||||
```js
|
||||
import { EventListenersManager } from '@vates/event-listeners-manager'
|
||||
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
// adding listeners
|
||||
events.add('foo', onFoo).add('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// removing a specific listener
|
||||
events.remove('foo', onFoo)
|
||||
|
||||
// removing all listeners for a specific event
|
||||
events.removeAll('foo')
|
||||
|
||||
// removing all listeners
|
||||
events.removeAll()
|
||||
```
|
||||
|
||||
### Typical use case
|
||||
|
||||
> Removing all listeners when no longer necessary.
|
||||
|
||||
Manually:
|
||||
|
||||
```js
|
||||
const onFoo = () => {}
|
||||
const onBar = () => {}
|
||||
const onBaz = () => {}
|
||||
emitter.on('foo', onFoo).on('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
emitter.off('foo', onFoo).off('bar', onBar).off('baz', onBaz)
|
||||
```
|
||||
|
||||
With this library:
|
||||
|
||||
```js
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
events.add('foo', () => {})).add('bar', () => {})).add('baz', () => {}))
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
events.removeAll()
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -1,56 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
exports.EventListenersManager = class EventListenersManager {
|
||||
constructor(emitter) {
|
||||
this._listeners = new Map()
|
||||
|
||||
this._add = (emitter.addListener || emitter.addEventListener).bind(emitter)
|
||||
this._remove = (emitter.removeListener || emitter.removeEventListener).bind(emitter)
|
||||
}
|
||||
|
||||
add(type, listener) {
|
||||
let listeners = this._listeners.get(type)
|
||||
if (listeners === undefined) {
|
||||
listeners = new Set()
|
||||
this._listeners.set(type, listeners)
|
||||
}
|
||||
|
||||
// don't add the same listener multiple times (allowed on Node.js)
|
||||
if (!listeners.has(listener)) {
|
||||
listeners.add(listener)
|
||||
this._add(type, listener)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
remove(type, listener) {
|
||||
const allListeners = this._listeners
|
||||
const listeners = allListeners.get(type)
|
||||
if (listeners !== undefined && listeners.delete(listener)) {
|
||||
this._remove(type, listener)
|
||||
if (listeners.size === 0) {
|
||||
allListeners.delete(type)
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
removeAll(type) {
|
||||
const allListeners = this._listeners
|
||||
const remove = this._remove
|
||||
const types = type !== undefined ? [type] : allListeners.keys()
|
||||
for (const type of types) {
|
||||
const listeners = allListeners.get(type)
|
||||
if (listeners !== undefined) {
|
||||
allListeners.delete(type)
|
||||
for (const listener of listeners) {
|
||||
remove(type, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const t = require('tap')
|
||||
const { EventEmitter } = require('events')
|
||||
|
||||
const { EventListenersManager } = require('./')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
// function spy (impl = Function.prototype) {
|
||||
// function spy() {
|
||||
// spy.calls.push([Array.from(arguments), this])
|
||||
// }
|
||||
// spy.calls = []
|
||||
// return spy
|
||||
// }
|
||||
|
||||
function assertListeners(t, event, listeners) {
|
||||
t.strictSame(t.context.ee.listeners(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)
|
||||
})
|
||||
|
||||
t.test('.add adds a listener', function (t) {
|
||||
t.context.em.add('foo', noop)
|
||||
|
||||
assertListeners(t, 'foo', [noop])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.add does not add a duplicate listener', function (t) {
|
||||
t.context.em.add('foo', noop).add('foo', noop)
|
||||
|
||||
assertListeners(t, 'foo', [noop])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.remove removes a listener', function (t) {
|
||||
t.context.em.add('foo', noop).remove('foo', noop)
|
||||
|
||||
assertListeners(t, 'foo', [])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.removeAll removes all listeners of a given type', function (t) {
|
||||
t.context.em.add('foo', noop).add('bar', noop).removeAll('foo')
|
||||
|
||||
assertListeners(t, 'foo', [])
|
||||
assertListeners(t, 'bar', [noop])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.removeAll removes all listeners', function (t) {
|
||||
t.context.em.add('foo', noop).add('bar', noop).removeAll()
|
||||
|
||||
assertListeners(t, 'foo', [])
|
||||
assertListeners(t, 'bar', [])
|
||||
|
||||
t.end()
|
||||
})
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"private": false,
|
||||
"name": "@vates/event-listeners-manager",
|
||||
"descriptions": "Easy way to clean up event listeners",
|
||||
"keywords": [
|
||||
"add",
|
||||
"addEventListener",
|
||||
"addListener",
|
||||
"browser",
|
||||
"clear",
|
||||
"DOM",
|
||||
"emitter",
|
||||
"event",
|
||||
"EventEmitter",
|
||||
"EventTarget",
|
||||
"management",
|
||||
"manager",
|
||||
"node",
|
||||
"remove",
|
||||
"removeEventListener",
|
||||
"removeListener"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/event-listeners-manager",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/event-listeners-manager",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "tap --allow-incomplete-coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^18.7.0"
|
||||
}
|
||||
}
|
||||
3
@vates/fatfs/.gitignore
vendored
3
@vates/fatfs/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.DS_Store
|
||||
.vscode/
|
||||
node_modules/
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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); }
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
});
|
||||
@@ -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); }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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`
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,59 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/fuse-vhd
|
||||
|
||||
[](https://npmjs.org/package/@vates/fuse-vhd)  [](https://bundlephobia.com/result?p=@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)
|
||||
@@ -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))
|
||||
@@ -1,70 +0,0 @@
|
||||
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(),
|
||||
atime: st.atime || new Date(),
|
||||
ctime: st.ctime || new Date(),
|
||||
size: st.size !== undefined ? st.size : 0,
|
||||
mode: st.mode === 'dir' ? 16877 : st.mode === 'file' ? 33188 : st.mode === 'link' ? 41453 : st.mode,
|
||||
uid: st.uid !== undefined ? st.uid : process.getuid(),
|
||||
gid: st.gid !== undefined ? st.gid : process.getgid(),
|
||||
})
|
||||
|
||||
export const mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
|
||||
const vhd = yield VhdSynthetic.fromVhdChain(handler, diskPath)
|
||||
|
||||
const cache = new LRU({
|
||||
max: 16, // each cached block is 2MB in size
|
||||
})
|
||||
await vhd.readBlockAllocationTable()
|
||||
const fuse = new Fuse(mountDir, {
|
||||
async readdir(path, cb) {
|
||||
if (path === '/') {
|
||||
return cb(null, ['vhd0'])
|
||||
}
|
||||
cb(Fuse.ENOENT)
|
||||
},
|
||||
async getattr(path, cb) {
|
||||
if (path === '/') {
|
||||
return cb(
|
||||
null,
|
||||
stat({
|
||||
mode: 'dir',
|
||||
size: 4096,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (path === '/vhd0') {
|
||||
return cb(
|
||||
null,
|
||||
stat({
|
||||
mode: 'file',
|
||||
size: vhd.footer.currentSize,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
cb(Fuse.ENOENT)
|
||||
},
|
||||
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)
|
||||
})
|
||||
}
|
||||
throw new Error(`read file ${path} not exists`)
|
||||
},
|
||||
})
|
||||
return new Disposable(
|
||||
() => fromCallback(cb => fuse.unmount(cb)),
|
||||
fromCallback(cb => fuse.mount(cb))
|
||||
)
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "@vates/fuse-vhd",
|
||||
"version": "2.1.2",
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/fuse-vhd",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,51 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/json-hash
|
||||
|
||||
[](https://npmjs.org/package/@vates/json-hash)  [](https://bundlephobia.com/result?p=@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)
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/multi-key-map):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/multi-key-map
|
||||
```
|
||||
> npm install --save @vates/multi-key-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
class Node {
|
||||
constructor(value) {
|
||||
this.children = new Map()
|
||||
@@ -36,31 +34,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 +67,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 +77,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 +84,4 @@ exports.MultiKeyMap = class MultiKeyMap {
|
||||
set(keys, value) {
|
||||
this._root = set(this._root, 0, keys, value)
|
||||
}
|
||||
|
||||
values() {
|
||||
return values(this._root)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { MultiKeyMap } = require('./')
|
||||
|
||||
@@ -19,7 +16,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,17 +24,11 @@ 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])
|
||||
expect(map.get(key.slice())).toBe(values[i])
|
||||
map.delete(key.slice())
|
||||
assert.strictEqual(map.get(key.slice()), undefined)
|
||||
expect(map.get(key.slice())).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -18,12 +18,11 @@
|
||||
"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"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
### `new NdbClient({address, exportname, secure = true, port = 10809})`
|
||||
|
||||
create a new nbd client
|
||||
|
||||
```js
|
||||
import NbdClient from '@vates/nbd-client'
|
||||
const client = new NbdClient({
|
||||
address: 'MY_NBD_HOST',
|
||||
exportname: 'MY_SECRET_EXPORT',
|
||||
cert: 'Server certificate', // optional, will use encrypted link if provided
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
const block = await client.readBlock(blockIndex, BlockSize)
|
||||
await client.disconnect()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,47 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/nbd-client
|
||||
|
||||
[](https://npmjs.org/package/@vates/nbd-client)  [](https://bundlephobia.com/result?p=@vates/nbd-client) [](https://npmjs.org/package/@vates/nbd-client)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/nbd-client):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/nbd-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### `new NdbClient({address, exportname, secure = true, port = 10809})`
|
||||
|
||||
create a new nbd client
|
||||
|
||||
```js
|
||||
import NbdClient from '@vates/nbd-client'
|
||||
const client = new NbdClient({
|
||||
address: 'MY_NBD_HOST',
|
||||
exportname: 'MY_SECRET_EXPORT',
|
||||
cert: 'Server certificate', // optional, will use encrypted link if provided
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
const block = await client.readBlock(blockIndex, BlockSize)
|
||||
await client.disconnect()
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -1,41 +0,0 @@
|
||||
export const INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
|
||||
export const OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
|
||||
export const NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
|
||||
export const NBD_OPT_EXPORT_NAME = 1
|
||||
export const NBD_OPT_ABORT = 2
|
||||
export const NBD_OPT_LIST = 3
|
||||
export const NBD_OPT_STARTTLS = 5
|
||||
export const NBD_OPT_INFO = 6
|
||||
export const NBD_OPT_GO = 7
|
||||
|
||||
export const NBD_FLAG_HAS_FLAGS = 1 << 0
|
||||
export const NBD_FLAG_READ_ONLY = 1 << 1
|
||||
export const NBD_FLAG_SEND_FLUSH = 1 << 2
|
||||
export const NBD_FLAG_SEND_FUA = 1 << 3
|
||||
export const NBD_FLAG_ROTATIONAL = 1 << 4
|
||||
export const NBD_FLAG_SEND_TRIM = 1 << 5
|
||||
|
||||
export const NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
|
||||
|
||||
export const NBD_CMD_FLAG_FUA = 1 << 0
|
||||
export const NBD_CMD_FLAG_NO_HOLE = 1 << 1
|
||||
export const NBD_CMD_FLAG_DF = 1 << 2
|
||||
export const NBD_CMD_FLAG_REQ_ONE = 1 << 3
|
||||
export const NBD_CMD_FLAG_FAST_ZERO = 1 << 4
|
||||
|
||||
export const NBD_CMD_READ = 0
|
||||
export const NBD_CMD_WRITE = 1
|
||||
export const NBD_CMD_DISC = 2
|
||||
export const NBD_CMD_FLUSH = 3
|
||||
export const NBD_CMD_TRIM = 4
|
||||
export const NBD_CMD_CACHE = 5
|
||||
export const NBD_CMD_WRITE_ZEROES = 6
|
||||
export const NBD_CMD_BLOCK_STATUS = 7
|
||||
export const NBD_CMD_RESIZE = 8
|
||||
|
||||
export const NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
|
||||
export const NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
|
||||
export const NBD_REPLY_ACK = 1
|
||||
|
||||
export const NBD_DEFAULT_PORT = 10809
|
||||
export const NBD_DEFAULT_BLOCK_SIZE = 64 * 1024
|
||||
@@ -1,346 +0,0 @@
|
||||
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 { readChunkStrict } from '@vates/read-chunk'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import {
|
||||
INIT_PASSWD,
|
||||
NBD_CMD_READ,
|
||||
NBD_DEFAULT_BLOCK_SIZE,
|
||||
NBD_DEFAULT_PORT,
|
||||
NBD_FLAG_FIXED_NEWSTYLE,
|
||||
NBD_FLAG_HAS_FLAGS,
|
||||
NBD_OPT_EXPORT_NAME,
|
||||
NBD_OPT_REPLY_MAGIC,
|
||||
NBD_OPT_STARTTLS,
|
||||
NBD_REPLY_ACK,
|
||||
NBD_REPLY_MAGIC,
|
||||
NBD_REQUEST_MAGIC,
|
||||
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
|
||||
|
||||
export default class NbdClient {
|
||||
#serverAddress
|
||||
#serverCert
|
||||
#serverPort
|
||||
#serverSocket
|
||||
|
||||
#exportName
|
||||
#exportSize
|
||||
|
||||
#waitBeforeReconnect
|
||||
#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
|
||||
|
||||
#waitingForResponse // there is already a listenner waiting for a response
|
||||
#nextCommandQueryId = BigInt(0)
|
||||
#commandQueryBacklog // map of command waiting for an response queryId => { size/*in byte*/, resolve, reject}
|
||||
#connected = false
|
||||
|
||||
#reconnectingPromise
|
||||
constructor(
|
||||
{ address, port = NBD_DEFAULT_PORT, exportname, cert },
|
||||
{
|
||||
connectTimeout = 6e4,
|
||||
messageTimeout = 6e4,
|
||||
waitBeforeReconnect = 1e3,
|
||||
readBlockRetries = 5,
|
||||
reconnectRetry = 5,
|
||||
} = {}
|
||||
) {
|
||||
this.#serverAddress = address
|
||||
this.#serverPort = port
|
||||
this.#exportName = exportname
|
||||
this.#serverCert = cert
|
||||
this.#waitBeforeReconnect = waitBeforeReconnect
|
||||
this.#readBlockRetries = readBlockRetries
|
||||
this.#reconnectRetry = reconnectRetry
|
||||
this.#connectTimeout = connectTimeout
|
||||
this.#messageTimeout = messageTimeout
|
||||
}
|
||||
|
||||
get exportSize() {
|
||||
return this.#exportSize
|
||||
}
|
||||
|
||||
async #tlsConnect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#serverSocket = connect({
|
||||
socket: this.#serverSocket,
|
||||
rejectUnauthorized: false,
|
||||
cert: this.#serverCert,
|
||||
})
|
||||
this.#serverSocket.once('error', reject)
|
||||
this.#serverSocket.once('secureConnect', () => {
|
||||
this.#serverSocket.removeListener('error', reject)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// mandatory , at least to start the handshake
|
||||
async #unsecureConnect() {
|
||||
this.#serverSocket = new Socket()
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#serverSocket.connect(this.#serverPort, this.#serverAddress)
|
||||
this.#serverSocket.once('error', reject)
|
||||
this.#serverSocket.once('connect', () => {
|
||||
this.#serverSocket.removeListener('error', reject)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async #connect() {
|
||||
// first we connect to the server without tls, and then we upgrade the connection
|
||||
// to tls during the handshake
|
||||
await this.#unsecureConnect()
|
||||
await this.#handshake()
|
||||
this.#connected = true
|
||||
// reset internal state if we reconnected a nbd client
|
||||
this.#commandQueryBacklog = new Map()
|
||||
this.#waitingForResponse = false
|
||||
}
|
||||
async connect() {
|
||||
return pTimeout.call(this.#connect(), this.#connectTimeout)
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
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()
|
||||
}
|
||||
this.#serverSocket = undefined
|
||||
this.#connected = false
|
||||
}
|
||||
|
||||
#clearReconnectPromise = () => {
|
||||
this.#reconnectingPromise = undefined
|
||||
}
|
||||
|
||||
async #reconnect() {
|
||||
await this.disconnect().catch(() => {})
|
||||
await pDelay(this.#waitBeforeReconnect) // need to let the xapi clean things on its side
|
||||
await this.connect()
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
// we need to ensure reconnections do not occur in parallel
|
||||
if (this.#reconnectingPromise === undefined) {
|
||||
this.#reconnectingPromise = pRetry(() => this.#reconnect(), {
|
||||
tries: this.#reconnectRetry,
|
||||
})
|
||||
this.#reconnectingPromise.then(this.#clearReconnectPromise, this.#clearReconnectPromise)
|
||||
}
|
||||
|
||||
return this.#reconnectingPromise
|
||||
}
|
||||
|
||||
// we can use individual read/write from the socket here since there is no concurrency
|
||||
async #sendOption(option, buffer = Buffer.alloc(0)) {
|
||||
await this.#write(OPTS_MAGIC)
|
||||
await this.#writeInt32(option)
|
||||
await this.#writeInt32(buffer.length)
|
||||
await this.#write(buffer)
|
||||
assert.strictEqual(await this.#readInt64(), NBD_OPT_REPLY_MAGIC) // magic number everywhere
|
||||
assert.strictEqual(await this.#readInt32(), option) // the option passed
|
||||
assert.strictEqual(await this.#readInt32(), NBD_REPLY_ACK) // ACK
|
||||
const length = await this.#readInt32()
|
||||
assert.strictEqual(length, 0) // length
|
||||
}
|
||||
|
||||
// we can use individual read/write from the socket here since there is only one handshake at once, no concurrency
|
||||
async #handshake() {
|
||||
assert((await this.#read(8)).equals(INIT_PASSWD))
|
||||
assert((await this.#read(8)).equals(OPTS_MAGIC))
|
||||
const flagsBuffer = await this.#read(2)
|
||||
const flags = flagsBuffer.readInt16BE(0)
|
||||
assert.strictEqual(flags & NBD_FLAG_FIXED_NEWSTYLE, NBD_FLAG_FIXED_NEWSTYLE) // only FIXED_NEWSTYLE one is supported from the server options
|
||||
await this.#writeInt32(NBD_FLAG_FIXED_NEWSTYLE) // client also support NBD_FLAG_C_FIXED_NEWSTYLE
|
||||
|
||||
if (this.#serverCert !== undefined) {
|
||||
// upgrade socket to TLS if needed
|
||||
await this.#sendOption(NBD_OPT_STARTTLS)
|
||||
await this.#tlsConnect()
|
||||
}
|
||||
|
||||
// send export name we want to access.
|
||||
// it's implictly closing the negociation phase.
|
||||
await this.#write(OPTS_MAGIC)
|
||||
await this.#writeInt32(NBD_OPT_EXPORT_NAME)
|
||||
const exportNameBuffer = Buffer.from(this.#exportName)
|
||||
await this.#writeInt32(exportNameBuffer.length)
|
||||
await this.#write(exportNameBuffer)
|
||||
|
||||
// 8 (export size ) + 2 (flags) + 124 zero = 134
|
||||
// must read all to ensure nothing stays in the buffer
|
||||
const answer = await this.#read(134)
|
||||
this.#exportSize = answer.readBigUInt64BE(0)
|
||||
const transmissionFlags = answer.readInt16BE(8)
|
||||
assert.strictEqual(transmissionFlags & NBD_FLAG_HAS_FLAGS, NBD_FLAG_HAS_FLAGS, 'NBD_FLAG_HAS_FLAGS') // must always be 1 by the norm
|
||||
|
||||
// note : xapi server always send NBD_FLAG_READ_ONLY (3) as a flag
|
||||
}
|
||||
|
||||
#read(length) {
|
||||
const promise = readChunkStrict(this.#serverSocket, length)
|
||||
return pTimeout.call(promise, this.#messageTimeout)
|
||||
}
|
||||
|
||||
#write(buffer) {
|
||||
const promise = fromCallback.call(this.#serverSocket, 'write', buffer)
|
||||
return pTimeout.call(promise, this.#messageTimeout)
|
||||
}
|
||||
|
||||
async #readInt32() {
|
||||
const buffer = await this.#read(4)
|
||||
return buffer.readInt32BE(0)
|
||||
}
|
||||
|
||||
async #readInt64() {
|
||||
const buffer = await this.#read(8)
|
||||
return buffer.readBigUInt64BE(0)
|
||||
}
|
||||
|
||||
#writeInt32(int) {
|
||||
const buffer = Buffer.alloc(4)
|
||||
buffer.writeInt32BE(int)
|
||||
return this.#write(buffer)
|
||||
}
|
||||
|
||||
// when one read fail ,stop everything
|
||||
async #rejectAll(error) {
|
||||
this.#commandQueryBacklog.forEach(({ reject }) => {
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
async #readBlockResponse() {
|
||||
// ensure at most one read occur in parallel
|
||||
if (this.#waitingForResponse) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.#waitingForResponse = true
|
||||
const buffer = await this.#read(16)
|
||||
const magic = buffer.readInt32BE(0)
|
||||
|
||||
if (magic !== NBD_REPLY_MAGIC) {
|
||||
throw new Error(`magic number for block answer is wrong : ${magic} ${NBD_REPLY_MAGIC}`)
|
||||
}
|
||||
|
||||
const error = buffer.readInt32BE(4)
|
||||
if (error !== 0) {
|
||||
// @todo use error code from constants.mjs
|
||||
throw new Error(`GOT ERROR CODE : ${error}`)
|
||||
}
|
||||
|
||||
const blockQueryId = buffer.readBigUInt64BE(8)
|
||||
const query = this.#commandQueryBacklog.get(blockQueryId)
|
||||
if (!query) {
|
||||
throw new Error(` no query associated with id ${blockQueryId}`)
|
||||
}
|
||||
this.#commandQueryBacklog.delete(blockQueryId)
|
||||
const data = await this.#read(query.size)
|
||||
query.resolve(data)
|
||||
this.#waitingForResponse = false
|
||||
if (this.#commandQueryBacklog.size > 0) {
|
||||
// it doesn't throw directly but will throw all relevant promise on failure
|
||||
this.#readBlockResponse()
|
||||
}
|
||||
} catch (error) {
|
||||
// reject all the promises
|
||||
// we don't need to call readBlockResponse on failure
|
||||
// since we will empty the backlog
|
||||
await this.#rejectAll(error)
|
||||
}
|
||||
}
|
||||
|
||||
async #readBlock(index, size) {
|
||||
// we don't want to add anything in backlog while reconnecting
|
||||
if (this.#reconnectingPromise) {
|
||||
await this.#reconnectingPromise
|
||||
}
|
||||
|
||||
const queryId = this.#nextCommandQueryId
|
||||
this.#nextCommandQueryId++
|
||||
|
||||
// create and send command at once to ensure there is no concurrency issue
|
||||
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 simple block read
|
||||
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.writeInt32BE(size, 24)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
function decoratedReject(error) {
|
||||
error.index = index
|
||||
error.size = size
|
||||
reject(error)
|
||||
}
|
||||
|
||||
// this will handle one block response, but it can be another block
|
||||
// since server does not guaranty to handle query in order
|
||||
this.#commandQueryBacklog.set(queryId, {
|
||||
size,
|
||||
resolve,
|
||||
reject: decoratedReject,
|
||||
})
|
||||
// really send the command to the server
|
||||
this.#write(buffer).catch(decoratedReject)
|
||||
|
||||
// #readBlockResponse never throws directly
|
||||
// but if it fails it will reject all the promises in the backlog
|
||||
this.#readBlockResponse()
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/nbd-client",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/nbd-client",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/nbd-client",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "3.1.2",
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^18.7.0",
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test-integration": "tap --allow-incomplete-coverage"
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
Public Key Info:
|
||||
Public Key Algorithm: RSA
|
||||
Key Security Level: High (3072 bits)
|
||||
|
||||
modulus:
|
||||
00:be:92:be:df:de:0a:ab:38:fc:1a:c0:1a:58:4d:86
|
||||
b8:1f:25:10:7d:19:05:17:bf:02:3d:e9:ef:f8:c0:04
|
||||
5d:6f:98:de:5c:dd:c3:0f:e2:61:61:e4:b5:9c:42:ac
|
||||
3e:af:fd:30:10:e1:54:32:66:75:f6:80:90:85:05:a0
|
||||
6a:14:a2:6f:a7:2e:f0:f3:52:94:2a:f2:34:fc:0d:b4
|
||||
fb:28:5d:1c:11:5c:59:6e:63:34:ba:b3:fd:73:b1:48
|
||||
35:00:84:53:da:6a:9b:84:ab:64:b1:a1:2b:3a:d1:5a
|
||||
d7:13:7c:12:2a:4e:72:e9:96:d6:30:74:c5:71:05:14
|
||||
4b:2d:01:94:23:67:4e:37:3c:1e:c1:a0:bc:34:04:25
|
||||
21:11:fb:4b:6b:53:74:8f:90:93:57:af:7f:3b:78:d6
|
||||
a4:87:fe:7d:ed:20:11:8b:70:54:67:b8:c9:f5:c0:6b
|
||||
de:4e:e7:a5:79:ff:f7:ad:cf:10:57:f5:51:70:7b:54
|
||||
68:28:9e:b9:c2:10:7b:ab:aa:11:47:9f:ec:e6:2f:09
|
||||
44:4a:88:5b:dd:8c:10:b4:c4:03:25:06:d9:e0:9f:a0
|
||||
0d:cf:94:4b:3b:fa:a5:17:2c:e4:67:c4:17:6a:ab:d8
|
||||
c8:7a:16:41:b9:91:b7:9c:ae:8c:94:be:26:61:51:71
|
||||
c1:a6:39:39:97:75:28:a9:0e:21:ea:f0:bd:71:4a:8c
|
||||
e1:f8:1d:a9:22:2f:10:a8:1b:e5:a4:9a:fd:0f:fa:c6
|
||||
20:bc:96:99:79:c6:ba:a4:1f:3e:d4:91:c5:af:bb:71
|
||||
0a:5a:ef:69:9c:64:69:ce:5a:fe:3f:c2:24:f4:26:d4
|
||||
3d:ab:ab:9a:f0:f6:f1:b1:64:a9:f4:e2:34:6a:ab:2e
|
||||
95:47:b9:07:5a:39:c6:95:9c:a9:e8:ed:71:dd:c1:21
|
||||
16:c8:2d:4c:2c:af:06:9d:c6:fa:fe:c5:2a:6c:b4:c3
|
||||
d5:96:fc:5e:fd:ec:1c:30:b4:9d:cb:29:ef:a8:50:1c
|
||||
21:
|
||||
|
||||
public exponent:
|
||||
01:00:01:
|
||||
|
||||
private exponent:
|
||||
25:37:c5:7d:35:01:02:65:73:9e:c9:cb:9b:59:30:a9
|
||||
3e:b3:df:5f:7f:06:66:97:d0:19:45:59:af:4b:d8:ce
|
||||
62:a0:09:35:3b:bd:ff:99:27:89:95:bf:fe:0f:6b:52
|
||||
26:ce:9c:97:7f:5a:11:29:bf:79:ef:ab:c9:be:ca:90
|
||||
4d:0d:58:1e:df:65:01:30:2c:6d:a2:b5:c4:4f:ec:fb
|
||||
6b:eb:9b:32:ac:c5:6e:70:83:78:be:f4:0d:a7:1e:c1
|
||||
f3:22:e4:b9:70:3e:85:0f:6f:ef:dc:d8:f3:78:b5:73
|
||||
f1:83:36:8c:fa:9b:28:91:63:ad:3c:f0:de:5c:ae:94
|
||||
eb:ea:36:03:20:06:bf:74:c7:50:eb:52:36:1a:65:21
|
||||
eb:40:17:7f:93:61:dd:33:d0:02:bc:ec:6d:31:f1:41
|
||||
5a:a9:d1:f0:00:66:4c:c4:18:47:d5:67:e3:cd:bb:83
|
||||
44:07:ab:62:83:21:dc:d8:e6:89:37:08:bb:9d:ea:62
|
||||
c2:5d:ce:85:c2:dc:48:27:0c:a4:23:61:b7:30:e7:26
|
||||
44:dc:1e:5c:2e:16:35:2b:2e:a6:e6:a4:ce:1f:9b:e9
|
||||
fe:96:fa:49:1d:fb:2a:df:bc:bf:46:da:52:f8:37:8a
|
||||
84:ab:e4:73:e6:46:56:b5:b4:3d:e1:63:eb:02:8e:d7
|
||||
67:96:c4:dc:28:6d:6b:b6:0c:a3:0b:db:87:29:ad:f9
|
||||
ec:73:b6:55:a3:40:32:13:84:c7:2f:33:74:04:dc:42
|
||||
00:11:9c:fb:fc:62:35:b3:82:c3:3c:28:80:e8:09:a8
|
||||
97:c7:c1:2e:3d:27:fa:4f:9b:fc:c2:34:58:41:5c:a1
|
||||
e2:70:2e:2f:82:ad:bd:bd:8e:dd:23:12:25:de:89:70
|
||||
60:75:48:90:80:ac:55:74:51:6f:49:9e:7f:63:41:8b
|
||||
3c:b1:f5:c3:6b:4b:5a:50:a6:4d:38:e8:82:c2:04:c8
|
||||
30:fd:06:9b:c1:04:27:b6:63:3a:5e:f5:4d:00:c3:d1
|
||||
|
||||
|
||||
prime1:
|
||||
00:f6:00:2e:7d:89:61:24:16:5e:87:ca:18:6c:03:b8
|
||||
b4:33:df:4a:a7:7f:db:ed:39:15:41:12:61:4f:4e:b4
|
||||
de:ab:29:d9:0c:6c:01:7e:53:2e:ee:e7:5f:a2:e4:6d
|
||||
c6:4b:07:4e:d8:a3:ae:45:06:97:bd:18:a3:e9:dd:29
|
||||
54:64:6d:f0:af:08:95:ae:ae:3e:71:63:76:2a:a1:18
|
||||
c4:b1:fc:bc:3d:42:15:74:b3:c5:38:1f:5d:92:f1:b2
|
||||
c6:3f:10:fe:35:1a:c6:b1:ce:70:38:ff:08:5c:de:61
|
||||
79:c7:50:91:22:4d:e9:c8:18:49:e2:5c:91:84:86:e2
|
||||
4d:0f:6e:9b:0d:81:df:aa:f3:59:75:56:e9:33:18:dd
|
||||
ab:39:da:e2:25:01:05:a1:6e:23:59:15:2c:89:35:c7
|
||||
ae:9c:c7:ea:88:9a:1a:f3:48:07:11:82:59:79:8c:62
|
||||
53:06:37:30:14:b3:82:b1:50:fc:ae:b8:f7:1c:57:44
|
||||
7d:
|
||||
|
||||
prime2:
|
||||
00:c6:51:cc:dc:88:2e:cf:98:90:10:19:e0:d3:a4:d1
|
||||
3f:dc:b0:29:d3:bb:26:ee:eb:00:17:17:d1:d1:bb:9b
|
||||
34:b1:4e:af:b5:6c:1c:54:53:b4:bb:55:da:f7:78:cd
|
||||
38:b4:2e:3a:8c:63:80:3b:64:9c:b4:2b:cd:dd:50:0b
|
||||
05:d2:00:7a:df:8e:c3:e6:29:e0:9c:d8:40:b7:11:09
|
||||
f4:38:df:f6:ed:93:1e:18:d4:93:fa:8d:ee:82:9c:0f
|
||||
c1:88:26:84:9d:4f:ae:8a:17:d5:55:54:4c:c6:0a:ac
|
||||
4d:ec:33:51:68:0f:4b:92:2e:04:57:fe:15:f5:00:46
|
||||
5c:8e:ad:09:2c:e7:df:d5:36:7a:4e:bd:da:21:22:d7
|
||||
58:b4:72:93:94:af:34:cc:e2:b8:d0:4f:0b:5d:97:08
|
||||
12:19:17:34:c5:15:49:00:48:56:13:b8:45:4e:3b:f8
|
||||
bc:d5:ab:d9:6d:c2:4a:cc:01:1a:53:4d:46:50:49:3b
|
||||
75:
|
||||
|
||||
coefficient:
|
||||
63:67:50:29:10:6a:85:a3:dc:51:90:20:76:86:8c:83
|
||||
8e:d5:ff:aa:75:fd:b5:f8:31:b0:96:6c:18:1d:5b:ed
|
||||
a4:2e:47:8d:9c:c2:1e:2c:a8:6d:4b:10:a5:c2:53:46
|
||||
8a:9a:84:91:d7:fc:f5:cc:03:ce:b9:3d:5c:01:d2:27
|
||||
99:7b:79:89:4f:a1:12:e3:05:5d:ee:10:f6:8c:e6:ce
|
||||
5e:da:32:56:6d:6f:eb:32:b4:75:7b:94:49:d8:2d:9e
|
||||
4d:19:59:2e:e4:0b:bc:95:df:df:65:67:a1:dd:c6:2b
|
||||
99:f4:76:e8:9f:fa:57:1d:ca:f9:58:a9:ce:9b:30:5c
|
||||
42:8a:ba:05:e7:e2:15:45:25:bc:e9:68:c1:8b:1a:37
|
||||
cc:e1:aa:45:2e:94:f5:81:47:1e:64:7f:c0:c1:b7:a8
|
||||
21:58:18:a9:a0:ed:e0:27:75:bf:65:81:6b:e4:1d:5a
|
||||
b7:7e:df:d8:28:c6:36:21:19:c8:6e:da:ca:9e:da:84
|
||||
|
||||
|
||||
exp1:
|
||||
00:ba:d7:fe:77:a9:0d:98:2c:49:56:57:c0:5e:e2:20
|
||||
ba:f6:1f:26:03:bc:d0:5d:08:9b:45:16:61:c4:ab:e2
|
||||
22:b1:dc:92:17:a6:3d:28:26:a4:22:1e:a8:7b:ff:86
|
||||
05:33:5d:74:9c:85:0d:cb:2d:ab:b8:9b:6b:7c:28:57
|
||||
c8:da:92:ca:59:17:6b:21:07:05:34:78:37:fb:3e:ea
|
||||
a2:13:12:04:23:7e:fa:ee:ed:cf:e0:c5:a9:fb:ff:0a
|
||||
2b:1b:21:9c:02:d7:b8:8c:ba:60:70:59:fc:8f:14:f4
|
||||
f2:5a:d9:ad:b2:61:7d:2c:56:8e:5f:98:b1:89:f8:2d
|
||||
10:1c:a5:84:ad:28:b4:aa:92:34:a3:34:04:e1:a3:84
|
||||
52:16:1a:52:e3:8a:38:2d:99:8a:cd:91:90:87:12:ca
|
||||
fc:ab:e6:08:14:03:00:6f:41:88:e4:da:9d:7c:fd:8c
|
||||
7c:c4:de:cb:ed:1d:3f:29:d0:7a:6b:76:df:71:ae:32
|
||||
bd:
|
||||
|
||||
exp2:
|
||||
4a:e9:d3:6c:ea:b4:64:0e:c9:3c:8b:c9:f5:a8:a8:b2
|
||||
6a:f6:d0:95:fe:78:32:7f:ea:c4:ce:66:9f:c7:32:55
|
||||
b1:34:7c:03:18:17:8b:73:23:2e:30:bc:4a:07:03:de
|
||||
8b:91:7a:e4:55:21:b7:4d:c6:33:f8:e8:06:d5:99:94
|
||||
55:43:81:26:b9:93:1e:7a:6b:32:54:2d:fd:f9:1d:bd
|
||||
77:4e:82:c4:33:72:87:06:a5:ef:5b:75:e1:38:7a:6b
|
||||
2c:b7:00:19:3c:64:3e:1d:ca:a4:34:f7:db:47:64:d6
|
||||
fa:86:58:15:ea:d1:2d:22:dc:d9:30:4d:b3:02:ab:91
|
||||
83:03:b2:17:98:6f:60:e6:f7:44:8f:4a:ba:81:a2:bf
|
||||
0b:4a:cc:9c:b9:a2:44:52:d0:65:3f:b6:97:5f:d9:d8
|
||||
9c:49:bb:d1:46:bd:10:b2:42:71:a8:85:e5:8b:99:e6
|
||||
1b:00:93:5d:76:ab:32:6c:a8:39:17:53:9c:38:4d:91
|
||||
|
||||
|
||||
|
||||
Public Key PIN:
|
||||
pin-sha256:ISh/UeFjUG5Gwrpx6hMUGQPvg9wOKjOkHmRbs4YjZqs=
|
||||
Public Key ID:
|
||||
sha256:21287f51e163506e46c2ba71ea13141903ef83dc0e2a33a41e645bb3862366ab
|
||||
sha1:1a48455111ac45fb5807c5cdb7b20b896c52f0b6
|
||||
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIG4wIBAAKCAYEAvpK+394Kqzj8GsAaWE2GuB8lEH0ZBRe/Aj3p7/jABF1vmN5c
|
||||
3cMP4mFh5LWcQqw+r/0wEOFUMmZ19oCQhQWgahSib6cu8PNSlCryNPwNtPsoXRwR
|
||||
XFluYzS6s/1zsUg1AIRT2mqbhKtksaErOtFa1xN8EipOcumW1jB0xXEFFEstAZQj
|
||||
Z043PB7BoLw0BCUhEftLa1N0j5CTV69/O3jWpIf+fe0gEYtwVGe4yfXAa95O56V5
|
||||
//etzxBX9VFwe1RoKJ65whB7q6oRR5/s5i8JREqIW92MELTEAyUG2eCfoA3PlEs7
|
||||
+qUXLORnxBdqq9jIehZBuZG3nK6MlL4mYVFxwaY5OZd1KKkOIerwvXFKjOH4Haki
|
||||
LxCoG+Wkmv0P+sYgvJaZeca6pB8+1JHFr7txClrvaZxkac5a/j/CJPQm1D2rq5rw
|
||||
9vGxZKn04jRqqy6VR7kHWjnGlZyp6O1x3cEhFsgtTCyvBp3G+v7FKmy0w9WW/F79
|
||||
7BwwtJ3LKe+oUBwhAgMBAAECggGAJTfFfTUBAmVznsnLm1kwqT6z319/BmaX0BlF
|
||||
Wa9L2M5ioAk1O73/mSeJlb/+D2tSJs6cl39aESm/ee+ryb7KkE0NWB7fZQEwLG2i
|
||||
tcRP7Ptr65syrMVucIN4vvQNpx7B8yLkuXA+hQ9v79zY83i1c/GDNoz6myiRY608
|
||||
8N5crpTr6jYDIAa/dMdQ61I2GmUh60AXf5Nh3TPQArzsbTHxQVqp0fAAZkzEGEfV
|
||||
Z+PNu4NEB6tigyHc2OaJNwi7nepiwl3OhcLcSCcMpCNhtzDnJkTcHlwuFjUrLqbm
|
||||
pM4fm+n+lvpJHfsq37y/RtpS+DeKhKvkc+ZGVrW0PeFj6wKO12eWxNwobWu2DKML
|
||||
24cprfnsc7ZVo0AyE4THLzN0BNxCABGc+/xiNbOCwzwogOgJqJfHwS49J/pPm/zC
|
||||
NFhBXKHicC4vgq29vY7dIxIl3olwYHVIkICsVXRRb0mef2NBizyx9cNrS1pQpk04
|
||||
6ILCBMgw/QabwQQntmM6XvVNAMPRAoHBAPYALn2JYSQWXofKGGwDuLQz30qnf9vt
|
||||
ORVBEmFPTrTeqynZDGwBflMu7udfouRtxksHTtijrkUGl70Yo+ndKVRkbfCvCJWu
|
||||
rj5xY3YqoRjEsfy8PUIVdLPFOB9dkvGyxj8Q/jUaxrHOcDj/CFzeYXnHUJEiTenI
|
||||
GEniXJGEhuJND26bDYHfqvNZdVbpMxjdqzna4iUBBaFuI1kVLIk1x66cx+qImhrz
|
||||
SAcRgll5jGJTBjcwFLOCsVD8rrj3HFdEfQKBwQDGUczciC7PmJAQGeDTpNE/3LAp
|
||||
07sm7usAFxfR0bubNLFOr7VsHFRTtLtV2vd4zTi0LjqMY4A7ZJy0K83dUAsF0gB6
|
||||
347D5ingnNhAtxEJ9Djf9u2THhjUk/qN7oKcD8GIJoSdT66KF9VVVEzGCqxN7DNR
|
||||
aA9Lki4EV/4V9QBGXI6tCSzn39U2ek692iEi11i0cpOUrzTM4rjQTwtdlwgSGRc0
|
||||
xRVJAEhWE7hFTjv4vNWr2W3CSswBGlNNRlBJO3UCgcEAutf+d6kNmCxJVlfAXuIg
|
||||
uvYfJgO80F0Im0UWYcSr4iKx3JIXpj0oJqQiHqh7/4YFM110nIUNyy2ruJtrfChX
|
||||
yNqSylkXayEHBTR4N/s+6qITEgQjfvru7c/gxan7/worGyGcAte4jLpgcFn8jxT0
|
||||
8lrZrbJhfSxWjl+YsYn4LRAcpYStKLSqkjSjNATho4RSFhpS44o4LZmKzZGQhxLK
|
||||
/KvmCBQDAG9BiOTanXz9jHzE3svtHT8p0Hprdt9xrjK9AoHASunTbOq0ZA7JPIvJ
|
||||
9aiosmr20JX+eDJ/6sTOZp/HMlWxNHwDGBeLcyMuMLxKBwPei5F65FUht03GM/jo
|
||||
BtWZlFVDgSa5kx56azJULf35Hb13ToLEM3KHBqXvW3XhOHprLLcAGTxkPh3KpDT3
|
||||
20dk1vqGWBXq0S0i3NkwTbMCq5GDA7IXmG9g5vdEj0q6gaK/C0rMnLmiRFLQZT+2
|
||||
l1/Z2JxJu9FGvRCyQnGoheWLmeYbAJNddqsybKg5F1OcOE2RAoHAY2dQKRBqhaPc
|
||||
UZAgdoaMg47V/6p1/bX4MbCWbBgdW+2kLkeNnMIeLKhtSxClwlNGipqEkdf89cwD
|
||||
zrk9XAHSJ5l7eYlPoRLjBV3uEPaM5s5e2jJWbW/rMrR1e5RJ2C2eTRlZLuQLvJXf
|
||||
32Vnod3GK5n0duif+lcdyvlYqc6bMFxCiroF5+IVRSW86WjBixo3zOGqRS6U9YFH
|
||||
HmR/wMG3qCFYGKmg7eAndb9lgWvkHVq3ft/YKMY2IRnIbtrKntqE
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,178 +0,0 @@
|
||||
import { spawn, exec } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import { test } from 'tap'
|
||||
import tmp from 'tmp'
|
||||
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
|
||||
|
||||
async function createTempFile(size) {
|
||||
const tmpPath = await pFromCallback(cb => tmp.file(cb))
|
||||
const data = Buffer.alloc(size, 0)
|
||||
for (let i = 0; i < size; i += 4) {
|
||||
data.writeUInt32BE(i, i)
|
||||
}
|
||||
await fs.writeFile(tmpPath, data)
|
||||
|
||||
return tmpPath
|
||||
}
|
||||
|
||||
async function spawnNbdKit(path) {
|
||||
let tries = 5
|
||||
// wait for server to be ready
|
||||
|
||||
const nbdServer = spawn(
|
||||
'nbdkit',
|
||||
[
|
||||
'file',
|
||||
path,
|
||||
'--newstyle', //
|
||||
'--exit-with-parent',
|
||||
'--read-only',
|
||||
'--export-name=MY_SECRET_EXPORT',
|
||||
'--tls=on',
|
||||
'--tls-certificates=./tests/',
|
||||
// '--tls-verify-peer',
|
||||
// '--verbose',
|
||||
'--exit-with-parent',
|
||||
],
|
||||
{
|
||||
stdio: ['inherit', 'inherit', 'inherit'],
|
||||
}
|
||||
)
|
||||
nbdServer.on('error', err => {
|
||||
console.error(err)
|
||||
})
|
||||
do {
|
||||
try {
|
||||
const socket = new Socket()
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.connect(NBD_DEFAULT_PORT, 'localhost')
|
||||
socket.once('error', reject)
|
||||
socket.once('connect', resolve)
|
||||
})
|
||||
socket.destroy()
|
||||
break
|
||||
} catch (err) {
|
||||
tries--
|
||||
if (tries <= 0) {
|
||||
throw err
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
} while (true)
|
||||
return nbdServer
|
||||
}
|
||||
|
||||
async function killNbdKit() {
|
||||
return new Promise((resolve, reject) =>
|
||||
exec('pkill -9 -f -o nbdkit', err => {
|
||||
err ? reject(err) : resolve()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
test('it works secured 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-----
|
||||
MIIDazCCAlOgAwIBAgIUeHpQ0IeD6BmP2zgsv3LV3J4BI/EwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MTcxMzU1MzBaFw0yNDA1
|
||||
MTYxMzU1MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQC/8wLopj/iZY6ijmpvgCJsl+zY0hQZQcIoaCs0H75u
|
||||
8PPSzHedtOLURAkJeMmIS40UY/eIvHh7yZolevaSJLNT2Iolscvc2W9NCF4N1V6y
|
||||
zs4pDzP+YPF7Q8ldNaQIX0bAk4PfaMSM+pLh67u+uI40732AfQqD01BNCTD/uHRB
|
||||
lKnQuqQpe9UM9UzRRVejpu1r19D4dJruAm6y2SJVTeT4a1sSJixl6I1YPmt80FJh
|
||||
gq9O2KRGbXp1xIjemWgW99MHg63pTgxEiULwdJOGgmqGRDzgZKJS5UUpxe/ViEO4
|
||||
59I18vIkgibaRYhENgmnP3lIzTOLlUe07tbSML5RGBbBAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBR/8+zYoL0H0LdWfULHg1LynFdSbzAfBgNVHSMEGDAWgBR/8+zYoL0H0LdW
|
||||
fULHg1LynFdSbzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBD
|
||||
OF5bTmbDEGoZ6OuQaI0vyya/T4FeaoWmh22gLeL6dEEmUVGJ1NyMTOvG9GiGJ8OM
|
||||
QhD1uHJei45/bXOYIDGey2+LwLWye7T4vtRFhf8amYh0ReyP/NV4/JoR/U3pTSH6
|
||||
tns7GZ4YWdwUhvOOlm17EQKVO/hP3t9mp74gcjdL4bCe5MYSheKuNACAakC1OR0U
|
||||
ZakJMP9ijvQuq8spfCzrK+NbHKNHR9tEgQw+ax/t1Au4dGVtFbcoxqCrx2kTl0RP
|
||||
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,
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
tap.equal(client.exportSize, BigInt(FILE_SIZE))
|
||||
const indexes = []
|
||||
for (let i = 0; i < FILE_SIZE / CHUNK_SIZE; i++) {
|
||||
indexes.push(i)
|
||||
}
|
||||
const nbdIterator = client.readBlocks(function* () {
|
||||
for (const index of indexes) {
|
||||
yield { index, size: CHUNK_SIZE }
|
||||
}
|
||||
})
|
||||
let i = 0
|
||||
for await (const block of nbdIterator) {
|
||||
let blockOk = block.length === Math.min(CHUNK_SIZE, FILE_SIZE - CHUNK_SIZE * i)
|
||||
let firstFail
|
||||
for (let j = 0; j < block.length; j += 4) {
|
||||
const wanted = i * CHUNK_SIZE + j
|
||||
const found = block.readUInt32BE(j)
|
||||
blockOk = blockOk && found === wanted
|
||||
if (!blockOk && firstFail === undefined) {
|
||||
firstFail = j
|
||||
}
|
||||
}
|
||||
tap.ok(blockOk, `check block ${i} content ${block.length}`)
|
||||
i++
|
||||
|
||||
// flaky server is flaky
|
||||
if (i % 7 === 0) {
|
||||
// kill the older nbdkit process
|
||||
await killNbdKit()
|
||||
nbdServer = await spawnNbdKit(path)
|
||||
}
|
||||
}
|
||||
assert.rejects(() => client.readBlock(100, CHUNK_SIZE))
|
||||
|
||||
await client.disconnect()
|
||||
// double disconnection shouldn't pose any problem
|
||||
await client.disconnect()
|
||||
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())
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIUeHpQ0IeD6BmP2zgsv3LV3J4BI/EwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MTcxMzU1MzBaFw0yNDA1
|
||||
MTYxMzU1MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQC/8wLopj/iZY6ijmpvgCJsl+zY0hQZQcIoaCs0H75u
|
||||
8PPSzHedtOLURAkJeMmIS40UY/eIvHh7yZolevaSJLNT2Iolscvc2W9NCF4N1V6y
|
||||
zs4pDzP+YPF7Q8ldNaQIX0bAk4PfaMSM+pLh67u+uI40732AfQqD01BNCTD/uHRB
|
||||
lKnQuqQpe9UM9UzRRVejpu1r19D4dJruAm6y2SJVTeT4a1sSJixl6I1YPmt80FJh
|
||||
gq9O2KRGbXp1xIjemWgW99MHg63pTgxEiULwdJOGgmqGRDzgZKJS5UUpxe/ViEO4
|
||||
59I18vIkgibaRYhENgmnP3lIzTOLlUe07tbSML5RGBbBAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBR/8+zYoL0H0LdWfULHg1LynFdSbzAfBgNVHSMEGDAWgBR/8+zYoL0H0LdW
|
||||
fULHg1LynFdSbzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBD
|
||||
OF5bTmbDEGoZ6OuQaI0vyya/T4FeaoWmh22gLeL6dEEmUVGJ1NyMTOvG9GiGJ8OM
|
||||
QhD1uHJei45/bXOYIDGey2+LwLWye7T4vtRFhf8amYh0ReyP/NV4/JoR/U3pTSH6
|
||||
tns7GZ4YWdwUhvOOlm17EQKVO/hP3t9mp74gcjdL4bCe5MYSheKuNACAakC1OR0U
|
||||
ZakJMP9ijvQuq8spfCzrK+NbHKNHR9tEgQw+ax/t1Au4dGVtFbcoxqCrx2kTl0RP
|
||||
CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
|
||||
57ABhfYpMlZ4aHjuN1bL
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/8wLopj/iZY6i
|
||||
jmpvgCJsl+zY0hQZQcIoaCs0H75u8PPSzHedtOLURAkJeMmIS40UY/eIvHh7yZol
|
||||
evaSJLNT2Iolscvc2W9NCF4N1V6yzs4pDzP+YPF7Q8ldNaQIX0bAk4PfaMSM+pLh
|
||||
67u+uI40732AfQqD01BNCTD/uHRBlKnQuqQpe9UM9UzRRVejpu1r19D4dJruAm6y
|
||||
2SJVTeT4a1sSJixl6I1YPmt80FJhgq9O2KRGbXp1xIjemWgW99MHg63pTgxEiULw
|
||||
dJOGgmqGRDzgZKJS5UUpxe/ViEO459I18vIkgibaRYhENgmnP3lIzTOLlUe07tbS
|
||||
ML5RGBbBAgMBAAECggEATLYiafcTHfgnZmjTOad0WoDnC4n9tVBV948WARlUooLS
|
||||
duL3RQRHCLz9/ZaTuFA1XDpNcYyc/B/IZoU7aJGZR3+JSmJBjowpUphu+klVNNG4
|
||||
i6lDRrzYlUI0hfdLjHsDTDBIKi91KcB0lix/VkvsrVQvDHwsiR2ZAIiVWAWQFKrR
|
||||
5O3DhSTHbqyq47uR58rWr4Zf3zvZaUl841AS1yELzCiZqz7AenvyWphim0c0XA5d
|
||||
I63CEShntHnEAA9OMcP8+BNf/3AmqB4welY+m8elB3aJNH+j7DKq/AWqaM5nl2PC
|
||||
cS6qgpxwOyTxEOyj1xhwK5ZMRR3heW3NfutIxSOPlwKBgQDB9ZkrBeeGVtCISO7C
|
||||
eCANzSLpeVrahTvaCSQLdPHsLRLDUc+5mxdpi3CaRlzYs3S1OWdAtyWX9mBryltF
|
||||
qDPhCNjFDyHok4D3wLEWdS9oUVwEKUM8fOPW3tXLLiMM7p4862Qo7LqnqHzPqsnz
|
||||
22iZo5yjcc7aLJ+VmFrbAowwOwKBgQD9WNCvczTd7Ymn7zEvdiAyNoS0OZ0orwEJ
|
||||
zGaxtjqVguGklNfrb/UB+eKNGE80+YnMiSaFc9IQPetLntZdV0L7kWYdCI8kGDNA
|
||||
DbVRCOp+z8DwAojlrb/zsYu23anQozT3WeHxVU66lNuyEQvSW2tJa8gN1htrD7uY
|
||||
5KLibYrBMwKBgEM0iiHyJcrSgeb2/mO7o7+keJhVSDm3OInP6QFfQAQJihrLWiKB
|
||||
rpcPjbCm+LzNUX8JqNEvpIMHB1nR/9Ye9frfSdzd5W3kzicKSVHywL5wkmWOtpFa
|
||||
5Mcq5wFDtzlf5MxO86GKhRJauwRptRgdyhySKFApuva1x4XaCIEiXNjJAoGBAN82
|
||||
t3c+HCBEv3o05rMYcrmLC1T3Rh6oQlPtwbVmByvfywsFEVCgrc/16MPD3VWhXuXV
|
||||
GRmPuE8THxLbead30M5xhvShq+xzXgRbj5s8Lc9ZIHbW5OLoOS1vCtgtaQcoJOyi
|
||||
Rs4pCVqe+QpktnO6lEZ2Libys+maTQEiwNibBxu9AoGAUG1V5aKMoXa7pmGeuFR6
|
||||
ES+1NDiCt6yDq9BsLZ+e2uqvWTkvTGLLwvH6xf9a0pnnILd0AUTKAAaoUdZS6++E
|
||||
cGob7fxMwEE+UETp0QBgLtfjtExMOFwr2avw8PV4CYEUkPUAm2OFB2Twh+d/PNfr
|
||||
FAxF1rN47SBPNbFI8N4TFsg=
|
||||
-----END PRIVATE KEY-----
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user