247 lines
6.6 KiB
JavaScript
247 lines
6.6 KiB
JavaScript
|
|
||
|
var crypto = require('crypto')
|
||
|
var Step = require('step')
|
||
|
var fs = require('fs')
|
||
|
var _ = require('underscore')
|
||
|
var PSQL = require(global.settings.app_root + '/app/models/psql')
|
||
|
var spawn = require('child_process').spawn
|
||
|
|
||
|
function shp() {
|
||
|
}
|
||
|
|
||
|
shp.prototype = {
|
||
|
|
||
|
id: "shp",
|
||
|
|
||
|
is_file: true,
|
||
|
|
||
|
getQuery: function(sql, options) {
|
||
|
return null; // dont execute the query
|
||
|
},
|
||
|
|
||
|
getContentType: function(){
|
||
|
return "application/zip; charset=utf-8";
|
||
|
},
|
||
|
|
||
|
getFileExtension: function() {
|
||
|
return "zip"
|
||
|
},
|
||
|
|
||
|
transform: function(result, options, callback) {
|
||
|
throw "should not be called for file formats"
|
||
|
},
|
||
|
|
||
|
getKey: function(options) {
|
||
|
return [this.id,
|
||
|
options.dbname,
|
||
|
options.user_id,
|
||
|
options.gn,
|
||
|
generateMD5(options.sql)].concat(options.skipfields).join(':');
|
||
|
},
|
||
|
|
||
|
generate: function(options, callback) {
|
||
|
var o = options;
|
||
|
toSHP(o.database, o.user_id, o.gn, o.sql, o.skipfields, o.filename, callback);
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
function generateMD5(data){
|
||
|
var hash = crypto.createHash('md5');
|
||
|
hash.update(data);
|
||
|
return hash.digest('hex');
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// Return database username from user_id
|
||
|
// NOTE: a "null" user_id is a request to use the public user
|
||
|
function userid_to_dbuser(user_id) {
|
||
|
if ( _.isString(user_id) )
|
||
|
return _.template(global.settings.db_user, {user_id: user_id});
|
||
|
return "publicuser" // FIXME: make configurable
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
// Internal function usable by all OGR-driven outputs
|
||
|
function toOGR(dbname, user_id, gcol, sql, skipfields, out_format, out_filename, callback) {
|
||
|
var ogr2ogr = 'ogr2ogr'; // FIXME: make configurable
|
||
|
var dbhost = global.settings.db_host;
|
||
|
var dbport = global.settings.db_port;
|
||
|
var dbuser = userid_to_dbuser(user_id);
|
||
|
var dbpass = ''; // turn into a parameter..
|
||
|
|
||
|
var columns = [];
|
||
|
|
||
|
// Drop ending semicolon (ogr doens't like it)
|
||
|
sql = sql.replace(/;\s*$/, '');
|
||
|
|
||
|
Step (
|
||
|
|
||
|
function fetchColumns() {
|
||
|
var colsql = 'SELECT * FROM (' + sql + ') as _cartodbsqlapi LIMIT 1';
|
||
|
var pg = new PSQL(user_id, dbname, 1, 0);
|
||
|
pg.query(colsql, this);
|
||
|
},
|
||
|
function spawnDumper(err, result) {
|
||
|
if (err) throw err;
|
||
|
|
||
|
//if ( ! result.rows.length ) throw new Error("Query returns no rows");
|
||
|
|
||
|
// Skip system columns
|
||
|
if ( result.rows.length ) {
|
||
|
for (var k in result.rows[0]) {
|
||
|
if ( skipfields.indexOf(k) != -1 ) continue;
|
||
|
if ( out_format != 'CSV' && k == "the_geom_webmercator" ) continue; // TODO: drop ?
|
||
|
if ( out_format == 'CSV' ) columns.push('"' + k + '"::text');
|
||
|
else columns.push('"' + k + '"');
|
||
|
}
|
||
|
} else columns.push('*');
|
||
|
//console.log(columns.join(','));
|
||
|
|
||
|
var next = this;
|
||
|
|
||
|
sql = 'SELECT ' + columns.join(',')
|
||
|
+ ' FROM (' + sql + ') as _cartodbsqlapi';
|
||
|
|
||
|
var child = spawn(ogr2ogr, [
|
||
|
'-f', out_format,
|
||
|
'-lco', 'ENCODING=UTF-8',
|
||
|
'-lco', 'LINEFORMAT=CRLF',
|
||
|
out_filename,
|
||
|
"PG:host=" + dbhost
|
||
|
+ " user=" + dbuser
|
||
|
+ " dbname=" + dbname
|
||
|
+ " password=" + dbpass
|
||
|
+ " tables=fake" // trick to skip query to geometry_columns
|
||
|
+ "",
|
||
|
'-sql', sql
|
||
|
]);
|
||
|
|
||
|
/*
|
||
|
console.log(['ogr2ogr',
|
||
|
'-f', '"'+out_format+'"',
|
||
|
out_filename,
|
||
|
"'PG:host=" + dbhost
|
||
|
+ " user=" + dbuser
|
||
|
+ " dbname=" + dbname
|
||
|
+ " password=" + dbpass
|
||
|
+ " tables=fake" // trick to skip query to geometry_columns
|
||
|
+ "'",
|
||
|
"-sql '", sql, "'"].join(' '));
|
||
|
*/
|
||
|
|
||
|
var stdout = '';
|
||
|
child.stdout.on('data', function(data) {
|
||
|
stdout += data;
|
||
|
//console.log('stdout: ' + data);
|
||
|
});
|
||
|
|
||
|
var stderr;
|
||
|
var logErrPat = new RegExp(/^ERROR/);
|
||
|
child.stderr.on('data', function(data) {
|
||
|
data = data.toString(); // know of a faster way ?
|
||
|
// Store only the first ERROR line
|
||
|
if ( ! stderr && data.match(logErrPat) ) stderr = data;
|
||
|
console.log('ogr2ogr stderr: ' + data);
|
||
|
});
|
||
|
|
||
|
child.on('exit', function(code) {
|
||
|
if ( code ) {
|
||
|
var emsg = stderr.split('\n')[0];
|
||
|
// TODO: add more info about this error ?
|
||
|
//if ( RegExp(/attempt to write non-.*geometry.*to.*type shapefile/i).exec(emsg) )
|
||
|
next(new Error(emsg));
|
||
|
} else {
|
||
|
next(null);
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
function finish(err) {
|
||
|
callback(err, out_filename);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function toSHP(dbname, user_id, gcol, sql, skipfields, filename, callback) {
|
||
|
var zip = 'zip'; // FIXME: make configurable
|
||
|
var tmpdir = global.settings.tmpDir || '/tmp';
|
||
|
var reqKey = [ 'shp', dbname, user_id, gcol, generateMD5(sql) ].concat(skipfields).join(':');
|
||
|
var outdirpath = tmpdir + '/sqlapi-' + reqKey;
|
||
|
var zipfile = outdirpath + '.zip';
|
||
|
var shapefile = outdirpath + '/' + filename + '.shp';
|
||
|
|
||
|
// TODO: following tests:
|
||
|
// - fetch query with no "the_geom" column
|
||
|
|
||
|
Step (
|
||
|
function createOutDir() {
|
||
|
fs.mkdir(outdirpath, 0777, this);
|
||
|
},
|
||
|
function spawnDumper(err) {
|
||
|
if ( err ) throw err;
|
||
|
toOGR(dbname, user_id, gcol, sql, skipfields, 'ESRI Shapefile', shapefile, this);
|
||
|
},
|
||
|
function doZip(err) {
|
||
|
if ( err ) throw err;
|
||
|
|
||
|
var next = this;
|
||
|
|
||
|
var child = spawn(zip, ['-qrj', zipfile, outdirpath ]);
|
||
|
|
||
|
child.on('exit', function(code) {
|
||
|
//console.log("Zip complete, zip return code was " + code);
|
||
|
if (code) {
|
||
|
next(new Error("Zip command return code " + code));
|
||
|
//res.statusCode = 500;
|
||
|
}
|
||
|
next(null);
|
||
|
});
|
||
|
|
||
|
},
|
||
|
function cleanupDir(topError) {
|
||
|
|
||
|
var next = this;
|
||
|
|
||
|
//console.log("Cleaning up " + outdirpath);
|
||
|
|
||
|
// Unlink the dir content
|
||
|
var unlinkall = function(dir, files, finish) {
|
||
|
var f = files.shift();
|
||
|
if ( ! f ) { finish(null); return; }
|
||
|
var fn = dir + '/' + f;
|
||
|
fs.unlink(fn, function(err) {
|
||
|
if ( err ) {
|
||
|
console.log("Unlinking " + fn + ": " + err);
|
||
|
finish(err);
|
||
|
}
|
||
|
else unlinkall(dir, files, finish)
|
||
|
});
|
||
|
}
|
||
|
fs.readdir(outdirpath, function(err, files) {
|
||
|
if ( err ) {
|
||
|
if ( err.code != 'ENOENT' ) {
|
||
|
next(new Error([topError, err].join('\n')));
|
||
|
} else {
|
||
|
next(topError);
|
||
|
}
|
||
|
} else {
|
||
|
unlinkall(outdirpath, files, function(err) {
|
||
|
fs.rmdir(outdirpath, function(err) {
|
||
|
if ( err ) console.log("Removing dir " + path + ": " + err);
|
||
|
callback(topError, zipfile);
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
module.exports = new shp();
|
||
|
module.exports.toOGR = toOGR;
|
||
|
module.exports.generateMD5 = generateMD5
|
||
|
|