const { Injectable } = require('flitter-di') const moment = require('moment') class UniversalPath extends Injectable { static get services() { return [...super.services, 'hosts'] } static PATH_TYPE_FILE = Symbol('file') static PATH_TYPE_DIRECTORY = Symbol('directory') static PATH_TYPE_UNKNOWN = Symbol('unknown') _directory_classify_command = 'test -d' _file_classify_command = 'test -f' _datetime_format = 'Y-MM-DD_HH-mm-ss-SSSS' static fromString(path_string) { const parts = path_string.split('::') const hostname = parts.length > 1 ? parts[0] : 'localhost' const path = parts.length > 1 ? parts[1] : parts[0] const host = this.prototype.hosts.get(hostname) return new this(host, path) } constructor(host, host_path, type) { super() this._host = host this._path = host_path this._type = type ? type : this.constructor.PATH_TYPE_UNKNOWN } get host() { return this._host } get path() { return this._path } get type() { return this._type } async resolve() { return this._host.resolve_path(this._path) } is_directory() { return this.type === this.constructor.PATH_TYPE_DIRECTORY } is_file() { return this.type === this.constructor.PATH_TYPE_FILE } is_valid() { return this.is_file() || this.is_directory() } async classify() { const dir_result = await this._host.execute(`${this._directory_classify_command} ${this._path}`) if ( dir_result.exit_code === 0 ) { this._type = this.constructor.PATH_TYPE_DIRECTORY return } const file_result = await this._host.execute(`${this._file_classify_command} ${this._path}`) if ( file_result.exit_code === 0 ) { this._type = this.constructor.PATH_TYPE_FILE return } this._type = this.constructor.PATH_TYPE_UNKNOWN } async open_read_stream() { await this.classify() if ( this._type === this.constructor.PATH_TYPE_FILE ) { return this._host.open_file_read_stream(this._path) } else { throw new Error('Maestro does not support generation of read streams for this type of path: '+this._type.description) } } async open_write_stream() { await this.classify() if ( [this.constructor.PATH_TYPE_FILE, this.constructor.PATH_TYPE_UNKNOWN].includes(this._type) ) { return this._host.open_file_write_stream(this._path) } else { throw new Error('Maestro does not support generation of write streams for this type of path: '+this._type.description) } } async copy_from(other_path) { const read = await other_path.open_read_stream() const write = await this.open_write_stream() await read.pipe(write) } async copy_to(other_path) { const read = await this.open_read_stream() const write = await other_path.open_write_stream() await read.pipe(write) } async unlink() { await this.host.delete_path(this._path) } async move_from(other_path) { await this.copy_from(other_path) await other_path.unlink() } async move_to(other_path) { await this.copy_to(other_path) await this.unlink() } async touch(directory = false) { await this.classify() if ( !directory ) { const base_dir = this._path.split('/').slice(0, -1).join('/') await this._host.run(`mkdir -p ${base_dir}`) await this._host.run(`touch ${this._path}`) } else { await this._host.run(`mkdir -p ${this._path}`) } } async echo(text) { await this.classify() if ( this.is_directory() ) { throw new Error('Cannot echo to a directory.') } if ( !this.is_valid() ) { await this.touch() } const ws = await this.open_write_stream() ws.write(text) return new Promise(resolve => { ws.on('finish', () => resolve()) ws.end() }) } async hash() { const line = await this._host.run_line_result(`sha256sum ${this._path}`) return line.split(' ')[0] } async backup() { const now = moment().format(this._datetime_format) const backup_path = `${this}.maestro-backup-${now}` const backup_target = this.constructor.fromString(backup_path) await this.copy_to(backup_target) } toString() { return `${this._host.name}::${this._path}` } } module.exports = exports = UniversalPath