Skip to content

Instantly share code, notes, and snippets.

@TimWolla
Created September 5, 2013 20:38
Show Gist options
  • Save TimWolla/6455822 to your computer and use it in GitHub Desktop.
Save TimWolla/6455822 to your computer and use it in GitHub Desktop.
var app, async, config, e, express, filename, fs, getPackageXml, lastUpdate, logger, packageList, path, readPackages, serverVersion, tar, updateTime, updateTimeout, updateWatcher, updating, watcher, writer, xml;
serverVersion = '1.0.0';
express = require('express');
fs = require('fs');
path = require('path');
async = require('async');
tar = require('tar');
xml = {
parser: (require('xml2js')).Parser,
writer: require('xml-writer')
};
logger = new (require('caterpillar')).Logger({
level: 6
});
((logger.pipe(new (require('caterpillar-filter').Filter))).pipe(new (require('caterpillar-human').Human))).pipe(process.stdout);
try {
filename = "" + __dirname + "/config.js";
if (process.argv[2]) {
if (process.argv[2].substring(0, 1) === '/') {
filename = process.argv[2];
} else {
filename = "" + __dirname + "/../" + process.argv[2];
}
}
filename = fs.realpathSync(filename);
logger.log("info", "Using config '" + filename + "'");
config = require(filename);
} catch (_error) {
e = _error;
logger.log("warn", e.message);
config = {};
}
if (config.port == null) {
config.port = 9001;
}
if (config.packageFolder == null) {
config.packageFolder = "" + __dirname + "/packages/";
}
if (!/\/$/.test(config.packageFolder)) {
config.packageFolder += '/';
}
config.enableManualUpdate = true;
app = express();
app.disable('x-powered-by');
app.use(express.compress());
packageList = {};
updating = false;
updateTimeout = null;
writer = null;
lastUpdate = new Date;
updateTime = 0;
watcher = [];
updateWatcher = function() {
async.each(watcher, function(obj, callback) {
obj.close();
return callback();
});
watcher = [];
watcher.push(fs.watch(config.packageFolder, function(event, filename) {
logger.log("note", "The package folder was changed (" + config.packageFolder + filename + ": " + event + ")");
if (updateTimeout != null) {
clearTimeout(updateTimeout);
}
return updateTimeout = setTimeout(readPackages, 1e3);
}));
return fs.readdir(config.packageFolder, function(err, files) {
return async.each(files, function(file, callback) {
return watcher.push(fs.watch(config.packageFolder + file, function(event, filename) {
logger.log("note", "The package folder was changed (" + config.packageFolder + file + "/" + filename + ": " + event + ")");
if (updateTimeout != null) {
clearTimeout(updateTimeout);
}
return updateTimeout = setTimeout(readPackages, 1e3);
}));
});
});
};
getPackageXml = function(filename, callback) {
var packageXmlFound, stream, tarStream;
stream = fs.createReadStream(filename);
tarStream = stream.pipe(tar.Parse());
packageXmlFound = false;
stream.on('end', function() {
if (!packageXmlFound) {
return callback("package.xml is missing in " + filename, null);
}
});
return tarStream.on('entry', function(e) {
var packageXml;
if (e.props.path !== 'package.xml') {
return;
}
packageXmlFound = true;
packageXml = '';
e.on('data', function(chunk) {
return packageXml += chunk.toString();
});
return e.on('end', function() {
return (new xml.parser()).parseString(packageXml, function(err, contents) {
if (err != null) {
callback("Error parsing package.xml of " + filename + ": " + err, null);
}
return callback(null, contents);
});
});
});
};
readPackages = function(callback) {
var updateStart;
if (updating) {
return;
}
updating = true;
updateStart = (new Date).getTime();
return fs.readdir(config.packageFolder, function(err, files) {
var newPackageList;
logger.log("info", "Starting update");
if (err != null) {
logger.log("crit", err);
return;
}
newPackageList = {};
return async.eachSeries(files, function(file, fileCallback) {
var packageFolder;
if (file === '.gitignore') {
fileCallback(null);
return;
}
packageFolder = config.packageFolder + file;
logger.log("debug", "Parsing " + packageFolder);
return fs.stat(packageFolder, function(err, packageFolderStat) {
var currentPackage, latest, parsingFinished;
if (err != null) {
fileCallback(err);
return;
}
if (!packageFolderStat.isDirectory()) {
logger.log("warn", "" + packageFolder + " is not a folder");
return;
}
latest = packageFolder + '/latest';
logger.log("debug", "Parsing " + latest);
currentPackage = {};
parsingFinished = function() {
newPackageList[currentPackage.name] = currentPackage;
return fileCallback(null);
};
return fs.exists(latest, function(latestExists) {
if (!latestExists) {
logger.log("warn", "" + latest + " is missing");
fileCallback(null);
return;
}
return getPackageXml(latest, function(err, latestPackageXml) {
if (err != null) {
fileCallback("Error parsing package.xml of " + latest + ": " + err);
return;
}
logger.log("debug", "Finished parsing " + latest);
currentPackage.name = latestPackageXml["package"].$.name;
if (currentPackage.name !== path.basename(packageFolder)) {
fileCallback("package name does not match folder (" + currentPackage.name + " != " + (path.basename(packageFolder)) + ")");
return;
}
currentPackage.packageinformation = latestPackageXml["package"].packageinformation[0];
currentPackage.authorinformation = latestPackageXml["package"].authorinformation[0];
currentPackage.versions = {};
return fs.readdir(packageFolder, function(err, versions) {
if (err != null) {
fileCallback(err);
return;
}
return async.eachSeries(versions, function(versionFile, versionsCallback) {
if (versionFile === 'latest') {
versionsCallback(null);
return;
}
versionFile = packageFolder + '/' + versionFile;
logger.log("debug", "Parsing " + versionFile);
return fs.stat(versionFile, function(err, versionFileStat) {
if (err != null) {
versionsCallback(versionFileStat);
return;
}
if (!versionFileStat.isFile()) {
logger.log("warn", "" + versionFile + " is not a file");
return;
}
return getPackageXml(versionFile, function(err, versionPackageXml) {
var currentVersion, instruction, versionNumber, _ref;
if (err != null) {
versionsCallback(err);
return;
}
versionNumber = versionPackageXml["package"].packageinformation[0].version[0];
currentVersion = {};
currentVersion.versionnumber = versionNumber;
currentVersion.license = (_ref = versionPackageXml["package"].packageinformation[0].license) != null ? _ref[0] : void 0;
if ((currentVersion.versionnumber.replace(new RegExp(' ', 'g'), '_')) !== path.basename(versionFile, '.tar')) {
fileCallback("version number does not match file (" + (currentVersion.versionnumber.replace(' ', '_')) + " != " + (path.basename(versionFile, '.tar')) + ")");
return;
}
currentVersion.fromversions = (function() {
var _i, _len, _ref1, _results;
_ref1 = versionPackageXml["package"].instructions;
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
instruction = _ref1[_i];
if (instruction.$.fromversion != null) {
_results.push(instruction.$.fromversion);
}
}
return _results;
})();
currentVersion.requiredpackages = versionPackageXml["package"].requiredpackages[0];
currentVersion.timestamp = versionFileStat.mtime;
currentPackage.versions[versionNumber] = currentVersion;
logger.log("debug", "Finished parsing " + versionFile);
return versionsCallback(null);
});
});
}, function(err) {
if (err != null) {
fileCallback(err);
return;
}
return parsingFinished();
});
});
});
});
});
}, function(err) {
updateWatcher();
if (err != null) {
logger.log("crit", "Error reading package list: " + err);
updating = false;
if (callback != null) {
callback(false);
}
return;
}
packageList = newPackageList;
writer = null;
lastUpdate = new Date;
updateTime = ((lastUpdate.getTime()) - updateStart) / 1e3;
logger.log("info", "Finished update");
updating = false;
if (callback != null) {
return callback(true);
}
});
});
};
app.all('/', function(req, res) {
var end, fromVersion, host, packageName, requiredPackage, result, start, version, versionNumber, _i, _j, _len, _len1, _package, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
host = (_ref = config.basePath) != null ? _ref : "" + req.protocol + "://" + (req.header('host'));
if (((_ref1 = req.query) != null ? _ref1.packageName : void 0) != null) {
if (req.query.packageVersion != null) {
res.redirect(301, "" + host + "/" + req.query.packageName + "/" + (req.query.packageVersion.replace(new RegExp(' ', 'g'), '_')));
} else {
res.redirect(301, "" + host + "/" + req.query.packageName);
}
return;
}
req.accepts('xml');
res.type('xml');
if (writer == null) {
start = (new Date).getTime();
writer = new xml.writer(true);
writer.startDocument('1.0', 'UTF-8');
writer.startElement('section');
writer.writeAttribute('name', 'packages');
writer.writeAttribute('xmlns', 'http://www.woltlab.com');
writer.writeAttributeNS('xmlns', 'xsi', 'http://www.w3.org/2001/XMLSchema-instance');
writer.writeAttributeNS('xsi', 'schemaLocation', 'http://www.woltlab.com https://www.woltlab.com/XSD/packageUpdateServer.xsd');
for (packageName in packageList) {
_package = packageList[packageName];
writer.startElement('package');
writer.writeAttribute('name', _package.name);
writer.startElement('packageinformation');
writer.writeElement('packagename', (_ref2 = _package.packageinformation.packagename[0]._) != null ? _ref2 : _package.packageinformation.packagename[0]);
writer.writeElement('packagedescription', (_ref3 = _package.packageinformation.packagedescription[0]._) != null ? _ref3 : _package.packageinformation.packagedescription[0]);
writer.writeElement('isapplication', String((_ref4 = _package.packageinformation.isapplication) != null ? _ref4 : 0));
writer.endElement();
if (_package.authorinformation != null) {
writer.startElement('authorinformation');
if (_package.authorinformation.author[0] != null) {
writer.writeElement('author', _package.authorinformation.author[0]);
}
if (_package.authorinformation.authorurl[0] != null) {
writer.writeElement('authorurl', _package.authorinformation.authorurl[0]);
}
writer.endElement();
}
writer.startElement('versions');
_ref5 = _package.versions;
for (versionNumber in _ref5) {
version = _ref5[versionNumber];
writer.startElement('version');
writer.writeAttribute('name', versionNumber);
writer.writeAttribute('accessible', "true");
writer.writeAttribute('isCritical', /pl/i.test(versionNumber) ? "true" : "false");
if (version.fromversions.length) {
writer.startElement('fromversions');
_ref6 = version.fromversions;
for (_i = 0, _len = _ref6.length; _i < _len; _i++) {
fromVersion = _ref6[_i];
writer.writeElement('fromversion', fromVersion);
}
writer.endElement();
}
if (version.requiredpackages.requiredpackage.length) {
writer.startElement('requiredpackages');
_ref7 = version.requiredpackages.requiredpackage;
for (_j = 0, _len1 = _ref7.length; _j < _len1; _j++) {
requiredPackage = _ref7[_j];
writer.startElement('requiredpackage');
if (requiredPackage.$.minversion != null) {
writer.writeAttribute('minversion', requiredPackage.$.minversion);
}
writer.text(requiredPackage._);
writer.endElement();
}
writer.endElement();
}
writer.writeElement('timestamp', String(Math.floor((version.timestamp.getTime()) / 1000)));
writer.writeElement('file', "{{packageServerHost}}/" + _package.name + "/" + (versionNumber.replace(new RegExp(' ', 'g'), '_')));
if (version.license) {
result = /^(.*?)(?:\s<(https?:\/\/.*)>)?$/.exec(version.license);
writer.startElement('license');
if (result[2] != null) {
writer.writeAttribute('url', result[2]);
}
writer.text(result[1]);
writer.endElement();
}
writer.endElement();
}
writer.endElement();
writer.endElement();
}
end = (new Date).getTime();
writer.writeComment("xml generated in " + ((end - start) / 1e3) + " seconds");
writer.writeComment("packages scanned in " + updateTime + " seconds");
writer.writeComment("last update " + lastUpdate);
writer.writeComment("This list was presented by Tims Package Server " + serverVersion);
writer.endElement();
writer.endDocument();
}
return res.end((writer.toString()).replace(/\{\{packageServerHost\}\}/g, host));
});
app.all(/^\/([a-z0-9_-]+\.[a-z0-9_-]+(?:\.[a-z0-9_-]+)+)\/([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)\/?(?:\?.*)?$/i, function(req, res) {
res.attachment("" + req.params[0] + "_" + req.params[1] + ".tar");
return res.sendfile("" + config.packageFolder + "/" + req.params[0] + "/" + req.params[1] + ".tar", function(err) {
if (err != null) {
res.statusCode = 404;
return res.end();
}
});
});
app.all(/^\/([a-z0-9_-]+\.[a-z0-9_-]+(?:\.[a-z0-9_-]+)+)\/?(?:\?.*)?$/i, function(req, res) {
res.attachment("" + req.params[0] + ".tar");
return res.sendfile("" + config.packageFolder + "/" + req.params[0] + "/latest", function(err) {
if (err != null) {
res.statusCode = 404;
return res.end();
}
});
});
if (config.enableManualUpdate) {
app.get('/update', function(req, res) {
logger.log("info", 'Manual update was requested');
return readPackages(function() {
var _ref;
return res.redirect(303, (_ref = config.basePath) != null ? _ref : "" + req.protocol + "://" + (req.header('host')) + "/");
});
});
}
app.all('*', function(req, res) {
res.statusCode = 404;
return res.end();
});
readPackages(function() {
return app.listen(config.port);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment