Compare commits

...

10 Commits

95 changed files with 1355 additions and 853 deletions

View File

@@ -21,7 +21,7 @@ module.exports = {
overrides: [
{
files: ['cli.js', '*-cli.js', 'packages/*cli*/**/*.js'],
files: ['cli.js', '*-cli.js', '**/*cli*/**/*.js'],
rules: {
'no-console': 'off',
},

View File

@@ -36,7 +36,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -0,0 +1,369 @@
#!/usr/bin/env node
const args = process.argv.slice(2)
if (
args.length === 0 ||
/^(?:-h|--help)$/.test(args[0]) ||
args[0] !== 'clean-vms'
) {
console.log('Usage: xo-backups clean-vms [--force] xo-vm-backups/*')
// eslint-disable-next-line no-process-exit
return process.exit(1)
}
// remove `clean-vms` arg which is the only available command ATM
args.splice(0, 1)
// only act (ie delete files) if `--force` is present
const force = args[0] === '--force'
if (force) {
args.splice(0, 1)
}
// -----------------------------------------------------------------------------
const assert = require('assert')
const lockfile = require('proper-lockfile')
const { default: Vhd } = require('vhd-lib')
const { curryRight, flatten } = require('lodash')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const { pipe, promisifyAll } = require('promise-toolbox')
const fs = promisifyAll(require('fs'))
const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })
// -----------------------------------------------------------------------------
const asyncMap = curryRight((iterable, fn) =>
Promise.all(
Array.isArray(iterable) ? iterable.map(fn) : Array.from(iterable, fn)
)
)
const filter = (...args) => thisArg => thisArg.filter(...args)
// 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:
// - 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
//
// FIXME: the heuristic does not work if the XVA is compressed, we need to
// implement a specific test for it
//
// 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 path => {
try {
const fd = await fs.open(path, 'r')
try {
const { size } = await fs.fstat(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)
} finally {
fs.close(fd).catch(noop)
}
} catch (error) {
// never throw, log and report as valid to avoid side effects
console.error('isValidTar', path, error)
return true
}
}
const noop = Function.prototype
const readDir = path =>
fs.readdir(path).then(entries => {
entries.forEach((entry, i) => {
entries[i] = `${path}/${entry}`
})
return entries
})
// -----------------------------------------------------------------------------
// 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',
readDir,
asyncMap(readDir),
flatten,
asyncMap(readDir),
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 readDir(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 isValidTar(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)
}),
])
}
// -----------------------------------------------------------------------------
asyncMap(args, 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()
}
}).catch(error => console.error('main', error))

View File

@@ -0,0 +1,27 @@
{
"bin": {
"xo-backups": "index.js"
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/fs": "^0.10.1",
"lodash": "^4.17.15",
"promise-toolbox": "^0.14.0",
"proper-lockfile": "^4.1.1",
"vhd-lib": "^0.7.0"
},
"engines": {
"node": ">=8.16.1"
},
"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

@@ -46,7 +46,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -34,7 +34,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -33,7 +33,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -30,7 +30,7 @@
"get-stream": "^4.0.0",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"readable-stream": "^3.0.6",
"through2": "^3.0.0",
"tmp": "^0.1.0",
@@ -46,7 +46,7 @@
"@babel/preset-flow": "^7.0.0",
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"dotenv": "^8.0.0",
"index-modules": "^0.3.0",
"rimraf": "^3.0.0"

View File

@@ -31,14 +31,14 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.13.0"
"promise-toolbox": "^0.14.0"
},
"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": "^5.1.3",
"cross-env": "^6.0.3",
"index-modules": "^0.3.0",
"rimraf": "^3.0.0"
},

View File

@@ -36,7 +36,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -28,7 +28,7 @@
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -8,14 +8,27 @@
### Released packages
- xo-server v5.51.0
- xo-web v5.51.0
## **5.39.1** (2019-10-11)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Enhancements
- [Support] Ability to check the XOA on the user interface [#4513](https://github.com/vatesfr/xen-orchestra/issues/4513) (PR [#4574](https://github.com/vatesfr/xen-orchestra/pull/4574))
### Bug fixes
- [VM/new-vm] Fix template selection on creating new VM for resource sets [#4565](https://github.com/vatesfr/xen-orchestra/issues/4565) (PR [#4568](https://github.com/vatesfr/xen-orchestra/pull/4568))
- [VM] Clearer invalid cores per socket error [#4120](https://github.com/vatesfr/xen-orchestra/issues/4120) (PR [#4187](https://github.com/vatesfr/xen-orchestra/pull/4187))
### Released packages
- xo-web v5.50.3
## **5.39.0** (2019-09-30)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Highlights
- [VM/console] Add a button to connect to the VM via the local SSH client (PR [#4415](https://github.com/vatesfr/xen-orchestra/pull/4415))

View File

@@ -3,18 +3,19 @@
> Keep in mind the changelog is addressed to **users** and should be
> understandable by them.
### Breaking changes
- `xo-server` requires Node 8.
### Enhancements
> Users must be able to say: “Nice enhancement, I'm eager to test it”
[Support] Ability to check the XOA on the user interface [#4513](https://github.com/vatesfr/xen-orchestra/issues/4513) (PR [#4574](https://github.com/vatesfr/xen-orchestra/pull/4574))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [VM/new-vm] Fix template selection on creating new VM for resource sets [#4565](https://github.com/vatesfr/xen-orchestra/issues/4565) (PR [#4568](https://github.com/vatesfr/xen-orchestra/pull/4568))
- [VM] Clearer invalid cores per socket error [#4120](https://github.com/vatesfr/xen-orchestra/issues/4120) (PR [#4187](https://github.com/vatesfr/xen-orchestra/pull/4187))
### Released packages

View File

@@ -12,18 +12,18 @@
"eslint-config-standard-jsx": "^8.1.0",
"eslint-plugin-eslint-comments": "^3.1.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^9.0.1",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.106.3",
"flow-bin": "^0.109.0",
"globby": "^10.0.0",
"husky": "^3.0.0",
"jest": "^24.1.0",
"lodash": "^4.17.4",
"prettier": "^1.10.2",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"sorted-object": "^2.0.1"
},
"engines": {

View File

@@ -35,7 +35,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.1",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -33,7 +33,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -39,10 +39,10 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"execa": "^2.0.2",
"index-modules": "^0.3.0",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"rimraf": "^3.0.0",
"tmp": "^0.1.0"
},

View File

@@ -26,7 +26,7 @@
"from2": "^2.3.0",
"fs-extra": "^8.0.1",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"struct-fu": "^1.2.0",
"uuid": "^3.0.1"
},
@@ -37,7 +37,7 @@
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.10.1",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"execa": "^2.0.2",
"fs-promise": "^2.0.0",
"get-stream": "^5.1.0",

View File

@@ -13,7 +13,7 @@ import {
import { fuFooter, fuHeader, checksumStruct } from './_structs'
import { test as mapTestBit } from './_bitmap'
export default async function createSyntheticStream(handler, path) {
export default async function createSyntheticStream(handler, paths) {
const fds = []
const cleanup = () => {
for (let i = 0, n = fds.length; i < n; ++i) {
@@ -24,7 +24,7 @@ export default async function createSyntheticStream(handler, path) {
}
try {
const vhds = []
while (true) {
const open = async path => {
const fd = await handler.openFile(path, 'r')
fds.push(fd)
const vhd = new Vhd(handler, fd)
@@ -32,11 +32,18 @@ export default async function createSyntheticStream(handler, path) {
await vhd.readHeaderAndFooter()
await vhd.readBlockAllocationTable()
if (vhd.footer.diskType === DISK_TYPE_DYNAMIC) {
break
return vhd
}
if (typeof paths === 'string') {
let path = paths
let vhd
while ((vhd = await open(path)).footer.diskType !== DISK_TYPE_DYNAMIC) {
path = resolveRelativeFromFile(path, vhd.header.parentUnicodeName)
}
} else {
for (const path of paths) {
await open(path)
}
path = resolveRelativeFromFile(path, vhd.header.parentUnicodeName)
}
const nVhds = vhds.length

View File

@@ -48,7 +48,7 @@
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.5",
"babel-plugin-lodash": "^3.2.11",
"cross-env": "^5.1.4",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -46,7 +46,7 @@
"make-error": "^1.3.0",
"minimist": "^1.2.0",
"ms": "^2.1.1",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"pw": "0.0.4",
"xmlrpc": "^1.3.2",
"xo-collection": "^0.4.1"
@@ -60,7 +60,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -8,7 +8,7 @@ import execPromise from 'exec-promise'
import minimist from 'minimist'
import pw from 'pw'
import { asCallback, fromCallback } from 'promise-toolbox'
import { filter, find, isArray } from 'lodash'
import { filter, find } from 'lodash'
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
import { start as createRepl } from 'repl'
@@ -110,7 +110,7 @@ const main = async args => {
asCallback.call(
fromCallback(cb => {
evaluate.call(repl, cmd, context, filename, cb)
}).then(value => (isArray(value) ? Promise.all(value) : value)),
}).then(value => (Array.isArray(value) ? Promise.all(value) : value)),
cb
)
})(repl.eval)

View File

@@ -4,7 +4,7 @@ import kindOf from 'kindof'
import ms from 'ms'
import httpRequest from 'http-request-plus'
import { EventEmitter } from 'events'
import { isArray, map, noop, omit } from 'lodash'
import { map, noop, omit } from 'lodash'
import {
cancelable,
defer,
@@ -113,7 +113,7 @@ export class Xapi extends EventEmitter {
this._watchedTypes = undefined
const { watchEvents } = opts
if (watchEvents !== false) {
if (isArray(watchEvents)) {
if (Array.isArray(watchEvents)) {
this._watchedTypes = watchEvents
}
this.watchEvents()
@@ -1075,7 +1075,7 @@ export class Xapi extends EventEmitter {
const $field = (field in RESERVED_FIELDS ? '$$' : '$') + field
const value = data[field]
if (isArray(value)) {
if (Array.isArray(value)) {
if (value.length === 0 || isOpaqueRef(value[0])) {
getters[$field] = function() {
const value = this[field]

View File

@@ -38,16 +38,16 @@
"human-format": "^0.10.0",
"l33teral": "^3.0.3",
"lodash": "^4.17.4",
"micromatch": "^3.1.3",
"micromatch": "^4.0.2",
"mkdirp": "^0.5.1",
"nice-pipe": "0.0.0",
"pretty-ms": "^4.0.0",
"pretty-ms": "^5.0.0",
"progress-stream": "^2.0.0",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"pump": "^3.0.0",
"pw": "^0.0.4",
"strip-indent": "^2.0.0",
"xdg-basedir": "^3.0.0",
"strip-indent": "^3.0.0",
"xdg-basedir": "^4.0.0",
"xo-lib": "^0.9.0"
},
"devDependencies": {
@@ -56,7 +56,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -7,7 +7,6 @@ const promisify = require('bluebird').promisify
const readFile = promisify(require('fs').readFile)
const writeFile = promisify(require('fs').writeFile)
const assign = require('lodash/assign')
const l33t = require('l33teral')
const mkdirp = promisify(require('mkdirp'))
const xdgBasedir = require('xdg-basedir')
@@ -41,7 +40,7 @@ const save = (exports.save = function(config) {
exports.set = function(data) {
return load().then(function(config) {
return save(assign(config, data))
return save(Object.assign(config, data))
})
}

View File

@@ -17,7 +17,6 @@ const getKeys = require('lodash/keys')
const hrp = require('http-request-plus').default
const humanFormat = require('human-format')
const identity = require('lodash/identity')
const isArray = require('lodash/isArray')
const isObject = require('lodash/isObject')
const micromatch = require('micromatch')
const nicePipe = require('nice-pipe')
@@ -298,7 +297,11 @@ async function listCommands(args) {
str.push(
name,
'=<',
type == null ? 'unknown type' : isArray(type) ? type.join('|') : type,
type == null
? 'unknown type'
: Array.isArray(type)
? type.join('|')
: type,
'>'
)

View File

@@ -34,7 +34,7 @@
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"event-to-promise": "^0.8.0",
"rimraf": "^3.0.0"
},

View File

@@ -36,7 +36,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -1,5 +1,5 @@
import { BaseError } from 'make-error'
import { isArray, iteratee } from 'lodash'
import { iteratee } from 'lodash'
class XoError extends BaseError {
constructor({ code, message, data }) {
@@ -77,7 +77,7 @@ export const serverUnreachable = create(9, objectId => ({
}))
export const invalidParameters = create(10, (message, errors) => {
if (isArray(message)) {
if (Array.isArray(message)) {
errors = message
message = undefined
}

View File

@@ -41,7 +41,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -32,7 +32,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"deep-freeze": "^0.0.1",
"rimraf": "^3.0.0"
},

View File

@@ -40,7 +40,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -39,14 +39,14 @@
"inquirer": "^7.0.0",
"ldapjs": "^1.0.1",
"lodash": "^4.17.4",
"promise-toolbox": "^0.13.0"
"promise-toolbox": "^0.14.0"
},
"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": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -40,7 +40,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-preset-env": "^1.6.1",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -48,7 +48,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -39,7 +39,7 @@
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -32,7 +32,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -1,6 +1,6 @@
import JSON5 from 'json5'
import { createSchedule } from '@xen-orchestra/cron'
import { assign, forOwn, map, mean } from 'lodash'
import { forOwn, map, mean } from 'lodash'
import { utcParse } from 'd3-time-format'
const COMPARATOR_FN = {
@@ -483,7 +483,7 @@ ${monitorBodies.join('\n')}`
result.rrd = await this.getRrd(result.object, observationPeriod)
if (result.rrd !== null) {
const data = parseData(result.rrd, result.object.uuid)
assign(result, {
Object.assign(result, {
data,
value: data.getDisplayableValue(),
shouldAlarm: data.shouldAlarm(),
@@ -496,7 +496,7 @@ ${monitorBodies.join('\n')}`
definition.alarmTriggerLevel
)
const data = getter(result.object)
assign(result, {
Object.assign(result, {
value: data.getDisplayableValue(),
shouldAlarm: data.shouldAlarm(),
})

View File

@@ -25,12 +25,12 @@
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/preset-env": "^7.4.4",
"cross-env": "^5.2.0"
"cross-env": "^6.0.3"
},
"dependencies": {
"@xen-orchestra/log": "^0.2.0",
"lodash": "^4.17.11",
"node-openssl-cert": "^0.0.97",
"node-openssl-cert": "^0.0.98",
"promise-toolbox": "^0.14.0",
"uuid": "^3.3.2"
},

View File

@@ -36,7 +36,7 @@
"golike-defer": "^0.4.1",
"jest": "^24.8.0",
"lodash": "^4.17.11",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"xo-collection": "^0.4.1",
"xo-common": "^0.2.0",
"xo-lib": "^0.9.0"

View File

@@ -6,7 +6,7 @@ import expect from 'must'
// ===================================================================
import { getConfig, getMainConnection, getSrId, waitObjectState } from './util'
import { map, assign } from 'lodash'
import { map } from 'lodash'
import eventToPromise from 'event-to-promise'
// ===================================================================
@@ -27,7 +27,7 @@ describe('disk', () => {
const config = await getConfig()
serverId = await xo.call(
'server.add',
assign({ autoConnect: false }, config.xenServer1)
Object.assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', { id: serverId })
await eventToPromise(xo.objects, 'finish')

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
import { assign, find, map } from 'lodash'
import { find, map } from 'lodash'
import { config, rejectionOf, xo } from './util'
@@ -151,7 +151,7 @@ describe('server', () => {
it('connects to a Xen server', async () => {
const serverId = await addServer(
assign({ autoConnect: false }, config.xenServer1)
Object.assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', {
@@ -184,7 +184,7 @@ describe('server', () => {
let serverId
beforeEach(async () => {
serverId = await addServer(
assign({ autoConnect: false }, config.xenServer1)
Object.assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', {
id: serverId,

View File

@@ -12,7 +12,7 @@ import {
getOneHost,
waitObjectState,
} from './util'
import { assign, map } from 'lodash'
import { map } from 'lodash'
import eventToPromise from 'event-to-promise'
// ===================================================================
@@ -33,7 +33,7 @@ describe('vbd', () => {
serverId = await xo.call(
'server.add',
assign({ autoConnect: false }, config.xenServer1)
Object.assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', { id: serverId })
await eventToPromise(xo.objects, 'finish')

View File

@@ -34,14 +34,14 @@
"dependencies": {
"nodemailer": "^6.1.0",
"nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.13.0"
"promise-toolbox": "^0.14.0"
},
"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": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -39,7 +39,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-preset-env": "^1.5.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -33,14 +33,14 @@
"node": ">=6"
},
"dependencies": {
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"slack-node": "^0.1.8"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -40,7 +40,7 @@
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -42,14 +42,14 @@
"html-minifier": "^4.0.0",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.13.0"
"promise-toolbox": "^0.14.0"
},
"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": "^5.1.3",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -5,7 +5,6 @@ import humanFormat from 'human-format'
import { createSchedule } from '@xen-orchestra/cron'
import { minify } from 'html-minifier'
import {
assign,
concat,
differenceBy,
filter,
@@ -418,7 +417,7 @@ function computeGlobalVmsStats({ haltedVms, vmsStats, xo }) {
}))
)
return assign(
return Object.assign(
computeMeans(vmsStats, [
'cpu',
'ram',
@@ -446,7 +445,7 @@ function computeGlobalHostsStats({ haltedHosts, hostsStats, xo }) {
}))
)
return assign(
return Object.assign(
computeMeans(hostsStats, [
'cpu',
'ram',

View File

@@ -30,7 +30,7 @@
"bin": "bin"
},
"engines": {
"node": ">=6"
"node": ">=8"
},
"dependencies": {
"@iarna/toml": "^2.2.1",
@@ -58,16 +58,15 @@
"debug": "^4.0.1",
"decorator-synchronized": "^0.5.0",
"deptree": "^1.0.0",
"escape-string-regexp": "^1.0.5",
"event-to-promise": "^0.8.0",
"exec-promise": "^0.7.0",
"execa": "^1.0.0",
"execa": "^2.0.5",
"express": "^4.16.2",
"express-session": "^1.15.6",
"fatfs": "^0.10.4",
"from2": "^2.3.0",
"fs-extra": "^8.0.1",
"get-stream": "^4.0.0",
"get-stream": "^5.1.0",
"golike-defer": "^0.4.1",
"hashy": "^0.7.1",
"helmet": "^3.9.0",
@@ -91,7 +90,7 @@
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4",
"make-error": "^1",
"micromatch": "^3.1.4",
"micromatch": "^4.0.2",
"minimist": "^1.2.0",
"moment-timezone": "^0.5.14",
"ms": "^2.1.1",
@@ -103,7 +102,7 @@
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pretty-format": "^24.0.0",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"proxy-agent": "^3.0.0",
"pug": "^2.0.0-rc.4",
"pump": "^3.0.0",
@@ -123,7 +122,7 @@
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"ws": "^7.1.2",
"xen-api": "^0.27.2",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
@@ -148,7 +147,7 @@
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"babel-plugin-transform-dev": "^2.0.1",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"index-modules": "^0.3.0",
"rimraf": "^3.0.0"
},

View File

@@ -1,7 +1,7 @@
// FIXME: rename to disk.*
import { invalidParameters } from 'xo-common/api-errors'
import { isArray, reduce } from 'lodash'
import { reduce } from 'lodash'
import { parseSize } from '../utils'
@@ -85,7 +85,7 @@ export async function set(params) {
continue
}
for (const field of isArray(fields) ? fields : [fields]) {
for (const field of Array.isArray(fields) ? fields : [fields]) {
await xapi.call(`VDI.set_${field}`, ref, `${params[param]}`)
}
}

View File

@@ -1,7 +1,7 @@
import Model from './model'
import { BaseError } from 'make-error'
import { EventEmitter } from 'events'
import { isArray, isObject, map } from './utils'
import { isObject, map } from './utils'
// ===================================================================
@@ -30,7 +30,7 @@ export default class Collection extends EventEmitter {
}
async add(models, opts) {
const array = isArray(models)
const array = Array.isArray(models)
if (!array) {
models = [models]
}
@@ -66,7 +66,7 @@ export default class Collection extends EventEmitter {
}
async remove(ids) {
if (!isArray(ids)) {
if (!Array.isArray(ids)) {
ids = [ids]
}
@@ -77,8 +77,8 @@ export default class Collection extends EventEmitter {
}
async update(models) {
const array = isArray(models)
if (!isArray(models)) {
const array = Array.isArray(models)
if (!array) {
models = [models]
}

View File

@@ -29,13 +29,7 @@ import { ensureDir, readdir, readFile } from 'fs-extra'
import parseDuration from './_parseDuration'
import Xo from './xo'
import {
forEach,
isArray,
isFunction,
mapToArray,
pFromCallback,
} from './utils'
import { forEach, mapToArray, pFromCallback } from './utils'
import bodyParser from 'body-parser'
import connectFlash from 'connect-flash'
@@ -281,15 +275,16 @@ async function registerPlugin(pluginPath, pluginName) {
// The default export can be either a factory or directly a plugin
// instance.
const instance = isFunction(factory)
? factory({
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
: factory
const instance =
typeof factory === 'function'
? factory({
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
: factory
await this.registerPlugin(
pluginName,
@@ -468,7 +463,7 @@ const setUpProxies = (express, opts, xo) => {
const setUpStaticFiles = (express, opts) => {
forEach(opts, (paths, url) => {
if (!isArray(paths)) {
if (!Array.isArray(paths)) {
paths = [paths]
}

View File

@@ -8,19 +8,21 @@ const parse = createParser({
keyTransform: key => key.slice(5).toLowerCase(),
})
const makeFunction = command => async (fields, ...args) => {
return splitLines(
await execa.stdout(command, [
'--noheading',
'--nosuffix',
'--nameprefixes',
'--unbuffered',
'--units',
'b',
'-o',
String(fields),
...args,
])
).map(Array.isArray(fields) ? parse : line => parse(line)[fields])
const { stdout } = await execa(command, [
'--noheading',
'--nosuffix',
'--nameprefixes',
'--unbuffered',
'--units',
'b',
'-o',
String(fields),
...args,
])
return splitLines(stdout).map(
Array.isArray(fields) ? parse : line => parse(line)[fields]
)
}
export const lvs = makeFunction('lvs')

View File

@@ -1,5 +1,3 @@
import assign from 'lodash/assign'
const _combine = (vectors, n, cb) => {
if (!n) {
return
@@ -35,7 +33,7 @@ export const combine = vectors => cb => _combine(vectors, vectors.length, cb)
// Merge the properties of an objects set in one object.
//
// Ex: mergeObjects([ { a: 1 }, { b: 2 } ]) => { a: 1, b: 2 }
export const mergeObjects = objects => assign({}, ...objects)
export const mergeObjects = objects => Object.assign({}, ...objects)
// Compute a cross product between vectors.
//

View File

@@ -256,10 +256,8 @@ export const safeDateParse = utcParse('%Y%m%dT%H%M%SZ')
//
// Exports them from here to avoid direct dependencies on lodash/
export { default as forEach } from 'lodash/forEach'
export { default as isArray } from 'lodash/isArray'
export { default as isBoolean } from 'lodash/isBoolean'
export { default as isEmpty } from 'lodash/isEmpty'
export { default as isFunction } from 'lodash/isFunction'
export { default as isInteger } from 'lodash/isInteger'
export { default as isObject } from 'lodash/isObject'
export { default as isString } from 'lodash/isString'

View File

@@ -3,7 +3,6 @@ import ensureArray from './_ensureArray'
import {
extractProperty,
forEach,
isArray,
isEmpty,
mapFilter,
mapToArray,
@@ -27,7 +26,7 @@ function link(obj, prop, idField = '$id') {
return dynamicValue // Properly handles null and undefined.
}
if (isArray(dynamicValue)) {
if (Array.isArray(dynamicValue)) {
return mapToArray(dynamicValue, idField)
}

View File

@@ -42,7 +42,6 @@ import pRetry from '../_pRetry'
import {
camelToSnakeCase,
forEach,
isFunction,
map,
mapToArray,
pAll,
@@ -82,7 +81,7 @@ export const TAG_COPY_SRC = 'xo:copy_of'
// FIXME: remove this work around when fixed, https://phabricator.babeljs.io/T2877
// export * from './utils'
require('lodash/assign')(module.exports, require('./utils'))
Object.assign(module.exports, require('./utils'))
// VDI formats. (Raw is not available for delta vdi.)
export const VDI_FORMAT_VHD = 'vhd'
@@ -174,7 +173,7 @@ export default class Xapi extends XapiBase {
//
// TODO: implements a timeout.
_waitObject(predicate) {
if (isFunction(predicate)) {
if (typeof predicate === 'function') {
const { promise, resolve } = defer()
const unregister = this._registerGenericWatcher(obj => {

View File

@@ -9,9 +9,7 @@ import { satisfies as versionSatisfies } from 'semver'
import {
camelToSnakeCase,
forEach,
isArray,
isBoolean,
isFunction,
isInteger,
isString,
map,
@@ -48,7 +46,7 @@ export const prepareXapiParam = param => {
if (isBoolean(param)) {
return asBoolean(param)
}
if (isArray(param)) {
if (Array.isArray(param)) {
return map(param, prepareXapiParam)
}
if (isPlainObject(param)) {
@@ -142,7 +140,7 @@ export const makeEditObject = specs => {
return get
}
const normalizeSet = (set, name) => {
if (isFunction(set)) {
if (typeof set === 'function') {
return set
}
@@ -176,7 +174,7 @@ export const makeEditObject = specs => {
}
}
if (!isArray(set)) {
if (!Array.isArray(set)) {
throw new Error('must be an array, a function or a string')
}
@@ -212,7 +210,7 @@ export const makeEditObject = specs => {
}
forEach(spec.constraints, (constraint, constraintName) => {
if (!isFunction(constraint)) {
if (typeof constraint !== 'function') {
throw new Error('constraint must be a function')
}

View File

@@ -2,7 +2,7 @@ import createLogger from '@xen-orchestra/log'
import kindOf from 'kindof'
import ms from 'ms'
import schemaInspector from 'schema-inspector'
import { forEach, isFunction } from 'lodash'
import { forEach } from 'lodash'
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
import { MethodNotFound } from 'json-rpc-peer'
@@ -183,7 +183,7 @@ export default class Api {
const addMethod = (method, name) => {
name = base + name
if (isFunction(method)) {
if (typeof method === 'function') {
removes.push(this.addApiMethod(name, method))
return
}

View File

@@ -10,6 +10,7 @@ import { type Pattern, createPredicate } from 'value-matcher'
import { type Readable, PassThrough } from 'stream'
import { AssertionError } from 'assert'
import { basename, dirname } from 'path'
import { pipeline } from 'readable-stream'
import {
countBy,
findLast,
@@ -29,7 +30,7 @@ import {
CancelToken,
ignoreErrors,
pFinally,
pFromEvent,
pFromCallback,
timeout,
} from 'promise-toolbox'
import Vhd, {
@@ -343,8 +344,7 @@ const writeStream = async (
const tmpPath = `${dirname(path)}/.${basename(path)}`
const output = await handler.createOutputStream(tmpPath, { checksum })
try {
input.pipe(output)
await pFromEvent(output, 'finish')
await pFromCallback(pipeline, input, output)
await output.checksumWritten
// $FlowFixMe
await input.task

View File

@@ -1,7 +1,6 @@
import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log'
import deferrable from 'golike-defer'
import escapeStringRegexp from 'escape-string-regexp'
import execa from 'execa'
import splitLines from 'split-lines'
import { CancelToken, fromEvent, ignoreErrors } from 'promise-toolbox'
@@ -10,7 +9,16 @@ import { createReadStream, readdir, stat } from 'fs'
import { satisfies as versionSatisfies } from 'semver'
import { utcFormat } from 'd3-time-format'
import { basename, dirname } from 'path'
import { filter, find, includes, once, range, sortBy, trim } from 'lodash'
import {
escapeRegExp,
filter,
find,
includes,
once,
range,
sortBy,
trim,
} from 'lodash'
import {
chainVhd,
createSyntheticStream as createVhdReadStream,
@@ -139,22 +147,20 @@ const listPartitions = (() => {
})
return device =>
execa
.stdout('partx', [
'--bytes',
'--output=NR,START,SIZE,NAME,UUID,TYPE',
'--pairs',
device.path,
])
.then(stdout =>
mapFilter(splitLines(stdout), line => {
const partition = parseLine(line)
const { type } = partition
if (type != null && !IGNORED[+type]) {
return partition
}
})
)
execa('partx', [
'--bytes',
'--output=NR,START,SIZE,NAME,UUID,TYPE',
'--pairs',
device.path,
]).then(({ stdout }) =>
mapFilter(splitLines(stdout), line => {
const partition = parseLine(line)
const { type } = partition
if (type != null && !IGNORED[+type]) {
return partition
}
})
)
})()
// handle LVM logical volumes automatically
@@ -271,7 +277,7 @@ const mountLvmPv = (device, partition) => {
}
args.push('--show', '-f', device.path)
return execa.stdout('losetup', args).then(stdout => {
return execa('losetup', args).then(({ stdout }) => {
const path = trim(stdout)
return {
path,
@@ -862,7 +868,7 @@ export default class {
const files = await handler.list('.')
const reg = new RegExp(
'^[^_]+_' + escapeStringRegexp(`${tag}_${vm.name_label}.xva`)
'^[^_]+_' + escapeRegExp(`${tag}_${vm.name_label}.xva`)
)
const backups = sortBy(filter(files, fileName => reg.test(fileName)))
@@ -887,9 +893,7 @@ export default class {
xapi._assertHealthyVdiChains(vm)
const reg = new RegExp(
'^rollingSnapshot_[^_]+_' + escapeStringRegexp(tag) + '_'
)
const reg = new RegExp('^rollingSnapshot_[^_]+_' + escapeRegExp(tag) + '_')
const snapshots = sortBy(
filter(vm.$snapshots, snapshot => reg.test(snapshot.name_label)),
'name_label'
@@ -926,9 +930,7 @@ export default class {
const transferStart = Date.now()
tag = 'DR_' + tag
const reg = new RegExp(
'^' +
escapeStringRegexp(`${vm.name_label}_${tag}_`) +
'[0-9]{8}T[0-9]{6}Z$'
'^' + escapeRegExp(`${vm.name_label}_${tag}_`) + '[0-9]{8}T[0-9]{6}Z$'
)
const targetXapi = this._xo.getXapi(sr)

View File

@@ -87,7 +87,7 @@ async function mountLvmPhysicalVolume(devicePath, partition) {
args.push('-o', partition.start * 512)
}
args.push('--show', '-f', devicePath)
const path = (await execa.stdout('losetup', args)).trim()
const path = (await execa('losetup', args)).stdout.trim()
await execa('pvscan', ['--cache', path])
return {
@@ -251,7 +251,7 @@ export default class BackupNgFileRestore {
}
async _listPartitions(devicePath, inspectLvmPv = true) {
const stdout = await execa.stdout('partx', [
const { stdout } = await execa('partx', [
'--bytes',
'--output=NR,START,SIZE,NAME,UUID,TYPE',
'--pairs',

View File

@@ -1,7 +1,7 @@
import asyncMap from '@xen-orchestra/async-map'
import { createPredicate } from 'value-matcher'
import { timeout } from 'promise-toolbox'
import { assign, filter, isEmpty, map, mapValues } from 'lodash'
import { filter, isEmpty, map, mapValues } from 'lodash'
import { crossProduct } from '../../math'
import { serializeError, thunkToArray } from '../../utils'
@@ -82,7 +82,11 @@ export default async function executeJobCall({
params,
start: Date.now(),
})
let promise = app.callApiMethod(session, job.method, assign({}, params))
let promise = app.callApiMethod(
session,
job.method,
Object.assign({}, params)
)
if (job.timeout) {
promise = promise::timeout(job.timeout)
}

View File

@@ -4,7 +4,7 @@ import { invalidParameters, noSuchObject } from 'xo-common/api-errors'
import * as sensitiveValues from '../sensitive-values'
import { PluginsMetadata } from '../models/plugin-metadata'
import { isFunction, mapToArray } from '../utils'
import { mapToArray } from '../utils'
// ===================================================================
@@ -65,9 +65,9 @@ export default class {
id,
instance,
name,
testable: isFunction(instance.test),
testable: typeof instance.test === 'function',
testSchema,
unloadable: isFunction(instance.unload),
unloadable: typeof instance.unload === 'function',
version,
})

View File

@@ -1,7 +1,6 @@
import asyncMap from '@xen-orchestra/async-map'
import synchronized from 'decorator-synchronized'
import {
assign,
every,
forEach,
isObject,
@@ -123,7 +122,7 @@ export default class {
}
async computeVmResourcesUsage(vm) {
return assign(
return Object.assign(
computeVmResourcesUsage(this._xo.getXapi(vm).getObject(vm._xapiId)),
await this._xo.computeVmIpPoolsUsage(vm)
)

View File

@@ -2,7 +2,7 @@ import levelup from 'level-party'
import sublevel from 'level-sublevel'
import { ensureDir } from 'fs-extra'
import { forEach, isFunction, promisify } from '../utils'
import { forEach, promisify } from '../utils'
// ===================================================================
@@ -32,7 +32,7 @@ const levelHas = db => {
const levelPromise = db => {
const dbP = {}
forEach(db, (value, name) => {
if (!isFunction(value)) {
if (typeof value !== 'function') {
return
}

View File

@@ -9,7 +9,6 @@ import {
forEach,
includes,
isEmpty,
isFunction,
isString,
iteratee,
map as mapToArray,
@@ -210,7 +209,7 @@ export default class Xo extends EventEmitter {
}
// For security, prevent from accessing `this`.
if (isFunction(value)) {
if (typeof value === 'function') {
value = (value =>
function() {
return value.apply(thisArg, arguments)

View File

@@ -27,7 +27,7 @@
"child-process-promise": "^2.0.3",
"core-js": "^3.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"tmp": "^0.1.0",
"vhd-lib": "^0.7.0"
},
@@ -36,7 +36,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"cross-env": "^6.0.3",
"event-to-promise": "^0.8.0",
"execa": "^2.0.2",
"fs-extra": "^8.0.1",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.50.2",
"version": "5.50.3",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -97,7 +97,7 @@
"moment-timezone": "^0.5.14",
"notifyjs": "^3.0.0",
"otplib": "^11.0.0",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.14.0",
"prop-types": "^15.6.0",
"qrcode": "^1.3.2",
"random-password": "^0.1.2",

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'
import React from 'react'
import { isFunction } from 'lodash'
import Button from './button'
import Component from './base-component'
@@ -93,9 +92,10 @@ export default class ActionButton extends Component {
const { redirectOnSuccess } = props
if (redirectOnSuccess !== undefined) {
const to = isFunction(redirectOnSuccess)
? redirectOnSuccess(result, handlerParam)
: redirectOnSuccess
const to =
typeof redirectOnSuccess === 'function'
? redirectOnSuccess(result, handlerParam)
: redirectOnSuccess
if (to !== undefined) {
return this.context.router.push(to)
}

View File

@@ -1,6 +1,6 @@
import { PureComponent } from 'react'
import { cowSet } from 'utils'
import { includes, isArray, forEach, map } from 'lodash'
import { includes, forEach, map } from 'lodash'
import getEventValue from './get-event-value'
@@ -15,7 +15,7 @@ const get = (object, path, depth) => {
}
const prop = path[depth++]
return isArray(object) && prop === '*'
return Array.isArray(object) && prop === '*'
? map(object, value => get(value, path, depth))
: get(object[prop], path, depth)
}

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'
import React from 'react'
import PropTypes from 'prop-types'
import { isEmpty, isFunction, isString, map, pick } from 'lodash'
import { isEmpty, isString, map, pick } from 'lodash'
import _ from '../intl'
import Component from '../base-component'
@@ -100,7 +100,7 @@ class Editable extends Component {
return this.__save(
() => this.state.previous,
isFunction(onUndo) ? onUndo : props.onChange
typeof onUndo === 'function' ? onUndo : props.onChange
)
}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
import { every, isFunction, isString } from 'lodash'
import { every, isString } from 'lodash'
import locales from './locales'
import messages from './messages'
@@ -32,7 +32,7 @@ const getMessage = (props, messageId, values, render) => {
throw new Error(`no message defined for ${messageId}`)
}
if (isFunction(values)) {
if (typeof values === 'function') {
render = values
values = undefined
}

View File

@@ -1,6 +1,5 @@
import forEachRight from 'lodash/forEachRight'
import forEach from 'lodash/forEach'
import isArray from 'lodash/isArray'
import isIp from 'is-ip'
import some from 'lodash/some'
@@ -76,7 +75,7 @@ export const getNextIpV4 = ip => {
}
export const formatIps = ips => {
if (!isArray(ips)) {
if (!Array.isArray(ips)) {
throw new Error('ips must be an array')
}
if (ips.length === 0) {

View File

@@ -1,6 +1,5 @@
import React from 'react'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import marked from 'marked'
import { Col, Row } from 'grid'
@@ -14,7 +13,7 @@ export const getType = schema => {
const type = schema.type
if (isArray(type)) {
if (Array.isArray(type)) {
if (includes(type, 'integer')) {
return 'integer'
}

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'
import React, { Component, cloneElement } from 'react'
import { createSelector } from 'selectors'
import { identity, isArray, isString, map } from 'lodash'
import { identity, isString, map } from 'lodash'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
@@ -35,7 +35,7 @@ const modal = (content, onClose, props) => {
}
const _addRef = (component, ref) => {
if (isString(component) || isArray(component)) {
if (isString(component) || Array.isArray(component)) {
return component
}

View File

@@ -5,7 +5,7 @@ import React from 'react'
import { createSchedule } from '@xen-orchestra/cron'
import { FormattedDate, FormattedTime } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'
import { flatten, forEach, identity, map, sortedIndex } from 'lodash'
import _ from './intl'
import Button from './button'
@@ -262,7 +262,7 @@ const TimePicker = decorate([
provideState({
effects: {
onChange: (_, value) => ({ optionsValues }, { onChange }) => {
if (isArray(value)) {
if (Array.isArray(value)) {
value = value.length === optionsValues.length ? '*' : value.join(',')
} else {
value = `*/${value}`

View File

@@ -2,13 +2,11 @@ import React from 'react'
import PropTypes from 'prop-types'
import { parse as parseRemote } from 'xo-remote-parser'
import {
assign,
filter,
flatten,
forEach,
groupBy,
includes,
isArray,
isEmpty,
isInteger,
isString,
@@ -62,7 +60,7 @@ const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }
const getIds = value =>
value == null || isString(value) || isInteger(value)
? value
: isArray(value)
: Array.isArray(value)
? map(value, getIds)
: value.id
@@ -87,7 +85,7 @@ const options = props => ({
})
const getObjectsById = objects =>
keyBy(isArray(objects) ? objects : flatten(toArray(objects)), 'id')
keyBy(Array.isArray(objects) ? objects : flatten(toArray(objects)), 'id')
// ===================================================================
@@ -155,7 +153,7 @@ class GenericSelect extends React.Component {
})
}
}
if (isArray(ids)) {
if (Array.isArray(ids)) {
ids.forEach(addIfMissing)
} else {
addIfMissing(ids)
@@ -188,7 +186,7 @@ class GenericSelect extends React.Component {
let options
if (containers === undefined) {
if (__DEV__ && !isArray(objects)) {
if (__DEV__ && !Array.isArray(objects)) {
throw new Error(
`${name}: without xoContainers, xoObjects must be an array`
)
@@ -199,7 +197,7 @@ class GenericSelect extends React.Component {
: objects
).map(getOption)
} else {
if (__DEV__ && isArray(objects)) {
if (__DEV__ && Array.isArray(objects)) {
throw new Error(
`${name}: with xoContainers, xoObjects must be an object`
)
@@ -249,7 +247,7 @@ class GenericSelect extends React.Component {
this._getObjectsById,
value => value,
(objectsById, value) =>
isArray(value)
Array.isArray(value)
? map(value, value => objectsById[value.value])
: objectsById[value.value]
)
@@ -642,7 +640,7 @@ export const SelectHighLevelObject = makeStoreSelect(
getSrs,
getVms,
(hosts, networks, pools, srs, vms) =>
sortBy(assign({}, hosts, networks, pools, srs, vms), [
sortBy(Object.assign({}, hosts, networks, pools, srs, vms), [
'type',
'name_label',
])

View File

@@ -8,10 +8,8 @@ import {
forEach,
groupBy,
identity,
isArray,
isArrayLike,
isEmpty,
isFunction,
keys,
map,
orderBy,
@@ -71,7 +69,7 @@ const _SELECTOR_PLACEHOLDER = Symbol('selector placeholder')
const _create2 = (...inputs) => {
const resultFn = inputs.pop()
if (inputs.length === 1 && isArray(inputs[0])) {
if (inputs.length === 1 && Array.isArray(inputs[0])) {
inputs = inputs[0]
}
@@ -81,10 +79,10 @@ const _create2 = (...inputs) => {
for (let i = 0; i < n; ++i) {
const input = inputs[i]
if (isFunction(input)) {
if (typeof input === 'function') {
inputSelectors.push(input)
inputs[i] = _SELECTOR_PLACEHOLDER
} else if (isArray(input) && input.length === 1) {
} else if (Array.isArray(input) && input.length === 1) {
inputs[i] = input[0]
}
}
@@ -352,7 +350,9 @@ export const createSortForType = invoke(() => {
const getOrders = type => ordersByType[type]
const autoSelector = (type, fn) =>
isFunction(type) ? (state, props) => fn(type(state, props)) : [fn(type)]
typeof type === 'function'
? (state, props) => fn(type(state, props))
: [fn(type)]
return (type, collection) =>
createSort(
@@ -423,9 +423,11 @@ const _extendCollectionSelector = (selector, objectsType) => {
// - sort: returns a selector which returns the objects appropriately
// sorted (groupBy can be chained)
export const createGetObjectsOfType = type => {
const getObjects = isFunction(type)
? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT
: state => state.objects.byType[type] || EMPTY_OBJECT
const getObjects =
typeof type === 'function'
? (state, props) =>
state.objects.byType[type(state, props)] || EMPTY_OBJECT
: state => state.objects.byType[type] || EMPTY_OBJECT
return _extendCollectionSelector(
createFilter(getObjects, _getPermissionsPredicate),

View File

@@ -19,9 +19,7 @@ import {
findIndex,
forEach,
get as getProperty,
isArray,
isEmpty,
isFunction,
map,
sortBy,
} from 'lodash'
@@ -215,17 +213,17 @@ const Action = decorate([
provideState({
computed: {
disabled: ({ items }, { disabled, userData }) =>
isFunction(disabled) ? disabled(items, userData) : disabled,
typeof disabled === 'function' ? disabled(items, userData) : disabled,
handler: ({ items }, { handler, userData }) => () =>
handler(items, userData),
icon: ({ items }, { icon, userData }) =>
isFunction(icon) ? icon(items, userData) : icon,
typeof icon === 'function' ? icon(items, userData) : icon,
items: (_, { items, grouped }) =>
isArray(items) || !grouped ? items : [items],
Array.isArray(items) || !grouped ? items : [items],
label: ({ items }, { label, userData }) =>
isFunction(label) ? label(items, userData) : label,
typeof label === 'function' ? label(items, userData) : label,
level: ({ items }, { level, userData }) =>
isFunction(level) ? level(items, userData) : level,
typeof level === 'function' ? level(items, userData) : level,
},
}),
injectState,
@@ -493,7 +491,9 @@ export default class SortedTable extends Component {
if (item !== undefined) {
if (rowLink !== undefined) {
this.context.router.push(
isFunction(rowLink) ? rowLink(item, userData) : rowLink
typeof rowLink === 'function'
? rowLink(item, userData)
: rowLink
)
} else if (rowAction !== undefined) {
rowAction(item, userData)
@@ -785,7 +785,7 @@ export default class SortedTable extends Component {
className={state.highlighted === i ? styles.highlight : undefined}
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
to={typeof rowLink === 'function' ? rowLink(item, userData) : rowLink}
>
{selectionColumn}
{columns}

View File

@@ -8,7 +8,6 @@ import {
clone,
every,
forEach,
isArray,
isEmpty,
isFunction,
isPlainObject,
@@ -81,12 +80,12 @@ const _normalizeMapStateToProps = mapper => {
return state => pick(state, mapper)
}
if (isFunction(mapper)) {
if (typeof mapper === 'function') {
const factoryOrMapper = (state, props) => {
const result = mapper(state, props)
// Properly handles factory pattern.
if (isFunction(result)) {
if (typeof result === 'function') {
mapper = result
return factoryOrMapper
}
@@ -258,10 +257,10 @@ const NotFound = () => <h1>{_('errorPageNotFound')}</h1>
//
// TODO: add support for function childRoutes (getChildRoutes).
export const routes = (indexRoute, childRoutes) => target => {
if (isArray(indexRoute)) {
if (Array.isArray(indexRoute)) {
childRoutes = indexRoute
indexRoute = undefined
} else if (isFunction(indexRoute)) {
} else if (typeof indexRoute === 'function') {
indexRoute = {
component: indexRoute,
}

View File

@@ -11,7 +11,6 @@ import Xo from 'xo-lib'
import { createBackoff } from 'jsonrpc-websocket-client'
import { SelectHost } from 'select-objects'
import {
assign,
filter,
forEach,
get,
@@ -159,7 +158,7 @@ export const connectStore = store => {
return
}
assign(updates, notification.params.items)
Object.assign(updates, notification.params.items)
sendUpdates()
})
subscribePermissions(permissions =>

View File

@@ -1,4 +1,3 @@
import assign from 'lodash/assign'
import Client, {
AbortedConnection,
ConnectionError,
@@ -298,7 +297,7 @@ class XoaUpdater extends EventEmitter {
} catch (error) {
return this._xoaStateError(error)
} finally {
this.emit('trialState', assign({}, this._xoaState))
this.emit('trialState', Object.assign({}, this._xoaState))
}
}
@@ -367,7 +366,7 @@ class XoaUpdater extends EventEmitter {
while (this._log.length > 10) {
this._log.shift()
}
this.emit('log', map(this._log, item => assign({}, item)))
this.emit('log', map(this._log, item => Object.assign({}, item)))
}
async getConfiguration() {
@@ -377,7 +376,7 @@ class XoaUpdater extends EventEmitter {
} catch (error) {
this._configuration = {}
} finally {
this.emit('configuration', assign({}, this._configuration))
this.emit('configuration', Object.assign({}, this._configuration))
}
}
@@ -406,7 +405,7 @@ class XoaUpdater extends EventEmitter {
} catch (error) {
this._configuration = {}
} finally {
this.emit('configuration', assign({}, this._configuration))
this.emit('configuration', Object.assign({}, this._configuration))
}
}
}

View File

@@ -1,6 +1,5 @@
import logError from 'log-error'
import React from 'react'
import { assign, isFunction } from 'lodash'
// Avoid global breakage if a component fails to render.
//
@@ -32,18 +31,18 @@ React.createElement = (createElement => {
}
return function(Component) {
if (isFunction(Component)) {
if (typeof Component === 'function') {
const patched = Component._patched
if (patched) {
arguments[0] = patched
} else {
const { prototype } = Component
let render
if (prototype && isFunction((render = prototype.render))) {
if (prototype && typeof (render = prototype.render) === 'function') {
prototype.render = wrapRender(render)
Component._patched = Component // itself
} else {
arguments[0] = Component._patched = assign(
arguments[0] = Component._patched = Object.assign(
wrapRender(Component),
Component
)

View File

@@ -17,7 +17,6 @@ import {
subscribeRemotes,
} from 'xo'
import {
assign,
filter,
find,
flatMap,
@@ -149,7 +148,7 @@ export default class Restore extends Component {
count++
})
assign(data, { first, last, count, id: vmId })
Object.assign(data, { first, last, count, id: vmId })
})
forEach(backupDataByVm, ({ backups }, vmId) => {

View File

@@ -11,7 +11,6 @@ import { confirm } from 'modal'
import { error } from 'notification'
import { FormattedDate } from 'react-intl'
import {
assign,
filter,
find,
flatMap,
@@ -174,7 +173,7 @@ export default class Restore extends Component {
}
})
assign(data, { first, last, count, id: vmId, size })
Object.assign(data, { first, last, count, id: vmId, size })
})
forEach(backupDataByVm, ({ backups }, vmId) => {

View File

@@ -22,7 +22,7 @@ import { createGetObjectsOfType, getUser } from 'selectors'
import { createSelector } from 'reselect'
import { generateUiSchema } from 'xo-json-schema-input'
import { SelectSubject } from 'select-objects'
import { forEach, isArray, map, mapValues, noop } from 'lodash'
import { forEach, map, mapValues, noop } from 'lodash'
import { createJob, createSchedule, getRemote, editJob, editSchedule } from 'xo'
@@ -362,7 +362,7 @@ export default class New extends Component {
$pool: destructPattern($pool),
power_state: pattern.power_state,
tags: destructPattern(tags, tags =>
map(tags, tag => (isArray(tag) ? tag[0] : tag))
map(tags, tag => (Array.isArray(tag) ? tag[0] : tag))
),
},
}
@@ -423,7 +423,7 @@ export default class New extends Component {
key: backupInfo.jobKey,
paramsVector: {
type: 'crossProduct',
items: isArray(vms.vms)
items: Array.isArray(vms.vms)
? [
{
type: 'set',
@@ -550,7 +550,7 @@ export default class New extends Component {
const vms = this._getVmsParam()
const backupInfo = BACKUP_METHOD_TO_INFO[method]
const smartBackupMode = !isArray(vms.vms)
const smartBackupMode = !Array.isArray(vms.vms)
return (
<Upgrade place='newBackup' required={2}>

View File

@@ -21,7 +21,7 @@ import {
createGetObjectsOfType,
createSelector,
} from 'selectors'
import { assign, isEmpty, map, pick, sortBy } from 'lodash'
import { isEmpty, map, pick, sortBy } from 'lodash'
import TabAdvanced from './tab-advanced'
import TabConsole from './tab-console'
@@ -338,7 +338,7 @@ export default class Host extends Component {
if (!host) {
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(
const childProps = Object.assign(
pick(this.props, [
'host',
'hostPatches',

View File

@@ -2,7 +2,6 @@ import Component from 'base-component'
import cookies from 'cookies-js'
import DocumentTitle from 'react-document-title'
import Icon from 'icon'
import isArray from 'lodash/isArray'
import Link from 'link'
import map from 'lodash/map'
import PropTypes from 'prop-types'
@@ -188,7 +187,9 @@ export default class XoApp extends Component {
message && (
<Row key={`${contextKey}_${key}`}>
<Col size={2} className='text-xs-right'>
<strong>{isArray(keys) ? keys[0] : keys}</strong>
<strong>
{Array.isArray(keys) ? keys[0] : keys}
</strong>
</Col>
<Col size={10}>{message}</Col>
</Row>

View File

@@ -1,5 +1,4 @@
import _ from 'intl'
import assign from 'lodash/assign'
import Copiable from 'copiable'
import Icon from 'icon'
import PoolActionBar from './action-bar'
@@ -163,7 +162,7 @@ export default class Pool extends Component {
if (!pool) {
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(
const childProps = Object.assign(
pick(this.props, [
'hosts',
'logs',

View File

@@ -11,7 +11,7 @@ import { Container, Row, Col } from 'grid'
import { editSr } from 'xo'
import { NavLink, NavTabs } from 'nav'
import { Text } from 'editable'
import { assign, map, pick } from 'lodash'
import { map, pick } from 'lodash'
import { connectStore, routes } from 'utils'
import {
createGetObject,
@@ -184,7 +184,7 @@ export default class Sr extends Component {
if (!sr) {
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(
const childProps = Object.assign(
pick(this.props, [
'hosts',
'logs',

View File

@@ -8,7 +8,7 @@ import PropTypes from 'prop-types'
import React, { cloneElement } from 'react'
import { Host, Pool } from 'render-xo-item'
import { Text, XoSelect } from 'editable'
import { assign, isEmpty, map, pick } from 'lodash'
import { isEmpty, map, pick } from 'lodash'
import { editVm, fetchVmStats, isVmRunning, migrateVm } from 'xo'
import { Container, Row, Col } from 'grid'
import { connectStore, routes } from 'utils'
@@ -295,7 +295,7 @@ export default class Vm extends BaseComponent {
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(
const childProps = Object.assign(
pick(this.props, [
'container',
'pool',

View File

@@ -39,7 +39,6 @@ import {
osFamily,
} from 'utils'
import {
assign,
every,
filter,
find,
@@ -165,7 +164,7 @@ class ResourceSetItem extends Component {
() => this.props.resourceSets,
() => this.props.id,
(resourceSets, id) =>
assign(find(resourceSets, { id }), { type: 'resourceSet' })
Object.assign(find(resourceSets, { id }), { type: 'resourceSet' })
)
render() {

1251
yarn.lock

File diff suppressed because it is too large Load Diff