|
|
|
import getFiles from './get-files';
|
|
|
|
import hash from './hash';
|
|
|
|
import retry from './retry';
|
|
|
|
import Agent from './agent';
|
|
|
|
import EventEmitter from 'events';
|
|
|
|
import { resolve } from 'path';
|
|
|
|
import { stat, readFile } from 'fs-promise';
|
|
|
|
|
|
|
|
export default class Now extends EventEmitter {
|
|
|
|
constructor (token, { forceNew = false, debug = false }) {
|
|
|
|
super();
|
|
|
|
this._token = token;
|
|
|
|
this._debug = debug;
|
|
|
|
this._forceNew = forceNew;
|
|
|
|
this._agent = new Agent('api.now.sh', { debug });
|
|
|
|
this._onRetry = this._onRetry.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
async create (path, { forceNew }) {
|
|
|
|
this._path = path;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await stat(path);
|
|
|
|
} catch (err) {
|
|
|
|
throw new Error(`Could not read directory ${path}.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
let pkg;
|
|
|
|
try {
|
|
|
|
pkg = await readFile(resolve(path, 'package.json'));
|
|
|
|
pkg = JSON.parse(pkg);
|
|
|
|
} catch (err) {
|
|
|
|
throw new Error(`Failed to read JSON in "${path}/package.json"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._debug) console.time('> [debug] Getting files');
|
|
|
|
const files = await getFiles(path, pkg);
|
|
|
|
if (this._debug) console.timeEnd('> [debug] Getting files');
|
|
|
|
|
|
|
|
if (this._debug) console.time('> [debug] Computing hashes');
|
|
|
|
const hashes = await hash(files);
|
|
|
|
if (this._debug) console.timeEnd('> [debug] Computing hashes');
|
|
|
|
|
|
|
|
this._files = hashes;
|
|
|
|
|
|
|
|
const deployment = await retry(async (bail) => {
|
|
|
|
const res = await this._fetch('/create', {
|
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
forceNew,
|
|
|
|
files: Array.from(this._files).map(([sha, { data }]) => {
|
|
|
|
return {
|
|
|
|
sha,
|
|
|
|
size: Buffer.byteLength(data)
|
|
|
|
};
|
|
|
|
})
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// no retry on 403
|
|
|
|
if (403 === res.status) {
|
|
|
|
if (this._debug) {
|
|
|
|
console.log('> [debug] bailing on creating due to 403');
|
|
|
|
}
|
|
|
|
return bail(responseError(res));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (200 !== res.status) {
|
|
|
|
throw new Error('Deployment initialization failed');
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.json();
|
|
|
|
}, { retries: 3, minTimeout: 2500, onRetry: this._onRetry });
|
|
|
|
|
|
|
|
this._id = deployment.deploymentId;
|
|
|
|
this._url = deployment.url;
|
|
|
|
this._missing = deployment.missing || [];
|
|
|
|
|
|
|
|
return this._url;
|
|
|
|
}
|
|
|
|
|
|
|
|
upload () {
|
|
|
|
Promise.all(this._missing.map((sha) => retry(async (bail) => {
|
|
|
|
const file = this._files.get(sha);
|
|
|
|
const { data, name } = file;
|
|
|
|
const res = await this._fetch('/sync', {
|
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
sha,
|
|
|
|
data: data.toString(),
|
|
|
|
file: toRelative(name, this._path),
|
|
|
|
deploymentId: this._id
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// no retry on 403
|
|
|
|
if (403 === res.status) {
|
|
|
|
if (this._debug) console.log('> [debug] bailing on creating due to 403');
|
|
|
|
return bail(responseError(res));
|
|
|
|
}
|
|
|
|
|
|
|
|
this.emit('upload', file);
|
|
|
|
}, { retries: 5, randomize: true, onRetry: this._onRetry })))
|
|
|
|
.then(() => this.emit('complete'))
|
|
|
|
.catch((err) => this.emit('error', err));
|
|
|
|
}
|
|
|
|
|
|
|
|
_onRetry (err) {
|
|
|
|
if (this._debug) {
|
|
|
|
console.log(`> [debug] Retrying: ${err.stack}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
close () {
|
|
|
|
this._agent.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
get url () {
|
|
|
|
return this._url;
|
|
|
|
}
|
|
|
|
|
|
|
|
get syncAmount () {
|
|
|
|
if (!this._syncAmount) {
|
|
|
|
this._syncAmount = this._missing
|
|
|
|
.map((sha) => Buffer.byteLength(this._files.get(sha).data))
|
|
|
|
.reduce((a, b) => a + b, 0);
|
|
|
|
}
|
|
|
|
return this._syncAmount;
|
|
|
|
}
|
|
|
|
|
|
|
|
async _fetch (url, opts) {
|
|
|
|
opts.headers = opts.headers || {};
|
|
|
|
opts.headers.authorization = `Bearer ${this._token}`;
|
|
|
|
return await this._agent.fetch(url, opts);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function toRelative (path, base) {
|
|
|
|
const fullBase = /\/$/.test(base) ? base + '/' : base;
|
|
|
|
const relative = path.substr(fullBase.length);
|
|
|
|
if (relative.startsWith('/')) return relative.substr(1);
|
|
|
|
return relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
function responseError (res) {
|
|
|
|
const err = new Error('Response error');
|
|
|
|
err.status = res.status;
|
|
|
|
return err;
|
|
|
|
}
|