CartoDB-SQL-API/lib/models/formats/ogr.js

340 lines
9.9 KiB
JavaScript
Raw Normal View History

2018-10-24 21:42:33 +08:00
'use strict';
2015-05-13 19:00:01 +08:00
var crypto = require('crypto');
var step = require('step');
var fs = require('fs');
var _ = require('underscore');
var PSQL = require('cartodb-psql');
var spawn = require('child_process').spawn;
// Keeps track of what's waiting baking for export
var bakingExports = {};
2019-12-24 01:19:08 +08:00
function OgrFormat (id) {
this.id = id;
}
2014-08-03 02:27:05 +08:00
OgrFormat.prototype = {
2019-12-24 01:19:08 +08:00
id: 'ogr',
2019-12-24 01:19:08 +08:00
is_file: true,
2019-12-24 01:19:08 +08:00
getQuery: function (/* sql, options */) {
return null; // dont execute the query
},
2019-12-24 01:19:08 +08:00
transform: function (/* result, options, callback */) {
2019-12-27 01:01:38 +08:00
throw new Error('should not be called for file formats');
2019-12-24 01:19:08 +08:00
},
2019-12-24 01:19:08 +08:00
getContentType: function () { return this._contentType; },
2019-12-24 01:19:08 +08:00
getFileExtension: function () { return this._fileExtension; },
2019-12-24 01:19:08 +08:00
getKey: function (options) {
return [this.id,
options.dbopts.dbname,
options.dbopts.user,
options.gn,
this.generateMD5(options.filename),
this.generateMD5(options.sql)].concat(options.skipfields).join(':');
},
2019-12-24 01:19:08 +08:00
generateMD5: function (data) {
var hash = crypto.createHash('md5');
hash.update(data);
return hash.digest('hex');
}
};
// Internal function usable by all OGR-driven outputs
2019-12-27 01:01:38 +08:00
OgrFormat.prototype.toOGR = function (options, outFormat, outFilename, callback) {
2019-12-24 01:19:08 +08:00
// var gcol = options.gn;
var sql = options.sql;
var skipfields = options.skipfields;
2019-12-27 01:01:38 +08:00
var outLayername = options.filename;
2019-12-24 01:19:08 +08:00
var dbopts = options.dbopts;
var ogr2ogr = global.settings.ogr2ogrCommand || 'ogr2ogr';
var dbhost = dbopts.host;
var dbport = dbopts.port;
var dbuser = dbopts.user;
var dbpass = dbopts.pass;
var dbname = dbopts.dbname;
var timeout = options.timeout;
var that = this;
var columns = [];
var geocol;
var pg;
// Drop ending semicolon (ogr doens't like it)
sql = sql.replace(/;\s*$/, '');
const theGeomFirst = (fieldA, fieldB) => {
if (fieldA.name === 'the_geom') {
return -1;
}
if (fieldB.name === 'the_geom') {
return 1;
}
return 0;
};
2019-12-24 01:19:08 +08:00
step(
2019-12-24 01:19:08 +08:00
function fetchColumns () {
var colsql = 'SELECT * FROM (' + sql + ') as _cartodbsqlapi LIMIT 0';
pg = new PSQL(dbopts);
pg.query(colsql, this);
},
function findSRS (err, result) {
if (err) {
throw err;
}
2019-12-24 01:19:08 +08:00
var needSRS = that._needSRS;
columns = result.fields
// skip columns
.filter(field => skipfields.indexOf(field.name) === -1)
// put "the_geom" first (if exists)
.sort(theGeomFirst)
// get first geometry to calculate SRID ("the_geom" if exists)
.map(field => {
if (needSRS && !geocol && pg.typeName(field.dataTypeID) === 'geometry') {
geocol = field.name;
}
return field;
})
// apply quotes to columns
2019-12-27 01:01:38 +08:00
.map(field => outFormat === 'CSV' ? pg.quoteIdentifier(field.name) + '::text' : pg.quoteIdentifier(field.name));
2019-12-24 01:19:08 +08:00
if (!needSRS || !geocol) {
return null;
}
2019-12-24 01:19:08 +08:00
var next = this;
2017-08-09 18:50:16 +08:00
2019-12-24 01:19:08 +08:00
var qgeocol = pg.quoteIdentifier(geocol);
var sridsql = 'SELECT ST_Srid(' + qgeocol + ') as srid, GeometryType(' +
qgeocol + ') as type FROM (' + sql + ') as _cartodbsqlapi WHERE ' +
qgeocol + ' is not null limit 1';
2019-12-24 01:19:08 +08:00
pg.query(sridsql, function (err, result) {
if (err) { next(err); return; }
if (result.rows.length) {
var srid = result.rows[0].srid;
var type = result.rows[0].type;
next(null, srid, type);
} else {
// continue as srid and geom type are not critical when there are no results
next(null);
}
});
},
function spawnDumper (err, srid, type) {
if (err) {
throw err;
}
2019-12-24 01:19:08 +08:00
var next = this;
2019-12-24 01:19:08 +08:00
var ogrsql = 'SELECT ' + columns.join(',') + ' FROM (' + sql + ') as _cartodbsqlapi';
2019-12-24 01:19:08 +08:00
var ogrargs = [
2019-12-27 01:01:38 +08:00
'-f', outFormat,
2019-12-24 01:19:08 +08:00
'-lco', 'RESIZE=YES',
'-lco', 'ENCODING=UTF-8',
'-lco', 'LINEFORMAT=CRLF',
2019-12-27 01:01:38 +08:00
outFilename,
2019-12-24 01:19:08 +08:00
'PG:host=' + dbhost + ' port=' + dbport + ' user=' + dbuser + ' dbname=' + dbname + ' password=' + dbpass,
'-sql', ogrsql
];
2019-12-24 01:19:08 +08:00
if (srid) {
ogrargs.push('-a_srs', 'EPSG:' + srid);
}
2017-08-09 18:50:16 +08:00
2019-12-24 01:19:08 +08:00
if (type) {
ogrargs.push('-nlt', type);
}
2019-12-24 01:19:08 +08:00
if (options.cmd_params) {
ogrargs = ogrargs.concat(options.cmd_params);
}
2019-12-27 01:01:38 +08:00
ogrargs.push('-nln', outLayername);
2019-12-24 01:19:08 +08:00
// TODO: research if `exec` could fit better than `spawn`
var child = spawn(ogr2ogr, ogrargs);
2019-12-24 01:19:08 +08:00
var timedOut = false;
var ogrTimeout;
if (timeout > 0) {
ogrTimeout = setTimeout(function () {
timedOut = true;
child.kill();
}, timeout);
}
2019-12-24 01:19:08 +08:00
child.on('error', function (err) {
clearTimeout(ogrTimeout);
next(err);
});
2019-12-24 01:19:08 +08:00
var stderrData = [];
child.stderr.setEncoding('utf8');
child.stderr.on('data', function (data) {
stderrData.push(data);
});
child.on('exit', function (code) {
clearTimeout(ogrTimeout);
if (timedOut) {
return next(new Error('statement timeout'));
}
if (code !== 0) {
var errMessage = 'ogr2ogr command return code ' + code;
if (stderrData.length > 0) {
errMessage += ', Error: ' + stderrData.join('\n');
}
return next(new Error(errMessage));
}
return next();
});
},
2019-12-24 01:19:08 +08:00
function finish (err) {
2019-12-27 01:01:38 +08:00
callback(err, outFilename);
}
2019-12-24 01:19:08 +08:00
);
};
OgrFormat.prototype.toOGR_SingleFile = function (options, fmt, callback) {
var dbname = options.dbopts.dbname;
2019-12-27 01:01:38 +08:00
var userId = options.dbopts.user;
2019-12-24 01:19:08 +08:00
var gcol = options.gcol;
var sql = options.sql;
var skipfields = options.skipfields;
var ext = this._fileExtension;
var layername = options.filename;
var tmpdir = global.settings.tmpDir || '/tmp';
var reqKey = [
fmt,
dbname,
2019-12-27 01:01:38 +08:00
userId,
2019-12-24 01:19:08 +08:00
gcol,
this.generateMD5(layername),
this.generateMD5(sql)
].concat(skipfields).join(':');
var outdirpath = tmpdir + '/sqlapi-' + process.pid + '-' + reqKey;
var dumpfile = outdirpath + ':cartodb-query.' + ext;
// TODO: following tests:
// - fetch query with no "the_geom" column
this.toOGR(options, fmt, dumpfile, callback);
};
OgrFormat.prototype.sendResponse = function (opts, callback) {
// var next = callback;
var reqKey = this.getKey(opts);
var qElem = new ExportRequest(opts.sink, callback, opts.beforeSink);
var baking = bakingExports[reqKey];
if (baking) {
baking.req.push(qElem);
} else {
baking = bakingExports[reqKey] = { req: [qElem] };
this.generate(opts, function (err, dumpfile) {
if (opts.profiler) {
opts.profiler.done('generate');
}
step(
function sendResults () {
var nextPipe = function (finish) {
var r = baking.req.shift();
if (!r) { finish(null); return; }
r.sendFile(err, dumpfile, function () {
nextPipe(finish);
});
};
if (!err) {
nextPipe(this);
} else {
_.each(baking.req, function (r) {
r.cb(err);
});
return true;
}
},
function cleanup (/* err */) {
delete bakingExports[reqKey];
// unlink dump file (sync to avoid race condition)
console.log('removing', dumpfile);
try { fs.unlinkSync(dumpfile); } catch (e) {
if (e.code !== 'ENOENT') {
console.log('Could not unlink dumpfile ' + dumpfile + ': ' + e);
}
}
}
);
});
}
};
2019-12-24 01:19:08 +08:00
function ExportRequest (ostream, callback, beforeSink) {
this.cb = callback;
this.beforeSink = beforeSink;
this.ostream = ostream;
this.istream = null;
this.canceled = false;
2019-12-24 01:19:08 +08:00
var that = this;
2019-12-24 01:19:08 +08:00
this.ostream.on('close', function () {
that.canceled = true;
if (that.istream) {
that.istream.destroy();
}
});
}
ExportRequest.prototype.sendFile = function (err, filename, callback) {
2019-12-27 01:01:38 +08:00
if (err) {
return callback(err);
}
2019-12-24 01:19:08 +08:00
var that = this;
if (!this.canceled) {
this.istream = fs.createReadStream(filename)
.on('open', function (/* fd */) {
if (that.beforeSink) {
that.beforeSink();
}
that.istream.pipe(that.ostream);
callback();
})
.on('error', function (e) {
console.log("Can't send response: " + e);
that.ostream.end();
callback();
});
} else {
callback();
}
this.cb();
2014-08-03 02:23:47 +08:00
};
2014-08-03 02:27:05 +08:00
module.exports = OgrFormat;