Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
766175b4a0 feat(xo-server): multi processes 2018-05-15 15:47:32 +02:00
1007 changed files with 26189 additions and 67005 deletions

View File

@@ -3,12 +3,63 @@
# Julien Fontanet's configuration
# https://gist.github.com/julien-f/8096213
# Top-most EditorConfig file.
root = true
# Common config.
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
# CoffeeScript
#
# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md
[*.{,lit}coffee]
indent_size = 2
indent_style = space
# Markdown
[*.{md,mdwn,mdown,markdown}]
indent_size = 4
indent_style = space
# Package.json
#
# This indentation style is the one used by npm.
[package.json]
indent_size = 2
indent_style = space
# Pug (Jade)
[*.{jade,pug}]
indent_size = 2
indent_style = space
# JavaScript
#
# Two spaces seems to be the standard most common style, at least in
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
[*.{js,jsx,ts,tsx}]
indent_size = 2
indent_style = space
# Less
[*.less]
indent_size = 2
indent_style = space
# Sass
#
# Style used for http://libsass.com
[*.s[ac]ss]
indent_size = 2
indent_style = space
# YAML
#
# Only spaces are allowed.
[*.yaml]
indent_size = 2
indent_style = space

View File

@@ -1,2 +0,0 @@
# xo_fs_nfs=nfs://ip:/folder
# xo_fs_smb=smb://login:pass@domain\\ip\folder

View File

@@ -1,56 +1,22 @@
module.exports = {
extends: [
'plugin:eslint-comments/recommended',
'standard',
'standard-jsx',
'prettier',
'prettier/standard',
'prettier/react',
],
extends: ['standard', 'standard-jsx'],
globals: {
__DEV__: true,
$Dict: true,
$Diff: true,
$ElementType: true,
$Exact: true,
$Keys: true,
$PropertyType: true,
$Shape: true,
},
overrides: [
{
files: ['cli.js', '*-cli.js', '**/*cli*/**/*.js'],
rules: {
'no-console': 'off',
},
},
],
parser: 'babel-eslint',
parserOptions: {
ecmaFeatures: {
legacyDecorators: true,
},
},
rules: {
// disabled because XAPI objects are using camel case
camelcase: ['off'],
'react/jsx-handler-names': 'off',
// disabled because not always relevant, we might reconsider in the future
//
// enabled by https://github.com/standard/eslint-config-standard/commit/319b177750899d4525eb1210686f6aca96190b2f
//
// example: https://github.com/vatesfr/xen-orchestra/blob/31ed3767c67044ca445658eb6b560718972402f2/packages/xen-api/src/index.js#L156-L157
'lines-between-class-members': 'off',
'no-console': ['error', { allow: ['warn', 'error'] }],
'comma-dangle': ['error', 'always-multiline'],
indent: 'off',
'no-var': 'error',
'node/no-extraneous-import': 'error',
'node/no-extraneous-require': 'error',
'prefer-const': 'error',
'react/jsx-indent': 'off',
},
}

View File

@@ -9,7 +9,6 @@
[options]
esproposal.decorators=ignore
esproposal.optional_chaining=enable
include_warnings=true
module.use_strict=true

3
.gitignore vendored
View File

@@ -1,4 +1,3 @@
/_book/
/coverage/
/node_modules/
/lerna-debug.log
@@ -11,7 +10,6 @@
/packages/vhd-cli/src/commands/index.js
/packages/xen-api/examples/node_modules/
/packages/xen-api/plot.dat
/packages/xo-server/.xo-server.*
@@ -30,4 +28,3 @@ pnpm-debug.log
pnpm-debug.log.*
yarn-error.log
yarn-error.log.*
.env

View File

@@ -1,5 +1,4 @@
module.exports = {
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',

View File

@@ -2,6 +2,7 @@ language: node_js
node_js:
#- stable # disable for now due to an issue of indirect dep upath with Node 9
- 8
- 6
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
@@ -9,9 +10,9 @@ sudo: false
addons:
apt:
packages:
- qemu-utils
- blktap-utils
- vmdk-stream-converter
- qemu-utils
- blktap-utils
- vmdk-stream-converter
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
@@ -21,4 +22,5 @@ cache:
yarn: true
script:
- yarn run travis-tests
- yarn run test
- yarn run test-integration

View File

@@ -1,3 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -1,49 +0,0 @@
# @xen-orchestra/async-map [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> ${pkg.description}
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/async-map):
```
> npm install --save @xen-orchestra/async-map
```
## Usage
**TODO**
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## 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 © [Vates SAS](https://vates.fr)

View File

@@ -1,53 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/async-map",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/async-map",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/async-map",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"dependencies": {
"lodash": "^4.17.4"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepare": "yarn run build",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,43 +0,0 @@
// type MaybePromise<T> = Promise<T> | T
//
// declare export function asyncMap<T1, T2>(
// collection: MaybePromise<T1[]>,
// (T1, number) => MaybePromise<T2>
// ): Promise<T2[]>
// declare export function asyncMap<K, V1, V2>(
// collection: MaybePromise<{ [K]: V1 }>,
// (V1, K) => MaybePromise<V2>
// ): Promise<V2[]>
import map from 'lodash/map'
// Similar to map() + Promise.all() but wait for all promises to
// settle before rejecting (with the first error)
const asyncMap = (collection, iteratee) => {
let then
if (collection != null && typeof (then = collection.then) === 'function') {
return then.call(collection, collection => asyncMap(collection, iteratee))
}
let errorContainer
const onError = error => {
if (errorContainer === undefined) {
errorContainer = { error }
}
}
return Promise.all(
map(collection, (item, key, collection) =>
new Promise(resolve => {
resolve(iteratee(item, key, collection))
}).catch(onError)
)
).then(values => {
if (errorContainer !== undefined) {
throw errorContainer.error
}
return values
})
}
export { asyncMap as default }

View File

@@ -1,79 +1,47 @@
'use strict'
const PLUGINS_RE = /^(?:@babel\/|babel-)plugin-.+$/
const PLUGINS_RE = /^(?:@babel\/plugin-.+|babel-plugin-lodash)$/
const PRESETS_RE = /^@babel\/preset-.+$/
const NODE_ENV = process.env.NODE_ENV || 'development'
const __PROD__ = NODE_ENV === 'production'
const __TEST__ = NODE_ENV === 'test'
const configs = {
'@babel/plugin-proposal-decorators': {
legacy: true,
},
'@babel/plugin-proposal-pipeline-operator': {
proposal: 'minimal',
},
'@babel/preset-env'(pkg) {
return {
debug: !__TEST__,
module.exports = function (pkg, plugins, presets) {
plugins === undefined && (plugins = {})
// disabled until https://github.com/babel/babel/issues/8323 is resolved
// loose: true,
shippedProposals: true,
targets: (() => {
presets === undefined && (presets = {})
presets['@babel/preset-env'] = {
debug: !__TEST__,
loose: true,
shippedProposals: true,
targets: __PROD__
? (() => {
let node = (pkg.engines || {}).node
if (node !== undefined) {
const trimChars = '^=>~'
while (trimChars.includes(node[0])) {
node = node.slice(1)
}
return { node: node }
}
return { browsers: pkg.browserslist, node }
})(),
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
}
},
}
const getConfig = (key, ...args) => {
const config = configs[key]
return config === undefined
? {}
: typeof config === 'function'
? config(...args)
: config
}
// some plugins must be used in a specific order
const pluginsOrder = [
'@babel/plugin-proposal-decorators',
'@babel/plugin-proposal-class-properties',
]
module.exports = function(pkg, plugins, presets) {
plugins === undefined && (plugins = {})
presets === undefined && (presets = {})
})()
: { browsers: '', node: 'current' },
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
}
Object.keys(pkg.devDependencies || {}).forEach(name => {
if (!(name in presets) && PLUGINS_RE.test(name)) {
plugins[name] = getConfig(name, pkg)
plugins[name] = {}
} else if (!(name in presets) && PRESETS_RE.test(name)) {
presets[name] = getConfig(name, pkg)
presets[name] = {}
}
})
return {
comments: !__PROD__,
ignore: __TEST__ ? undefined : [/\.spec\.js$/],
plugins: Object.keys(plugins)
.map(plugin => [plugin, plugins[plugin]])
.sort(([a], [b]) => {
const oA = pluginsOrder.indexOf(a)
const oB = pluginsOrder.indexOf(b)
return oA !== -1 && oB !== -1 ? oA - oB : a < b ? -1 : 1
}),
plugins: Object.keys(plugins).map(plugin => [plugin, plugins[plugin]]),
presets: Object.keys(presets).map(preset => [preset, presets[preset]]),
}
}

View File

@@ -5,11 +5,7 @@
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/babel-config",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/babel-config",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"engines": {
"node": ">=6"
}
}

View File

@@ -1,7 +0,0 @@
const curryRight = require('lodash/curryRight')
module.exports = curryRight((iterable, fn) =>
Promise.all(
Array.isArray(iterable) ? iterable.map(fn) : Array.from(iterable, fn)
)
)

View File

@@ -1,32 +0,0 @@
const getopts = require('getopts')
const { version } = require('./package.json')
module.exports = commands =>
async function(args, prefix) {
const opts = getopts(args, {
alias: {
help: 'h',
},
boolean: ['help'],
stopEarly: true,
})
const commandName = opts.help || args.length === 0 ? 'help' : args[0]
const command = commands[commandName]
if (command === undefined) {
process.stdout.write(`Usage:
${Object.keys(commands)
.filter(command => command !== 'help')
.map(command => ` ${prefix} ${command} ${commands[command].usage || ''}`)
.join('\n\n')}
xo-backups v${version}
`)
process.exitCode = commandName === 'help' ? 0 : 1
return
}
return command.main(args.slice(1), prefix + ' ' + commandName)
}

View File

@@ -1,57 +0,0 @@
const { dirname } = require('path')
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
module.exports = fs
fs.mktree = async function mkdirp(path) {
try {
await fs.mkdir(path)
} catch (error) {
const { code } = error
if (code === 'EEXIST') {
await fs.readdir(path)
return
}
if (code === 'ENOENT') {
await mkdirp(dirname(path))
return mkdirp(path)
}
throw error
}
}
// - easier:
// - single param for direct use in `Array#map`
// - files are prefixed with directory path
// - safer: returns empty array if path is missing or not a directory
fs.readdir2 = path =>
fs.readdir(path).then(
entries => {
entries.forEach((entry, i) => {
entries[i] = `${path}/${entry}`
})
return entries
},
error => {
if (
error != null &&
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
) {
console.warn('WARN: readdir(%s)', path, error)
return []
}
throw error
}
)
fs.symlink2 = async (target, path) => {
try {
await fs.symlink(target, path)
} catch (error) {
if (error.code === 'EEXIST' && (await fs.readlink(path)) === target) {
return
}
throw error
}
}

View File

@@ -1,308 +0,0 @@
#!/usr/bin/env node
// assigned when options are parsed by the main function
let force
// -----------------------------------------------------------------------------
const assert = require('assert')
const flatten = require('lodash/flatten')
const getopts = require('getopts')
const isValidXva = require('@xen-orchestra/backups/isValidXva')
const lockfile = require('proper-lockfile')
const pipe = require('promise-toolbox/pipe')
const { default: Vhd } = require('vhd-lib')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const asyncMap = require('../_asyncMap')
const fs = require('../_fs')
const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })
// -----------------------------------------------------------------------------
// chain is an array of VHDs from child to parent
//
// the whole chain will be merged into parent, parent will be renamed to child
// and all the others will deleted
async function mergeVhdChain(chain) {
assert(chain.length >= 2)
const child = chain[0]
const parent = chain[chain.length - 1]
const children = chain.slice(0, -1).reverse()
console.warn('Unused parents of VHD', child)
chain
.slice(1)
.reverse()
.forEach(parent => {
console.warn(' ', parent)
})
force && console.warn(' merging…')
console.warn('')
if (force) {
// `mergeVhd` does not work with a stream, either
// - make it accept a stream
// - or create synthetic VHD which is not a stream
return console.warn('TODO: implement merge')
// await mergeVhd(
// handler,
// parent,
// handler,
// children.length === 1
// ? child
// : await createSyntheticStream(handler, children)
// )
}
await Promise.all([
force && fs.rename(parent, child),
asyncMap(children.slice(0, -1), child => {
console.warn('Unused VHD', child)
force && console.warn(' deleting…')
console.warn('')
return force && handler.unlink(child)
}),
])
}
const listVhds = pipe([
vmDir => vmDir + '/vdis',
fs.readdir2,
asyncMap(fs.readdir2),
flatten,
asyncMap(fs.readdir2),
flatten,
_ => _.filter(_ => _.endsWith('.vhd')),
])
async function handleVm(vmDir) {
const vhds = new Set()
const vhdParents = { __proto__: null }
const vhdChildren = { __proto__: null }
// remove broken VHDs
await asyncMap(await listVhds(vmDir), async path => {
try {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
vhds.add(path)
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
vhdParents[path] = parent
if (parent in vhdChildren) {
const error = new Error(
'this script does not support multiple VHD children'
)
error.parent = parent
error.child1 = vhdChildren[parent]
error.child2 = path
throw error // should we throw?
}
vhdChildren[parent] = path
}
} catch (error) {
console.warn('Error while checking VHD', path)
console.warn(' ', error)
if (error != null && error.code === 'ERR_ASSERTION') {
force && console.warn(' deleting…')
console.warn('')
force && (await handler.unlink(path))
}
}
})
// remove VHDs with missing ancestors
{
const deletions = []
// return true if the VHD has been deleted or is missing
const deleteIfOrphan = vhd => {
const parent = vhdParents[vhd]
if (parent === undefined) {
return
}
// no longer needs to be checked
delete vhdParents[vhd]
deleteIfOrphan(parent)
if (!vhds.has(parent)) {
vhds.delete(vhd)
console.warn('Error while checking VHD', vhd)
console.warn(' missing parent', parent)
force && console.warn(' deleting…')
console.warn('')
force && deletions.push(handler.unlink(vhd))
}
}
// > A property that is deleted before it has been visited will not be
// > visited later.
// >
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
for (const child in vhdParents) {
deleteIfOrphan(child)
}
await Promise.all(deletions)
}
const [jsons, xvas] = await fs
.readdir2(vmDir)
.then(entries => [
entries.filter(_ => _.endsWith('.json')),
new Set(entries.filter(_ => _.endsWith('.xva'))),
])
await asyncMap(xvas, async path => {
// check is not good enough to delete the file, the best we can do is report
// it
if (!(await isValidXva(path))) {
console.warn('Potential broken XVA', path)
console.warn('')
}
})
const unusedVhds = new Set(vhds)
const unusedXvas = new Set(xvas)
// compile the list of unused XVAs and VHDs, and remove backup metadata which
// reference a missing XVA/VHD
await asyncMap(jsons, async json => {
const metadata = JSON.parse(await fs.readFile(json))
const { mode } = metadata
if (mode === 'full') {
const linkedXva = resolve(vmDir, metadata.xva)
if (xvas.has(linkedXva)) {
unusedXvas.delete(linkedXva)
} else {
console.warn('Error while checking backup', json)
console.warn(' missing file', linkedXva)
force && console.warn(' deleting…')
console.warn('')
force && (await handler.unlink(json))
}
} else if (mode === 'delta') {
const linkedVhds = (() => {
const { vhds } = metadata
return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
})()
// FIXME: find better approach by keeping as much of the backup as
// possible (existing disks) even if one disk is missing
if (linkedVhds.every(_ => vhds.has(_))) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
} else {
console.warn('Error while checking backup', json)
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
console.warn(
' %i/%i missing VHDs',
missingVhds.length,
linkedVhds.length
)
missingVhds.forEach(vhd => {
console.warn(' ', vhd)
})
force && console.warn(' deleting…')
console.warn('')
force && (await handler.unlink(json))
}
}
})
// TODO: parallelize by vm/job/vdi
const unusedVhdsDeletion = []
{
// VHD chains (as list from child to ancestor) to merge indexed by last
// ancestor
const vhdChainsToMerge = { __proto__: null }
const toCheck = new Set(unusedVhds)
const getUsedChildChainOrDelete = vhd => {
if (vhd in vhdChainsToMerge) {
const chain = vhdChainsToMerge[vhd]
delete vhdChainsToMerge[vhd]
return chain
}
if (!unusedVhds.has(vhd)) {
return [vhd]
}
// no longer needs to be checked
toCheck.delete(vhd)
const child = vhdChildren[vhd]
if (child !== undefined) {
const chain = getUsedChildChainOrDelete(child)
if (chain !== undefined) {
chain.push(vhd)
return chain
}
}
console.warn('Unused VHD', vhd)
force && console.warn(' deleting…')
console.warn('')
force && unusedVhdsDeletion.push(handler.unlink(vhd))
}
toCheck.forEach(vhd => {
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
})
Object.keys(vhdChainsToMerge).forEach(key => {
const chain = vhdChainsToMerge[key]
if (chain !== undefined) {
unusedVhdsDeletion.push(mergeVhdChain(chain))
}
})
}
await Promise.all([
unusedVhdsDeletion,
asyncMap(unusedXvas, path => {
console.warn('Unused XVA', path)
force && console.warn(' deleting…')
console.warn('')
return force && handler.unlink(path)
}),
])
}
// -----------------------------------------------------------------------------
module.exports = async function main(args) {
const opts = getopts(args, {
alias: {
force: 'f',
},
boolean: ['force'],
default: {
force: false,
},
})
;({ force } = opts)
await asyncMap(opts._, async vmDir => {
vmDir = resolve(vmDir)
// TODO: implement this in `xo-server`, not easy because not compatible with
// `@xen-orchestra/fs`.
const release = await lockfile.lock(vmDir)
try {
await handleVm(vmDir)
} catch (error) {
console.error('handleVm', vmDir, error)
} finally {
await release()
}
})
}

View File

@@ -1,28 +0,0 @@
const filenamify = require('filenamify')
const get = require('lodash/get')
const { dirname, join, relative } = require('path')
const asyncMap = require('../_asyncMap')
const { mktree, readdir2, readFile, symlink2 } = require('../_fs')
module.exports = async function createSymlinkIndex([backupDir, fieldPath]) {
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
await mktree(indexDir)
await asyncMap(await readdir2(backupDir), async vmDir =>
asyncMap(
(await readdir2(vmDir)).filter(_ => _.endsWith('.json')),
async json => {
const metadata = JSON.parse(await readFile(json))
const value = get(metadata, fieldPath)
if (value !== undefined) {
const target = relative(indexDir, dirname(json))
const path = join(indexDir, filenamify(String(value)))
await symlink2(target, path).catch(error => {
console.warn('symlink(%s, %s)', target, path, error)
})
}
}
)
)
}

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env node
require('./_composeCommands')({
'clean-vms': {
get main() {
return require('./commands/clean-vms')
},
usage: '[--force] xo-vm-backups/*',
},
'create-symlink-index': {
get main() {
return require('./commands/create-symlink-index')
},
usage: 'xo-vm-backups <field path>',
},
})(process.argv.slice(2), 'xo-backups').catch(error => {
console.error('main', error)
process.exitCode = 1
})

View File

@@ -1,35 +0,0 @@
{
"private": false,
"bin": {
"xo-backups": "index.js"
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/backups": "^0.0.0",
"@xen-orchestra/fs": "^0.10.2",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.15.0",
"proper-lockfile": "^4.1.1",
"vhd-lib": "^0.7.2"
},
"engines": {
"node": ">=7.10.1"
},
"files": [
"commands",
"*.js"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
"name": "@xen-orchestra/backups-cli",
"repository": {
"directory": "@xen-orchestra/backups-cli",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"scripts": {
"postversion": "npm publish --access public"
},
"version": "0.0.0"
}

View File

@@ -1,65 +0,0 @@
const assert = require('assert')
const fs = require('fs-extra')
const isGzipFile = async fd => {
// https://tools.ietf.org/html/rfc1952.html#page-5
const magicNumber = Buffer.allocUnsafe(2)
assert.strictEqual(
await fs.read(fd, magicNumber, 0, magicNumber.length, 0),
magicNumber.length
)
return magicNumber[0] === 31 && magicNumber[1] === 139
}
// TODO: better check?
//
// our heuristic is not good enough, there has been some false positives
// (detected as invalid by us but valid by `tar` and imported with success),
// either THOUGH THEY MAY HAVE BEEN COMPRESSED FILES:
// - these files were normal but the check is incorrect
// - these files were invalid but without data loss
// - these files were invalid but with silent data loss
//
// maybe reading the end of the file looking for a file named
// /^Ref:\d+/\d+\.checksum$/ and then validating the tar structure from it
//
// https://github.com/npm/node-tar/issues/234#issuecomment-538190295
const isValidTar = async (size, fd) => {
if (size <= 1024 || size % 512 !== 0) {
return false
}
const buf = Buffer.allocUnsafe(1024)
assert.strictEqual(
await fs.read(fd, buf, 0, buf.length, size - buf.length),
buf.length
)
return buf.every(_ => _ === 0)
}
// TODO: find an heuristic for compressed files
const isValidXva = async path => {
try {
const fd = await fs.open(path, 'r')
try {
const { size } = await fs.fstat(fd)
if (size < 20) {
// neither a valid gzip not tar
return false
}
return (await isGzipFile(fd))
? true // gzip files cannot be validated at this time
: await isValidTar(size, fd)
} finally {
fs.close(fd).catch(noop)
}
} catch (error) {
// never throw, log and report as valid to avoid side effects
console.error('isValidXva', path, error)
return true
}
}
exports.isValidXva = isValidXva
const noop = Function.prototype

View File

@@ -1,21 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/backups",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/backups",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.0.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public"
},
"dependencies": {
"fs-extra": "^8.1.0"
}
}

View File

@@ -1,108 +0,0 @@
#!/usr/bin/env node
const defer = require('golike-defer').default
const { NULL_REF, Xapi } = require('xen-api')
const pkg = require('./package.json')
Xapi.prototype.getVmDisks = async function(vm) {
const disks = { __proto__: null }
await Promise.all([
...vm.VBDs.map(async vbdRef => {
const vbd = await this.getRecord('VBD', vbdRef)
let vdiRef
if (vbd.type === 'Disk' && (vdiRef = vbd.VDI) !== NULL_REF) {
disks[vbd.userdevice] = await this.getRecord('VDI', vdiRef)
}
}),
])
return disks
}
defer(async function main($defer, args) {
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
const cliName = Object.keys(pkg.bin)[0]
return console.error(
'%s',
`
Usage: ${cliName} <source XAPI URL> <source snapshot UUID> <target XAPI URL> <target VM UUID> <backup job id> <backup schedule id>
${cliName} v${pkg.version}
`
)
}
const [
srcXapiUrl,
srcSnapshotUuid,
tgtXapiUrl,
tgtVmUuid,
jobId,
scheduleId,
] = args
const srcXapi = new Xapi({
allowUnauthorized: true,
url: srcXapiUrl,
watchEvents: false,
})
await srcXapi.connect()
defer.call(srcXapi, 'disconnect')
const tgtXapi = new Xapi({
allowUnauthorized: true,
url: tgtXapiUrl,
watchEvents: false,
})
await tgtXapi.connect()
defer.call(tgtXapi, 'disconnect')
const [srcSnapshot, tgtVm] = await Promise.all([
srcXapi.getRecordByUuid('VM', srcSnapshotUuid),
tgtXapi.getRecordByUuid('VM', tgtVmUuid),
])
const srcVm = await srcXapi.getRecord('VM', srcSnapshot.snapshot_of)
const metadata = {
'xo:backup:datetime': srcSnapshot.snapshot_time,
'xo:backup:job': jobId,
'xo:backup:schedule': scheduleId,
'xo:backup:vm': srcVm.uuid,
}
const [srcDisks, tgtDisks] = await Promise.all([
srcXapi.getVmDisks(srcSnapshot),
tgtXapi.getVmDisks(tgtVm),
])
const userDevices = Object.keys(tgtDisks)
const tgtSr = await tgtXapi.getRecord(
'SR',
tgtDisks[Object.keys(tgtDisks)[0]].SR
)
await Promise.all([
srcSnapshot.update_other_config(metadata),
srcSnapshot.update_other_config('xo:backup:exported', 'true'),
tgtVm.set_name_label(`${srcVm.name_label} (${srcSnapshot.snapshot_time})`),
tgtVm.update_other_config(metadata),
tgtVm.update_other_config({
'xo:backup:sr': tgtSr.uuid,
'xo:copy_of': srcSnapshotUuid,
}),
tgtVm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
),
Promise.all(
userDevices.map(userDevice => {
const srcDisk = srcDisks[userDevice]
const tgtDisk = tgtDisks[userDevice]
return tgtDisk.update_other_config({
'xo:copy_of': srcDisk.uuid,
})
})
),
])
})(process.argv.slice(2)).catch(console.error.bind(console, 'Fatal error:'))

View File

@@ -1,25 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/cr-seed-cli",
"version": "0.2.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/cr-seed-cli",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/cr-seed-cli",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"engines": {
"node": ">=8"
},
"bin": {
"xo-cr-seed": "./index.js"
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.27.4"
},
"scripts": {
"postversion": "npm publish"
}
}

View File

@@ -16,6 +16,7 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cron)
<minute> <hour> <day of month> <month> <day of week>
```
Each entry can be:
- a single value
@@ -28,12 +29,12 @@ A wildcard (`*`) can be used as a shortcut for the whole range
Step values can be used in conjunctions with ranges. For instance,
`1-7/2` is the same as `1,3,5,7`.
| Field | Allowed values |
| ---------------- | ------------------------------------------------------------------ |
| minute | 0-59 |
| hour | 0-23 |
| day of the month | 1-31 or 3-letter names (`jan`, `feb`, …) |
| month | 0-11 |
| Field | Allowed values |
|------------------|----------------|
| minute | 0-59 |
| hour | 0-23 |
| day of the month | 1-31 or 3-letter names (`jan`, `feb`, …) |
| month | 0-11 |
| day of week | 0-7 (0 and 7 both mean Sunday) or 3-letter names (`mon`, `tue`, …) |
> Note: the month range is 0-11 to be compatible with
@@ -130,7 +131,7 @@ job.stop()
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:

View File

@@ -1,7 +1,6 @@
{
"private": false,
"name": "@xen-orchestra/cron",
"version": "1.0.6",
"version": "1.0.3",
"license": "ISC",
"description": "Focused, well maintained, cron parser/scheduler",
"keywords": [
@@ -18,7 +17,6 @@
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/cron",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/cron",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
@@ -43,12 +41,12 @@
"moment-timezone": "^0.5.14"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -56,7 +54,6 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -6,28 +6,9 @@ import parse from './parse'
const MAX_DELAY = 2 ** 31 - 1
class Job {
constructor(schedule, fn) {
let scheduledDate
constructor (schedule, fn) {
const wrapper = () => {
const now = Date.now()
if (scheduledDate > now) {
// we're early, delay
//
// no need to check _isEnabled, we're just delaying the existing timeout
//
// see https://github.com/vatesfr/xen-orchestra/issues/4625
this._timeout = setTimeout(wrapper, scheduledDate - now)
return
}
this._isRunning = true
let result
try {
result = fn()
} catch (_) {
// catch any thrown value to ensure it does not break the job
}
const result = fn()
let then
if (result != null && typeof (then = result.then) === 'function') {
then.call(result, scheduleNext, scheduleNext)
@@ -36,56 +17,41 @@ class Job {
}
}
const scheduleNext = () => {
this._isRunning = false
if (this._isEnabled) {
const now = schedule._createDate()
scheduledDate = +next(schedule._schedule, now)
const delay = scheduledDate - now
this._timeout =
delay < MAX_DELAY
? setTimeout(wrapper, delay)
: setTimeout(scheduleNext, MAX_DELAY)
}
const delay = schedule._nextDelay()
this._timeout =
delay < MAX_DELAY
? setTimeout(wrapper, delay)
: setTimeout(scheduleNext, MAX_DELAY)
}
this._isEnabled = false
this._isRunning = false
this._scheduleNext = scheduleNext
this._timeout = undefined
}
start() {
start () {
this.stop()
this._isEnabled = true
if (!this._isRunning) {
this._scheduleNext()
}
this._scheduleNext()
}
stop() {
this._isEnabled = false
stop () {
clearTimeout(this._timeout)
}
}
class Schedule {
constructor(pattern, zone = 'utc') {
constructor (pattern, zone = 'utc') {
this._schedule = parse(pattern)
this._createDate =
zone.toLowerCase() === 'utc'
? moment.utc
: zone === 'local'
? moment
: () => moment.tz(zone)
: zone === 'local' ? moment : () => moment.tz(zone)
}
createJob(fn) {
createJob (fn) {
return new Job(this, fn)
}
next(n) {
next (n) {
const dates = new Array(n)
const schedule = this._schedule
let date = this._createDate()
@@ -95,7 +61,12 @@ class Schedule {
return dates
}
startJob(fn) {
_nextDelay () {
const now = this._createDate()
return next(this._schedule, now) - now
}
startJob (fn) {
const job = this.createJob(fn)
job.start()
return job.stop.bind(job)

View File

@@ -1,78 +0,0 @@
/* eslint-env jest */
import { createSchedule } from './'
const wrap = value => () => value
describe('issues', () => {
let originalDateNow
beforeAll(() => {
originalDateNow = Date.now
})
afterAll(() => {
Date.now = originalDateNow
originalDateNow = undefined
})
test('stop during async execution', async () => {
let nCalls = 0
let resolve, promise
const schedule = createSchedule('* * * * *')
const job = schedule.createJob(() => {
++nCalls
// eslint-disable-next-line promise/param-names
promise = new Promise(r => {
resolve = r
})
return promise
})
job.start()
Date.now = wrap(+schedule.next(1)[0])
jest.runAllTimers()
expect(nCalls).toBe(1)
job.stop()
resolve()
await promise
jest.runAllTimers()
expect(nCalls).toBe(1)
})
test('stop then start during async job execution', async () => {
let nCalls = 0
let resolve, promise
const schedule = createSchedule('* * * * *')
const job = schedule.createJob(() => {
++nCalls
// eslint-disable-next-line promise/param-names
promise = new Promise(r => {
resolve = r
})
return promise
})
job.start()
Date.now = wrap(+schedule.next(1)[0])
jest.runAllTimers()
expect(nCalls).toBe(1)
job.stop()
job.start()
resolve()
await promise
Date.now = wrap(+schedule.next(1)[0])
jest.runAllTimers()
expect(nCalls).toBe(2)
})
})

View File

@@ -1,3 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -1,49 +0,0 @@
# @xen-orchestra/defined [![Build Status](https://travis-ci.org/${pkg.shortGitHubPath}.png?branch=master)](https://travis-ci.org/${pkg.shortGitHubPath})
> ${pkg.description}
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/defined):
```
> npm install --save @xen-orchestra/defined
```
## Usage
**TODO**
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## 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 © [Vates SAS](https://vates.fr)

View File

@@ -1,50 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/defined",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/defined",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/defined",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,3 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -1,74 +0,0 @@
# @xen-orchestra/emit-async [![Build Status](https://travis-ci.org/${pkg.shortGitHubPath}.png?branch=master)](https://travis-ci.org/${pkg.shortGitHubPath})
> ${pkg.description}
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/emit-async):
```
> npm install --save @xen-orchestra/emit-async
```
## Usage
```js
import EE from 'events'
import emitAsync from '@xen-orchestra/emit-async'
const ee = new EE()
ee.emitAsync = emitAsync
ee.on('start', async function() {
// whatever
})
// similar to EventEmmiter#emit() but returns a promise which resolves when all
// listeners have resolved
await ee.emitAsync('start')
// by default, it will rejects as soon as one listener reject, you can customise
// error handling though:
await ee.emitAsync(
{
onError(error) {
console.warn(error)
},
},
'start'
)
```
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## 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 © [Vates SAS](https://vates.fr)

View File

@@ -1,49 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/emit-async",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/emit-async",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/emit-async",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,26 +0,0 @@
export default function emitAsync(event) {
let opts
let i = 1
// an option object has been passed as first param
if (typeof event !== 'string') {
opts = event
event = arguments[i++]
}
const n = arguments.length - i
const args = new Array(n)
for (let j = 0; j < n; ++j) {
args[j] = arguments[j + i]
}
const onError = opts != null && opts.onError
return Promise.all(
this.listeners(event).map(listener =>
new Promise(resolve => {
resolve(listener.apply(this, args))
}).catch(onError)
)
)
}

View File

@@ -1,14 +1,12 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "0.10.2",
"version": "0.0.0",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/fs",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
@@ -19,38 +17,31 @@
"dist/"
],
"engines": {
"node": ">=8.10"
"node": ">=6"
},
"dependencies": {
"@marsaud/smb2": "^0.15.0",
"@sindresorhus/df": "^3.1.1",
"@xen-orchestra/async-map": "^0.0.0",
"decorator-synchronized": "^0.5.0",
"execa": "^3.2.0",
"fs-extra": "^8.0.1",
"get-stream": "^5.1.0",
"limit-concurrency-decorator": "^0.4.0",
"@babel/runtime": "^7.0.0-beta.44",
"@marsaud/smb2-promise": "^0.2.1",
"execa": "^0.10.0",
"fs-extra": "^5.0.0",
"get-stream": "^3.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.15.0",
"readable-stream": "^3.0.6",
"through2": "^3.0.0",
"tmp": "^0.1.0",
"xo-remote-parser": "^0.5.0"
"promise-toolbox": "^0.9.5",
"through2": "^2.0.3",
"tmp": "^0.0.33",
"xo-remote-parser": "^0.3"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.1.6",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"async-iterator-to-stream": "^1.1.0",
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.44",
"@babel/plugin-transform-runtime": "^7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^6.0.3",
"dotenv": "^8.0.0",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",
"rimraf": "^3.0.0"
"rimraf": "^2.6.2"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -58,7 +49,6 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepare": "yarn run build",
"postversion": "npm publish"
"prepare": "yarn run build"
}
}

View File

@@ -1,108 +0,0 @@
import execa from 'execa'
import fs from 'fs-extra'
import { ignoreErrors } from 'promise-toolbox'
import { join } from 'path'
import { tmpdir } from 'os'
import LocalHandler from './local'
const sudoExeca = (command, args, opts) =>
execa('sudo', [command, ...args], opts)
export default class MountHandler extends LocalHandler {
constructor(
remote,
{
mountsDir = join(tmpdir(), 'xo-fs-mounts'),
useSudo = false,
...opts
} = {},
params
) {
super(remote, opts)
this._execa = useSudo ? sudoExeca : execa
this._keeper = undefined
this._params = {
...params,
options: [params.options, remote.options]
.filter(_ => _ !== undefined)
.join(','),
}
this._realPath = join(
mountsDir,
remote.id ||
Math.random()
.toString(36)
.slice(2)
)
}
async _forget() {
const keeper = this._keeper
if (keeper === undefined) {
return
}
this._keeper = undefined
await fs.close(keeper)
await ignoreErrors.call(
this._execa('umount', [this._getRealPath()], {
env: {
LANG: 'C',
},
})
)
}
_getRealPath() {
return this._realPath
}
async _sync() {
// in case of multiple `sync`s, ensure we properly close previous keeper
{
const keeper = this._keeper
if (keeper !== undefined) {
this._keeper = undefined
ignoreErrors.call(fs.close(keeper))
}
}
const realPath = this._getRealPath()
await fs.ensureDir(realPath)
try {
const { type, device, options, env } = this._params
await this._execa(
'mount',
['-t', type, device, realPath, '-o', options],
{
env: {
LANG: 'C',
...env,
},
}
)
} catch (error) {
try {
// the failure may mean it's already mounted, use `findmnt` to check
// that's the case
await this._execa('findmnt', [realPath], {
stdio: 'ignore',
})
} catch (_) {
throw error
}
}
// keep an open file on the mount to prevent it from being unmounted if used
// by another handler/process
const keeperPath = `${realPath}/.keeper_${Math.random()
.toString(36)
.slice(2)}`
this._keeper = await fs.open(keeperPath, 'w')
ignoreErrors.call(fs.unlink(keeperPath))
}
}

View File

@@ -1,9 +0,0 @@
import path from 'path'
const { resolve } = path.posix
// normalize the path:
// - does not contains `.` or `..` (cannot escape root dir)
// - always starts with `/`
const normalizePath = path => resolve('/', path)
export { normalizePath as default }

View File

@@ -1,197 +1,173 @@
// @flow
// $FlowFixMe
import getStream from 'get-stream'
import asyncMap from '@xen-orchestra/async-map'
import limit from 'limit-concurrency-decorator'
import path from 'path'
import synchronized from 'decorator-synchronized'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
import { parse } from 'xo-remote-parser'
import { randomBytes } from 'crypto'
import { fromCallback, fromEvent, ignoreErrors } from 'promise-toolbox'
import { type Readable, type Writable } from 'stream'
import { parse } from 'xo-remote-parser'
import normalizePath from './_normalizePath'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
const { dirname } = path.posix
type Data = Buffer | Readable | string
type FileDescriptor = {| fd: mixed, path: string |}
type LaxReadable = Readable & Object
type LaxWritable = Writable & Object
type RemoteInfo = { used?: number, size?: number }
type File = FileDescriptor | string
const checksumFile = file => file + '.checksum'
const computeRate = (hrtime: number[], size: number) => {
const seconds = hrtime[0] + hrtime[1] / 1e9
return size / seconds
}
const DEFAULT_TIMEOUT = 6e5 // 10 min
const DEFAULT_MAX_PARALLEL_OPERATIONS = 10
const ignoreEnoent = error => {
if (error == null || error.code !== 'ENOENT') {
throw error
}
}
class PrefixWrapper {
constructor(handler, prefix) {
this._prefix = prefix
this._handler = handler
}
get type() {
return this._handler.type
}
// necessary to remove the prefix from the path with `prependDir` option
async list(dir, opts) {
const entries = await this._handler.list(this._resolve(dir), opts)
if (opts != null && opts.prependDir) {
const n = this._prefix.length
entries.forEach((entry, i, entries) => {
entries[i] = entry.slice(n)
})
}
return entries
}
rename(oldPath, newPath) {
return this._handler.rename(this._resolve(oldPath), this._resolve(newPath))
}
_resolve(path) {
return this._prefix + normalizePath(path)
}
}
export default class RemoteHandlerAbstract {
_remote: Object
_timeout: number
constructor(remote: any, options: Object = {}) {
if (remote.url === 'test://') {
this._remote = remote
} else {
this._remote = { ...remote, ...parse(remote.url) }
if (this._remote.type !== this.type) {
throw new Error('Incorrect remote type')
}
constructor (remote: any) {
this._remote = { ...remote, ...parse(remote.url) }
if (this._remote.type !== this.type) {
throw new Error('Incorrect remote type')
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
const sharedLimit = limit(
options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS
)
this.closeFile = sharedLimit(this.closeFile)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
this.mkdir = sharedLimit(this.mkdir)
this.openFile = sharedLimit(this.openFile)
this.outputFile = sharedLimit(this.outputFile)
this.read = sharedLimit(this.read)
this.readFile = sharedLimit(this.readFile)
this.rename = sharedLimit(this.rename)
this.rmdir = sharedLimit(this.rmdir)
this.truncate = sharedLimit(this.truncate)
this.unlink = sharedLimit(this.unlink)
this.write = sharedLimit(this.write)
this.writeFile = sharedLimit(this.writeFile)
}
// Public members
get type(): string {
get type (): string {
throw new Error('Not implemented')
}
addPrefix(prefix: string) {
prefix = normalizePath(prefix)
return prefix === '/' ? this : new PrefixWrapper(this, prefix)
/**
* Asks the handler to sync the state of the effective remote with its' metadata
*/
async sync (): Promise<mixed> {
return this._sync()
}
async closeFile(fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), this._timeout)
async _sync (): Promise<mixed> {
throw new Error('Not implemented')
}
async createOutputStream(
file: File,
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
if (typeof file === 'string') {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(
this._createOutputStream(file, {
flags: 'wx',
...options,
}),
this._timeout
)
if (!checksum) {
return streamP
}
const checksumStream = createChecksumStream()
const forwardError = error => {
checksumStream.emit('error', error)
}
const stream = await streamP
stream.on('error', forwardError)
checksumStream.pipe(stream)
// $FlowFixMe
checksumStream.checksumWritten = checksumStream.checksum
.then(value =>
this._outputFile(checksumFile(path), value, { flags: 'wx' })
)
.catch(forwardError)
return checksumStream
/**
* Free the resources possibly dedicated to put the remote at work, when it is no more needed
*/
async forget (): Promise<void> {
await this._forget()
}
createReadStream(
file: File,
async _forget (): Promise<void> {
throw new Error('Not implemented')
}
async test (): Promise<Object> {
const testFileName = `${Date.now()}.test`
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
let step = 'write'
try {
await this.outputFile(testFileName, data)
step = 'read'
const read = await this.readFile(testFileName)
if (data.compare(read) !== 0) {
throw new Error('output and input did not match')
}
return {
success: true,
}
} catch (error) {
return {
success: false,
step,
file: testFileName,
error: error.message || String(error),
}
} finally {
ignoreErrors.call(this.unlink(testFileName))
}
}
async outputFile (file: string, data: Data, options?: Object): Promise<void> {
return this._outputFile(file, data, {
flags: 'wx',
...options,
})
}
async _outputFile (file: string, data: Data, options?: Object): Promise<void> {
const stream = await this.createOutputStream(file, options)
const promise = fromEvent(stream, 'finish')
stream.end(data)
await promise
}
async readFile (file: string, options?: Object): Promise<Buffer> {
return this._readFile(file, options)
}
_readFile (file: string, options?: Object): Promise<Buffer> {
return this.createReadStream(file, options).then(getStream.buffer)
}
async rename (
oldPath: string,
newPath: string,
{ checksum = false }: Object = {}
) {
let p = this._rename(oldPath, newPath)
if (checksum) {
p = Promise.all([
p,
this._rename(checksumFile(oldPath), checksumFile(newPath)),
])
}
return p
}
async _rename (oldPath: string, newPath: string) {
throw new Error('Not implemented')
}
async list (
dir: string = '.',
{
filter,
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
const entries = await this._list(dir)
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = dir + '/' + entry
})
}
return filter === undefined ? entries : entries.filter(filter)
}
async _list (dir: string): Promise<string[]> {
throw new Error('Not implemented')
}
createReadStream (
file: string,
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
): Promise<LaxReadable> {
if (typeof file === 'string') {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout
.call(this._createReadStream(file, options), this._timeout)
.then(stream => {
// detect early errors
let promise = fromEvent(stream, 'readable')
const streamP = this._createReadStream(file, options).then(stream => {
// detect early errors
let promise = fromEvent(stream, 'readable')
// try to add the length prop if missing and not a range stream
if (
stream.length === undefined &&
options.end === undefined &&
options.start === undefined
) {
promise = Promise.all([
promise,
ignoreErrors.call(
this._getSize(file).then(size => {
stream.length = size
})
),
])
}
// try to add the length prop if missing and not a range stream
if (
stream.length === undefined &&
options.end === undefined &&
options.start === undefined
) {
promise = Promise.all([
promise,
ignoreErrors.call(
this.getSize(file).then(size => {
stream.length = size
})
),
])
}
return promise.then(() => stream)
})
return promise.then(() => stream)
})
if (!checksum) {
return streamP
@@ -200,7 +176,7 @@ export default class RemoteHandlerAbstract {
// avoid a unhandled rejection warning
ignoreErrors.call(streamP)
return this._readFile(checksumFile(path), { flags: 'r' }).then(
return this.readFile(checksumFile(path)).then(
checksum =>
streamP.then(stream => {
const { length } = stream
@@ -221,429 +197,92 @@ export default class RemoteHandlerAbstract {
)
}
createWriteStream(
file: File,
options: { end?: number, flags?: string, start?: number } = {}
): Promise<LaxWritable> {
return timeout.call(
this._createWriteStream(
typeof file === 'string' ? normalizePath(file) : file,
{
flags: 'wx',
...options,
}
)
)
}
// Free the resources possibly dedicated to put the remote at work, when it
// is no more needed
//
// FIXME: Some handlers are implemented based on system-wide mecanisms (such
// as mount), forgetting them might breaking other processes using the same
// remote.
@synchronized()
async forget(): Promise<void> {
await this._forget()
}
async getInfo(): Promise<RemoteInfo> {
return timeout.call(this._getInfo(), this._timeout)
}
async getSize(file: File): Promise<number> {
return timeout.call(
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
this._timeout
)
}
async list(
dir: string,
{
filter,
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
const virtualDir = normalizePath(dir)
dir = normalizePath(dir)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
entries = entries.filter(filter)
}
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = virtualDir + '/' + entry
})
}
return entries
}
async mkdir(dir: string): Promise<void> {
dir = normalizePath(dir)
try {
await this._mkdir(dir)
} catch (error) {
if (error == null || error.code !== 'EEXIST') {
throw error
}
// this operation will throw if it's not already a directory
await this._list(dir)
}
}
async mktree(dir: string): Promise<void> {
await this._mktree(normalizePath(dir))
}
async openFile(path: string, flags: string): Promise<FileDescriptor> {
path = normalizePath(path)
return {
fd: await timeout.call(this._openFile(path, flags), this._timeout),
path,
}
}
async outputFile(
async _createReadStream (
file: string,
data: Data,
{ flags = 'wx' }: { flags?: string } = {}
): Promise<void> {
await this._outputFile(normalizePath(file), data, { flags })
options?: Object
): Promise<LaxReadable> {
throw new Error('Not implemented')
}
async read(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
return this._read(
typeof file === 'string' ? normalizePath(file) : file,
buffer,
position
)
async openFile (path: string, flags?: string): Promise<FileDescriptor> {
return { fd: await this._openFile(path, flags), path }
}
async readFile(
file: string,
{ flags = 'r' }: { flags?: string } = {}
): Promise<Buffer> {
return this._readFile(normalizePath(file), { flags })
async _openFile (path: string, flags?: string): Promise<mixed> {
throw new Error('Not implemented')
}
async refreshChecksum(path: string): Promise<void> {
path = normalizePath(path)
async closeFile (fd: FileDescriptor): Promise<void> {
await this._closeFile(fd.fd)
}
const stream = (await this._createReadStream(path, { flags: 'r' })).pipe(
async _closeFile (fd: mixed): Promise<void> {
throw new Error('Not implemented')
}
async refreshChecksum (path: string): Promise<void> {
const stream = (await this.createReadStream(path)).pipe(
createChecksumStream()
)
stream.resume() // start reading the whole file
await this._outputFile(checksumFile(path), await stream.checksum, {
await this.outputFile(checksumFile(path), await stream.checksum)
}
async createOutputStream (
file: File,
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
const path = typeof file === 'string' ? file : file.path
const streamP = this._createOutputStream(file, {
flags: 'wx',
...options,
})
}
async rename(
oldPath: string,
newPath: string,
{ checksum = false }: Object = {}
) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([
p,
this._rename(checksumFile(oldPath), checksumFile(newPath)),
])
if (!checksum) {
return streamP
}
return p
}
async rmdir(dir: string): Promise<void> {
await timeout.call(
this._rmdir(normalizePath(dir)).catch(ignoreEnoent),
this._timeout
)
}
async rmtree(dir: string): Promise<void> {
await this._rmtree(normalizePath(dir))
}
// Asks the handler to sync the state of the effective remote with its'
// metadata
//
// This method MUST ALWAYS be called before using the handler.
@synchronized()
async sync(): Promise<void> {
await this._sync()
}
async test(): Promise<Object> {
const SIZE = 1024 * 1024 * 10
const testFileName = normalizePath(`${Date.now()}.test`)
const data = await fromCallback(randomBytes, SIZE)
let step = 'write'
try {
const writeStart = process.hrtime()
await this._outputFile(testFileName, data, { flags: 'wx' })
const writeDuration = process.hrtime(writeStart)
step = 'read'
const readStart = process.hrtime()
const read = await this._readFile(testFileName, { flags: 'r' })
const readDuration = process.hrtime(readStart)
if (!data.equals(read)) {
throw new Error('output and input did not match')
}
return {
success: true,
writeRate: computeRate(writeDuration, SIZE),
readRate: computeRate(readDuration, SIZE),
}
} catch (error) {
return {
success: false,
step,
file: testFileName,
error: error.message || String(error),
}
} finally {
ignoreErrors.call(this._unlink(testFileName))
const checksumStream = createChecksumStream()
const forwardError = error => {
checksumStream.emit('error', error)
}
const stream = await streamP
stream.on('error', forwardError)
checksumStream.pipe(stream)
// $FlowFixMe
checksumStream.checksumWritten = checksumStream.checksum
.then(value => this.outputFile(checksumFile(path), value))
.catch(forwardError)
return checksumStream
}
async truncate(file: string, len: number): Promise<void> {
await this._truncate(file, len)
async _createOutputStream (
file: mixed,
options?: Object
): Promise<LaxWritable> {
throw new Error('Not implemented')
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = normalizePath(file)
async unlink (file: string, { checksum = true }: Object = {}): Promise<void> {
if (checksum) {
ignoreErrors.call(this._unlink(checksumFile(file)))
}
await this._unlink(file).catch(ignoreEnoent)
await this._unlink(file)
}
async write(
file: File,
buffer: Buffer,
position: number
): Promise<{| bytesWritten: number, buffer: Buffer |}> {
await this._write(
typeof file === 'string' ? normalizePath(file) : file,
buffer,
position
)
}
async writeFile(
file: string,
data: Data,
{ flags = 'wx' }: { flags?: string } = {}
): Promise<void> {
await this._writeFile(normalizePath(file), data, { flags })
}
// Methods that can be implemented by inheriting classes
async _closeFile(fd: mixed): Promise<void> {
async _unlink (file: mixed): Promise<void> {
throw new Error('Not implemented')
}
async _createOutputStream(file: File, options: Object): Promise<LaxWritable> {
try {
return await this._createWriteStream(file, options)
} catch (error) {
if (typeof file !== 'string' || error.code !== 'ENOENT') {
throw error
}
}
await this._mktree(dirname(file))
return this._createOutputStream(file, options)
async getSize (file: mixed): Promise<number> {
return this._getSize(file)
}
async _createReadStream(file: File, options?: Object): Promise<LaxReadable> {
throw new Error('Not implemented')
}
async _createWriteStream(file: File, options: Object): Promise<LaxWritable> {
throw new Error('Not implemented')
}
// called to finalize the remote
async _forget(): Promise<void> {}
async _getInfo(): Promise<Object> {
return {}
}
async _getSize(file: File): Promise<number> {
throw new Error('Not implemented')
}
async _list(dir: string): Promise<string[]> {
throw new Error('Not implemented')
}
async _mkdir(dir: string): Promise<void> {
throw new Error('Not implemented')
}
async _mktree(dir: string): Promise<void> {
try {
return await this.mkdir(dir)
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
await this._mktree(dirname(dir))
return this._mktree(dir)
}
async _openFile(path: string, flags: string): Promise<mixed> {
throw new Error('Not implemented')
}
async _outputFile(
file: string,
data: Data,
options: { flags?: string }
): Promise<void> {
try {
return await this._writeFile(file, data, options)
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
await this._mktree(dirname(file))
return this._outputFile(file, data, options)
}
_read(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
throw new Error('Not implemented')
}
_readFile(file: string, options?: Object): Promise<Buffer> {
return this._createReadStream(file, options).then(getStream.buffer)
}
async _rename(oldPath: string, newPath: string) {
throw new Error('Not implemented')
}
async _rmdir(dir: string) {
throw new Error('Not implemented')
}
async _rmtree(dir: string) {
try {
return await this._rmdir(dir)
} catch (error) {
if (error.code !== 'ENOTEMPTY') {
throw error
}
}
const files = await this._list(dir)
await asyncMap(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
if (error.code === 'EISDIR') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
)
return this._rmtree(dir)
}
// called to initialize the remote
async _sync(): Promise<void> {}
async _unlink(file: string): Promise<void> {
throw new Error('Not implemented')
}
async _write(file: File, buffer: Buffer, position: number): Promise<void> {
const isPath = typeof file === 'string'
if (isPath) {
file = await this.openFile(file, 'r+')
}
try {
return await this._writeFd(file, buffer, position)
} finally {
if (isPath) {
await this.closeFile(file)
}
}
}
async _writeFd(
fd: FileDescriptor,
buffer: Buffer,
position: number
): Promise<void> {
throw new Error('Not implemented')
}
async _writeFile(
file: string,
data: Data,
options: { flags?: string }
): Promise<void> {
async _getSize (file: mixed): Promise<number> {
throw new Error('Not implemented')
}
}
function createPrefixWrapperMethods() {
const pPw = PrefixWrapper.prototype
const pRha = RemoteHandlerAbstract.prototype
const {
defineProperty,
getOwnPropertyDescriptor,
prototype: { hasOwnProperty },
} = Object
Object.getOwnPropertyNames(pRha).forEach(name => {
let descriptor, value
if (
hasOwnProperty.call(pPw, name) ||
name[0] === '_' ||
typeof (value = (descriptor = getOwnPropertyDescriptor(pRha, name))
.value) !== 'function'
) {
return
}
descriptor.value = function() {
let path
if (arguments.length !== 0 && typeof (path = arguments[0]) === 'string') {
arguments[0] = this._resolve(path)
}
return value.apply(this._handler, arguments)
}
defineProperty(pPw, name, descriptor)
})
}
createPrefixWrapperMethods()

View File

@@ -1,127 +0,0 @@
/* eslint-env jest */
import { TimeoutError } from 'promise-toolbox'
import AbstractHandler from './abstract'
const TIMEOUT = 10e3
class TestHandler extends AbstractHandler {
constructor(impl) {
super({ url: 'test://' }, { timeout: TIMEOUT })
Object.keys(impl).forEach(method => {
this[`_${method}`] = impl[method]
})
}
}
jest.useFakeTimers()
describe('closeFile()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
closeFile: () => new Promise(() => {}),
})
const promise = testHandler.closeFile({ fd: undefined, path: '' })
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('createOutputStream()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
createOutputStream: () => new Promise(() => {}),
})
const promise = testHandler.createOutputStream('File')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('createReadStream()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
createReadStream: () => new Promise(() => {}),
})
const promise = testHandler.createReadStream('file')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('getInfo()', () => {
it('throws in case of timeout', async () => {
const testHandler = new TestHandler({
getInfo: () => new Promise(() => {}),
})
const promise = testHandler.getInfo()
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('getSize()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
getSize: () => new Promise(() => {}),
})
const promise = testHandler.getSize('')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('list()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
list: () => new Promise(() => {}),
})
const promise = testHandler.list('.')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('openFile()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
openFile: () => new Promise(() => {}),
})
const promise = testHandler.openFile('path')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('rename()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
rename: () => new Promise(() => {}),
})
const promise = testHandler.rename('oldPath', 'newPath')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('rmdir()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
rmdir: () => new Promise(() => {}),
})
const promise = testHandler.rmdir('dir')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})

View File

@@ -1,5 +1,6 @@
// @flow
// $FlowFixMe
import through2 from 'through2'
import { createHash } from 'crypto'
import { defer, fromEvent } from 'promise-toolbox'
@@ -84,8 +85,8 @@ export const validChecksumOfReadStream = (
callback(
checksum !== expectedChecksum
? new Error(
`Bad checksum (${checksum}), expected: ${expectedChecksum}`
)
`Bad checksum (${checksum}), expected: ${expectedChecksum}`
)
: null
)
}

View File

@@ -0,0 +1,26 @@
/* eslint-env jest */
import rimraf from 'rimraf'
import tmp from 'tmp'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import { getHandler } from '.'
const initialDir = process.cwd()
beforeEach(async () => {
const dir = await pFromCallback(cb => tmp.dir(cb))
process.chdir(dir)
})
afterEach(async () => {
const tmpDir = process.cwd()
process.chdir(initialDir)
await pFromCallback(cb => rimraf(tmpDir, cb))
})
test("fs test doesn't crash", async () => {
const handler = getHandler({ url: 'file://' + process.cwd() })
const result = await handler.test()
expect(result.success).toBeTruthy()
})

View File

@@ -1,379 +0,0 @@
/* eslint-env jest */
import 'dotenv/config'
import asyncIteratorToStream from 'async-iterator-to-stream'
import getStream from 'get-stream'
import { forOwn, random } from 'lodash'
import { fromCallback } from 'promise-toolbox'
import { pipeline } from 'readable-stream'
import { tmpdir } from 'os'
import { getHandler } from '.'
// https://gist.github.com/julien-f/3228c3f34fdac01ade09
const unsecureRandomBytes = n => {
const bytes = Buffer.alloc(n)
const odd = n & 1
for (let i = 0, m = n - odd; i < m; i += 2) {
bytes.writeUInt16BE((Math.random() * 65536) | 0, i)
}
if (odd) {
bytes.writeUInt8((Math.random() * 256) | 0, n - 1)
}
return bytes
}
const TEST_DATA_LEN = 1024
const TEST_DATA = unsecureRandomBytes(TEST_DATA_LEN)
const createTestDataStream = asyncIteratorToStream(function*() {
yield TEST_DATA
})
const rejectionOf = p =>
p.then(
value => {
throw value
},
reason => reason
)
const handlers = [`file://${tmpdir()}`]
if (process.env.xo_fs_nfs) handlers.push(process.env.xo_fs_nfs)
if (process.env.xo_fs_smb) handlers.push(process.env.xo_fs_smb)
handlers.forEach(url => {
describe(url, () => {
let handler
const testWithFileDescriptor = (path, flags, fn) => {
it('with path', () => fn({ file: path, flags }))
it('with file descriptor', async () => {
const file = await handler.openFile(path, flags)
try {
await fn({ file })
} finally {
await handler.closeFile(file)
}
})
}
beforeAll(async () => {
handler = getHandler({ url }).addPrefix(`xo-fs-tests-${Date.now()}`)
await handler.sync()
})
afterAll(async () => {
await handler.forget()
handler = undefined
})
beforeEach(async () => {
// ensure test dir exists
await handler.mkdir('.')
})
afterEach(async () => {
await handler.rmtree('.')
})
describe('#type', () => {
it('returns the type of the remote', () => {
expect(typeof handler.type).toBe('string')
})
})
describe('#createOutputStream()', () => {
it('creates parent dir if missing', async () => {
const stream = await handler.createOutputStream('dir/file')
await fromCallback(pipeline, createTestDataStream(), stream)
await expect(await handler.readFile('dir/file')).toEqual(TEST_DATA)
})
})
describe('#createReadStream()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
testWithFileDescriptor('file', 'r', async ({ file, flags }) => {
await expect(
await getStream.buffer(
await handler.createReadStream(file, { flags })
)
).toEqual(TEST_DATA)
})
})
describe('#createWriteStream()', () => {
testWithFileDescriptor('file', 'wx', async ({ file, flags }) => {
const stream = await handler.createWriteStream(file, { flags })
await fromCallback(pipeline, createTestDataStream(), stream)
await expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('fails if parent dir is missing', async () => {
const error = await rejectionOf(handler.createWriteStream('dir/file'))
expect(error.code).toBe('ENOENT')
})
})
describe('#getInfo()', () => {
let info
beforeAll(async () => {
info = await handler.getInfo()
})
it('should return an object with info', async () => {
expect(typeof info).toBe('object')
})
it('should return correct type of attribute', async () => {
if (info.size !== undefined) {
expect(typeof info.size).toBe('number')
}
if (info.used !== undefined) {
expect(typeof info.used).toBe('number')
}
})
})
describe('#getSize()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
testWithFileDescriptor('file', 'r', async () => {
expect(await handler.getSize('file')).toEqual(TEST_DATA_LEN)
})
})
describe('#list()', () => {
it(`should list the content of folder`, async () => {
await handler.outputFile('file', TEST_DATA)
await expect(await handler.list('.')).toEqual(['file'])
})
it('can prepend the directory to entries', async () => {
await handler.outputFile('dir/file', '')
expect(await handler.list('dir', { prependDir: true })).toEqual([
'/dir/file',
])
})
it('can prepend the directory to entries', async () => {
await handler.outputFile('dir/file', '')
expect(await handler.list('dir', { prependDir: true })).toEqual([
'/dir/file',
])
})
})
describe('#mkdir()', () => {
it('creates a directory', async () => {
await handler.mkdir('dir')
await expect(await handler.list('.')).toEqual(['dir'])
})
it('does not throw on existing directory', async () => {
await handler.mkdir('dir')
await handler.mkdir('dir')
})
it('throws ENOTDIR on existing file', async () => {
await handler.outputFile('file', '')
const error = await rejectionOf(handler.mkdir('file'))
expect(error.code).toBe('ENOTDIR')
})
})
describe('#mktree()', () => {
it('creates a tree of directories', async () => {
await handler.mktree('dir/dir')
await expect(await handler.list('.')).toEqual(['dir'])
await expect(await handler.list('dir')).toEqual(['dir'])
})
it('does not throw on existing directory', async () => {
await handler.mktree('dir/dir')
await handler.mktree('dir/dir')
})
it('throws ENOTDIR on existing file', async () => {
await handler.outputFile('dir/file', '')
const error = await rejectionOf(handler.mktree('dir/file'))
expect(error.code).toBe('ENOTDIR')
})
it('throws ENOTDIR on existing file in path', async () => {
await handler.outputFile('file', '')
const error = await rejectionOf(handler.mktree('file/dir'))
expect(error.code).toBe('ENOTDIR')
})
})
describe('#outputFile()', () => {
it('writes data to a file', async () => {
await handler.outputFile('file', TEST_DATA)
expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('throws on existing files', async () => {
await handler.outputFile('file', '')
const error = await rejectionOf(handler.outputFile('file', ''))
expect(error.code).toBe('EEXIST')
})
})
describe('#read()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
const start = random(TEST_DATA_LEN)
const size = random(TEST_DATA_LEN)
testWithFileDescriptor('file', 'r', async ({ file }) => {
const buffer = Buffer.alloc(size)
const result = await handler.read(file, buffer, start)
expect(result.buffer).toBe(buffer)
expect(result).toEqual({
buffer,
bytesRead: Math.min(size, TEST_DATA_LEN - start),
})
})
})
describe('#readFile', () => {
it('returns a buffer containing the contents of the file', async () => {
await handler.outputFile('file', TEST_DATA)
expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('throws on missing file', async () => {
const error = await rejectionOf(handler.readFile('file'))
expect(error.code).toBe('ENOENT')
})
})
describe('#rename()', () => {
it(`should rename the file`, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.rename('file', `file2`)
expect(await handler.list('.')).toEqual(['file2'])
expect(await handler.readFile(`file2`)).toEqual(TEST_DATA)
})
})
describe('#rmdir()', () => {
it('should remove an empty directory', async () => {
await handler.mkdir('dir')
await handler.rmdir('dir')
expect(await handler.list('.')).toEqual([])
})
it(`should throw on non-empty directory`, async () => {
await handler.outputFile('dir/file', '')
const error = await rejectionOf(handler.rmdir('.'))
await expect(error.code).toEqual('ENOTEMPTY')
})
it('does not throw on missing directory', async () => {
await handler.rmdir('dir')
})
})
describe('#rmtree', () => {
it(`should remove a directory resursively`, async () => {
await handler.outputFile('dir/file', '')
await handler.rmtree('dir')
expect(await handler.list('.')).toEqual([])
})
})
describe('#test()', () => {
it('tests the remote appears to be working', async () => {
const answer = await handler.test()
expect(answer.success).toBe(true)
expect(typeof answer.writeRate).toBe('number')
expect(typeof answer.readRate).toBe('number')
})
})
describe('#unlink()', () => {
it(`should remove the file`, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.unlink('file')
await expect(await handler.list('.')).toEqual([])
})
it('does not throw on missing file', async () => {
await handler.unlink('file')
})
})
describe('#write()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
const PATCH_DATA_LEN = Math.ceil(TEST_DATA_LEN / 2)
const PATCH_DATA = unsecureRandomBytes(PATCH_DATA_LEN)
forOwn(
{
'dont increase file size': (() => {
const offset = random(0, TEST_DATA_LEN - PATCH_DATA_LEN)
const expected = Buffer.from(TEST_DATA)
PATCH_DATA.copy(expected, offset)
return { offset, expected }
})(),
'increase file size': (() => {
const offset = random(
TEST_DATA_LEN - PATCH_DATA_LEN + 1,
TEST_DATA_LEN
)
const expected = Buffer.alloc(offset + PATCH_DATA_LEN)
TEST_DATA.copy(expected)
PATCH_DATA.copy(expected, offset)
return { offset, expected }
})(),
},
({ offset, expected }, title) => {
describe(title, () => {
testWithFileDescriptor('file', 'r+', async ({ file }) => {
await handler.write(file, PATCH_DATA, offset)
await expect(await handler.readFile('file')).toEqual(expected)
})
})
}
)
})
describe('#truncate()', () => {
forOwn(
{
'shrinks file': (() => {
const length = random(0, TEST_DATA_LEN)
const expected = TEST_DATA.slice(0, length)
return { length, expected }
})(),
'grows file': (() => {
const length = random(TEST_DATA_LEN, TEST_DATA_LEN * 2)
const expected = Buffer.alloc(length)
TEST_DATA.copy(expected)
return { length, expected }
})(),
},
({ length, expected }, title) => {
it(title, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.truncate('file', length)
await expect(await handler.readFile('file')).toEqual(expected)
})
}
)
})
})
})

View File

@@ -1,28 +1,20 @@
// @flow
import execa from 'execa'
import type RemoteHandler from './abstract'
import RemoteHandlerLocal from './local'
import RemoteHandlerNfs from './nfs'
import RemoteHandlerSmb from './smb'
import RemoteHandlerSmbMount from './smb-mount'
export type { default as RemoteHandler } from './abstract'
export type Remote = { url: string }
const HANDLERS = {
file: RemoteHandlerLocal,
smb: RemoteHandlerSmb,
nfs: RemoteHandlerNfs,
}
try {
execa.sync('mount.cifs', ['-V'])
HANDLERS.smb = RemoteHandlerSmbMount
} catch (_) {
HANDLERS.smb = RemoteHandlerSmb
}
export const getHandler = (remote: Remote, ...rest: any): RemoteHandler => {
export const getHandler = (remote: Remote): RemoteHandler => {
// FIXME: should be done in xo-remote-parser.
const type = remote.url.split('://')[0]
@@ -30,5 +22,5 @@ export const getHandler = (remote: Remote, ...rest: any): RemoteHandler => {
if (!Handler) {
throw new Error('Unhandled remote type')
}
return new Handler(remote, ...rest)
return new Handler(remote)
}

View File

@@ -1,135 +1,114 @@
import df from '@sindresorhus/df'
import fs from 'fs-extra'
import { fromEvent } from 'promise-toolbox'
import { dirname, resolve } from 'path'
import { noop, startsWith } from 'lodash'
import RemoteHandlerAbstract from './abstract'
export default class LocalHandler extends RemoteHandlerAbstract {
get type() {
get type () {
return 'file'
}
_getRealPath() {
_getRealPath () {
return this._remote.path
}
_getFilePath(file) {
return this._getRealPath() + file
}
async _closeFile(fd) {
return fs.close(fd)
}
async _createReadStream(file, options) {
if (typeof file === 'string') {
const stream = fs.createReadStream(this._getFilePath(file), options)
await fromEvent(stream, 'open')
return stream
_getFilePath (file) {
const realPath = this._getRealPath()
const parts = [realPath]
if (file) {
parts.push(file)
}
return fs.createReadStream('', {
autoClose: false,
...options,
fd: file.fd,
})
}
async _createWriteStream(file, options) {
if (typeof file === 'string') {
const stream = fs.createWriteStream(this._getFilePath(file), options)
await fromEvent(stream, 'open')
return stream
const path = resolve.apply(null, parts)
if (!startsWith(path, realPath)) {
throw new Error('Remote path is unavailable')
}
return fs.createWriteStream('', {
autoClose: false,
...options,
fd: file.fd,
})
return path
}
async _getInfo() {
// df.file() resolves with an object with the following properties:
// filesystem, type, size, used, available, capacity and mountpoint.
// size, used, available and capacity may be `NaN` so we remove any `NaN`
// value from the object.
const info = await df.file(this._getFilePath('/'))
Object.keys(info).forEach(key => {
if (Number.isNaN(info[key])) {
delete info[key]
async _sync () {
if (this._remote.enabled) {
try {
const path = this._getRealPath()
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
} catch (exc) {
this._remote.enabled = false
this._remote.error = exc.message
}
}
return this._remote
}
async _forget () {
return noop()
}
async _outputFile (file, data, options) {
const path = this._getFilePath(file)
await fs.ensureDir(dirname(path))
await fs.writeFile(path, data, options)
}
async _readFile (file, options) {
return fs.readFile(this._getFilePath(file), options)
}
async _rename (oldPath, newPath) {
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
}
async _list (dir = '.') {
return fs.readdir(this._getFilePath(dir))
}
async _createReadStream (file, options) {
if (typeof file === 'string') {
return fs.createReadStream(this._getFilePath(file), options)
} else {
return fs.createReadStream('', {
autoClose: false,
...options,
fd: file.fd,
})
}
}
async _createOutputStream (file, options) {
if (typeof file === 'string') {
const path = this._getFilePath(file)
await fs.ensureDir(dirname(path))
return fs.createWriteStream(path, options)
} else {
return fs.createWriteStream('', {
autoClose: false,
...options,
fd: file.fd,
})
}
}
async _unlink (file) {
return fs.unlink(this._getFilePath(file)).catch(error => {
// do not throw if the file did not exist
if (error == null || error.code !== 'ENOENT') {
throw error
}
})
return info
}
async _getSize(file) {
async _getSize (file) {
const stats = await fs.stat(
this._getFilePath(typeof file === 'string' ? file : file.path)
)
return stats.size
}
async _list(dir) {
return fs.readdir(this._getFilePath(dir))
}
_mkdir(dir) {
return fs.mkdir(this._getFilePath(dir))
}
async _openFile(path, flags) {
async _openFile (path, flags) {
return fs.open(this._getFilePath(path), flags)
}
async _read(file, buffer, position) {
const needsClose = typeof file === 'string'
file = needsClose ? await fs.open(this._getFilePath(file), 'r') : file.fd
try {
return await fs.read(
file,
buffer,
0,
buffer.length,
position === undefined ? null : position
)
} finally {
if (needsClose) {
await fs.close(file)
}
}
}
async _readFile(file, options) {
return fs.readFile(this._getFilePath(file), options)
}
async _rename(oldPath, newPath) {
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
}
async _rmdir(dir) {
return fs.rmdir(this._getFilePath(dir))
}
async _sync() {
const path = this._getRealPath('/')
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
}
_truncate(file, len) {
return fs.truncate(this._getFilePath(file), len)
}
async _unlink(file) {
return fs.unlink(this._getFilePath(file))
}
_writeFd(file, buffer, position) {
return fs.write(file.fd, buffer, 0, buffer.length, position)
}
_writeFile(file, data, { flags }) {
return fs.writeFile(this._getFilePath(file), data, { flag: flags })
async _closeFile (fd) {
return fs.close(fd)
}
}

View File

@@ -1,20 +1,96 @@
import { parse } from 'xo-remote-parser'
import execa from 'execa'
import fs from 'fs-extra'
import { forEach } from 'lodash'
import MountHandler from './_mount'
import LocalHandler from './local'
const DEFAULT_NFS_OPTIONS = 'vers=3'
export default class NfsHandler extends MountHandler {
constructor(remote, opts) {
const { host, port, path } = parse(remote.url)
super(remote, opts, {
type: 'nfs',
device: `${host}${port !== undefined ? ':' + port : ''}:${path}`,
options: DEFAULT_NFS_OPTIONS,
})
}
get type() {
export default class NfsHandler extends LocalHandler {
get type () {
return 'nfs'
}
_getRealPath () {
return `/run/xo-server/mounts/${this._remote.id}`
}
async _loadRealMounts () {
let stdout
const mounted = {}
try {
stdout = await execa.stdout('findmnt', [
'-P',
'-t',
'nfs,nfs4',
'--output',
'SOURCE,TARGET',
'--noheadings',
])
const regex = /^SOURCE="([^:]*):(.*)" TARGET="(.*)"$/
forEach(stdout.split('\n'), m => {
if (m) {
const match = regex.exec(m)
mounted[match[3]] = {
host: match[1],
share: match[2],
}
}
})
} catch (exc) {
// When no mounts are found, the call pretends to fail...
if (exc.stderr !== '') {
throw exc
}
}
this._realMounts = mounted
return mounted
}
_matchesRealMount () {
return this._getRealPath() in this._realMounts
}
async _mount () {
await fs.ensureDir(this._getRealPath())
return execa('mount', [
'-t',
'nfs',
'-o',
'vers=3',
`${this._remote.host}:${this._remote.path}`,
this._getRealPath(),
])
}
async _sync () {
await this._loadRealMounts()
if (this._matchesRealMount() && !this._remote.enabled) {
try {
await this._umount(this._remote)
} catch (exc) {
this._remote.enabled = true
this._remote.error = exc.message
}
} else if (!this._matchesRealMount() && this._remote.enabled) {
try {
await this._mount()
} catch (exc) {
this._remote.enabled = false
this._remote.error = exc.message
}
}
return this._remote
}
async _forget () {
try {
await this._umount(this._remote)
} catch (_) {
// We have to go on...
}
}
async _umount (remote) {
await execa('umount', ['--force', this._getRealPath()])
}
}

View File

@@ -1,25 +0,0 @@
import { parse } from 'xo-remote-parser'
import MountHandler from './_mount'
import normalizePath from './_normalizePath'
export default class SmbMountHandler extends MountHandler {
constructor(remote, opts) {
const { domain = 'WORKGROUP', host, password, path, username } = parse(
remote.url
)
super(remote, opts, {
type: 'cifs',
device: '//' + host + normalizePath(path),
options: `domain=${domain}`,
env: {
USER: username,
PASSWD: password,
},
})
}
get type() {
return 'smb'
}
}

View File

@@ -1,177 +1,217 @@
import Smb2 from '@marsaud/smb2'
import Smb2 from '@marsaud/smb2-promise'
import { lastly as pFinally } from 'promise-toolbox'
import RemoteHandlerAbstract from './abstract'
const noop = () => {}
// Normalize the error code for file not found.
const wrapError = (error, code) => ({
__proto__: error,
cause: error,
code,
})
const normalizeError = (error, shouldBeDirectory) => {
const normalizeError = error => {
const { code } = error
throw code === 'STATUS_DIRECTORY_NOT_EMPTY'
? wrapError(error, 'ENOTEMPTY')
: code === 'STATUS_FILE_IS_A_DIRECTORY'
? wrapError(error, 'EISDIR')
: code === 'STATUS_NOT_A_DIRECTORY'
? wrapError(error, 'ENOTDIR')
: code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
? wrapError(error, 'ENOENT')
: code === 'STATUS_OBJECT_NAME_COLLISION'
? wrapError(error, 'EEXIST')
: code === 'STATUS_NOT_SUPPORTED' || code === 'STATUS_INVALID_PARAMETER'
? wrapError(error, shouldBeDirectory ? 'ENOTDIR' : 'EISDIR')
return code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
? Object.create(error, {
code: {
configurable: true,
readable: true,
value: 'ENOENT',
writable: true,
},
})
: error
}
const normalizeDirError = error => normalizeError(error, true)
export default class SmbHandler extends RemoteHandlerAbstract {
constructor(remote, opts) {
super(remote, opts)
// defined in _sync()
this._client = undefined
const prefix = this._remote.path
this._prefix = prefix !== '' ? prefix + '\\' : prefix
constructor (remote) {
super(remote)
this._forget = noop
}
get type() {
get type () {
return 'smb'
}
_getFilePath(file) {
return (
this._prefix +
(typeof file === 'string' ? file : file.path)
.slice(1)
.replace(/\//g, '\\')
)
}
_dirname(file) {
const parts = file.split('\\')
parts.pop()
return parts.join('\\')
}
_closeFile(file) {
return this._client.close(file).catch(normalizeError)
}
_createReadStream(file, options) {
if (typeof file === 'string') {
file = this._getFilePath(file)
} else {
options = { autoClose: false, ...options, fd: file.fd }
file = ''
}
return this._client.createReadStream(file, options).catch(normalizeError)
}
_createWriteStream(file, options) {
if (typeof file === 'string') {
file = this._getFilePath(file)
} else {
options = { autoClose: false, ...options, fd: file.fd }
file = ''
}
return this._client.createWriteStream(file, options).catch(normalizeError)
}
_forget() {
const client = this._client
this._client = undefined
return client.disconnect()
}
_getSize(file) {
return this._client.getSize(this._getFilePath(file)).catch(normalizeError)
}
_list(dir) {
return this._client.readdir(this._getFilePath(dir)).catch(normalizeDirError)
}
_mkdir(dir) {
return this._client.mkdir(this._getFilePath(dir)).catch(normalizeDirError)
}
// TODO: add flags
_openFile(path, flags) {
return this._client
.open(this._getFilePath(path), flags)
.catch(normalizeError)
}
async _read(file, buffer, position) {
const client = this._client
const needsClose = typeof file === 'string'
file = needsClose ? await client.open(this._getFilePath(file)) : file.fd
try {
return await client.read(file, buffer, 0, buffer.length, position)
} catch (error) {
normalizeError(error)
} finally {
if (needsClose) {
await client.close(file)
}
}
}
_readFile(file, options) {
return this._client
.readFile(this._getFilePath(file), options)
.catch(normalizeError)
}
_rename(oldPath, newPath) {
return this._client
.rename(this._getFilePath(oldPath), this._getFilePath(newPath), {
replace: true,
})
.catch(normalizeError)
}
_rmdir(dir) {
return this._client.rmdir(this._getFilePath(dir)).catch(normalizeDirError)
}
_sync() {
const remote = this._remote
this._client = new Smb2({
_getClient (remote) {
return new Smb2({
share: `\\\\${remote.host}`,
domain: remote.domain,
username: remote.username,
password: remote.password,
autoCloseTimeout: 0,
})
// Check access (smb2 does not expose connect in public so far...)
return this.list('.')
}
_truncate(file, len) {
return this._client
.truncate(this._getFilePath(file), len)
.catch(normalizeError)
_getFilePath (file) {
if (file === '.') {
file = undefined
}
let path = this._remote.path !== '' ? this._remote.path : ''
// Ensure remote path is a directory.
if (path !== '' && path[path.length - 1] !== '\\') {
path += '\\'
}
if (file) {
path += file.replace(/\//g, '\\')
}
return path
}
_unlink(file) {
return this._client.unlink(this._getFilePath(file)).catch(normalizeError)
_dirname (file) {
const parts = file.split('\\')
parts.pop()
return parts.join('\\')
}
_writeFd(file, buffer, position) {
return this._client.write(file.fd, buffer, 0, buffer.length, position)
async _sync () {
if (this._remote.enabled) {
try {
// Check access (smb2 does not expose connect in public so far...)
await this.list()
} catch (error) {
this._remote.enabled = false
this._remote.error = error.message
}
}
return this._remote
}
_writeFile(file, data, options) {
return this._client
.writeFile(this._getFilePath(file), data, options)
.catch(normalizeError)
async _outputFile (file, data, options = {}) {
const client = this._getClient(this._remote)
const path = this._getFilePath(file)
const dir = this._dirname(path)
if (dir) {
await client.ensureDir(dir)
}
return client.writeFile(path, data, options)::pFinally(() => {
client.close()
})
}
async _readFile (file, options = {}) {
const client = this._getClient(this._remote)
let content
try {
content = await client
.readFile(this._getFilePath(file), options)
::pFinally(() => {
client.close()
})
} catch (error) {
throw normalizeError(error)
}
return content
}
async _rename (oldPath, newPath) {
const client = this._getClient(this._remote)
try {
await client
.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
::pFinally(() => {
client.close()
})
} catch (error) {
throw normalizeError(error)
}
}
async _list (dir = '.') {
const client = this._getClient(this._remote)
let list
try {
list = await client.readdir(this._getFilePath(dir))::pFinally(() => {
client.close()
})
} catch (error) {
throw normalizeError(error)
}
return list
}
async _createReadStream (file, options = {}) {
if (typeof file !== 'string') {
file = file.path
}
const client = this._getClient(this._remote)
let stream
try {
// FIXME ensure that options are properly handled by @marsaud/smb2
stream = await client.createReadStream(this._getFilePath(file), options)
stream.on('end', () => client.close())
} catch (error) {
throw normalizeError(error)
}
return stream
}
async _createOutputStream (file, options = {}) {
if (typeof file !== 'string') {
file = file.path
}
const client = this._getClient(this._remote)
const path = this._getFilePath(file)
const dir = this._dirname(path)
let stream
try {
if (dir) {
await client.ensureDir(dir)
}
stream = await client.createWriteStream(path, options) // FIXME ensure that options are properly handled by @marsaud/smb2
} catch (err) {
client.close()
throw err
}
stream.on('finish', () => client.close())
return stream
}
async _unlink (file) {
const client = this._getClient(this._remote)
try {
await client.unlink(this._getFilePath(file))::pFinally(() => {
client.close()
})
} catch (error) {
throw normalizeError(error)
}
}
async _getSize (file) {
const client = await this._getClient(this._remote)
let size
try {
size = await client
.getSize(this._getFilePath(typeof file === 'string' ? file : file.path))
::pFinally(() => {
client.close()
})
} catch (error) {
throw normalizeError(error)
}
return size
}
// this is a fake
async _openFile (path) {
return this._getFilePath(path)
}
async _closeFile (fd) {}
}

View File

@@ -17,45 +17,16 @@ Everywhere something should be logged:
```js
import { createLogger } from '@xen-orchestra/log'
const log = createLogger('my-module')
log.debug('only useful for debugging')
log.info('this information is relevant to the user')
log.warn('something went wrong but did not prevent current action')
log.error('something went wrong')
log.fatal('service/app is going down')
// you can add contextual info
log.debug('new API request', {
method: 'foo',
params: [ 'bar', 'baz' ]
user: 'qux'
})
// by convention, errors go into the `error` field
log.error('could not join server', {
error,
server: 'example.org',
})
const log = createLogger('xo-server-api')
log.warn('foo')
```
Then, at application level, configure the logs are handled:
Then at application level you can choose how to handle these logs:
```js
import { createLogger } from '@xen-orchestra/log'
import { configure, catchGlobalErrors } from '@xen-orchestra/log/configure'
import transportConsole from '@xen-orchestra/log/transports/console'
import transportEmail from '@xen-orchestra/log/transports/email'
const transport = transportEmail({
service: 'gmail',
auth: {
user: 'jane.smith@gmail.com',
pass: 'H&NbECcpXF|pyXe#%ZEb',
},
from: 'jane.smith@gmail.com',
to: ['jane.smith@gmail.com', 'sam.doe@yahoo.com'],
})
import configure from '@xen-orchestra/log/configure'
import createConsoleTransport from '@xen-orchestra/log/transports/console'
import createEmailTransport from '@xen-orchestra/log/transports/email'
configure([
{
@@ -64,19 +35,26 @@ configure([
// matched against the namespace of the logs
filter: process.env.DEBUG,
transport: transportConsole(),
transport: createConsoleTransport()
},
{
// only levels >= warn
level: 'warn',
transport,
},
transport: createEmaileTransport({
service: 'gmail',
auth: {
user: 'jane.smith@gmail.com',
pass: 'H&NbECcpXF|pyXe#%ZEb'
},
from: 'jane.smith@gmail.com',
to: [
'jane.smith@gmail.com',
'sam.doe@yahoo.com'
]
})
}
])
// send all global errors (uncaught exceptions, warnings, unhandled rejections)
// to this logger
catchGlobalErrors(createLogger('app'))
```
### Transports
@@ -84,9 +62,9 @@ catchGlobalErrors(createLogger('app'))
#### Console
```js
import transportConsole from '@xen-orchestra/log/transports/console'
import createConsoleTransport from '@xen-orchestra/log/transports/console'
configure(transportConsole())
configure(createConsoleTransport())
```
#### Email
@@ -100,19 +78,20 @@ Optional dependency:
Configuration:
```js
import transportEmail from '@xen-orchestra/log/transports/email'
import createEmailTransport from '@xen-orchestra/log/transports/email'
configure(
transportEmail({
service: 'gmail',
auth: {
user: 'jane.smith@gmail.com',
pass: 'H&NbECcpXF|pyXe#%ZEb',
},
from: 'jane.smith@gmail.com',
to: ['jane.smith@gmail.com', 'sam.doe@yahoo.com'],
})
)
configure(createEmailTransport({
service: 'gmail',
auth: {
user: 'jane.smith@gmail.com',
pass: 'H&NbECcpXF|pyXe#%ZEb'
},
from: 'jane.smith@gmail.com',
to: [
'jane.smith@gmail.com',
'sam.doe@yahoo.com'
]
}))
```
#### Syslog
@@ -126,13 +105,13 @@ Optional dependency:
Configuration:
```js
import transportSyslog from '@xen-orchestra/log/transports/syslog'
import createSyslogTransport from '@xen-orchestra/log/transports/syslog'
// By default, log to udp://localhost:514
configure(transportSyslog())
configure(createSyslogTransport())
// But TCP, a different host, or a different port can be used
configure(transportSyslog('tcp://syslog.company.lan'))
configure(createSyslogTransport('tcp://syslog.company.lan'))
```
## Development
@@ -156,7 +135,7 @@ configure(transportSyslog('tcp://syslog.company.lan'))
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:

View File

@@ -1,14 +1,13 @@
{
"private": false,
"private": true,
"name": "@xen-orchestra/log",
"version": "0.2.0",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/log",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/log",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
@@ -20,28 +19,27 @@
"main": "dist/",
"bin": {},
"files": [
"configure.js",
"dist/",
"transports/"
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
"node": ">=4"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.42",
"lodash": "^4.17.4",
"promise-toolbox": "^0.15.0"
"promise-toolbox": "^0.9.5"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/cli": "7.0.0-beta.42",
"@babel/core": "7.0.0-beta.42",
"@babel/preset-env": "7.0.0-beta.42",
"@babel/preset-flow": "7.0.0-beta.42",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^6.0.3",
"index-modules": "^0.3.0",
"rimraf": "^3.0.0"
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -49,7 +47,6 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepare": "yarn run build",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -12,15 +12,14 @@ const createTransport = config => {
if (Array.isArray(config)) {
const transports = config.map(createTransport)
const { length } = transports
return function() {
return function () {
for (let i = 0; i < length; ++i) {
transports[i].apply(this, arguments)
}
}
}
let { filter } = config
let transport = createTransport(config.transport)
let { filter, transport } = config
const level = resolve(config.level)
if (filter !== undefined) {
@@ -30,14 +29,14 @@ const createTransport = config => {
}
const orig = transport
transport = function(log) {
transport = function (log) {
if ((level !== undefined && log.level >= level) || filter(log)) {
return orig.apply(this, arguments)
}
}
} else if (level !== undefined) {
const orig = transport
transport = function(log) {
transport = function (log) {
if (log.level >= level) {
return orig.apply(this, arguments)
}
@@ -47,23 +46,23 @@ const createTransport = config => {
return transport
}
const symbol =
typeof Symbol !== 'undefined'
? Symbol.for('@xen-orchestra/log')
: '@@@xen-orchestra/log'
const { env } = process
global[symbol] = createTransport({
let transport = createTransport({
// display warnings or above, and all that are enabled via DEBUG or
// NODE_DEBUG env
filter: [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(','),
level: resolve(env.LOG_LEVEL, LEVELS.INFO),
filter: process.env.DEBUG || process.env.NODE_DEBUG,
level: LEVELS.INFO,
transport: createConsoleTransport(),
})
const symbol =
typeof Symbol !== 'undefined'
? Symbol.for('@xen-orchestra/log')
: '@@@xen-orchestra/log'
global[symbol] = log => transport(log)
export const configure = config => {
global[symbol] = createTransport(config)
transport = createTransport(config)
}
// -------------------------------------------------------------------
@@ -87,12 +86,10 @@ export const catchGlobalErrors = logger => {
const EventEmitter = require('events')
const { prototype } = EventEmitter
const { emit } = prototype
function patchedEmit(event, error) {
if (event === 'error' && this.listenerCount(event) === 0) {
logger.error('unhandled error event', { error })
return false
}
return emit.apply(this, arguments)
function patchedEmit (event, error) {
event === 'error' && !this.listenerCount(event)
? logger.error('unhandled error event', { error })
: emit.apply(this, arguments)
}
prototype.emit = patchedEmit

View File

@@ -1,5 +1,5 @@
import createTransport from './transports/console'
import LEVELS, { resolve } from './levels'
import LEVELS from './levels'
const symbol =
typeof Symbol !== 'undefined'
@@ -9,13 +9,12 @@ if (!(symbol in global)) {
// the default behavior, without requiring `configure` is to avoid
// logging anything unless it's a real error
const transport = createTransport()
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
global[symbol] = log => log.level >= level && transport(log)
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
}
// -------------------------------------------------------------------
function Log(data, level, namespace, message, time) {
function Log (data, level, namespace, message, time) {
this.data = data
this.level = level
this.namespace = namespace
@@ -23,7 +22,7 @@ function Log(data, level, namespace, message, time) {
this.time = time
}
function Logger(namespace) {
function Logger (namespace) {
this._namespace = namespace
// bind all logging methods
@@ -38,29 +37,18 @@ const { prototype } = Logger
for (const name in LEVELS) {
const level = LEVELS[name]
prototype[name.toLowerCase()] = function(message, data) {
if (typeof message !== 'string') {
if (message instanceof Error) {
data = { error: message }
;({ message = 'an error has occurred' } = message)
} else {
return this.warn('incorrect value passed to logger', {
level,
value: message,
})
}
}
prototype[name.toLowerCase()] = function (message, data) {
global[symbol](new Log(data, level, this._namespace, message, new Date()))
}
}
prototype.wrap = function(message, fn) {
prototype.wrap = function (message, fn) {
const logger = this
const warnAndRethrow = error => {
logger.warn(message, { error })
throw error
}
return function() {
return function () {
try {
const result = fn.apply(this, arguments)
const then = result != null && result.then
@@ -73,5 +61,5 @@ prototype.wrap = function(message, fn) {
}
}
export const createLogger = namespace => new Logger(namespace)
export { createLogger as default }
const createLogger = namespace => new Logger(namespace)
export { createLogger }

View File

@@ -2,7 +2,7 @@ const LEVELS = Object.create(null)
export { LEVELS as default }
// https://github.com/trentm/node-bunyan#levels
LEVELS.FATAL = 60 // service/app is going down
LEVELS.FATAL = 60 // service/app is going to down
LEVELS.ERROR = 50 // fatal for current action
LEVELS.WARN = 40 // something went wrong but it's not fatal
LEVELS.INFO = 30 // detail on unusual but normal operation
@@ -13,22 +13,11 @@ for (const name in LEVELS) {
NAMES[LEVELS[name]] = name
}
// resolves to the number representation of a level
//
// returns `defaultLevel` if invalid
export const resolve = (level, defaultLevel) => {
const type = typeof level
if (type === 'number') {
if (level in NAMES) {
return level
}
} else if (type === 'string') {
const nLevel = LEVELS[level.toUpperCase()]
if (nLevel !== undefined) {
return nLevel
}
export const resolve = level => {
if (typeof level === 'string') {
level = LEVELS[level.toUpperCase()]
}
return defaultLevel
return level
}
Object.freeze(LEVELS)

View File

@@ -1,23 +1,20 @@
import LEVELS, { NAMES } from '../levels'
// Bind console methods (necessary for browsers)
const debugConsole = console.log.bind(console)
const infoConsole = console.info.bind(console)
const warnConsole = console.warn.bind(console)
const errorConsole = console.error.bind(console)
const { ERROR, INFO, WARN } = LEVELS
const consoleTransport = ({ data, level, namespace, message, time }) => {
const fn =
/* eslint-disable no-console */
level < INFO
? console.log
: level < WARN
? console.info
: level < ERROR
? console.warn
: console.error
/* eslint-enable no-console */
? debugConsole
: level < WARN ? infoConsole : level < ERROR ? warnConsole : errorConsole
const args = [time.toISOString(), namespace, NAMES[level], message]
if (data != null) {
args.push(data)
}
fn.apply(console, args)
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
data != null && fn(data)
}
export default () => consoleTransport

View File

@@ -1,6 +1,6 @@
import fromCallback from 'promise-toolbox/fromCallback'
import prettyFormat from 'pretty-format' // eslint-disable-line node/no-extraneous-import
import { createTransport } from 'nodemailer' // eslint-disable-line node/no-extraneous-import
import { fromCallback } from 'promise-toolbox'
import { evalTemplate, required } from '../utils'
import { NAMES } from '../levels'
@@ -53,12 +53,12 @@ export default ({
fromCallback(cb =>
transporter.sendMail(
{
subject: evalTemplate(subject, key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
subject: evalTemplate(
subject,
key =>
key === 'level'
? NAMES[log.level]
: key === 'time' ? log.time.toISOString() : log[key]
),
text: prettyFormat(log.data),
},

View File

@@ -1,7 +0,0 @@
export default () => {
const memoryLogger = log => {
logs.push(log)
}
const logs = (memoryLogger.logs = [])
return memoryLogger
}

View File

@@ -1,6 +1,7 @@
import fromCallback from 'promise-toolbox/fromCallback'
import splitHost from 'split-host'
import { createClient, Facility, Severity, Transport } from 'syslog-client'
import splitHost from 'split-host' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import { createClient, Facility, Severity, Transport } from 'syslog-client' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import { fromCallback } from 'promise-toolbox'
import { startsWith } from 'lodash'
import LEVELS from '../levels'
@@ -18,12 +19,12 @@ const facility = Facility.User
export default target => {
const opts = {}
if (target !== undefined) {
if (target.startsWith('tcp://')) {
if (startsWith(target, 'tcp://')) {
target = target.slice(6)
opts.transport = Transport.Tcp
} else if (target.startsWith('udp://')) {
} else if (startsWith(target, 'udp://')) {
target = target.slice(6)
opts.transport = Transport.Udp
opts.transport = Transport.Ucp
}
;({ host: target, port: opts.port } = splitHost(target))

View File

@@ -55,8 +55,7 @@ export const required = name => {
// -------------------------------------------------------------------
export const serializeError = error => ({
...error, // Copy enumerable properties.
code: error.code,
...error,
message: error.message,
name: error.name,
stack: error.stack,

View File

@@ -0,0 +1 @@
dist/transports

View File

@@ -1 +0,0 @@
module.exports = require('../dist/transports/console.js')

View File

@@ -1 +0,0 @@
module.exports = require('../dist/transports/email.js')

View File

@@ -1 +0,0 @@
module.exports = require('../dist/transports/memory.js')

View File

@@ -1 +0,0 @@
module.exports = require('../dist/transports/syslog.js')

View File

@@ -1,3 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -1,49 +0,0 @@
# @xen-orchestra/mixin [![Build Status](https://travis-ci.org/${pkg.shortGitHubPath}.png?branch=master)](https://travis-ci.org/${pkg.shortGitHubPath})
> ${pkg.description}
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/mixin):
```
> npm install --save @xen-orchestra/mixin
```
## Usage
**TODO**
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## 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 © [Vates SAS](https://vates.fr)

View File

@@ -1,52 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/mixin",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/mixin",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/mixin",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"dependencies": {
"bind-property-descriptor": "^1.0.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,130 +0,0 @@
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
// ===================================================================
const { defineProperties, getOwnPropertyDescriptor } = Object
const isIgnoredProperty = name => name[0] === '_' || name === 'constructor'
const IGNORED_STATIC_PROPERTIES = {
__proto__: null,
arguments: true,
caller: true,
length: true,
name: true,
prototype: true,
}
const isIgnoredStaticProperty = name => name in IGNORED_STATIC_PROPERTIES
const ownKeys =
(typeof Reflect !== 'undefined' && Reflect.ownKeys) ||
(({ getOwnPropertyNames: names, getOwnPropertySymbols: symbols }) =>
symbols !== undefined ? obj => names(obj).concat(symbols(obj)) : names)(
Object
)
// -------------------------------------------------------------------
const mixin = Mixins => Class => {
if (__DEV__ && !Array.isArray(Mixins)) {
throw new TypeError('Mixins should be an array')
}
const { name } = Class
// Copy properties of plain object mix-ins to the prototype.
{
const allMixins = Mixins
Mixins = []
const { prototype } = Class
const descriptors = { __proto__: null }
allMixins.forEach(Mixin => {
if (typeof Mixin === 'function') {
Mixins.push(Mixin)
return
}
for (const prop of ownKeys(Mixin)) {
if (__DEV__ && prop in prototype) {
throw new Error(`${name}#${prop} is already defined`)
}
;(descriptors[prop] = getOwnPropertyDescriptor(
Mixin,
prop
)).enumerable = false // Object methods are enumerable but class methods are not.
}
})
defineProperties(prototype, descriptors)
}
const n = Mixins.length
function DecoratedClass(...args) {
const instance = new Class(...args)
for (let i = 0; i < n; ++i) {
const Mixin = Mixins[i]
const { prototype } = Mixin
const mixinInstance = new Mixin(instance, ...args)
const descriptors = { __proto__: null }
const props = ownKeys(prototype)
for (let j = 0, m = props.length; j < m; ++j) {
const prop = props[j]
if (isIgnoredProperty(prop)) {
continue
}
if (prop in instance) {
throw new Error(`${name}#${prop} is already defined`)
}
descriptors[prop] = getBoundPropertyDescriptor(
prototype,
prop,
mixinInstance
)
}
defineProperties(instance, descriptors)
}
return instance
}
// Copy original and mixed-in static properties on Decorator class.
const descriptors = { __proto__: null }
ownKeys(Class).forEach(prop => {
let descriptor
if (
!(
isIgnoredStaticProperty(prop) &&
// if they already exist...
(descriptor = getOwnPropertyDescriptor(DecoratedClass, prop)) !==
undefined &&
// and are not configurable.
!descriptor.configurable
)
) {
descriptors[prop] = getOwnPropertyDescriptor(Class, prop)
}
})
Mixins.forEach(Mixin => {
ownKeys(Mixin).forEach(prop => {
if (isIgnoredStaticProperty(prop)) {
return
}
if (__DEV__ && prop in descriptors) {
throw new Error(`${name}.${prop} is already defined`)
}
descriptors[prop] = getOwnPropertyDescriptor(Mixin, prop)
})
})
defineProperties(DecoratedClass, descriptors)
return DecoratedClass
}
export { mixin as default }

View File

@@ -1,3 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -1,65 +0,0 @@
# @xen-orchestra/template [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/template):
```
> npm install --save @xen-orchestra/template
```
## Usage
Create a string replacer based on a pattern and a list of rules.
```js
const myReplacer = compileTemplate('{name}_COPY_{name}_{id}_%%', {
'{name}': vm => vm.name_label,
'{id}': vm => vm.id,
'%': (_, i) => i,
})
const newString = myReplacer(
{
name_label: 'foo',
id: 42,
},
32
)
newString === 'foo_COPY_{name}_42_32%' // true
```
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## 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 © [Vates SAS](https://vates.fr)

View File

@@ -1,47 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/template",
"version": "0.1.0",
"license": "ISC",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/template",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/template",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish --access public"
},
"dependencies": {
"lodash": "^4.17.15"
}
}

View File

@@ -1,19 +0,0 @@
import escapeRegExp from 'lodash/escapeRegExp'
const compareLengthDesc = (a, b) => b.length - a.length
export function compileTemplate(pattern, rules) {
const matches = Object.keys(rules)
.sort(compareLengthDesc)
.map(escapeRegExp)
.join('|')
const regExp = new RegExp(`\\\\(?:\\\\|${matches})|${matches}`, 'g')
return (...params) =>
pattern.replace(regExp, match => {
if (match[0] === '\\') {
return match.slice(1)
}
const rule = rules[match]
return typeof rule === 'function' ? rule(...params) : rule
})
}

View File

@@ -1,14 +0,0 @@
/* eslint-env jest */
import { compileTemplate } from '.'
it("correctly replaces the template's variables", () => {
const replacer = compileTemplate(
'{property}_\\{property}_\\\\{property}_{constant}_%_FOO',
{
'{property}': obj => obj.name,
'{constant}': 1235,
'%': (_, i) => i,
}
)
expect(replacer({ name: 'bar' }, 5)).toBe('bar_{property}_\\bar_1235_5_FOO')
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
> This file contains all changes that have not been released yet.
>
> Keep in mind the changelog is addressed to **users** and should be
> understandable by them.
### Enhancements
> Users must be able to say: “Nice enhancement, I'm eager to test it”
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
### Released packages
> Packages will be released in the order they are here, therefore, they should
> be listed by inverse order of dependency.
>
> Rule of thumb: add packages on top.
- xo-server v5.55.0
- xo-web v5.55.0

View File

@@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities

View File

@@ -14,9 +14,9 @@ the issues :)
- **XO origin**: the sources / XO Appliance
- **Versions**:
- Node: **FILL HERE**
- xo-web: **FILL HERE**
- xo-server: **FILL HERE**
- Node: **FILL HERE**
- xo-web: **FILL HERE**
- xo-server: **FILL HERE**
### Expected behavior

View File

@@ -1,28 +0,0 @@
### Check list
> Check if done, if not relevant leave unchecked.
- [ ] PR reference the relevant issue (e.g. `Fixes #007` or `See xoa-support#42`)
- [ ] if UI changes, a screenshot has been added to the PR
- [ ] documentation updated
- `CHANGELOG.unreleased.md`:
- [ ] enhancement/bug fix entry added
- [ ] list of packages to release updated (`${name} v${new version}`)
- **I have tested added/updated features** (and impacted code)
- [ ] unit tests (e.g. [`cron/parse.spec.js`](https://github.com/vatesfr/xen-orchestra/blob/b24400b21de1ebafa1099c56bac1de5c988d9202/%40xen-orchestra/cron/src/parse.spec.js))
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
- [ ] at least manual testing
### Process
1. create a PR as soon as possible
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
1. when you want a review, add a reviewer (and only one)
1. if necessary, update your PR, and re- add a reviewer
From [_the Four Agreements_](https://en.wikipedia.org/wiki/Don_Miguel_Ruiz#The_Four_Agreements):
1. Be impeccable with your word.
1. Don't take anything personally.
1. Don't make assumptions.
1. Always do your best.

View File

@@ -1,4 +1,4 @@
# Xen Orchestra [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
# Xen Orchestra [![Chat with us](https://storage.crisp.im/plugins/images/936925df-f37b-4ba8-bab0-70cd2edcb0be/badge.svg)](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
![](http://i.imgur.com/tRffA5y.png)

View File

@@ -1,5 +0,0 @@
module.exports = {
// Necessary for jest to be able to find the `.babelrc.js` closest to the file
// instead of only the one in this directory.
babelrcRoots: true,
}

View File

@@ -1,9 +0,0 @@
{
"gitbook": ">=3.0.0",
"root": "./docs",
"plugins": [
"anchors",
"-edit-link"
],
"pluginsConfig": {}
}

View File

@@ -1,30 +0,0 @@
# Xen Orchestra
## Introduction
Welcome to the official Xen Orchestra (XO) documentation.
XO is a web interface to visualize and administer your XenServer (or XAPI enabled) hosts. **No agent** is required for it to work.
It aims to be easy to use on any device supporting modern web technologies (HTML 5, CSS 3, JavaScript), such as your desktop computer or your smartphone.
![Xen Orchestra logo](./assets/logo.png)
## XOA quick deploy
Log in to your account and use the deploy form available on [this page](https://xen-orchestra.com/#!/xoa)
> **Note:** no data will be sent to our servers, it's running only between your browser and your host!
[![](./assets/deploy_form.png)](https://xen-orchestra.com/#!/xoa)
### XOA credentials
- Web UI: `admin@admin.net` / `admin`
- Console/SSH: `xoa` / `xoa` (first login)
## Must read
- [XOA installation](xoa.md)
- [Main features](features.md)
- [Pro Support](support.md)

View File

@@ -1,81 +0,0 @@
# Summary
- [Introduction](README.md)
- [Architecture](architecture.md)
- [xo-server](xo-server.md)
- [xo-web](xo-web.md)
- [xo-cli](xo-cli.md)
- [others](others.md)
- [Installation](installation.md)
- [XOA](xoa.md)
- [Updater](updater.md)
- [Trial activation](trial_activation.md)
- [Plugins](plugins.md)
- [Logs](logs.md)
- [Compatibility](supported-version.md)
- [Troubleshooting](troubleshooting.md)
- [From the sources](from_the_sources.md)
- [Configuration](configuration.md)
- [Features](features.md)
- [Administration](administration.md)
- [Home view](user_interface.md)
- [Search and filters](search.md)
- [VM management](vm_management.md)
- [VM creation](vm_creation.md)
- [VM import and export](vm_import_export.md)
- [XenServer Patching](patching.md)
- [Docker support](docker_support.md)
- [Backup and DR](backups.md)
- [Full backups](full_backups.md)
- [Rolling snapshots](rolling_snapshots.md)
- [Continuous Delta backups](delta_backups.md)
- [Continuous Replication](continuous_replication.md)
- [Disaster recovery](disaster_recovery.md)
- [Smart Backup](smart_backup.md)
- [File level Restore](file_level_restore.md)
- [Metadata Backup](metadata_backup.md)
- [Backup Concurrency](concurrency.md)
- [Configure backup reports](backup_reports.md)
- [Backup troubleshooting](backup_troubleshooting.md)
- [User authentication](authentication.md)
- [Built-in](built-in.md)
- [LDAP](ldap.md)
- [SAML](saml.md)
- [GitHub](github.md)
- [Google](google.md)
- [Resources delegation](resources_delegation.md)
- [ACLs](acls.md)
- [CloudInit](cloudinit.md)
- [Self Service](self_service.md)
- [Visualizations](visualizations.md)
- [Health](health.md)
- [Job manager](scheduler.md)
- [Alerts](alerts.md)
- [Web hooks](web-hooks.md)
- [Load balancing](load_balancing.md)
- [Emergency Shutdown](emergency_shutdown.md)
- [Auto scalability](auto_scalability.md)
- [Forecaster](forecaster.md)
- [SDN Controller](sdn_controller.md)
- [Recipes](recipes.md)
- [Reverse proxy](reverse_proxy.md)
- [How to contribute?](contributing.md)
- [Support](support.md)
- [Roadmap](roadmap.md)
- [Purchase](purchase.md)
- [Direct purchase](directpurchase.md)
- [Through purchase department](through_purchase_department.md)
- [Reseller](reseller.md)
- [Editions](editions.md)
- [Trial](trial.md)
- [Invoices](invoices.md)
- [Upgrade](upgrade.md)
- [XOSAN](xosan.md)
- [Requirements](xosan_requirements.md)
- [Types](xosan_types.md)
- [Replicated](xosan_replicated.md)
- [Disperse](xosan_disperse.md)
- [Creation](xosan_create.md)
- [Trial](xosan_trial.md)
- [General Troubleshooting](general-troubleshooting.md)
- [Glossary](glossary.md)

View File

@@ -1,72 +0,0 @@
# ACLs
> ACLs are permissions that apply to pre-existing objects. Only a super admin (XO administrator) can create objects.
ACLs are the permissions for your users or groups. The ACLs view can be accessed in the "Settings" panel.
1. Select the user or group you want to apply permissions on
2. Select the object on which the permission will apply
3. Choose the role for this ACL
4. Click on "Create"
![](./assets/createacl.png)
> Pro tip: you can click to add multiple objects at the same time!
Your ACL is now available in the right list:
![](./assets/acllist.png)
You can edit/remove existing ACLs here.
## Roles
There are 3 different roles for your users:
- Admin
- Operator
- Viewer
### Admin
An object admin can do everything on it, even destroy it. E.g with its admin VM:
- remove it
- migrate it (to a host with admin permission on it)
- modify the VM resources, name and description
- clone it
- copy it
- convert it into a template
- snapshot it (even revert from a snapshot)
- export it
- attach/add visible disks
- same for network cards
### Operator
An operator can make everyday operations on assigned objects. E.g on a VM:
- eject a CD
- insert a CD (if he can view the ISO storage repository)
- start, restart, shutdown, suspend/resume it
All other operations are forbidden.
### Viewer
A viewer can only see the VM state and its metrics. That's all!
## Inheritance
Objects have a hierarchy: a Pool contains all its hosts, containing itself all its VMs.
If you give a _view_ permission to a user (or a group) on a pool, he will automatically see all the objects inside this pool (SRs, hosts, VMs).
## Examples
### Allow a user to install an OS
If the OS install needs an ISO, you need to give this user 2 permissions:
- _Operate_ on the VM (e.g to start it)
- _View_ on the ISO Storage containing the needed ISO.

View File

@@ -1,12 +0,0 @@
# Administration
This section contains everyday XenServer administration tasks.
- [Home view](user_interface.md)
- [Search and filters](search.md)
- [VM management](vm_management.md)
- [VM creation](vm_creation.md)
- [VM import and export](vm_import_export.md)
- [XenServer Patching](patching.md)
![](./assets/xo5homevms.png)

View File

@@ -1,35 +0,0 @@
# Alerts
Alerts are a way to warn the administrator about various events. The first kind of alerts will be emails and also in a dedicated area of `xo-web` to display them.
## Performances alerts
The administrator will configure alerts based on performance thresholds.
The configurable metrics are:
- CPU usage (VM, host)
- RAM usage (VM, host)
- network bandwidth (VM, host)
- load average (host)
- disk IO (VM)
- total IO (SR, only for XenServer Dundee and higher)
If any configured values exceed the threshold during a selected period of time, an alert will be sent.
Those alerts will be also stored and accessible in the web interface, and also later for the load balancing feature (helping it to solve those performance problems).
## Updates alerts
When your XOA detects new packages, you'll be notified by email.
## Backup alerts
Same story for backups: if a backup fails, you'll receive an email.
You can choose to be notified only if it fails or even after each backup job.
Current supported alerts system:
- Email
- XMPP

View File

@@ -1,27 +0,0 @@
# Architecture
Xen Orchestra (XO) is software built with a server and clients, such as the web client `xo-web`, but also a CLI capable client, called `xo-cli`.
> XO is totally agent-less: you don't have to install any program on your hosts to get it working!
## XOA
_Xen Orchestra Virtual Appliance_ (XOA) is a virtual machine with Xen Orchestra already installed, thus working out-of-the-box.
This is the easiest way to try Xen Orchestra quickly.
Your XOA is connected to all your hosts, or the pool master only if you are using Pools in XenServer:
![](./assets/partner2.jpg)
## Xen Orchestra (XO)
![](./assets/xo-arch.jpg)
Xen Orchestra itself is built as a modular solution. Each part has its role:
- The core is "[xo-server](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server/)" - a daemon dealing directly with XenServer or XAPI capable hosts. This is where users are stored, and it's the center point for talking to your whole Xen infrastructure.
- The web interface is "[xo-web](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-web)" - it runs directly from your browser. The connection with `xo-server` is done via _WebSockets_.
- "[xo-cli](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-cli)" is a module allowing you to send commands directly from the command line.
We have other modules as well (like the LDAP plugin for example). It allows us to use this modular architecture to add further parts as we need them. It's completely flexible, allowing us to adapt Xen Orchestra to every existing workflow.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

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