Compare commits

..

3 Commits

Author SHA1 Message Date
Julien Fontanet
6162ecff04 use passport-http 2019-12-06 17:57:08 +01:00
Julien Fontanet
19d859e047 feat(xo-server): support HTTP basic auth
See https://xcp-ng.org/forum/topic/2316/http-authentication
2019-12-06 12:32:06 +01:00
Julien Fontanet
049d62873e feat(xo-server): passport strategies can be unregistered
Now all authentication plugins can be unloaded.
2019-12-06 12:29:28 +01:00
5211 changed files with 84311 additions and 486015 deletions

View File

@@ -1 +0,0 @@
{ "extends": ["@commitlint/config-conventional"] }

View File

@@ -1,7 +1,13 @@
'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',
'prettier/standard',
'prettier/react',
],
globals: {
__DEV__: true,
$Dict: true,
@@ -15,318 +21,19 @@ module.exports = {
overrides: [
{
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js', '**/scripts/**.{,c,m}js'],
files: ['cli.js', '*-cli.js', '**/*cli*/**/*.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/extensions': [
'error',
'ignorePackages',
{
pattern: { ts: 'always', vue: 'always' },
checkTypeImports: true,
// '@core/*' and '@/*' look like scoped packages to the plugin, which 'ignorePackages' would skip
pathGroupOverrides: [
{ pattern: '@core/packages/*', action: 'ignore' }, // barrel files
{ pattern: '@core/icons', action: 'ignore' },
{ pattern: '@core/**', action: 'enforce' },
{ pattern: '@/**', action: 'enforce' },
],
},
],
'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-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}',
'@xen-orchestra/web/**/*.{vue,ts}',
'@xen-orchestra/web-core/**/*.{vue,ts,json}',
],
extends: ['plugin:@intlify/vue-i18n/base'],
settings: {
'vue-i18n': {
localeDir: ['@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',
'Xen Orchestra Appliance',
'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',
'es',
'sv',
'it',
'ru',
'uk',
'nl',
'pt-BR',
'da',
'ko',
'nb-NO',
'ja',
'fi',
'pl',
'zh-Hans',
'sk',
'pt',
'ar',
'tr',
],
},
],
},
},
// 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}/src/route-map.d.ts'],
parserOptions: {
sourceType: 'module',
},
rules: {
'eslint-comments/disable-enable-pair': 'off',
'eslint-comments/no-unlimited-disable': 'off',
},
},
{
files: ['*.d.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
},
rules: {
'n/no-unsupported-features/es-syntax': 'off',
'no-unused-vars': 'off',
},
},
],
ignorePatterns: ['@vates/fatfs/'],
parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 13,
sourceType: 'script',
ecmaFeatures: {
legacyDecorators: true,
},
},
rules: {
// disabled because XAPI objects are using camel case
camelcase: ['off'],
@@ -341,12 +48,9 @@ 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',
'no-var': 'error',
'node/no-extraneous-import': 'error',
'node/no-extraneous-require': 'error',
'prefer-const': 'error',
},
}

16
.flowconfig Normal file
View 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]

View File

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

View File

@@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,36 +0,0 @@
name: Changelog
# only pull requests targeting `master`: the check compares the branch to
# `origin/master`, it would report unrelated packages on a PR based on another
# branch
on:
pull_request:
branches:
- master
jobs:
changed-packages:
# the release PR bumps every listed package and empties the list
if: github.head_ref != 'release'
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- uses: actions/checkout@v4
with:
# the whole history is required to compute the merge base with `master`
fetch-depth: 0
# the check only needs commits and trees: `git diff --name-only
# --no-renames` compares tree entries and never reads file contents,
# skipping the historical blobs halves the fetch (~112 MiB → ~53 MiB)
filter: blob:none
# https://github.com/actions/setup-node
- name: Setup Node environment
uses: actions/setup-node@v4
with:
node-version: '>=22.12'
# the script only uses Node builtins, no `yarn install` necessary
- name: Ensure changed packages are listed in CHANGELOG.unreleased.md
run: ./scripts/check-changed-pkgs.js

View File

@@ -1,50 +0,0 @@
name: Continuous 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 open-iscsi
# 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: '>=22.12'
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

View File

@@ -1,73 +0,0 @@
name: Vectura
# only when vectura changes: the rest of the repository does not build it
#
# pull requests rather than every push: GitHub filters a push on the diff with
# the previous push, which after a rebase on `master` (or a merge of it)
# contains the vectura changes of `master`, whereas it filters a pull request
# on the diff with the merge base, i.e. the changes of the branch only
on:
pull_request:
paths:
- '@xen-orchestra/vmware-explorer/vectura/**'
- .github/workflows/vectura.yml
push:
branches:
- master
paths:
- '@xen-orchestra/vmware-explorer/vectura/**'
- .github/workflows/vectura.yml
defaults:
run:
working-directory: '@xen-orchestra/vmware-explorer/vectura'
jobs:
checks:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- uses: actions/checkout@v4
# rustup, on the runner image, installs the toolchain rust-toolchain.toml
# names on the first cargo command
- run: cargo fmt --all --check
- run: cargo clippy --all-targets --locked -- -D warnings
- run: cargo test --locked
# https://github.com/taiki-e/install-action
- uses: taiki-e/install-action@v2
with:
tool: cargo-deny@0.20.2,cargo-llvm-cov@0.8.5
- run: cargo deny --locked check
- run: cargo llvm-cov --locked --json --summary-only --output-path target/coverage.json
- name: Hold the coverage floor
run: cargo test --locked --test coverage -- --ignored
# builds the binary as release.sh does, checks it, then checks the one in git,
# which xo-server runs, is that build
binary:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- uses: actions/checkout@v4
# https://github.com/mlugg/setup-zig
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
# https://github.com/taiki-e/install-action
- uses: taiki-e/install-action@v2
with:
tool: cargo-zigbuild@0.23.4
- run: scripts/build.sh "$RUNNER_TEMP/build"
- run: scripts/check-binary.sh "$RUNNER_TEMP/build/vectura"
- name: The binary in git is the build of these sources
run: |
cmp "$RUNNER_TEMP/build/vectura" vectura || {
echo '::error::vectura is not the build of these sources: run scripts/build.sh . in vectura/ and commit the binary'
exit 1
}

23
.gitignore vendored
View File

@@ -1,25 +1,23 @@
/_book/
/coverage/
/node_modules/
/lerna-debug.log
/lerna-debug.log.*
/@vates/*/dist/
/@vates/*/node_modules/
/@vates/node-vsphere-soap/test/config-test.mjs
/@xen-orchestra/*/dist/
/@xen-orchestra/*/node_modules/
/@xen-orchestra/rest-api/open-api/spec/swagger.json
/@xen-orchestra/rest-api/src/open-api/routes/routes.ts
/packages/*/dist/
/packages/*/node_modules/
/packages/vhd-cli/src/commands/index.js
/packages/xen-api/examples/node_modules/
/packages/xen-api/plot.dat
/packages/xo-server/.xo-server.*
/packages/xo-server/src/api/index.mjs
/packages/xo-server/src/xapi/mixins/index.mjs
/packages/xo-server/src/xo-mixins/index.mjs
/packages/xo-server/src/api/index.js
/packages/xo-server/src/xapi/mixins/index.js
/packages/xo-server/src/xo-mixins/index.js
/packages/xo-server-auth-ldap/ldap.cache.conf
@@ -33,12 +31,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/

View File

@@ -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

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@@ -1,11 +0,0 @@
# Ignore Handlebars files because partials are not currently supported
# https://github.com/prettier/prettier/issues/11834
**/*.hbs
# Generated files
/@xen-orchestra/web/src/route-map.d.ts
/@xen-orchestra/lite/src/route-map.d.ts
# Docusaurus MDX content: prettier's markdown parser corrupts JSX
# comments ({/* … */} becomes {/_ … _/}) and other MDX constructs
/docs/docs/**/*.md

View File

@@ -1,14 +1,6 @@
'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
//
// https://team.vates.fr/vates/pl/a1i8af1b9id7pgzm3jcg4toacy
printWidth: 120,
}

24
.travis.yml Normal file
View File

@@ -0,0 +1,24 @@
language: node_js
node_js:
#- stable # disable for now due to an issue of indirect dep upath with Node 9
- 8
# 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

View File

@@ -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`: whether 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,
}
)
```

View File

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

View File

@@ -1,68 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/async-each
[![Package Version](https://badgen.net/npm/v/@vates/async-each)](https://npmjs.org/package/@vates/async-each) ![License](https://badgen.net/npm/license/@vates/async-each) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/async-each)](https://bundlephobia.com/result?p=@vates/async-each) [![Node compatibility](https://badgen.net/npm/node/@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`: whether 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)

View File

@@ -1,5 +0,0 @@
export function asyncEach<Item>(
iterable: Iterable<Item> | AsyncIterable<Item>,
iteratee: (item: Item, index: number, iterable: Iterable<Item> | AsyncIterable<Item>) => Promise<void>,
opts?: { concurrency?: number; signal?: AbortSignal; stopOnError?: boolean }
): Promise<void>

View File

@@ -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>|AsyncIterable<Item>} iterable
* @param {(item: Item, index: number, iterable: Iterable<Item>|AsyncIterable<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) {
try {
const cursor = await it.next()
if (cursor.done) {
next = () => {
if (running === 0) {
if (errors.length === 0) {
resolve()
} else {
reject(new AggregateError(errors))
}
}
}
} else {
++running
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()
})
}

View File

@@ -1,127 +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 error = new Error()
const iteratee = spy((_, i) => {
if (i === 1) {
throw error
}
})
assert.deepStrictEqual(
await rejectionOf(asyncEach(iterable, iteratee, { concurrency: 1, stopOnError: true })),
error
)
assert.deepStrictEqual(iteratee.callCount, 2)
})
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 ac = new AbortController()
const iteratee = spy((_, i) => {
if (i === 1) {
ac.abort()
}
})
await assert.rejects(asyncEach(iterable, iteratee, { concurrency: 1, signal: ac.signal }), {
message: 'asyncEach aborted',
})
assert.deepStrictEqual(iteratee.callCount, 2)
})
it('handle error on iterable', async () => {
const iteratee = spy(async () => {})
const iterable = async function* () {
for (const value of [1, 2, 3]) {
if (value === 2) {
throw new Error('iterator error')
}
await randomDelay()
yield value
}
}
await assert.rejects(() => asyncEach(iterable(), iteratee, { concurrency: 1 }), { message: 'iterator error' })
})
})
)
})

View File

@@ -1,39 +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",
"types": "index.d.ts",
"version": "1.0.3",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
},
"devDependencies": {
"sinon": "^18.0.0"
}
}

View File

@@ -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()
```

View File

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

View File

@@ -1,63 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/cached-dns.lookup
[![Package Version](https://badgen.net/npm/v/@vates/cached-dns.lookup)](https://npmjs.org/package/@vates/cached-dns.lookup) ![License](https://badgen.net/npm/license/@vates/cached-dns.lookup) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/cached-dns.lookup)](https://bundlephobia.com/result?p=@vates/cached-dns.lookup) [![Node compatibility](https://badgen.net/npm/node/@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)

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -1,13 +0,0 @@
```js
import { coalesceCalls } from '@vates/coalesce-calls'
const connect = coalesceCalls(async function () {
// async operation
})
connect()
// the previous promise result will be returned if the operation is not
// complete yet
connect()
```

View File

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

View File

@@ -1,46 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/coalesce-calls
[![Package Version](https://badgen.net/npm/v/@vates/coalesce-calls)](https://npmjs.org/package/@vates/coalesce-calls) ![License](https://badgen.net/npm/license/@vates/coalesce-calls) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/coalesce-calls)](https://bundlephobia.com/result?p=@vates/coalesce-calls) [![Node compatibility](https://badgen.net/npm/node/@vates/coalesce-calls)](https://npmjs.org/package/@vates/coalesce-calls)
> Wraps an async function so that concurrent calls will be coalesced
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
```sh
npm install --save @vates/coalesce-calls
```
## Usage
```js
import { coalesceCalls } from '@vates/coalesce-calls'
const connect = coalesceCalls(async function () {
// async operation
})
connect()
// the previous promise result will be returned if the operation is not
// complete yet
connect()
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,16 +0,0 @@
'use strict'
exports.coalesceCalls = function (fn) {
let promise
const clean = () => {
promise = undefined
}
return function () {
if (promise !== undefined) {
return promise
}
promise = fn.apply(this, arguments)
promise.then(clean, clean)
return promise
}
}

View File

@@ -1,36 +0,0 @@
'use strict'
const { describe, it } = require('node:test')
const assert = require('assert')
const { coalesceCalls } = require('./')
const pDefer = () => {
const r = {}
r.promise = new Promise((resolve, reject) => {
r.reject = reject
r.resolve = resolve
})
return r
}
describe('coalesceCalls', () => {
it('decorates an async function', async () => {
const fn = coalesceCalls(promise => promise)
const defer1 = pDefer()
const promise1 = fn(defer1.promise)
const defer2 = pDefer()
const promise2 = fn(defer2.promise)
defer1.resolve('foo')
assert.strictEqual(await promise1, 'foo')
assert.strictEqual(await promise2, 'foo')
const defer3 = pDefer()
const promise3 = fn(defer3.promise)
defer3.resolve('bar')
assert.strictEqual(await promise3, 'bar')
})
})

View File

@@ -1,36 +0,0 @@
{
"private": false,
"name": "@vates/coalesce-calls",
"description": "Wraps an async function so that concurrent calls will be coalesced",
"keywords": [
"async",
"calls",
"coalesce",
"decorate",
"decorator",
"merge",
"promise",
"wrap",
"wrapper"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/coalesce-calls",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/coalesce-calls",
"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"
}
}

View File

@@ -1,65 +0,0 @@
```js
import { compose } from '@vates/compose'
const add2 = x => x + 2
const mul3 = x => x * 3
// const f = x => mul3(add2(x))
const f = compose(add2, mul3)
console.log(f(5))
// → 21
```
> The call context (`this`) of the composed function is forwarded to all functions.
The first function is called with all arguments of the composed function:
```js
const add = (x, y) => x + y
const mul3 = x => x * 3
// const f = (x, y) => mul3(add(x, y))
const f = compose(add, mul3)
console.log(f(4, 5))
// → 27
```
Functions may also be passed in an array:
```js
const f = compose([add2, mul3])
```
Options can be passed as first parameter:
```js
const f = compose(
{
// compose async functions
async: true,
// compose from right to left
right: true,
},
[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.

View File

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

View File

@@ -1,98 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/compose
[![Package Version](https://badgen.net/npm/v/@vates/compose)](https://npmjs.org/package/@vates/compose) ![License](https://badgen.net/npm/license/@vates/compose) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/compose)](https://bundlephobia.com/result?p=@vates/compose) [![Node compatibility](https://badgen.net/npm/node/@vates/compose)](https://npmjs.org/package/@vates/compose)
> Compose functions from left to right
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/compose):
```sh
npm install --save @vates/compose
```
## Usage
```js
import { compose } from '@vates/compose'
const add2 = x => x + 2
const mul3 = x => x * 3
// const f = x => mul3(add2(x))
const f = compose(add2, mul3)
console.log(f(5))
// → 21
```
> The call context (`this`) of the composed function is forwarded to all functions.
The first function is called with all arguments of the composed function:
```js
const add = (x, y) => x + y
const mul3 = x => x * 3
// const f = (x, y) => mul3(add(x, y))
const f = compose(add, mul3)
console.log(f(4, 5))
// → 27
```
Functions may also be passed in an array:
```js
const f = compose([add2, mul3])
```
Options can be passed as first parameter:
```js
const f = compose(
{
// compose async functions
async: true,
// compose from right to left
right: true,
},
[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.
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,66 +0,0 @@
'use strict'
const defaultOpts = { async: false, right: false }
exports.compose = function compose(opts, fns) {
if (Array.isArray(opts)) {
fns = opts.slice() // don't mutate passed array
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 {
fns = Array.prototype.slice.call(arguments, 1)
}
} else {
fns = Array.from(arguments)
opts = defaultOpts
}
const n = fns.length
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]
}
if (opts.right) {
fns.reverse()
}
return opts.async
? async function () {
let value = await fns[0].apply(this, arguments)
for (let i = 1; i < n; ++i) {
value = await fns[i].call(this, value)
}
return value
}
: function () {
let value = fns[0].apply(this, arguments)
for (let i = 1; i < n; ++i) {
value = fns[i].call(this, value)
}
return value
}
}

View File

@@ -1,66 +0,0 @@
'use strict'
const { describe, it } = require('node:test')
const assert = require('node:assert').strict
const { compose } = require('./')
const add2 = x => x + 2
const mul3 = x => x * 3
describe('compose()', () => {
it('throws when no functions is passed', () => {
assert.throws(() => compose(), TypeError)
assert.throws(() => compose([]), TypeError)
})
it('applies from left to right', () => {
assert.strictEqual(compose(add2, mul3)(5), 21)
})
it('accepts functions in an array', () => {
assert.strictEqual(compose([add2, mul3])(5), 21)
})
it('can apply from right to left', () => {
assert.strictEqual(compose({ right: true }, add2, mul3)(5), 17)
})
it('accepts options with functions in an array', () => {
assert.strictEqual(compose({ right: true }, [add2, mul3])(5), 17)
})
it('can compose async functions', async () => {
assert.strictEqual(
await compose(
{ async: true },
async x => x + 2,
async x => x * 3
)(5),
21
)
})
it('forwards all args to first function', () => {
const expectedArgs = [Math.random(), Math.random()]
compose(
(...args) => {
assert.deepEqual(args, expectedArgs)
},
// add a second function to avoid the one function special case
Function.prototype
)(...expectedArgs)
})
it('forwards context to all functions', () => {
const expectedThis = {}
compose(
function () {
assert.strictEqual(this, expectedThis)
},
function () {
assert.strictEqual(this, expectedThis)
}
).call(expectedThis)
})
})

View File

@@ -1,25 +0,0 @@
{
"private": false,
"name": "@vates/compose",
"description": "Compose functions from left to right",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/compose",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/compose",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.1.0",
"engines": {
"node": ">=7.6"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
}
}

View File

@@ -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).

View File

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

View File

@@ -1,155 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/decorate-with
[![Package Version](https://badgen.net/npm/v/@vates/decorate-with)](https://npmjs.org/package/@vates/decorate-with) ![License](https://badgen.net/npm/license/@vates/decorate-with) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/decorate-with)](https://bundlephobia.com/result?p=@vates/decorate-with) [![Node compatibility](https://badgen.net/npm/node/@vates/decorate-with)](https://npmjs.org/package/@vates/decorate-with)
> Creates a decorator from a function wrapper
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
```sh
npm install --save @vates/decorate-with
```
## Usage
### `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).
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,52 +0,0 @@
'use strict'
exports.decorateWith = function decorateWith(fn, ...args) {
return (target, name, descriptor) => ({
...descriptor,
value: fn(descriptor.value, ...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) {
for (const name of Object.keys(map)) {
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)
}
}

View File

@@ -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)
})
})

View File

@@ -1,31 +0,0 @@
{
"private": false,
"name": "@vates/decorate-with",
"description": "Creates a decorator from a function wrapper",
"keywords": [
"apply",
"decorator",
"factory",
"wrapper"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/decorate-with",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/decorate-with",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
}
}

View File

@@ -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, [] ]
```

View File

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

View File

@@ -1,65 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/diff
[![Package Version](https://badgen.net/npm/v/@vates/diff)](https://npmjs.org/package/@vates/diff) ![License](https://badgen.net/npm/license/@vates/diff) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/diff)](https://bundlephobia.com/result?p=@vates/diff) [![Node compatibility](https://badgen.net/npm/node/@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)

View File

@@ -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
}

View File

@@ -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'],
])
})

View File

@@ -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"
}
}

View File

@@ -1,56 +0,0 @@
This library contains utilities for disposables as defined by the [`promise-toolbox` library](https://github.com/JsCommunity/promise-toolbox#resource-management).
### `deduped(fn, keyFn)`
Creates a new function that wraps `fn` and instead of creating a new disposables at each call, returns copies of the same one when `keyFn` returns the same keys.
Those copies contains the same value and can be disposed independently, the source disposable will only be disposed when all copies are disposed.
`keyFn` is called with the same context and arguments as the wrapping function and must returns an array of keys which will be used to identify which disposables should be grouped together.
```js
import { deduped } from '@vates/disposable/deduped'
// the connection with the passed host will be established once at the first call, then, it will be shared with the next calls
const getConnection = deduped(async function (host)) {
const connection = new Connection(host)
return new Disposable(connection, () => connection.close())
}, host => [host])
```
### `debounceResource(disposable, delay)`
Creates a new disposable with the same value and with a delayed disposer.
On calling this disposer, the source disposable will be disposed when the `delay` is passed.
```js
import { createDebounceResource } from '@vates/disposable/debounceResource'
const debounceResource = createDebounceResource()
// it will wait for 10 seconds before calling the disposer
Disposable.use(debounceResource(getConnection(host), 10e3), connection => {})
```
### `debounceResource.flushAll()`
Disposes all delayed disposers and cancels the delaying of the disposables that are in usage.
```js
import { createDebounceResource } from '@vates/disposable/debounceResource'
const debounceResource = createDebounceResource()
const res1 = await debounceResource(res, 10e3)
const res2 = await debounceResource(res, 10e3)
const res3 = await debounceResource(res, 10e3)
rest1.dispose()
rest2.dispose()
// res3 is in usage
debounceResource.flushAll()
// res1 and res2 are immediately disposed
// res3 will be disposed immediately when its disposer will be called
```

View File

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

View File

@@ -1,89 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/disposable
[![Package Version](https://badgen.net/npm/v/@vates/disposable)](https://npmjs.org/package/@vates/disposable) ![License](https://badgen.net/npm/license/@vates/disposable) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/disposable)](https://bundlephobia.com/result?p=@vates/disposable) [![Node compatibility](https://badgen.net/npm/node/@vates/disposable)](https://npmjs.org/package/@vates/disposable)
> Utilities for disposables
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/disposable):
```sh
npm install --save @vates/disposable
```
## Usage
This library contains utilities for disposables as defined by the [`promise-toolbox` library](https://github.com/JsCommunity/promise-toolbox#resource-management).
### `deduped(fn, keyFn)`
Creates a new function that wraps `fn` and instead of creating a new disposables at each call, returns copies of the same one when `keyFn` returns the same keys.
Those copies contains the same value and can be disposed independently, the source disposable will only be disposed when all copies are disposed.
`keyFn` is called with the same context and arguments as the wrapping function and must returns an array of keys which will be used to identify which disposables should be grouped together.
```js
import { deduped } from '@vates/disposable/deduped'
// the connection with the passed host will be established once at the first call, then, it will be shared with the next calls
const getConnection = deduped(async function (host)) {
const connection = new Connection(host)
return new Disposable(connection, () => connection.close())
}, host => [host])
```
### `debounceResource(disposable, delay)`
Creates a new disposable with the same value and with a delayed disposer.
On calling this disposer, the source disposable will be disposed when the `delay` is passed.
```js
import { createDebounceResource } from '@vates/disposable/debounceResource'
const debounceResource = createDebounceResource()
// it will wait for 10 seconds before calling the disposer
Disposable.use(debounceResource(getConnection(host), 10e3), connection => {})
```
### `debounceResource.flushAll()`
Disposes all delayed disposers and cancels the delaying of the disposables that are in usage.
```js
import { createDebounceResource } from '@vates/disposable/debounceResource'
const debounceResource = createDebounceResource()
const res1 = await debounceResource(res, 10e3)
const res2 = await debounceResource(res, 10e3)
const res3 = await debounceResource(res, 10e3)
rest1.dispose()
rest2.dispose()
// res3 is in usage
debounceResource.flushAll()
// res1 and res2 are immediately disposed
// res3 will be disposed immediately when its disposer will be called
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,58 +0,0 @@
'use strict'
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { warn } = createLogger('vates:disposable:debounceResource')
exports.createDebounceResource = () => {
const flushers = new Set()
async function debounceResource(pDisposable, delay = debounceResource.defaultDelay) {
if (delay === 0) {
return pDisposable
}
const disposable = await pDisposable
let timeoutId
const disposeWrapper = async () => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId)
timeoutId = undefined
flushers.delete(flusher)
try {
await disposable.dispose()
} catch (error) {
warn(error)
}
}
}
const flusher = () => {
const shouldDisposeNow = timeoutId !== undefined
if (shouldDisposeNow) {
return disposeWrapper()
} else {
// will dispose ASAP
delay = 0
}
}
flushers.add(flusher)
return {
dispose() {
timeoutId = setTimeout(disposeWrapper, delay)
},
value: disposable.value,
}
}
debounceResource.flushAll = () => {
// iterate on a sync way in order to not remove a flusher added on processing flushers
const promise = asyncMap(flushers, flush => flush())
flushers.clear()
return promise
}
return debounceResource
}

View File

@@ -1,32 +0,0 @@
'use strict'
const { describe, it } = require('node:test')
const { useFakeTimers, spy, assert } = require('sinon')
const { createDebounceResource } = require('./debounceResource')
const clock = useFakeTimers()
describe('debounceResource()', () => {
it('calls the resource disposer after 10 seconds', async () => {
const debounceResource = createDebounceResource()
const delay = 10e3
const dispose = spy()
const resource = await debounceResource(
Promise.resolve({
value: '',
dispose,
}),
delay
)
resource.dispose()
assert.notCalled(dispose)
clock.tick(delay)
assert.called(dispose)
})
})

View File

@@ -1,54 +0,0 @@
'use strict'
const ensureArray = require('ensure-array')
const { MultiKeyMap } = require('@vates/multi-key-map')
function State(factory) {
this.factory = factory
this.users = 0
}
const call = fn => fn()
exports.deduped = (factory, keyFn = (...args) => args) =>
(function () {
const states = new MultiKeyMap()
return function () {
const keys = ensureArray(keyFn.apply(this, arguments))
let state = states.get(keys)
if (state === undefined) {
const result = factory.apply(this, arguments)
const createFactory = disposable => {
const wrapper = {
dispose() {
if (--state.users === 0) {
states.delete(keys)
return disposable.dispose()
}
},
value: disposable.value,
}
return () => {
return wrapper
}
}
if (typeof result.then !== 'function') {
state = new State(createFactory(result))
} else {
result.catch(() => {
states.delete(keys)
})
const pFactory = result.then(createFactory)
state = new State(() => pFactory.then(call))
}
states.set(keys, state)
}
++state.users
return state.factory()
}
})()

View File

@@ -1,79 +0,0 @@
'use strict'
const { describe, it } = require('node:test')
const { spy, assert } = require('sinon')
const { deduped } = require('./deduped')
describe('deduped()', () => {
it('calls the resource function only once', async () => {
const value = {}
const getResource = spy(async () => ({
value,
dispose: Function.prototype,
}))
const dedupedGetResource = deduped(getResource)
const { value: v1 } = await dedupedGetResource()
const { value: v2 } = await dedupedGetResource()
assert.calledOnce(getResource)
assert.match(v1, value)
assert.match(v2, value)
})
it('only disposes the source disposable when its all copies dispose', async () => {
const dispose = spy()
const getResource = async () => ({
value: '',
dispose,
})
const dedupedGetResource = deduped(getResource)
const { dispose: d1 } = await dedupedGetResource()
const { dispose: d2 } = await dedupedGetResource()
d1()
assert.notCalled(dispose)
d2()
assert.calledOnce(dispose)
})
it('works with sync factory', () => {
const value = {}
const dispose = spy()
const dedupedGetResource = deduped(() => ({ value, dispose }))
const d1 = dedupedGetResource()
assert.match(d1.value, value)
const d2 = dedupedGetResource()
assert.match(d2.value, value)
d1.dispose()
assert.notCalled(dispose)
d2.dispose()
assert.calledOnce(dispose)
})
it('no race condition on dispose before async acquisition', async () => {
const dispose = spy()
const dedupedGetResource = deduped(async () => ({ value: 42, dispose }))
const d1 = await dedupedGetResource()
dedupedGetResource()
d1.dispose()
assert.notCalled(dispose)
})
})

View File

@@ -1,34 +0,0 @@
{
"private": false,
"name": "@vates/disposable",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/disposable",
"description": "Utilities for disposables",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/disposable",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.6",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
},
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.5",
"@xen-orchestra/log": "^0.7.2",
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^18.0.0"
}
}

View File

@@ -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()
```

View File

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

View File

@@ -1,81 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/event-listeners-manager
[![Package Version](https://badgen.net/npm/v/@vates/event-listeners-manager)](https://npmjs.org/package/@vates/event-listeners-manager) ![License](https://badgen.net/npm/license/@vates/event-listeners-manager) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/event-listeners-manager)](https://bundlephobia.com/result?p=@vates/event-listeners-manager) [![Node compatibility](https://badgen.net/npm/node/@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)

View File

@@ -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
}
}

View File

@@ -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()
})

View File

@@ -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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,49 +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>
```
The `remoteUrl` can be found using the following command from the XOA CLI, and then picking the right `url` property:
`xo-server-db ls remote`
The `vhdPathInRemote` follows a file structure described in [VM backups](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/backups/docs/VM%20backups/README.md).
## Restore a file from a VHD using `fuse-vhd` CLI
1. Mount a VHD to the Filesystem (see **Usage**).
2. Verify the VHD is Correctly Mounted:
`mount | grep fuse`
3. Get the `START` and `SIZE` of the disk whose file you want to restore. Multiply `START` by 512 (block size) to get the offset value. (`mountedVhdPath` is the `mountPoint` value from earlier, followed by `/vhd0`, for example). Depending on the partition type, you may need to do some additional setup to discover the partitions.
`partx --bytes --output=NR,START,SIZE,NAME,UUID,TYPE --pairs <mountedVhdPath>`
4. Mount the disk. Depending on the partition type, you may need to add some additional options. The `norecovery` option is used for ext3/ext4/xfs file systems; otherwise, remove this option.
`mount --options=loop,ro,norecovery,sizelimit=<SIZE>,offset=<START*512> --source=<mountedVhdPath> --target=<diskMountPoint>`
5. Copy your files from the mounted partition, then unmount the partition.

View File

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

View File

@@ -1,80 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/fuse-vhd
[![Package Version](https://badgen.net/npm/v/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd) ![License](https://badgen.net/npm/license/@vates/fuse-vhd) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/fuse-vhd)](https://bundlephobia.com/result?p=@vates/fuse-vhd) [![Node compatibility](https://badgen.net/npm/node/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/fuse-vhd):
```sh
npm install --save @vates/fuse-vhd
```
## Usage
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder:
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
The `remoteUrl` can be found using the following command from the XOA CLI, and then picking the right `url` property:
`xo-server-db ls remote`
The `vhdPathInRemote` follows a file structure described in [VM backups](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/backups/docs/VM%20backups/README.md).
## Restore a file from a VHD using `fuse-vhd` CLI
1. Mount a VHD to the Filesystem (see **Usage**).
2. Verify the VHD is Correctly Mounted:
`mount | grep fuse`
3. Get the `START` and `SIZE` of the disk whose file you want to restore. Multiply `START` by 512 (block size) to get the offset value. (`mountedVhdPath` is the `mountPoint` value from earlier, followed by `/vhd0`, for example). Depending on the partition type, you may need to do some additional setup to discover the partitions.
`partx --bytes --output=NR,START,SIZE,NAME,UUID,TYPE --pairs <mountedVhdPath>`
4. Mount the disk. Depending on the partition type, you may need to add some additional options. The `norecovery` option is used for ext3/ext4/xfs file systems; otherwise, remove this option.
`mount --options=loop,ro,norecovery,sizelimit=<SIZE>,offset=<START*512> --source=<mountedVhdPath> --target=<diskMountPoint>`
5. Copy your files from the mounted partition, then unmount the partition.
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

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

View File

@@ -1,139 +0,0 @@
import Fuse from 'fuse-native'
import LRU from 'lru-cache'
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 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)
await vhd.readBlockAllocationTable()
const { blockSize } = vhd.header
// A zeroed buffer returned for VHD blocks that are not present in the chain.
// Never mutated — safe to share across all reads.
const EMPTY_BLOCK = Buffer.alloc(blockSize, 0)
// --- coalescing block read queue ---
//
// FUSE read requests from NTFS-3g arrive in small chunks (typically 4 KiB)
// while VHD blocks are 2 MiB. Reading blocks one at a time from the backing
// store (CIFS/NFS/…) bounds libuv threadpool usage to 1 concurrent I/O call.
//
// Coalescing: all pending requests for the same blockId share a single
// readBlock() call. When the block resolves, every waiter is fulfilled at once.
//
// Together these two properties prevent the FUSE-on-FUSE threadpool deadlock:
// concurrent NTFS-3g reads no longer starve the VHD block reads they depend on.
// In-memory block cache. Each VHD block is 2 MiB; 16 blocks = 32 MiB.
// This is the primary tool against the 300× CIFS amplification: NTFS-3g
// reads a 2 MiB block in ~512 sequential 4 KiB FUSE requests. Without a
// cache, each request causes a full 2 MiB CIFS read. With the cache, only
// the first request reads from CIFS; the rest are served from RAM.
const blockCache = new LRU({ max: 64 })
const pendingBlocks = new Map() // blockId → Promise<Buffer>
const readQueue = [] // { blockId, resolve, reject }[]
let queueRunning = false
// log stats every 10 s while there is activity
function enqueueBlock(blockId) {
// 1. serve from cache if available
if (blockCache.has(blockId)) {
return Promise.resolve(blockCache.get(blockId))
}
// 2. coalesce: join an existing in-flight read for this block
if (pendingBlocks.has(blockId)) {
return pendingBlocks.get(blockId)
}
const p = new Promise((resolve, reject) => {
readQueue.push({ blockId, resolve, reject })
})
pendingBlocks.set(blockId, p)
p.then(
data => {
pendingBlocks.delete(blockId)
blockCache.set(blockId, data)
},
() => pendingBlocks.delete(blockId)
)
drainQueue()
return p
}
function drainQueue() {
if (queueRunning) return
queueRunning = true
// async IIFE — errors are forwarded to individual reject() callbacks,
// so the outer floating promise is always fulfilled
;(async () => {
while (readQueue.length > 0) {
const { blockId, resolve, reject } = readQueue.shift()
try {
resolve((await vhd.readBlock(blockId)).data)
} catch (err) {
reject(err)
}
}
queueRunning = false
})()
}
async function readData(buf, len, pos) {
if (len === 0) return 0
const startBlockId = Math.floor(pos / blockSize)
// use (pos + len - 1) so an exact block boundary doesn't include the next block
const endBlockId = Math.floor((pos + len - 1) / blockSize)
let copied = 0
for (let blockId = startBlockId; blockId <= endBlockId; blockId++) {
const data = vhd.containsBlock(blockId) ? await enqueueBlock(blockId) : EMPTY_BLOCK
const offsetStart = blockId === startBlockId ? pos % blockSize : 0
const offsetEnd = blockId === endBlockId ? ((pos + len - 1) % blockSize) + 1 : blockSize
data.copy(buf, copied, offsetStart, offsetEnd)
copied += offsetEnd - offsetStart
}
return copied
}
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 readData(buf, len, pos).then(cb, error => {
warn('read error', { path, len, pos, error })
cb(Fuse.EIO)
})
}
throw new Error(`read file ${path} not exists`)
},
})
return new Disposable(
() => {
return fromCallback(cb => fuse.unmount(cb))
},
fromCallback(cb => fuse.mount(cb))
)
})

View File

@@ -1,35 +0,0 @@
{
"name": "@vates/fuse-vhd",
"version": "2.1.3",
"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.9.5",
"@xen-orchestra/log": "^0.7.2",
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.16.2"
},
"bin": {
"xo-fuse-vhd": "./cli.mjs"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -1,58 +0,0 @@
A toolbox to ease the use of generator
### Timeout
wrap a source async generator to have it throw an error when timeout is reached
timeout is a positive number in milliseconds
```js
import { Timeout } from '@vates/generator-toolbox'
const wrappedGenerator = new Timeout(sourceGenerator, timeout)
```
### Throttle
wrap a source async generator to have it respect a max speed ( in bytes per seconds).
speed is either a strictly positive number or a function returning a strictly positive number. A speed change will be used for the next emitted packet.
The source generator must yield object with a length property.
Optimized for small yields regarding to the speed, since it won't split incoming packet.
If the generator reached the max speed it will be paused, limiting memory consumption.
```js
import { Throttle } from '@vates/generator-toolbox'
const wrappedGenerator = new Throttle(sourceGenerator, speed)
```
### Synchronized
Fork a generator. The rules are:
- if the source returns, all the forks return
- if the forks error, all the forks error with the same error
- if a fork returns, it is stopped, but the generator continue with the other
- if a fork errors, it is stopped, but the generator continue with the other
- if all the forks return, the source is stopped
- if all the forks error, the source is errored with the last error
- the source start producing a packet when the fastest forked ask for it
- the source forks get the packet only when all the forks asked for it, no buffer stores in memory
```ts
import { Synchronized } from '@vates/generator-toolbox'
async function consume(generator: AsyncGenerator) {
for await (const val of generator) {
console.log({ val })
}
}
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
await Promise.all([consume(first), consume(second)])
```
Note: you can stop early a generator by calling `generator.return()`, and you can stop in in error by calling `generator.throw(error)`

View File

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

View File

@@ -1,89 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/generator-toolbox
[![Package Version](https://badgen.net/npm/v/@vates/generator-toolbox)](https://npmjs.org/package/@vates/generator-toolbox) ![License](https://badgen.net/npm/license/@vates/generator-toolbox) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/generator-toolbox)](https://bundlephobia.com/result?p=@vates/generator-toolbox) [![Node compatibility](https://badgen.net/npm/node/@vates/generator-toolbox)](https://npmjs.org/package/@vates/generator-toolbox)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/generator-toolbox):
```sh
npm install --save @vates/generator-toolbox
```
## Usage
A toolbox to ease the use of generator
### Timeout
wrap a source async generator to have it throw an error when timeout is reached
timeout is a positive number in milliseconds
```js
import { Timeout } from '@vates/generator-toolbox'
const wrappedGenerator = new Timeout(sourceGenerator, timeout)
```
### Throttle
wrap a source async generator to have it respect a max speed ( in bytes per seconds).
speed is either a strictly positive number or a function returning a strictly positive number. A speed change will be used for the next emitted packet.
The source generator must yield object with a length property.
Optimized for small yields regarding to the speed, since it won't split incoming packet.
If the generator reached the max speed it will be paused, limiting memory consumption.
```js
import { Throttle } from '@vates/generator-toolbox'
const wrappedGenerator = new Throttle(sourceGenerator, speed)
```
### Synchronized
Fork a generator. The rules are:
- if the source returns, all the forks return
- if the forks error, all the forks error with the same error
- if a fork returns, it is stopped, but the generator continue with the other
- if a fork errors, it is stopped, but the generator continue with the other
- if all the forks return, the source is stopped
- if all the forks error, the source is errored with the last error
- the source start producing a packet when the fastest forked ask for it
- the source forks get the packet only when all the forks asked for it, no buffer stores in memory
```ts
import { Synchronized } from '@vates/generator-toolbox'
async function consume(generator: AsyncGenerator) {
for await (const val of generator) {
console.log({ val })
}
}
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
await Promise.all([consume(first), consume(second)])
```
Note: you can stop early a generator by calling `generator.return()`, and you can stop in in error by calling `generator.throw(error)`
## 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
[MIT](https://spdx.org/licenses/MIT) © [Vates SAS](https://vates.fr)

View File

@@ -1,6 +0,0 @@
// @ts-check
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended)

View File

@@ -1,38 +0,0 @@
{
"name": "@vates/generator-toolbox",
"version": "1.1.3",
"main": "dist/index.mjs",
"license": "MIT",
"private": false,
"type": "module",
"devDependencies": {
"@eslint/js": "^9.19.0",
"rimraf": "^6.0.1",
"@types/node": "^20.6",
"typescript": "~5.6",
"typescript-eslint": "^8.61.0"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"postversion": "npm publish --access public",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"test": "tsc && node --test **/*.test.mjs",
"clean": "rimraf dist/"
},
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/generator-toolbox",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/generator-toolbox",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"engines": {
"node": ">=20.18"
}
}

View File

@@ -1,3 +0,0 @@
export { Synchronized } from './synchronized.mjs'
export { Throttle } from './throttle.mjs'
export { Timeout } from './timeout.mjs'

View File

@@ -1,150 +0,0 @@
import assert from 'node:assert'
export class Synchronized<T, TReturn, TNext> {
#source: AsyncGenerator<T, TReturn, TNext>
#forks = new Map<string, Forked<T, TReturn, TNext>>()
#removedForks = new Set<string>()
#waitingForks = new Set<string>()
#started = false
#nextValueForksReady?: {
promise: Promise<IteratorResult<T>>
forksWaitingReject: (error: Error) => void
forksWaitingResolve: () => void
}
constructor(source: AsyncGenerator<T, TReturn, TNext>) {
this.#source = source
}
fork(uid: string): AsyncGenerator<T, TReturn, TNext> {
assert.strictEqual(this.#started, false, `can't create a fork after consuming the data`)
assert.strictEqual(this.#forks.has(uid), false, `a fork with id ${uid} already exists`)
const fork = new Forked<T, TReturn, TNext>(this, uid)
this.#forks.set(uid, fork)
return fork
}
async #resolveWhenAllForksReady(): Promise<IteratorResult<T>> {
if (!this.#nextValueForksReady) {
throw new Error('Can t wait forks if there are noone waiting')
}
const { promise, forksWaitingResolve } = this.#nextValueForksReady
if (this.#waitingForks.size === this.#forks.size) {
// reset value
this.#waitingForks.clear()
this.#nextValueForksReady = undefined
forksWaitingResolve() // for the other forks waiting
}
return promise
}
async next(uid: string): Promise<IteratorResult<T>> {
if (this.#removedForks.has(uid)) {
return { done: true, value: undefined }
}
if (!this.#forks.has(uid)) {
throw new Error(`trying to advance fork ${uid} that is not a fork of this one`)
}
if (this.#waitingForks.has(uid)) {
throw new Error(`Fork ${uid} is already waiting`)
}
this.#started = true
if (this.#nextValueForksReady === undefined) {
let forksWaitingResolve = () => {}
let forksWaitingReject: (reason?: Error) => void = () => {}
const next = this.#source.next().catch(async error => {
const e = new Error(`Error in the source generator ${error.message}`, { cause: error })
forksWaitingReject(e)
// source has failed, kill everything, and stop the forks
for (const uid of [...this.#forks.keys()]) {
await this.remove(uid, error)
}
})
const promise = Promise.all([
next,
new Promise((_resolve, _reject) => {
forksWaitingResolve = () => _resolve(undefined)
forksWaitingReject = _reject
}),
]).then(([_]) => _ as IteratorResult<T>)
this.#nextValueForksReady = { promise, forksWaitingResolve, forksWaitingReject }
}
this.#waitingForks.add(uid)
return this.#resolveWhenAllForksReady()
}
async remove(uid: string, error?: Error): Promise<IteratorResult<T>> {
const fork = this.#forks.get(uid)
if (fork === undefined) {
if (this.#removedForks.has(uid)) {
// already removed
return { done: true, value: undefined }
}
throw new Error(`trying to remove fork with uid ${uid} that is not a fork of this one`)
}
this.#forks.delete(uid)
this.#waitingForks.delete(uid)
this.#removedForks.add(uid)
try {
if (error === undefined) {
await fork.return()
} else {
await fork.throw(error)
}
} catch (cleaningError) {
console.error('Error while cleaning the forked', {
cleaningError,
sourceError: error,
})
}
if (this.#forks.size === 0) {
if (error === undefined) {
await this.#source.return(undefined as TReturn)
} else {
await this.#source.throw(error)
}
// Reject any pending forks waiting for the next value
if (this.#nextValueForksReady) {
this.#nextValueForksReady.forksWaitingReject(new Error('Source generator terminated.', { cause: error }))
this.#nextValueForksReady = undefined
}
// clear state
this.#removedForks.clear()
this.#waitingForks.clear()
} else {
// this fork was maybe blocking the others
if (this.#nextValueForksReady) {
await this.#resolveWhenAllForksReady()
}
}
return { done: true, value: undefined }
}
}
class Forked<T, TReturn, TNext> implements AsyncGenerator<T, TReturn, TNext> {
#parent: Synchronized<T, TReturn, TNext>
#uid: string
constructor(parent: Synchronized<T, TReturn, TNext>, uid: string) {
this.#parent = parent
this.#uid = uid
}
next(): Promise<IteratorResult<T>> {
return this.#parent.next(this.#uid)
}
async return(): Promise<IteratorResult<T>> {
return this.#parent.remove(this.#uid)
}
async throw(e: Error): Promise<IteratorResult<T>> {
return this.#parent.remove(this.#uid, e)
}
[Symbol.asyncIterator](): AsyncGenerator<T, TReturn, TNext> {
return this
}
}

View File

@@ -1,173 +0,0 @@
import assert from 'node:assert'
import { suite, test } from 'node:test'
import { Synchronized } from './synchronized.mjs'
async function* makeRangeGenerator(end = Infinity, progress = { yielded: 0 }, onYielded = (_val: unknown) => {}) {
for (let i = 0; i < end; i++) {
await new Promise(resolve => setTimeout(resolve, 10))
yield i
onYielded(i)
progress.yielded = i
}
return progress
}
async function consume(
iterable: AsyncGenerator,
delay = 2500,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onConsumed = (_val: unknown, _iterable: AsyncGenerator) => Promise.resolve(false)
) {
for await (const val of iterable) {
await new Promise(resolve => setTimeout(resolve, delay))
if (await onConsumed(val, iterable)) {
iterable.return(undefined)
}
}
}
suite('success', () => {
test('for-await-of break stops the source generator', async () => {
let sourceClosed = false
const source = (async function* () {
try {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 10))
yield i
}
} finally {
sourceClosed = true
}
})()
const forker = new Synchronized(source)
const fork = forker.fork('first')
// for-await-of must call fork.return() on break, which propagates to the source
for await (const val of fork) {
if (val === 2) break
}
assert.strictEqual(sourceClosed, true)
})
test('if works with multiple consumer', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(3, progress)
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
await Promise.all([consume(first, 50), consume(second, 500)])
assert.strictEqual(progress.yielded, 2)
})
test('if works with one consumer returning', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(10, progress)
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
await new Promise(resolve => setTimeout(resolve, 500))
await Promise.all([
consume(first, 1),
consume(second, 1, async (val: unknown) => {
return Promise.resolve(val === 2)
}),
])
assert.strictEqual(progress.yielded, 9)
})
test('if works with all consumers returning', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(10, progress)
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
await new Promise(resolve => setTimeout(resolve, 500))
await Promise.all([
consume(first, 1, async (val: unknown) => {
return Promise.resolve(val === 5)
}),
consume(second, 1, async (val: unknown) => {
return Promise.resolve(val === 2)
}),
])
assert.strictEqual(progress.yielded, 4)
})
})
suite('error', () => {
test('it fails all if error on source', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(5, progress, (val: unknown) => {
if (val === 2) {
throw new Error('source stop')
}
})
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
await assert.rejects(Promise.all([consume(first, 1), consume(second, 1)]))
assert.strictEqual(progress.yielded, 1)
})
test('it flows until the end if one consumer throws', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(10, progress)
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
const firstConsume = consume(first, 1)
const secondConsume = assert.rejects(() =>
consume(second, 1, async (val: unknown, iterable) => {
if (val === 1) {
await iterable.throw(new Error('error on second'))
}
return false
})
)
await Promise.allSettled([firstConsume, secondConsume])
assert.strictEqual(progress.yielded, 9)
})
test('it throw if ALL consumer throws', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(5, progress)
const forker = new Synchronized(generator)
const first = forker.fork('first')
const second = forker.fork('second')
const firstConsume = consume(first, 1, async (val: unknown, iterable) => {
if (val === 3) {
await iterable.throw(new Error('error on first'))
}
return false
})
const secondConsume = consume(second, 1, async (val: unknown, iterable) => {
if (val === 1) {
await iterable.throw(new Error('error on second'))
}
return false
})
await assert.rejects(Promise.all([firstConsume, secondConsume]))
assert.strictEqual(progress.yielded, 2)
})
test('It should not allow a new fork after the start', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(5, progress)
const forker = new Synchronized(generator)
const first = forker.fork('first')
await first.next()
assert.throws(() => forker.fork('second'))
})
test('It should not allow fork with the same uid', async () => {
const progress = { yielded: 0 }
const generator = makeRangeGenerator(5, progress)
const forker = new Synchronized(generator)
forker.fork('first')
forker.fork('second')
assert.throws(() => forker.fork('first'))
})
})

View File

@@ -1,68 +0,0 @@
import assert from 'node:assert'
/**
*
* This class will throttle the production of a group of generators
*
* limits: depends on the event loop, so don't expect it to be very precise
* works better with more small blocks
* The source generator must yield object with a length property
* Changing the speed will only be takin into account fot the next packets asked
*/
export class Throttle {
#previousSlot = 0
#bytesPerSecond: number | (() => number)
get speed(): number {
let speed: number
if (typeof this.#bytesPerSecond === 'function') {
speed = this.#bytesPerSecond()
} else {
speed = this.#bytesPerSecond
}
assert.ok(speed >= 0, `speed must be positive, ${speed} computed`)
return speed
}
constructor(speed: number | (() => number)) {
this.#bytesPerSecond = speed
}
getNextSlot(length: number): { timeout?: ReturnType<typeof setTimeout>; promise?: Promise<unknown> } {
assert.notStrictEqual(length, undefined, `throttled stream need to expose a length property }`)
assert.ok(length > 0, `throttled stream must expose a positive length property , ${length} given }`)
if (this.speed === 0) {
return {}
}
const previous = this.#previousSlot
const nextSlot = Math.round(previous + (length * 1000) / this.speed)
if (nextSlot < Date.now()) {
// we're above the limit, go now
this.#previousSlot = Date.now()
return {}
}
this.#previousSlot = nextSlot
// wait till the next slot
// it won't be extremely precise since the event loop is not
let timeout: ReturnType<typeof setTimeout> | undefined = undefined
const promise = new Promise(function (resolve) {
timeout = setTimeout(resolve, nextSlot - Date.now())
})
return { promise, timeout }
}
async *createThrottledGenerator(source: AsyncGenerator<{ length: number }>): AsyncGenerator<{ length: number }> {
let timeout: ReturnType<typeof setTimeout> | undefined
try {
for await (const value of source) {
const res = this.getNextSlot(value.length)
timeout = res.timeout
// wait for the time slot before yielding the data
if (res.promise !== undefined) {
await res.promise
}
yield value
}
} finally {
clearTimeout(timeout)
}
}
}

View File

@@ -1,63 +0,0 @@
import { Throttle } from './throttle.mjs'
import { suite, test } from 'node:test'
import assert from 'node:assert'
async function* makeGenerator(chunkSize = 1024 * 1024, nbChunks = 100) {
for (let i = 0; i < nbChunks; i++) {
yield Buffer.allocUnsafe(chunkSize)
}
}
async function consumes(generator: AsyncGenerator) {
// eslint-disable-next-line no-empty,@typescript-eslint/no-unused-vars
for await (const data of generator) {
}
}
suite('it throttle one', () => {
test('base test', async () => {
// 10MB/s
const throttler = new Throttle(10 * 1024 * 1024)
const throttled = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 20))
const start = Date.now()
await consumes(throttled)
const end = Date.now()
assert.strictEqual(Math.round((end - start) / 1000), 2)
})
test('variable speed test', async () => {
// 10MB/s
const throttler = new Throttle(() => 10 * 1024 * 1024)
const throttled = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 20))
const start = Date.now()
await consumes(throttled)
const end = Date.now()
assert.strictEqual(Math.round((end - start) / 1000), 2)
})
test('multiple generator test', async () => {
// 10MB/s
const throttler = new Throttle(10 * 1024 * 1024)
const first = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
const second = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
const start = Date.now()
await Promise.all([consumes(first), consumes(second)])
const end = Date.now()
assert.strictEqual(Math.round((end - start) / 1000), 2)
})
test(`it doesn't accept negative value`, async () => {
// 10MB/s
let throttler = new Throttle(-10 * 1024 * 1024)
let generator = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
await assert.rejects(() => consumes(generator))
let index = 5
throttler = new Throttle(() => {
index--
return index > 0 ? 10 * 1024 * 1024 : -10 * 1024 * 1024
})
generator = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
await assert.rejects(() => consumes(generator))
})
})

View File

@@ -1,49 +0,0 @@
import assert from 'node:assert'
const TIMEOUT = Symbol('timeout')
export class Timeout<T> implements AsyncGenerator {
#source: AsyncGenerator<T>
#timeout: number
constructor(source: AsyncGenerator<T>, timeout: number) {
assert.ok(timeout > 0, `Timeout value must be positive, ${timeout} received`)
this.#source = source
this.#timeout = timeout
}
async next(): Promise<IteratorResult<T>> {
let timeoutHandle: ReturnType<typeof setTimeout>
const sourceNext = this.#source.next()
const result = await Promise.race([
sourceNext.then(res => {
clearTimeout(timeoutHandle)
return res
}),
new Promise<typeof TIMEOUT>(resolve => {
timeoutHandle = setTimeout(() => resolve(TIMEOUT), this.#timeout)
}),
])
if (result === TIMEOUT) {
// stop the source once the in-flight next settles to avoid data loss on the next call
// we don't want to await for the end of timeouted code
sourceNext.then(
() => this.#source.return(undefined).catch(() => {}),
() => {} // source already errored, nothing to clean up
)
throw new Error('Timeout reached')
}
return result
}
return(): Promise<IteratorResult<T>> {
return this.#source.return(undefined)
}
throw(e: Error): Promise<IteratorResult<T>> {
return this.#source.throw(e)
}
[Symbol.asyncIterator](): AsyncGenerator<T> {
return this
}
}

View File

@@ -1,87 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { Timeout } from './timeout.mjs'
describe('Timeout class', () => {
it('should reject a timeout with a negative or zero value ', async () => {
// Create a mock async generator that never resolves
const mockGenerator = (async function* () {
yield 1
})()
assert.throws(() => new Timeout(mockGenerator, 0))
assert.throws(() => new Timeout(mockGenerator, -10))
})
it('should resolve with the value from the source generator if it completes before the timeout', async () => {
// Create a mock async generator that resolves immediately
const mockGenerator = (async function* () {
yield 1
})()
const timeout = new Timeout(mockGenerator, 100) // 100ms timeout
const result = await timeout.next()
assert.strictEqual(result.value, 1)
assert.strictEqual(result.done, false)
})
it('should reject with a timeout error if the source generator takes too long', async () => {
// Create a mock async generator that never resolves
// eslint-disable-next-line require-yield
const mockGenerator = (async function* () {
await new Promise(() => {}) // Never resolves
})()
const timeout = new Timeout(mockGenerator, 10) // 10ms timeout
await assert.rejects(timeout.next(), 'Expected timeout error was not thrown')
})
it('should allow iteration using for-await-of', async () => {
// Create a mock async generator that yields values
const mockGenerator: AsyncGenerator<number> = (async function* () {
yield 1
yield 2
yield 3
})()
const timeout = new Timeout(mockGenerator, 100) // 100ms timeout
const results: Array<number> = []
for await (const value of timeout) {
results.push(value as number)
}
assert.deepStrictEqual(results, [1, 2, 3])
})
it('should handle the throw method correctly', async () => {
// Create a mock async generator with a throw method
const mockGenerator = (async function* () {
yield 1
})()
const timeout = new Timeout(mockGenerator, 100) // 100ms timeout
await assert.rejects(timeout.throw(new Error('Test error')), 'Expected error was not thrown')
})
it('should close the source generator after a timeout', async () => {
let sourceClosed = false
const slowSource = (async function* () {
try {
// resolves after 100ms so the in-flight next() eventually settles
await new Promise(resolve => setTimeout(resolve, 100))
yield 1
} finally {
sourceClosed = true
}
})()
const timeout = new Timeout(slowSource, 10) // fires before source yields
await assert.rejects(() => timeout.next(), /Timeout reached/)
// wait for the in-flight next() to settle so source.return() can be called
await new Promise(resolve => setTimeout(resolve, 200))
assert.strictEqual(sourceClosed, true)
})
})

View File

@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "es2023",
"module": "ESNext",
"types": ["node"], // Only use types from Node.js and packages that are explicitly imported
"moduleResolution": "bundler",
"strict": true,
"declaration": true, // Generates `.d.ts` files for type safety
"outDir": "dist" // Outputs compiled code to the `dist` directory
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,65 +0,0 @@
### `create([opts], [requestListener])`
Creates a new server instance. `opts` may contain `createServer` and `createSecureServer` factories (defaults to `http.createServer` and `https.createServer`). `requestListener` is registered as a `request` event listener.
```js
import { create } from '@vates/http-server-plus'
const server = create(function onRequest(req, res) {
res.end('Hello')
})
```
### `server.listen(opts)`
Starts listening. Returns a promise that resolves to a human-readable address string, or rejects with `error.niceAddress` set.
```js
// HTTP on a specific port
await server.listen({ port: 8080 })
// HTTPS — detected automatically when cert+key are provided
await server.listen({
port: 443,
cert: fs.readFileSync('certificate.pem'),
key: fs.readFileSync('private_key.pem'),
})
// Unix socket
await server.listen({ socket: '/var/run/http.sock' })
// File descriptor
await server.listen({ fd: 3 })
// Systemd socket activation
await server.listen({ systemdSocket: 0 })
```
> As a convenience, if `hostname` is `'localhost'`, it will listen on both `127.0.0.1` and `::1`, similar to what Node does when no hostname is provided.
Multiple endpoints can be started on the same instance:
```js
const [httpAddr, httpsAddr] = await Promise.all([server.listen({ port: 80 }), server.listen({ port: 443, cert, key })])
console.log('listening on', httpAddr, httpsAddr)
```
### HTTP/2
Pass a custom `createSecureServer` factory to enable HTTP/2:
```js
import { createSecureServer } from 'node:http2'
import { create } from '@vates/http-server-plus'
const server = create({ createSecureServer }, app)
await server.listen({ port: 443, cert, key })
```
### `server.close()`
Closes all underlying servers. Returns a promise that resolves once the `close` event is emitted.
```js
await server.close()
```

View File

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

View File

@@ -1,98 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/http-server-plus
[![Package Version](https://badgen.net/npm/v/@vates/http-server-plus)](https://npmjs.org/package/@vates/http-server-plus) ![License](https://badgen.net/npm/license/@vates/http-server-plus) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/http-server-plus)](https://bundlephobia.com/result?p=@vates/http-server-plus) [![Node compatibility](https://badgen.net/npm/node/@vates/http-server-plus)](https://npmjs.org/package/@vates/http-server-plus)
> Augmented &#x60;http.Server&#x60;, HTTP/HTTPS/HTTP2 and multiple ports on the same instance
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/http-server-plus):
```sh
npm install --save @vates/http-server-plus
```
## Usage
### `create([opts], [requestListener])`
Creates a new server instance. `opts` may contain `createServer` and `createSecureServer` factories (defaults to `http.createServer` and `https.createServer`). `requestListener` is registered as a `request` event listener.
```js
import { create } from '@vates/http-server-plus'
const server = create(function onRequest(req, res) {
res.end('Hello')
})
```
### `server.listen(opts)`
Starts listening. Returns a promise that resolves to a human-readable address string, or rejects with `error.niceAddress` set.
```js
// HTTP on a specific port
await server.listen({ port: 8080 })
// HTTPS — detected automatically when cert+key are provided
await server.listen({
port: 443,
cert: fs.readFileSync('certificate.pem'),
key: fs.readFileSync('private_key.pem'),
})
// Unix socket
await server.listen({ socket: '/var/run/http.sock' })
// File descriptor
await server.listen({ fd: 3 })
// Systemd socket activation
await server.listen({ systemdSocket: 0 })
```
> As a convenience, if `hostname` is `'localhost'`, it will listen on both `127.0.0.1` and `::1`, similar to what Node does when no hostname is provided.
Multiple endpoints can be started on the same instance:
```js
const [httpAddr, httpsAddr] = await Promise.all([server.listen({ port: 80 }), server.listen({ port: 443, cert, key })])
console.log('listening on', httpAddr, httpsAddr)
```
### HTTP/2
Pass a custom `createSecureServer` factory to enable HTTP/2:
```js
import { createSecureServer } from 'node:http2'
import { create } from '@vates/http-server-plus'
const server = create({ createSecureServer }, app)
await server.listen({ port: 443, cert, key })
```
### `server.close()`
Closes all underlying servers. Returns a promise that resolves once the `close` event is emitted.
```js
await server.close()
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,268 +0,0 @@
'use strict'
// ===================================================================
const EventEmitter = require('events').EventEmitter
const formatUrl = require('url').format
const fromEvent = require('promise-toolbox/fromEvent')
const inherits = require('util').inherits
const resolvePath = require('path').resolve
// ===================================================================
const forwardedEvents = [
'checkContinue',
'checkExpectation',
'clientError',
'connect',
'connection',
'listening',
'request',
'secureConnection',
'upgrade',
]
function extractProperty(obj, prop) {
const value = obj[prop]
if (value !== undefined) {
delete obj[prop]
return value
}
}
function forOwn(obj, cb) {
const keys = Object.keys(obj)
for (let i = 0, n = keys.length; i < n; ++i) {
cb(obj[keys[i]])
}
}
function getSystemdFd(index) {
if (process.pid !== +process.env.LISTEN_PID) {
throw new Error('systemd sockets are not meant for us (LISTEN_PID)')
}
const listenFds = process.env.LISTEN_FDS
if (listenFds == null) {
throw new Error('no systemd sockets found (LISTEN_FDS)')
}
const max = listenFds - 1
if (index > max) {
throw new Error('no such systemd socket found')
}
return index + 3 // 3 = SD_LISTEN_FDS_START
}
const isEmpty = obj => Object.keys(obj).length === 0
// ===================================================================
function onNewListener(event, listener) {
if (forwardedEvents.includes(event)) {
forOwn(this._servers, server => {
server.on(event, listener)
})
}
}
function onRemoveListener(event, listener) {
if (forwardedEvents.includes(event)) {
forOwn(this._servers, server => {
server.off(event, listener)
})
}
}
function Server(opts) {
EventEmitter.call(this)
this._createServer = (opts != null && opts.createServer) || require('http').createServer
this._createSecureServer = (opts != null && opts.createSecureServer) || require('https').createServer
this._servers = Object.create(null)
this._niceAddresses = Object.create(null)
this.on('removeListener', onRemoveListener).on('newListener', onNewListener)
}
inherits(Server, EventEmitter)
const proto = Server.prototype
function getAddress(serverId) {
return this[serverId].address()
}
proto.addresses = function Server$addresses() {
const servers = this._servers
return Object.keys(servers).map(getAddress, servers)
}
function getNiceAddress(serverId) {
return this[serverId]
}
proto.niceAddresses = function Server$niceAddresses() {
const niceAddresses = this._niceAddresses
return Object.keys(niceAddresses).map(getNiceAddress, niceAddresses)
}
function close(server) {
server.close()
}
proto.close = function Server$close(callback) {
if (callback) {
this.once('close', callback)
}
// Emit the close event even if there are no registered servers.
if (isEmpty(this._servers)) {
const self = this
setImmediate(function () {
self.emit('close')
})
return Promise.resolve()
}
// Closes each servers.
forOwn(this._servers, close)
return fromEvent(this, 'close')
}
let nextId = 0
proto.listen = function Server$listen(opts) {
opts = Object.assign({}, opts)
if (opts.hostname === 'localhost') {
return Promise.all([
this.listen(Object.assign(opts, { hostname: '::1' })),
this.listen(Object.assign(opts, { hostname: '127.0.0.1' })),
]).then(addresses => addresses[0])
}
const hostname = extractProperty(opts, 'hostname')
let port = extractProperty(opts, 'port')
const systemdSocket = extractProperty(opts, 'systemdSocket')
let fd = extractProperty(opts, 'fd')
let socket = extractProperty(opts, 'socket')
const servers = this._servers
const niceAddresses = this._niceAddresses
let server, protocol
if (opts.pfx || opts.SNICallback || (opts.cert && opts.key)) {
server = this._createSecureServer(opts)
protocol = 'https'
} else {
server = this._createServer(opts)
protocol = 'http'
}
const id = nextId++
servers[id] = server
if (systemdSocket != null) {
fd = getSystemdFd(systemdSocket)
}
// Compute a temporary nice address to display in case of error.
let niceAddress
if (fd != null) {
server.listen({ fd })
niceAddress = protocol + '://<fd:' + fd + '>'
} else if (socket != null) {
socket = resolvePath(socket)
server.listen(socket)
niceAddress = protocol + '://' + socket
} else {
if (port == null) {
port = protocol === 'https' ? 443 : 80
}
server.listen(port, hostname)
niceAddress = formatUrl({
protocol,
// Hostname default to localhost.
hostname: hostname || 'localhost',
// No port means random, unknown for now.
port: port || '<unknown>',
})
}
const emit = this.emit.bind(this)
server.once('close', function onClose() {
delete servers[id]
delete niceAddresses[id]
if (isEmpty(servers)) {
emit('close')
}
})
server.on('error', function onError() {
delete servers[id]
// FIXME: Should it be forwarded and be fatal if there are no
// listeners?
})
forwardedEvents.forEach(event => {
this.listeners(event).forEach(listener => {
server.on(event, listener)
})
})
return fromEvent(server, 'listening').then(
function () {
const address = server.address()
const niceAddress =
typeof address === 'string'
? protocol + '://' + address
: formatUrl({
protocol,
hostname: address.address,
port: address.port,
})
niceAddresses[id] = niceAddress
return niceAddress
},
function (error) {
error.niceAddress = niceAddress
throw error
}
)
}
function ref(server) {
server.ref()
}
proto.ref = function Server$ref() {
forOwn(this._servers, ref)
}
function unref(server) {
server.unref()
}
proto.unref = function Server$unref() {
forOwn(this._servers, unref)
}
// ===================================================================
module.exports = exports = Server
exports.create = function create(opts, requestListener) {
if (typeof opts === 'function') {
requestListener = opts
opts = undefined
}
const server = new Server(opts)
if (requestListener !== undefined) {
server.on('request', requestListener)
}
return server
}

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