Skip to content

Instantly share code, notes, and snippets.

@trentm
Created March 23, 2015 02:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trentm/22129ce511cfbd29c725 to your computer and use it in GitHub Desktop.
Save trentm/22129ce511cfbd29c725 to your computer and use it in GitHub Desktop.
diff --git a/src/img/lib/common.js b/src/img/lib/common.js
index 6962fc3..120a0d8 100644
--- a/src/img/lib/common.js
+++ b/src/img/lib/common.js
@@ -300,6 +300,97 @@ function execFilePlus(args, cb) {
/**
+ * A convenience wrapper around `child_process.exec` to take away some
+ * logging and error handling boilerplate.
+ *
+ * @param args {Object}
+ * - command {String} Required.
+ * - execOpts {Array} Exec options.
+ * - log {Bunyan Logger} Required. Use to log details at trace level.
+ * @param cb {Function} `function (err, stdout, stderr)` where `err` here is
+ * an `errors.InternalError` wrapper around the child_process error.
+ */
+function execPlus(args, cb) {
+ assert.object(args, 'args');
+ assert.string(args.command, 'args.command');
+ assert.optionalObject(args.execOpts, 'args.execOpts');
+ assert.object(args.log, 'args.log');
+ assert.func(cb);
+ var command = args.command;
+ var execOpts = args.execOpts;
+
+ // args.log.trace({exec: true, command: command, execOpts: execOpts},
+ // 'exec start');
+ execFile(command, execOpts, function (err, stdout, stderr) {
+ args.log.trace({exec: true, command: command, execOpts: execOpts,
+ err: err, stdout: stdout, stderr: stderr}, 'exec done');
+ if (err) {
+ var msg = format(
+ 'exec error:\n'
+ + '\tcommand: %s\n'
+ + '\texit status: %s\n'
+ + '\tstdout:\n%s\n'
+ + '\tstderr:\n%s',
+ command, err.code, stdout.trim(), stderr.trim());
+ cb(new InternalError({cause: err, message: msg}), stdout, stderr);
+ } else {
+ cb(null, stdout, stderr);
+ }
+ });
+}
+
+
+/**
+ * Run a command via `spawn` and callback with the results a la `execFile`.
+ *
+ * @param args {Object}
+ * - argv {Array} Required.
+ * - log {Bunyan Logger} Required. Use to log details at trace level.
+ * - opts {Object} Optional `child_process.spawn` options.
+ * @param cb {Function} `function (err, stdout, stderr)` where `err` here is
+ * an `errors.InternalError` wrapper around the child_process error.
+ */
+function spawnRun(args, cb) {
+ assert.object(args, 'args');
+ assert.arrayOfString(args.argv, 'args.argv');
+ assert.ok(args.argv.length > 0, 'argv has at least one arg');
+ assert.object(args.log, 'args.log');
+ assert.func(cb);
+
+ args.log.trace({exec: true, argv: args.argv}, 'exec start');
+ var child = spawn(args.argv[0], args.argv.slice(1), args.opts);
+
+ var stdout = [];
+ var stderr = [];
+ child.stdout.setEncoding('utf8');
+ child.stdout.on('data', function (chunk) { stdout.push(chunk); });
+ child.stderr.setEncoding('utf8');
+ child.stderr.on('data', function (chunk) { stderr.push(chunk); });
+
+ child.on('close', function spawnClose(code, signal) {
+ stdout = stdout.join('');
+ stderr = stderr.join('');
+ args.log.trace({exec: true, argv: args.argv, code: code,
+ signal: signal, stdout: stdout, stderr: stderr}, 'exec done');
+ if (code || signal) {
+ var msg = format(
+ 'spawn error:\n'
+ + '\targv: %j\n'
+ + '\texit code: %s\n'
+ + '\texit signal: %s\n'
+ + '\tstdout:\n%s\n'
+ + '\tstderr:\n%s',
+ args.argv, code, signal, stdout.trim(), stderr.trim());
+ cb(new errors.InternalError({message: msg}),
+ stdout, stderr);
+ } else {
+ cb(null, stdout, stderr);
+ }
+ });
+}
+
+
+/**
* Call `vmadm stop UUID`.
*
* @param uuid {String} The current snapshot name.
@@ -687,6 +778,8 @@ module.exports = {
humanSizeFromBytes: humanSizeFromBytes,
normUrlFromUrl: normUrlFromUrl,
execFilePlus: execFilePlus,
+ execPlus: execPlus,
+ spawnRun: spawnRun,
vmStop: vmStop,
vmStart: vmStart,
diff --git a/src/img/lib/imgadm.js b/src/img/lib/imgadm.js
index 90d07c5..a8352ed 100644
--- a/src/img/lib/imgadm.js
+++ b/src/img/lib/imgadm.js
@@ -72,6 +72,7 @@ var common = require('./common'),
var configuration = require('./configuration');
var Database = require('./database');
var errors = require('./errors');
+var magic = require('./magic');
var mod_sources = require('./sources');
var upgrade = require('./upgrade');
@@ -1711,8 +1712,9 @@ IMGADM.prototype._importImage = function _importImage(opts, cb) {
var totalBytes = irecs
.map(function (irec) { return irec.imgMeta.size; })
.reduce(function (a, b) { return a + b; });
- logCb('Must download and install %d images (%s)',
- irecs.length, common.humanSizeFromBytes(totalBytes));
+ logCb('Must download and install %d image%s (%s)',
+ irecs.length, (irecs.length === 1 ? '' : 's'),
+ common.humanSizeFromBytes(totalBytes));
if (!opts.quiet && process.stderr.isTTY) {
bar = new ProgressBar({
size: totalBytes,
@@ -2092,13 +2094,49 @@ IMGADM.prototype._installDockerImage = function _installDockerImage(ctx, cb) {
]}, next);
},
+ function sniffCompression(_, next) {
+ assert.string(ctx.filePath, 'ctx.filePath');
+ magic.compressionTypeFromPath(ctx.filePath, function (err, cType) {
+ if (err) {
+ next(err);
+ return;
+ }
+ ctx.cType = cType; // one of: null, bzip2, gzip, xz
+ next();
+ });
+ },
+
// Dev Note: If we can just gtar a file and NOT do streaming into
// gtar, then it'll handle compression detection for us, in particular
// for docker files for which we don't track compression.
function extract(_, next) {
assert.string(ctx.filePath, 'ctx.filePath');
- var argv = ['/usr/bin/gtar', '-xf', ctx.filePath, '-C', zoneroot];
- execFilePlus({argv: argv, log: log}, next);
+ assert.optionalString(ctx.cType, 'ctx.cType');
+
+ if (ctx.cType === 'xz') {
+ common.execPlus({
+ command: format(
+ '/usr/bin/xz -dc %s | /usr/bin/gtar -x -C %s',
+ ctx.filePath, zoneroot),
+ log: log,
+ execOpts: {
+ maxBuffer: 2 * 1024 * 1024
+ }
+ }, next);
+ } else {
+ var tarOpt = {
+ bzip2: 'j',
+ gzip: 'z'
+ }[ctx.cType] || '';
+ common.execFilePlus({
+ argv: ['/usr/bin/gtar', '-x' + tarOpt + 'f',
+ ctx.filePath, '-C', zoneroot],
+ log: log,
+ execOpts: {
+ maxBuffer: 2 * 1024 * 1024
+ }
+ }, next);
+ }
},
function whiteout(_, next) {
diff --git a/src/img/lib/magic.js b/src/img/lib/magic.js
new file mode 100644
index 0000000..9af4c9c
--- /dev/null
+++ b/src/img/lib/magic.js
@@ -0,0 +1,104 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License"). You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at http://smartos.org/CDDL
+ *
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file.
+ *
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ * Copyright (c) 2015, Joyent, Inc. All rights reserved.
+ *
+ * * *
+ * Magic number sniffing of compression type of files.
+ */
+
+var p = console.log;
+var assert = require('assert-plus');
+var format = require('util').format;
+var fs = require('fs');
+var vasync = require('vasync');
+
+
+
+// ---- compression type sniffing
+
+var magicNumbers = {
+ // <compression type> : <magic number>
+ bzip2: new Buffer([0x42, 0x5A, 0x68]),
+ gzip: new Buffer([0x1F, 0x8B, 0x08]),
+ xz: new Buffer([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])
+};
+var maxMagicLen = 0;
+Object.keys(magicNumbers).forEach(function (type) {
+ maxMagicLen = Math.max(maxMagicLen, magicNumbers[type].length);
+});
+
+function bufNEquals(a, b, n) {
+ assert.ok(a.length >= n, format(
+ 'buffer "a" length (%d) is shorter than "n" (%d)', a.length, n));
+ assert.ok(b.length >= n, format(
+ 'buffer "b" length (%d) is shorter than "n" (%d)', b.length, n));
+
+ for (var i = 0; i < n; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function compressionTypeFromBufSync(buf) {
+ var types = Object.keys(magicNumbers);
+ for (var i = 0; i < types.length; i++) {
+ var type = types[i];
+ var magic = magicNumbers[type];
+ if (bufNEquals(buf, magic, magic.length)) {
+ return type;
+ }
+ }
+ return null;
+}
+
+function compressionTypeFromPath(path, cb) {
+ fs.open(path, 'r', function (oErr, fd) {
+ if (oErr) {
+ cb(oErr);
+ return;
+ }
+ var buf = new Buffer(maxMagicLen);
+ fs.read(fd, buf, 0, buf.length, 0, function (rErr, bytesRead, buffer) {
+ if (rErr) {
+ cb(rErr);
+ return;
+ }
+ fs.close(fd, function (cErr) {
+ if (cErr) {
+ cb(cErr);
+ return;
+ }
+ cb(null, compressionTypeFromBufSync(buf));
+ });
+ });
+ });
+}
+
+
+// ---- exports
+
+module.exports = {
+ compressionTypeFromPath: compressionTypeFromPath
+};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment