node-flywaydb
Version:
NodeJs wrapper for flywaydb cli
279 lines (234 loc) • 10.7 kB
JavaScript
const path = require('path');
const fs = require('fs');
const os = require('os');
const {parse: parseUrl} = require('url');
const parseXmlString = require('xml2js').parseString;
const extractZip = require('extract-zip');
const spawn = require('child_process').spawn;
const rimraf = require('rimraf');
const ONE_DAY_MS = 8.64e+7;
const NEVER_EXPIRE_DOWNLOADS = -1;
function createProxyAgent(protocol, env) {
if (protocol === 'http:') {
const proxy = env.npm_config_proxy ||
env.npm_config_http_proxy ||
env.HTTP_PROXY ||
env.http_proxy ||
env.npm_config_proxy;
if (proxy) {
const HttpProxyAgent = require('http-proxy-agent');
return new HttpProxyAgent(proxy);
}
} else if (protocol === 'https:') {
const proxy = env.npm_config_https_proxy ||
env.HTTPS_PROXY ||
env.https_proxy ||
env.npm_config_proxy;
if (proxy) {
const HttpsProxyAgent = require('https-proxy-agent');
return new HttpsProxyAgent(proxy);
}
}
}
function createRequest(options, agent, callback) {
var client;
if (options.protocol === 'http:') {
client = require('http');
if (!options.port) {
options.port = 80;
}
} else if (options.protocol === 'https:') {
client = require('https');
if (!options.port) {
options.port = 443;
}
} else {
return false;
}
options.method = 'GET';
options.agent = agent;
const req = client.request(options, callback);
req.end();
return true;
}
function saveCachedUrlToPath(destinationPath, url, downloadExpirationTimeMs) {
const expirationTime = downloadExpirationTimeMs || ONE_DAY_MS;
const stats = fs.existsSync(destinationPath) ? fs.statSync(destinationPath) : null;
const useCachedVersion = stats && (expirationTime === NEVER_EXPIRE_DOWNLOADS || (Date.now() - stats.mtimeMs < expirationTime));
if(useCachedVersion) {
return Promise.resolve(destinationPath);
}
console.log('DOWNLOADING', url);
return new Promise(function(resolve, reject) {
const options = parseUrl(url);
// Only returns an agent if the proxy env is provided.
const agent = createProxyAgent(options.protocol, process.env);
const requestSent = createRequest(options, agent, function(fileRes) {
if (fileRes.statusCode !== 200) {
const err = new Error('Request failed for ' + url + ' - ' + fileRes.statusCode);
err.statusCode = fileRes.statusCode;
err.type = 'HTTP_ERROR';
reject(err);
} else {
const fileWriter = fs.createWriteStream(destinationPath);
fileRes.on('error', reject);
fileRes.on('end', function() {
fileWriter.end(function() {
resolve(destinationPath);
});
});
fileRes.pipe(fileWriter);
}
});
if (!requestSent) {
reject(new Error('Unsupported download protocol'));
}
});
}
function nodePlatformToMavenSuffix() {
return ({
'win32': 'windows-x64.zip',
'linux': 'linux-x64.tar.gz',
'darwin': 'macosx-x64.tar.gz'
})[os.platform()];
}
function resolveMavenVersion(libDir, groupId, artifactId, version, downloadExpirationTimeMs) {
if(version && version !== 'latest') {
return Promise.resolve(version);
} else {
const latestCacheFile = path.join(libDir, `${groupId}_${artifactId}.latest`);
const xmlReqeust = saveCachedUrlToPath(latestCacheFile, `https://repo1.maven.org/maven2/${groupId.replace(/\./g, '/')}/${artifactId}/maven-metadata.xml`, downloadExpirationTimeMs);
return xmlReqeust
.then(function(manifestFilePath) {
return new Promise(function(resolve, reject) {
const manifestContent = fs.readFileSync(manifestFilePath, { encoding: 'utf8' });
parseXmlString(manifestContent, function(err, result) {
if(err) {
reject(err);
} else {
const nonTestVersions = result.metadata.versioning[0].versions[0].version.filter(function(version) { return version.match(/^[1-9]\.[0-9.]+$/); });
if(!nonTestVersions.length) {
reject(new Error(`Stable version of ${groupId}_${artifactId} not found`));
} else {
resolve(nonTestVersions[nonTestVersions.length - 1]);
}
}
});
});
});
}
}
function downloadMaven(libDir, groupId, artifactId, version, downloadExpirationTimeMs) {
return resolveMavenVersion(libDir, groupId, artifactId, version, downloadExpirationTimeMs)
.then(function(version) {
if(version.match(/^https/)) {
const flywaySavePath = path.join(libDir, path.basename(version));
return saveCachedUrlToPath(flywaySavePath, version, downloadExpirationTimeMs)
.then(function (fileSavePath) {
return {
version,
type: 'asset',
file: fileSavePath,
};
});
} else if(artifactId === 'flyway-commandline') {
const platformSuffix = nodePlatformToMavenSuffix();
const flywayUrl = `https://repo1.maven.org/maven2/${groupId.replace(/\./g, '/')}/${artifactId}/${version}/${artifactId}-${version}-${platformSuffix}`;
const flywaySavePath = path.join(libDir, `${artifactId}-${version}-${platformSuffix}`);
return saveCachedUrlToPath(flywaySavePath, flywayUrl, downloadExpirationTimeMs)
.then(function (fileSavePath) {
return {
version,
type: 'command',
file: fileSavePath,
};
});
} else {
// Assume non-flyway dependencies are simple jar files
const depUrl = `https://repo1.maven.org/maven2/${groupId.replace(/\./g, '/')}/${artifactId}/${version}/${artifactId}-${version}.jar`;
const depSavePath = path.join(libDir, `${artifactId}-${version}.jar`);
return saveCachedUrlToPath(depSavePath, depUrl, downloadExpirationTimeMs)
.then(function (fileSavePath) {
return {
version,
type: 'asset',
file: fileSavePath,
};
});
}
})
.then(function({ version, type, file }) {
const extractDir = path.join(libDir, `${artifactId}-${version}`);
const fileExt = path.extname(file);
if(fileExt === '.zip' || fileExt === '.gz' || fileExt === '.xz') {
if(fs.existsSync(extractDir)) {
return { version, type, dir: extractDir };
} else {
fs.mkdirSync(extractDir);
if(fileExt === '.zip') {
return new Promise(function(res, rej) {
extractZip(file, { dir: extractDir }, function(err) {
if(err) {
rimraf.sync(extractDir);
rej(err);
} else {
res({ version, type, dir: extractDir });
}
});
});
} else {
return new Promise(function(res, rej) {
spawn('tar', ['zxf', file], {
cwd: extractDir,
stdio: 'inherit'
}).on('close', function(code) {
if(code === 0) {
res({ version, type, dir: extractDir });
} else {
rimraf.sync(extractDir);
rej(new Error('Untaring file failed ' + code));
}
});
});
}
}
} else {
return { version, type, file };
}
});
}
function ensureWritableLibDir(libDir) {
if(!libDir) {
libDir = path.resolve(__dirname, '../jlib');
} else if(!path.isAbsolute(libDir)) {
libDir = path.resolve(libDir);
}
if(!fs.existsSync(libDir)) {
fs.mkdirSync(libDir);
} else {
fs.accessSync(libDir, fs.constants.W_OK);
}
return libDir;
}
module.exports = {
ensureArtifacts: function(config, callback) {
const libDir = ensureWritableLibDir(config.downloads && config.downloads.storageDirectory);
const downloadExpirationTimeMs = config.downloads && config.downloads.expirationTimeInMs;
var pendingDownloads = [downloadMaven(libDir, 'org.flywaydb', 'flyway-commandline', config.downloadUrl || config.version, downloadExpirationTimeMs)];
// Checking config.mavinPlugins to ensure backwards compatibility with older versions of node-flyway that defined it as 'config.mavinPlugins' and not 'config.MavenPlugins'
if(config.mavenPlugins || config.mavinPlugins) {
const plugins = config.mavinPlugins === undefined ? config.mavenPlugins : config.mavinPlugins;
pendingDownloads = pendingDownloads.concat(plugins.map(function(plugin) {
return downloadMaven(libDir, plugin.groupId, plugin.artifactId, plugin.downloadUrl || plugin.version, downloadExpirationTimeMs);
}));
}
Promise.all(pendingDownloads)
.then(function(assets) {
const binFile = os.platform() === 'win32'
? 'flyway.cmd'
: 'flyway';
callback(null, path.join(assets[0].dir, `flyway-${assets[0].version}`, binFile));
})
.catch(callback);
}
};