a working file watcher module based on chokidar
add excludeFrom to rsync several improvements to sync module including integrations with file watcher module.master
parent
b684bf2ca5
commit
ff6efd0b88
|
@ -1,2 +1,2 @@
|
||||||
/node_modules/
|
**/node_modules/**
|
||||||
/coverage/
|
/coverage/
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Sync from '../src/sync'
|
||||||
|
import onDeath from 'ondeath'
|
||||||
|
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const sync = new Sync({jobsDir:'./example/jobs'})
|
||||||
|
|
||||||
|
await sync.loadJob('example')
|
||||||
|
sync.live()
|
||||||
|
sync.watch('on')
|
||||||
|
console.log('ready and waiting')
|
||||||
|
|
||||||
|
onDeath( () => {
|
||||||
|
console.log('\nHe\'s dead Jim')
|
||||||
|
sync.watch('remove')
|
||||||
|
})
|
||||||
|
|
||||||
|
})().catch(err => {
|
||||||
|
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
||||||
|
})
|
|
@ -0,0 +1,15 @@
|
||||||
|
source: ./example/source/ # be sure to add trailing / to avoid making a subdirectory under destination
|
||||||
|
destination: ./example/destination
|
||||||
|
flags: 'av'
|
||||||
|
excludeFrom:
|
||||||
|
- ./example/source/.gitignore
|
||||||
|
- ./example/source/.npmignore
|
||||||
|
exclude:
|
||||||
|
- .gitignore
|
||||||
|
- .npmignore
|
||||||
|
set:
|
||||||
|
- delete
|
||||||
|
- delete-excluded
|
||||||
|
watch: # true
|
||||||
|
wait: 200
|
||||||
|
immediate: true
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/**
|
||||||
|
**/node_modules/**
|
||||||
|
coverage/
|
|
@ -0,0 +1,5 @@
|
||||||
|
tests/
|
||||||
|
test/
|
||||||
|
*.test.js
|
||||||
|
testing/
|
||||||
|
example/
|
14
package.json
14
package.json
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@uci/remote-code",
|
"name": "@uci/sync",
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"description": "module to copy, maintain, and launch hardware modules on other machines",
|
"description": "module to copy, maintain, and launch hardware modules on other machines",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
|
@ -7,9 +7,13 @@
|
||||||
"ssh": "./bin/ssh.js"
|
"ssh": "./bin/ssh.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sync": "node -r esm ./bin/ssh",
|
"sync": "node -r esm ./bin/sync",
|
||||||
|
"example": "node -r esm ./example/example",
|
||||||
|
"test": "./node_modules/.bin/mocha -r esm --timeout 30000",
|
||||||
"testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000'",
|
"testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000'",
|
||||||
"test": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log ./node_modules/.bin/mocha -r esm --timeout 30000 || exit 0",
|
"testdd": "UCI_LOG_LEVEL='trace' npm run testd",
|
||||||
|
"testde": "UCI_LOG_LEVEL='warn' npm run testd",
|
||||||
|
"testl": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log 0 npm run test || exit 0",
|
||||||
"testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true"
|
"testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true"
|
||||||
},
|
},
|
||||||
"author": "David Kebler",
|
"author": "David Kebler",
|
||||||
|
@ -33,6 +37,7 @@
|
||||||
"await-to-js": "^2.1.1",
|
"await-to-js": "^2.1.1",
|
||||||
"chokidar": "^2.0.4",
|
"chokidar": "^2.0.4",
|
||||||
"conf": "^2.2.0",
|
"conf": "^2.2.0",
|
||||||
|
"debounce-fn": "^1.0.0",
|
||||||
"esm": "^3.1.4",
|
"esm": "^3.1.4",
|
||||||
"fs-read-data": "^1.0.4",
|
"fs-read-data": "^1.0.4",
|
||||||
"globby": "^9.0.0",
|
"globby": "^9.0.0",
|
||||||
|
@ -46,6 +51,7 @@
|
||||||
"codecov": "^3.1.0",
|
"codecov": "^3.1.0",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"mocha": "^5.x",
|
"mocha": "^5.x",
|
||||||
"nodemon": "^1.18.9"
|
"nodemon": "^1.18.9",
|
||||||
|
"ondeath": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Sync from './sync'
|
||||||
|
import Watcher from './watcher'
|
||||||
|
|
||||||
|
export { Sync, Watcher}
|
||||||
|
export default Sync
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { readFile, writeFile } from 'fs'
|
||||||
|
const read = promisify(readFile)
|
||||||
|
const write = promisify(writeFile)
|
||||||
|
// alternative with new promise experimental fs
|
||||||
|
// import fs from 'fs'
|
||||||
|
// const read = fs.promises.readFile
|
||||||
|
import settle from 'p-settle'
|
||||||
|
import path from 'path'
|
||||||
|
import logger from '@uci/logger'
|
||||||
|
let log = logger({ package:'@cui/sync', file:'src/read-lines.js'})
|
||||||
|
|
||||||
|
// A promise helper function to return a list of paths to ignore from .npmignore, .gitignore, .rcignore
|
||||||
|
function readLines (files=[],dir) {
|
||||||
|
// console.log('additional files', files)
|
||||||
|
let list = []
|
||||||
|
if (!Array.isArray(files)) files=[files]
|
||||||
|
|
||||||
|
// each set in an the array is new line delimited set of ignore patterns
|
||||||
|
// settle returns array of error,value pairs
|
||||||
|
return settle(files.map(file => {
|
||||||
|
// console.log('directory',path.dirname(file))
|
||||||
|
if (path.dirname(file)==='.') file = dir+'/'+file
|
||||||
|
log.debug({function:'readLines',file:file,msg:'reading a file of lines into array'})
|
||||||
|
return read(file)
|
||||||
|
}))
|
||||||
|
.then((sets) => {
|
||||||
|
sets.forEach( set => {
|
||||||
|
if (set.isFulfilled) list.push(...set.value.toString().match(/.+/g))
|
||||||
|
else log.warn({function:'readLines', error:set.reason, msg:' was unable to read file'})
|
||||||
|
})
|
||||||
|
return Promise.resolve(list)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// only returned when something horrible is wrong
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// an ignore list should not be huge so can serailize at once
|
||||||
|
function writeLines (filePath,list) {
|
||||||
|
|
||||||
|
return write(filePath,list.join('\n'))
|
||||||
|
.then(() => {
|
||||||
|
log.info({function:'writeLines', file:filePath, msg:'wrote array to file of lines'})
|
||||||
|
})
|
||||||
|
.catch( err => {
|
||||||
|
log.fatal({function:'writeLines', error:err, msg:'unable to write array to file of lines'})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default readLines
|
||||||
|
export { readLines, writeLines }
|
12
src/rsync.js
12
src/rsync.js
|
@ -64,7 +64,10 @@ function Rsync(config) {
|
||||||
// ordered list of file patterns to include/exclude
|
// ordered list of file patterns to include/exclude
|
||||||
this._patterns = []
|
this._patterns = []
|
||||||
|
|
||||||
// list of exlude from getFiles
|
// just a list of exclude only patterns for use elsewhere like a watcher
|
||||||
|
this._exclude = []
|
||||||
|
|
||||||
|
// list of exclude files
|
||||||
this._excludeFiles = []
|
this._excludeFiles = []
|
||||||
|
|
||||||
// options
|
// options
|
||||||
|
@ -299,6 +302,9 @@ Rsync.prototype.exclude = function(patterns) {
|
||||||
patterns = [ patterns ]
|
patterns = [ patterns ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save this list for other uses
|
||||||
|
this._exclude = [...this._exclude,...patterns]
|
||||||
|
|
||||||
patterns.forEach(function(pattern) {
|
patterns.forEach(function(pattern) {
|
||||||
this._patterns.push({ action: '-', pattern: pattern })
|
this._patterns.push({ action: '-', pattern: pattern })
|
||||||
}, this)
|
}, this)
|
||||||
|
@ -321,10 +327,14 @@ Rsync.prototype.excludeFrom = function(filePaths) {
|
||||||
filePaths = [ filePaths ]
|
filePaths = [ filePaths ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO if it's not a path just a file try prepending the source directory
|
||||||
|
// TODO only add to list if file exsists
|
||||||
filePaths.forEach(function(filePath) {
|
filePaths.forEach(function(filePath) {
|
||||||
this._excludeFiles.push(filePath)
|
this._excludeFiles.push(filePath)
|
||||||
}, this)
|
}, this)
|
||||||
|
|
||||||
|
// this._excludeFiles = [...this._excludeFiles,...filePaths]
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
189
src/sync.js
189
src/sync.js
|
@ -1,5 +1,6 @@
|
||||||
// local imports
|
// local imports
|
||||||
import Rsync from './rsync'
|
import Rsync from './rsync'
|
||||||
|
import Watcher from './watcher'
|
||||||
// native imports
|
// native imports
|
||||||
import { EventEmitter as Emitter } from 'events'
|
import { EventEmitter as Emitter } from 'events'
|
||||||
import { dirname } from 'path'
|
import { dirname } from 'path'
|
||||||
|
@ -7,7 +8,8 @@ import { dirname } from 'path'
|
||||||
import merge from 'aggregation/es6'
|
import merge from 'aggregation/es6'
|
||||||
import { readFile } from 'fs-read-data'
|
import { readFile } from 'fs-read-data'
|
||||||
import Conf from 'conf'
|
import Conf from 'conf'
|
||||||
import getFiles from 'globby'
|
import debounce from 'debounce-fn'
|
||||||
|
// import getFiles from 'globby'
|
||||||
import pathExists from 'path-exists'
|
import pathExists from 'path-exists'
|
||||||
import to from 'await-to-js'
|
import to from 'await-to-js'
|
||||||
// uci imports
|
// uci imports
|
||||||
|
@ -19,14 +21,23 @@ class Sync extends merge(Rsync, Emitter) {
|
||||||
super()
|
super()
|
||||||
log = logger({ package:'@uci/sync'})
|
log = logger({ package:'@uci/sync'})
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
this.cli = opts.cli
|
this._debounce = opts.debounce || null
|
||||||
|
this.syncHandler = opts.syncHandler || (()=>{})
|
||||||
|
// TODO if opts include source and destination then call loadJob with them
|
||||||
|
// this._debounce = opts.debounce
|
||||||
this.config = new Conf({projectName:'sync'})
|
this.config = new Conf({projectName:'sync'})
|
||||||
this.jobsDir = process.env.SYNC_JOBS_DIR || opts.jobsDir || this.config.get('jobsDir') || dirname(this.config.path)
|
this.jobsDir = process.env.SYNC_JOBS_DIR || opts.jobsDir || this.config.get('jobsDir') || dirname(this.config.path)
|
||||||
this.sshDir = opts.sshDir || this.config.get('sshDir') || `${this.jobsDir}/ssh`
|
this.sshDir = opts.sshDir || this.config.get('sshDir') || `${this.jobsDir}/ssh`
|
||||||
this.optionsDir = opts.optionsDir || this.config.get('optionsDir') || `${this.jobsDir}/options`
|
this.optionsDir = opts.optionsDir || this.config.get('optionsDir') || `${this.jobsDir}/options`
|
||||||
log.debug({configsDir:this.configsDir, msg:'configuration files directory'})
|
log.debug({jobsDir:this.jobsDir, sshDir:this.sshDir, optionsDir:this.optionsDir, msg:'configuration file directories'})
|
||||||
this.jobs = []
|
this._watcher = new Watcher()
|
||||||
}
|
}
|
||||||
|
// getters and setters
|
||||||
|
get watching() {return this._watcher.watching } // watcher active?
|
||||||
|
get watcher() { return this._watcher} // in case one need set a listerner for other purposes
|
||||||
|
|
||||||
|
get defaultJobsDir() { return this.config.get('jobsDir', this.jobsDir) }
|
||||||
|
set defaultJobsDir(jobsDir) { this.config.set('jobsDir', jobsDir || this.jobsDir)}
|
||||||
|
|
||||||
setConfig(name,val) { this.config.set(name,val)}
|
setConfig(name,val) { this.config.set(name,val)}
|
||||||
getConfig(name) { return this.config.get(name)}
|
getConfig(name) { return this.config.get(name)}
|
||||||
|
@ -44,13 +55,39 @@ class Sync extends merge(Rsync, Emitter) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultJobsDir() { this.config.set('jobsDir', this.jobsDir)}
|
// job and options processing
|
||||||
getDefaultJobsDir() { this.config.get('jobsDir', this.jobsDir) }
|
|
||||||
|
|
||||||
async listJobFiles(dir) {
|
async runJob(options) {
|
||||||
return await getFiles(['**','*.yaml','*.yml'],{ onlyfiles:true, cwd:dir || this.jobsDir})
|
await this.loadJob(options)
|
||||||
|
this.live()
|
||||||
|
this.execute(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadJob (options) {
|
||||||
|
if (typeof options ==='string') options = await this.readOptionsFile(options,'job')
|
||||||
|
if (!options.error) {
|
||||||
|
for (const option in options) {
|
||||||
|
if(option === 'optionsFile') {
|
||||||
|
let opts = await this.readOptionsFile(options.optionsFile,'options')
|
||||||
|
if (!opts.error) {
|
||||||
|
Object.keys(opts).forEach( async opt => {
|
||||||
|
await this.processOption(opt,opts[opt])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.processOption(option,options[option])
|
||||||
|
}
|
||||||
|
} // end loop
|
||||||
|
this.dry() // dry run by default must .live() .unset('n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// async listJobFiles(dir) {
|
||||||
|
// return await getFiles(['**','*.yaml','*.yml'],{ onlyfiles:true, cwd:dir || this.jobsDir})
|
||||||
|
// }
|
||||||
|
|
||||||
async readOptionsFile(filePath,type='job') {
|
async readOptionsFile(filePath,type='job') {
|
||||||
let dir = {job:this.jobsDir,options:this.optionsDir,ssh:this.sshDir}
|
let dir = {job:this.jobsDir,options:this.optionsDir,ssh:this.sshDir}
|
||||||
let [err,res] = await to(readFile(`${dir[type]}/${filePath}`))
|
let [err,res] = await to(readFile(`${dir[type]}/${filePath}`))
|
||||||
|
@ -65,13 +102,91 @@ class Sync extends merge(Rsync, Emitter) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// executes a method on the instance (might be in prototype chain) which may take a value(s)
|
||||||
|
async processOption (method, value) {
|
||||||
|
log.debug({method:method, value:value, msg:`processing option ${method} with value ${value}`})
|
||||||
|
if (typeof(this[method]) === 'function') {
|
||||||
|
await this[method](value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// methods ====================
|
||||||
|
|
||||||
live() {
|
live() {
|
||||||
this.unset('n')
|
this.unset('n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set (option=[],value) {
|
||||||
|
if (typeof option === 'string') super.set(option,value) // pass through
|
||||||
|
else {
|
||||||
|
option.forEach( opt => {
|
||||||
|
typeof opt==='string' ? super.set(opt) : super.set(...Object.entries(opt).flat())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async watch(cmd) {
|
||||||
|
// TODO make into switch ?
|
||||||
|
log.debug(`watch command ${cmd}`)
|
||||||
|
|
||||||
|
if (isPlainObject(cmd) || cmd==null || (typeof cmd==='boolean' && cmd) || cmd==='init' || cmd==='add') {
|
||||||
|
if (cmd.wait) this.debounce(cmd)
|
||||||
|
let opts = {source:this._sources, excludeFrom:this._excludeFiles, ignored:this._exclude }
|
||||||
|
await this._watcher.init(opts)
|
||||||
|
this.syncHandler = syncHandler.call(this,log)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd==='remove') {
|
||||||
|
this._watcher.removeListener('changed', this.syncHandler )
|
||||||
|
this._watcher.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd ==='on'|| cmd==='start') {
|
||||||
|
this._watcher.on('changed', this.syncHandler)
|
||||||
|
this._watcher.start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd==='off' || cmd==='stop' ) {
|
||||||
|
this._watcher.removeListener('changed', this.syncHandler)
|
||||||
|
this._watcher.stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd==='pause' ) {
|
||||||
|
this._watcher.removeListener('changed', this.syncHandler)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd==='resume') {
|
||||||
|
this._watcher.on('changed', this.syncHandler)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'changed' event handler with optional debounce wrapper
|
||||||
|
function syncHandler(log) {
|
||||||
|
function sync (change) {
|
||||||
|
log.info({file:change.file, type:change.type, msg:`file ${change.file} was ${change.type}`})
|
||||||
|
this.execute()
|
||||||
|
}
|
||||||
|
log.debug(`in sync make Handler, ${this._debounce}, ${sync}`)
|
||||||
|
if (this._debounce==null) return sync.bind(this)
|
||||||
|
return debounce(sync.bind(this),{wait:this.opts.debounce})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce(opts) {
|
||||||
|
if (opts==null) this._debounce=null
|
||||||
|
this._debounce = opts
|
||||||
|
}
|
||||||
|
|
||||||
sshu (file) {
|
sshu (file) {
|
||||||
if (file && (typeof options==='string')) {
|
if (file && (typeof options==='string')) {
|
||||||
this.shell(`"sshu -c ${file} "`)
|
this.shell(`sshu -c ${file}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,52 +219,8 @@ class Sync extends merge(Rsync, Emitter) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
set (option={},value) {
|
|
||||||
if (typeof option === 'string') super.set(option,value) // pass through
|
|
||||||
else {
|
|
||||||
// if (!Array.isArray(option)) { option = [option] }
|
|
||||||
option.forEach( opt => {
|
|
||||||
console.log('---',option,opt)
|
|
||||||
typeof opt==='string' ? super.set(opt) : super.set(...Object.entries(opt).flat())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async loadJob (options) {
|
|
||||||
if (typeof options ==='string') options = await this.readOptionsFile(options,'job')
|
|
||||||
if (!options.error) {
|
|
||||||
for (const option in options) {
|
|
||||||
if(option === 'optionsFile') {
|
|
||||||
let opts = await this.readOptionsFile(options.optionsFile,'options')
|
|
||||||
if (!opts.error) {
|
|
||||||
Object.keys(opts).forEach( opt => {
|
|
||||||
this.processOption(opt,opts[opt])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.processOption(option,options[option])
|
|
||||||
}
|
|
||||||
} // end loop
|
|
||||||
this.dry() // dry run by default must .live() .unset('n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// executes a method on the instance (might be in prototype chain) which may take a value(s)
|
|
||||||
processOption (method, value) {
|
|
||||||
if (typeof(this[method]) === 'function') {
|
|
||||||
this[method](value)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
queueJobs () {}
|
|
||||||
runJobs () {}
|
|
||||||
|
|
||||||
async execute(opts={}) {
|
async execute(opts={}) {
|
||||||
log.info({cmd:this.command(), msg:'running rsync command'})
|
// log.info({cmd:this.command(), msg:'running rsync command'})
|
||||||
const superexecute = super.execute.bind(this)
|
const superexecute = super.execute.bind(this)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let status
|
let status
|
||||||
|
@ -161,7 +232,7 @@ class Sync extends merge(Rsync, Emitter) {
|
||||||
reject ({error:err, code:code, cmd:cmd, msg:'error during sync'})
|
reject ({error:err, code:code, cmd:cmd, msg:'error during sync'})
|
||||||
}
|
}
|
||||||
if (errors) log.warn({errors: errors, cmd:cmd, msg:'sync ran but with with errors'})
|
if (errors) log.warn({errors: errors, cmd:cmd, msg:'sync ran but with with errors'})
|
||||||
log.info({cmd:cmd, status:status, msg:'sync run'})
|
log.debug({cmd:cmd, status:status, msg:'sync run'})
|
||||||
resolve({cmd:cmd, errors:errors, status:status, msg:'sync run'})
|
resolve({cmd:cmd, errors:errors, status:status, msg:'sync run'})
|
||||||
}, function(data) {
|
}, function(data) {
|
||||||
status += data.toString()
|
status += data.toString()
|
||||||
|
@ -180,11 +251,9 @@ class Sync extends merge(Rsync, Emitter) {
|
||||||
export default Sync
|
export default Sync
|
||||||
|
|
||||||
|
|
||||||
|
function isPlainObject (obj) {
|
||||||
|
return Object.prototype.toString.call(obj) === '[object Object]'
|
||||||
// function isPlainObject (obj) {
|
}
|
||||||
// return Object.prototype.toString.call(obj) === '[object Object]'
|
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
// function escapeSpaces (str) {
|
// function escapeSpaces (str) {
|
||||||
// if (typeof str === 'string') {
|
// if (typeof str === 'string') {
|
||||||
|
|
144
src/watcher.js
144
src/watcher.js
|
@ -1,41 +1,133 @@
|
||||||
|
// native imports
|
||||||
import { EventEmitter as Emitter } from 'events'
|
import { EventEmitter as Emitter } from 'events'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import chokidar from 'chokidar'
|
// third party imports
|
||||||
|
import { watch } from 'chokidar'
|
||||||
|
// local imports
|
||||||
|
import ignores from './read-lines'
|
||||||
|
// UCI imports
|
||||||
|
import logger from '@uci/logger'
|
||||||
|
let log = {}
|
||||||
|
|
||||||
|
|
||||||
|
const READY_TIMEOUT = 2000
|
||||||
|
|
||||||
class Watcher extends Emitter {
|
class Watcher extends Emitter {
|
||||||
constructor(options) {
|
constructor(opts={}) {
|
||||||
super()
|
super()
|
||||||
// pass in ignores
|
log = logger({ package:'@uci/sync', class:'Watcher', file:'src/watcher.js'})
|
||||||
const opts = {
|
opts.unlinkDir = Object.hasOwnProperty(opts.unlinkDir) ? opts.unlinkDir : true
|
||||||
ignored: '**/node_modules/**',
|
this.opts = opts
|
||||||
ignoreInitial: true
|
this._ignored = []
|
||||||
}
|
this._ready=false
|
||||||
const watcher = chokidar.watch(options.source, opts)
|
this.watching=false
|
||||||
this.watcher = watcher
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
async init(opts) {
|
||||||
const handler = (type, f) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const fname = path.basename(f)
|
opts = opts || this.opts
|
||||||
if ( fname.toLowerCase() === 'package.json')
|
if (opts.excludeFrom) await this.getIgnoreLists(opts.excludeFrom)
|
||||||
if (type !=='change') {
|
if (opts.ignoreList) await this.getIgnoreLists(opts.ignoreList)
|
||||||
this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall'))
|
if (opts.source) {
|
||||||
return
|
opts.ignored = opts.ignored ? [...this._ignored,...opts.ignored] : this._ignored
|
||||||
} else{
|
log.debug({ignored:opts.ignored, msg:'all ignores'})
|
||||||
this.emit('install', f)
|
this._watcher = watch(opts.source,opts)
|
||||||
}
|
this._watcher.on('error', error => {
|
||||||
this.emit('sync', f)
|
log.error({error:error, msg:'Watcher error'})
|
||||||
} // end handler
|
})
|
||||||
this.watcher
|
this._watcher.on('ready', () => {
|
||||||
.on('add', handler.bind(this, 'add'))
|
clearTimeout(readyTimeout)
|
||||||
.on('change', handler.bind(this, 'change'))
|
log.info('initial scan sucessful, ready to start')
|
||||||
.on('unlink', handler.bind(this, 'remove'))
|
this._ready=true
|
||||||
|
this.opts = opts // save options
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
let readyTimeout = setTimeout(() =>{
|
||||||
|
log.fatal({options:opts, timeout:READY_TIMEOUT, msg:'Timeout: unabled to complete initial scan'})
|
||||||
|
reject('timeout during intial scan')
|
||||||
|
},READY_TIMEOUT)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.fatal('MUST provide a source directory(s) option to watch')
|
||||||
|
reject('no source provided')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async start(opts) {
|
||||||
|
if(this.watching) {
|
||||||
|
log.warn(`watching aleady running for ${this.opts.source}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!this._watcher) await this.init(opts)
|
||||||
|
if (this._ready) {
|
||||||
|
log.info(`now watching ${this.opts.source}`)
|
||||||
|
this._watcher.removeAllListeners() // just in case
|
||||||
|
this.watching = true
|
||||||
|
// define command listen handler
|
||||||
|
const handler =
|
||||||
|
(type, f) => {
|
||||||
|
log.debug(`file ${f} was ${type}`)
|
||||||
|
// convert this to a plugin/hook so it's not specific
|
||||||
|
const fname = path.basename(f)
|
||||||
|
if ( fname.toLowerCase() === 'package.json')
|
||||||
|
if (type !=='modified') {
|
||||||
|
this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall'))
|
||||||
|
return
|
||||||
|
} else{
|
||||||
|
this.emit('install', f)
|
||||||
|
}
|
||||||
|
// user might want to run debounce on the listener for this event
|
||||||
|
this.emit('changed', {file:f, type:type})
|
||||||
|
} // end handler
|
||||||
|
|
||||||
|
this._watcher
|
||||||
|
.on('add', handler.bind(this, 'added'))
|
||||||
|
.on('change', handler.bind(this, 'modified'))
|
||||||
|
.on('unlink', handler.bind(this, 'removed'))
|
||||||
|
if(this.opts.unlinkDir) this._watcher.on('unlinkDir', handler.bind(this, 'dir-deleted'))
|
||||||
|
if(this.opts.addDir) this._watcher.on('addDir', handler.bind(this, 'dir-added'))
|
||||||
|
} else {
|
||||||
|
log.warn('watcher is not ready to start, check options and try again')
|
||||||
|
return new Error('not ready to start check configuration')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.watcher.close()
|
if(this.watching) {
|
||||||
|
this.watching = false
|
||||||
|
this._watcher.close()
|
||||||
|
}
|
||||||
|
else log.warn('not watching, nothing to close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.stop()
|
||||||
|
delete(this._watcher)
|
||||||
|
this._ready=false
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart(opts) {
|
||||||
|
this.remove()
|
||||||
|
await this.start(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIgnoreLists(lists) {
|
||||||
|
// console.log('ignore lists', lists)
|
||||||
|
if (typeof lists === 'string') lists=[lists]
|
||||||
|
let ignored = await ignores(lists)
|
||||||
|
// console.log('==ignores from lists', ignored)
|
||||||
|
this._ignored = [...this._ignored,...ignored]
|
||||||
|
// console.log(this._ignored)
|
||||||
|
}
|
||||||
|
|
||||||
|
addIgnore(ignore) {
|
||||||
|
Array.isArray(ignore) ? this._ignored.push(...ignore) : this._ignored.push(ignore)
|
||||||
|
}
|
||||||
|
clearAllIgnore() { this._ignored = [] }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Watcher
|
export default Watcher
|
||||||
|
|
|
@ -3,6 +3,11 @@ destination: ./test/dest
|
||||||
flags: 'av'
|
flags: 'av'
|
||||||
excludeFrom:
|
excludeFrom:
|
||||||
- ./test/repo/.gitignore
|
- ./test/repo/.gitignore
|
||||||
|
- ./test/repo/.npmignore
|
||||||
|
exclude:
|
||||||
|
- .gitignore
|
||||||
|
- .npmignore
|
||||||
set:
|
set:
|
||||||
- delete
|
- delete
|
||||||
- delete-excluded
|
- delete-excluded
|
||||||
|
watch:
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
// let ignoreFiles = ['.npmignore','.gitignore']
|
||||||
|
|
||||||
|
import { readLines, writeLines } from '../src/read-lines'
|
||||||
|
import chai from 'chai'
|
||||||
|
import assertArrays from 'chai-arrays'
|
||||||
|
import { it } from 'mocha'
|
||||||
|
|
||||||
|
chai.use(assertArrays)
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe (
|
||||||
|
'Read a File of Lines to Array and vice versa',
|
||||||
|
function () {
|
||||||
|
readList()
|
||||||
|
writeList()
|
||||||
|
})
|
||||||
|
|
||||||
|
//****************** TESTS **********************
|
||||||
|
function readList() {
|
||||||
|
it('==> can read one or more files (no order) each line as element in an array with common directory', async function () {
|
||||||
|
const shouldbe = [ 'tests/',
|
||||||
|
'test/',
|
||||||
|
'*.test.js',
|
||||||
|
'testing/',
|
||||||
|
'example/',
|
||||||
|
'/node_modules/',
|
||||||
|
'/coverage/' ]
|
||||||
|
let result = await readLines(['.gitignore','.npmignore'],__dirname+'/repo')
|
||||||
|
expect(result, 'list build failed').to.be.containingAllOf(shouldbe)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('==> can read two files one relative the other absolute', async function () {
|
||||||
|
const shouldbe = [ 'tests/',
|
||||||
|
'test/',
|
||||||
|
'*.test.js',
|
||||||
|
'testing/',
|
||||||
|
'example/',
|
||||||
|
'/node_modules/',
|
||||||
|
'/coverage/' ]
|
||||||
|
let result = await readLines(['./test/repo/.gitignore',__dirname+'/repo/.npmignore'])
|
||||||
|
expect(result, 'list build failed').to.be.containingAllOf(shouldbe)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function writeList() {
|
||||||
|
|
||||||
|
it('==> can write an array items as lines in a file', async function () {
|
||||||
|
const shouldbe = await readLines(['.gitignore','.npmignore'],__dirname+'/repo')
|
||||||
|
await writeLines(__dirname+'/repo/combined.list',shouldbe)
|
||||||
|
const result = await readLines(['combined.list'],__dirname+'/repo')
|
||||||
|
expect(result, 'list build failed').to.be.containingAllOf(shouldbe)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,2 +1,2 @@
|
||||||
node_modules/
|
**/node_modules/
|
||||||
coverage/
|
coverage/
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('Sync Class Testing ',async ()=> {
|
||||||
// // sync.execute({cli:true})
|
// // sync.execute({cli:true})
|
||||||
// sync.live()
|
// sync.live()
|
||||||
// sync.execute({cli:true})
|
// sync.execute({cli:true})
|
||||||
await sync.loadJob('sample')
|
await sync.loadJob('local')
|
||||||
console.log('command to be run',sync.command())
|
console.log('command to be run',sync.command())
|
||||||
sync.live()
|
sync.live()
|
||||||
sync.execute({cli:true})
|
sync.execute({cli:true})
|
|
@ -0,0 +1,71 @@
|
||||||
|
import Sync from '../src/sync'
|
||||||
|
import Watcher from '../src/watcher'
|
||||||
|
import to from 'await-to-js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { it } from 'mocha'
|
||||||
|
import debounce from 'debounce-fn'
|
||||||
|
import logger from '@uci/logger'
|
||||||
|
// pause = require('@uci/utils').pPause
|
||||||
|
|
||||||
|
describe('Watcher Class Testing ',async ()=> {
|
||||||
|
|
||||||
|
// let log = logger({})
|
||||||
|
before(async () => {
|
||||||
|
// log = logger({ package:'@uci/sync', id: 'sync-test' })
|
||||||
|
let sync = new Sync()
|
||||||
|
// await sync.loadJob('local')
|
||||||
|
// console.log('command to be run',sync.command())
|
||||||
|
// console.log('===',sync.cwd())
|
||||||
|
// // sync.execute({cli:true})
|
||||||
|
await sync.loadJob('local')
|
||||||
|
sync.live()
|
||||||
|
let options = await sync.readOptionsFile('local')
|
||||||
|
let watcher = new Watcher(options)
|
||||||
|
await watcher.start()
|
||||||
|
watcher.on('changed',
|
||||||
|
// debounce(
|
||||||
|
(change) => {
|
||||||
|
console.log(`======= file ${change.file} was ${change.type} ==========`)
|
||||||
|
sync.execute({cli:true})
|
||||||
|
}
|
||||||
|
// ,{wait:100, immediate:true})
|
||||||
|
)
|
||||||
|
|
||||||
|
// console.log('command to be run',sync.command())
|
||||||
|
// sync.live()
|
||||||
|
// sync.execute({cli:true})
|
||||||
|
|
||||||
|
// await sync.configure('./test/config/sync')
|
||||||
|
// log.info({cmd:sync.command(), msg:'Rsync Command that will Run'})
|
||||||
|
// log.info(`making connection to ${remote.opts.host}`)
|
||||||
|
// log.info('ready for testing')
|
||||||
|
})
|
||||||
|
|
||||||
|
// after(async () => {
|
||||||
|
// remote.close()
|
||||||
|
// })
|
||||||
|
|
||||||
|
it('can sync a directory' , async function () {
|
||||||
|
// let [err,res] = await to(remote.exec('cd /opt && pwd'))
|
||||||
|
// if (err) {
|
||||||
|
// log.info('error running command aborting test', err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// log.info(`result of remote command ${res.command} => ${res.reply.toString().trim()}`)
|
||||||
|
expect('test', 'test failed').to.equal('/opt')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// function hooks(remote) {
|
||||||
|
//
|
||||||
|
|
||||||
|
// // beforeEach(async() => {
|
||||||
|
// // await someasyncfunctiontodobeforeeachtest()
|
||||||
|
// // })
|
||||||
|
//
|
||||||
|
// // after(async() => {
|
||||||
|
// // await someasyncfunctiontodoaftereeachtest()
|
||||||
|
// // })
|
||||||
|
//
|
||||||
|
// }
|
Loading…
Reference in New Issue