[Pkg-javascript-commits] [node-millstone] 09/10: Imported Upstream version 0.6.8
Jérémy Lal
kapouer at alioth.debian.org
Fri Oct 25 20:15:32 UTC 2013
This is an automated email from the git hooks/post-receive script.
kapouer pushed a commit to branch master
in repository node-millstone.
commit aed56fff5eb00284bd74746b9a9e47b8e56105a9
Author: Jérémy Lal <kapouer at melix.org>
Date: Fri Oct 25 22:14:51 2013 +0200
Imported Upstream version 0.6.8
---
.npmignore => .gitignore | 0
.npmignore | 3 +-
.travis.yml | 27 +
CHANGELOG.md | 153 ++++
README.md | 3 +
bin/millstone | 38 +
lib/millstone.js | 933 +++++++++++++++-----
lib/util.js | 159 +++-
package.json | 33 +-
test/UPPERCASE_EXT/project.mml | 13 +
test/UPPERCASE_EXT/style.mss | 1 +
test/UPPERCASE_EXT/test1.CSV | 8 +
test/cache/cache.mml | 12 +-
test/corrupt-zip.test.js | 57 ++
test/corrupt-zip/project.mml | 13 +
test/corrupt-zip/style.mss | 1 +
test/data/9368bdd9-zip_no_ext/.9368bdd9-zip_no_ext | 1 +
test/data/9368bdd9-zip_no_ext/9368bdd9-zip_no_ext | Bin 0 -> 1162 bytes
...e_10m_admin_0_boundary_lines_disputed_areas.zip | Bin 0 -> 49525 bytes
test/data/snow-cover.tif | Bin 0 -> 2876 bytes
test/error.test.js | 64 ++
test/image-noext.test.js | 39 +
test/image-noext/project.mml | 14 +
test/image-noext/style.mss | 1 +
test/invalid-json/broken.json | 8 +
test/invalid-json/project.mml | 13 +
test/invalid-json/style.mss | 1 +
test/macosx-zipped.test.js | 41 +
test/macosx-zipped/project.mml | 13 +
test/macosx-zipped/style.mss | 1 +
test/markers.test.js | 73 ++
test/markers/layers/points.csv | 2 +
test/markers/project.mml | 14 +
test/markers/style.mss | 2 +
test/missing-file-absolute/project.mml | 13 +
test/missing-file-absolute/style.mss | 1 +
test/missing-file-relative/project.mml | 13 +
test/missing-file-relative/style.mss | 1 +
test/multi-shape-zip.test.js | 48 +
test/multi-shape-zip/project.mml | 11 +
test/nosymlink.test.js | 102 +++
test/nosymlink/points.csv | 2 +
test/nosymlink/points.dbf | Bin 0 -> 33 bytes
test/nosymlink/points.prj | 1 +
test/nosymlink/points.shp | Bin 0 -> 100 bytes
test/nosymlink/points.shx | Bin 0 -> 100 bytes
test/nosymlink/points.vrt | 9 +
test/nosymlink/project.mml | 37 +
test/nosymlink/pshape.zip | Bin 0 -> 759 bytes
test/nosymlink/style.mss | 1 +
test/raster-linking.test.js | 48 +
test/raster-linking/project.mml | 12 +
test/support.js | 28 +
test/test.js | 273 ++++--
test/uppercase-ext.test.js | 49 +
test/zip-with-shapefile-and-txt.test.js | 38 +
test/zip-with-shapefile-and-txt/project.mml | 11 +
test/zipped-json/project.mml | 13 +
test/zipped-json/style.mss | 1 +
59 files changed, 2129 insertions(+), 324 deletions(-)
diff --git a/.npmignore b/.gitignore
similarity index 100%
copy from .npmignore
copy to .gitignore
diff --git a/.npmignore b/.npmignore
index 91dfed8..246583f 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,2 +1,3 @@
.DS_Store
-node_modules
\ No newline at end of file
+node_modules
+./test
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f06df33
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,27 @@
+language: node_js
+
+node_js:
+ - "0.10"
+ - "0.8"
+ - "0.6"
+
+before_install:
+ - sudo apt-get -qq update
+
+install:
+ - npm install
+
+before_script:
+ - npm test
+
+script:
+ - rm -rf ./node_modules
+ - sudo apt-get -qq install libgdal1-dev
+ - npm install --shared_gdal
+ - npm test
+ - sudo apt-add-repository --yes ppa:ubuntugis/ubuntugis-unstable
+ - sudo apt-get -qq update
+ - sudo apt-get -qq install libgdal1-dev
+ - rm -rf ./node_modules
+ - npm install --shared_gdal
+ - npm test
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..af2b6f3
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,153 @@
+## CHANGELOG
+
+#### 0.6.8
+
+* Fixed clearing of download tracker internal allowing carto to exit cleanly from using millstone from the command line
+
+#### 0.6.7
+
+* Upgraded to latest node-srs at 0.3.6 and node-zipfile at 0.4.2
+
+#### 0.6.6
+
+* Skipped accidentally
+
+#### 0.6.5
+
+* Upgraded to latest node-srs at 0.3.3
+
+#### 0.6.4
+
+* Fixed bug causing zip files to be uncompressed even when they had already been uncompressed
+
+#### 0.6.3
+
+* Added `millstone.drainPool` function to forcefully drain the downloads pool
+
+#### 0.6.2
+
+* Fixed reading for metafiles
+
+#### 0.6.1
+
+* Always honour filepath option in .download #105 (strk)
+
+#### 0.6.0
+
+* Supports node v0.10.x
+* Upgraded various deps: request, underscore, mime, generic-pool, optimist
+* Upgraded to node-srs at 0.3.0
+* Increased the download pool size from 5 to 10 and fixed up release logic
+* Various fixes to zipfile handling and upgrade of node-zipfile to v0.4.0
+
+#### 0.5.15
+
+* Added better error output when millstone is unable to detect the appropriate mapnik datasource
+ for a file based datasources
+* Added support for attempting to re-download zip archives that are cached but cannot be opened
+ (handles partial downloads that may have failed due to network failure)
+
+#### 0.5.14
+
+* Fixed detections and handling of files with upper or mixed case extensions.
+
+#### 0.5.13
+
+* Fixed a bug where millstone would hang if an absolute path to a shapefile was used and that
+ shapefile did not exist at that path.
+
+#### 0.5.12
+
+* Added command line millstone tool
+* Added support for reading any supported file in .zip archives
+* Better error messages if broken symlinks are encountered
+* Support added for downloading images at urls without clear image file extensions
+* Fixed handling of hidden files in zip archives
+* Switched to using console.error for log output
+
+#### 0.5.11
+
+* Will now throw if files do not exist (instead of throwing on missing/unknown srs)
+
+* Fixed support for loading layer datasource files from alternative windows drives
+
+* Moved to no-symlink/no-copy behavior on all windows versions
+
+* Updated node-srs version
+
+* Improved handling of known file extensions to better support guessing extensions via headers
+
+* Fixed handling of sqlite attach with absolute paths
+
+#### 0.5.10
+
+* Fixed missing error handling when localizing Carto URIs
+
+#### 0.5.9
+
+* Improved uri regex methods for carto urls - #68, #69, #70, #72, and #73
+
+* Use copy fallback on Windows platforms supporting symlinks but where the user does not have the symlink 'right' (#71)
+
+* Restored Node v0.4.x support
+
+#### 0.5.8
+
+* Improved uri regex methods for carto urls - amends #63
+
+#### 0.5.7
+
+* Fixed handling of multiple non-unique carto urls in the same stylesheet (#63)
+
+#### 0.5.6
+
+* Fixed extension handling for urls without an extension
+
+* Moved to streaming copy of data when in copy mode to avoid too much memory usage
+
+* Fixed race condition when localizing imag/svg icons in styles like point-file and marker-file.
+
+* Exposed the global downloads object so calling applications can see how many downloads millstone is currently handling
+
+* Removed node v0.8.x deprecation warnings
+
+* Added more agressive re-copying of data when it is out of date and millstone is in copy mode (win XP)
+
+* Moved to processing shapefile parts instead of the directory
+
+#### 0.5.5
+
+* Added a verbose mode that can be trigged by setting NODE_ENV = 'development'
+
+* Switched to request (dropped node-get) for better proxy support
+
+* Support for making relative the paths stored to the download cache
+
+* Support for zipfiles with no extension
+
+* Advertise node v8 support
+
+#### 0.5.4
+
+* Fixes to better support localization of carto resources
+
+#### 0.5.3
+
+* Updated node-get min version in order to fully support proxy auth
+* Improved cross-platform relative path detection
+
+#### 0.5.2
+
+* Improved regex used to detect content-disposition
+
+* Support for localizing uri's in stylesheet
+
+#### 0.5.1
+
+* Moved to mocha for tests
+
+* Made `nosymlink` option optional
+
+#### 0.5.0
+
+* Add `nosymlink` option for not downloading files
diff --git a/README.md b/README.md
index 3f43c8c..534a24a 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
# millstone
+[![Build Status](https://secure.travis-ci.org/mapbox/millstone.png?branch=master)](http://travis-ci.org/mapbox/millstone)
+[![Dependencies](https://david-dm.org/mapbox/millstone.png)](https://david-dm.org/mapbox/millstone)
+
As of [carto 0.2.0](https://github.com/mapbox/carto), the Carto module expects
all datasources and resources to be localized - remote references like
URLs and providers are not downloaded when maps are rendered.
diff --git a/bin/millstone b/bin/millstone
new file mode 100755
index 0000000..aea856e
--- /dev/null
+++ b/bin/millstone
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+var path = require('path');
+var fs = require('fs');
+var millstone = require('millstone');
+var optimist = require('optimist')
+ .usage("Usage: $0 <source MML file>")
+ .options('h', {alias:'help', describe:'Display this help message'})
+ .options('n', {alias:'nosymlink', boolean:true, describe:'Use unmodified paths instead of symlinking files'});
+
+var options = optimist.argv;
+
+if (options.help) {
+ optimist.showHelp();
+ process.exit(0);
+}
+
+var input = options._[0];
+
+if (!input) {
+ console.log('millstone: please provide a path to an mml file');
+ process.exit(1);
+}
+var mml = JSON.parse(fs.readFileSync(input));
+
+var options = {
+ mml: mml,
+ base: path.dirname(input),
+ cache: '/tmp/millstone-test',
+ nosymlink: options.nosymlink
+};
+
+millstone.resolve(options, function(err, resolved) {
+ if (err) throw err;
+ console.log(JSON.stringify(resolved,null,4));
+ // force exit to avoid wait for generic-pool to empty
+ process.exit(0);
+});
\ No newline at end of file
diff --git a/lib/millstone.js b/lib/millstone.js
index fb78d54..a968c23 100644
--- a/lib/millstone.js
+++ b/lib/millstone.js
@@ -4,16 +4,63 @@ var url = require('url');
var crypto = require('crypto');
var EventEmitter = require('events').EventEmitter;
var mime = require('mime');
+var mkdirp = require('mkdirp');
+
+var existsAsync = require('fs').exists || require('path').exists;
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+var env = process.env.NODE_ENV || 'development';
// Third party modules
var _ = require('underscore'),
srs = require('srs'),
- get = require('get'),
+ request = require('request'),
zipfile = require('zipfile'),
Step = require('step'),
utils = require('./util.js');
-var path_sep = process.platform === 'win32' ? '\\' : '/';
+
+// mapping of known file extensions
+// to mapnik datasource plugin name
+var valid_ds_extensions = {
+ '.shp':'shape',
+ '.csv':'csv',
+ '.tsv':'csv',
+ '.txt':'csv',
+ '.geotiff':'gdal',
+ '.geotif':'gdal',
+ '.tif':'gdal',
+ '.tiff':'gdal',
+ '.vrt':'gdal',
+ '.geojson':'ogr',
+ '.json':'ogr',
+ '.gml':'ogr',
+ '.osm':'osm',
+ '.kml':'ogr',
+ '.rss':'ogr',
+ '.gpx':'ogr',
+ '.db':'sqlite',
+ '.sqlite3':'sqlite',
+ '.sqlite':'sqlite',
+ '.spatialite':'sqlite'
+};
+
+// marker types readible by mapnik
+var valid_marker_extensions = {
+ '.svg':'svg',
+ '.png':'image',
+ '.tif':'image',
+ '.tiff':'image',
+ '.jpeg':'image',
+ '.jpg':'image'
+};
+
+var file_linking_method = utils.forcelink;
+
+var never_link = false;
+if (process.platform === 'win32') {
+ never_link = true;
+}
// Known SRS values
var SRS = {
@@ -24,6 +71,11 @@ var SRS = {
// on object of locks for concurrent downloads
var downloads = {};
+// object for tracking logging on downloads
+var download_log_interval = true;
+// last download count, in order to limit logging barf
+var download_last_count = 0;
+
var pool = require('generic-pool').Pool({
create: function(callback) {
callback(null, {});
@@ -31,59 +83,139 @@ var pool = require('generic-pool').Pool({
destroy: function(obj) {
obj = undefined;
},
- max: 5
+ max: 10
});
-function download(url, filepath, callback) {
- var dl = filepath + '.download';
+
+function download(url, options, callback) {
+ if (env == 'development') {
+ download_log_interval = setInterval(function() {
+ var in_use = Object.keys(downloads);
+ if (in_use.length > 0 && (download_last_count !== in_use.length)) {
+ var msg = "[millstone] currently downloading " + in_use.length + ' file';
+ if (in_use.length > 1) {
+ msg += 's';
+ }
+ msg += ': ' + _(in_use).map(function(f) { return path.basename(f); });
+ console.error(msg);
+ }
+ download_last_count = in_use.length;
+ },5000);
+ } else {
+ clearInterval(download_log_interval);
+ }
+ // https://github.com/mapbox/millstone/issues/39
+ url = unescape(url);
+ var dl = options.filepath + '.download';
// If this file is already being downloaded, attach the callback
// to the existing EventEmitter
- if (downloads[url]) {
- return downloads[url].once('done', callback);
+ var dkey = options.filepath + '|' + url;
+ if (downloads[dkey]) {
+ return downloads[dkey].once('done', callback);
} else {
- downloads[url] = new EventEmitter();
- pool.acquire(function(obj) {
- pool.release(obj);
- (new get(url)).toDisk(dl, function(err, file, response, g) {
- if (err) {
- downloads[url].emit('done', err);
- delete downloads[url];
- return callback(err);
+ pool.acquire(function(err, obj) {
+ var make_message = function() {
+ var msg = "Unable to download '" + url + "'";
+ if (options.name)
+ msg += " for '" + options.name + "'";
+ return msg;
+ };
+ var return_on_error = function(err) {
+ downloads[dkey].emit('done', err);
+ delete downloads[dkey];
+ err.message = make_message() + " ("+err.message+")";
+ pool.release(obj);
+ return callback(err);
+ }
+ downloads[dkey] = new EventEmitter();
+ if (err) {
+ return return_on_error(err);
+ } else {
+ if (env == 'development') console.error("[millstone] downloading: '" + url + "'");
+ var req;
+ try {
+ req = request({
+ url: url,
+ proxy: process.env.HTTP_PROXY
+ });
+ } catch (err) {
+ // catch Invalid URI error
+ return return_on_error(err);
}
- fs.rename(dl, filepath, function(err) {
- // We store the headers from the download in a hidden file
- // alongside the data for future reference. Currently, we
- // only use the `content-disposition` header to determine
- // what kind of file we downloaded if it doesn't have an
- // extension.
- fs.writeFile(metapath(filepath), JSON.stringify(response.headers), 'utf-8', function(err) {
- downloads[url].emit('done', err, filepath);
- delete downloads[url];
- return callback(err, filepath);
- });
+ req.on('error', function(err) {
+ return return_on_error(err);
});
- });
+ req.pipe(fs.createWriteStream(dl)).on('error', function(err) {
+ return return_on_error(err);
+ }).on('close', function() {
+ if (!req.response || (req.response && req.response.statusCode >= 400)) {
+ return return_on_error(new Error('server returned ' + req.response.statusCode));
+ } else {
+ pool.release(obj);
+ fs.rename(dl, options.filepath, function(err) {
+ if (err) {
+ downloads[dkey].emit('done', err);
+ delete downloads[dkey];
+ return callback(err);
+ } else {
+ if (env == 'development') console.error("[millstone] finished downloading '" + options.filepath + "'");
+ // We store the headers from the download in a hidden file
+ // alongside the data for future reference. Currently, we
+ // only use the `content-disposition` header to determine
+ // what kind of file we downloaded if it doesn't have an
+ // extension.
+ var req_meta = _(req.req.res.headers).clone();
+ if (req.req.res.request && req.req.res.request.path) {
+ req_meta.path = req.req.res.request.path;
+ }
+ fs.writeFile(metapath(options.filepath), JSON.stringify(req_meta), 'utf-8', function(err) {
+ downloads[dkey].emit('done', err, options.filepath);
+ delete downloads[dkey];
+ return callback(err, options.filepath);
+ });
+ }
+ });
+ }
+ });
+ }
});
}
}
// Retrieve a remote copy of a resource only if we don't already have it.
-function localize(url, filepath, next) {
- path.exists(filepath, function(exists) {
+function localize(url, options, callback) {
+ existsAsync(options.filepath, function(exists) {
if (exists) {
- next(null, filepath);
- } else {
- utils.mkdirP(path.dirname(filepath), 0755, function(err) {
- if (err && err.code !== 'EEXIST') {
- next(err);
- } else {
- download(url, filepath, function(err, filepath) {
- if (err) return next(err);
- next(null, filepath);
- });
+ var re_download = false;
+ // unideal workaround for frequently corrupt/partially downloaded zips
+ // https://github.com/mapbox/millstone/issues/85
+ if (path.extname(options.filepath) == '.zip') {
+ try {
+ var zf = new zipfile.ZipFile(options.filepath);
+ if (zf.names.length < 1) {
+ throw new Error("could not find any valid data in zip archive: '" + options.filepath + "'");
+ }
+ } catch (e) {
+ if (env == 'development') console.error('[millstone] could not open zip archive: "' + options.filepath + '" attempting to re-download from "'+url+"'");
+ re_download = true;
}
- });
+ }
+ if (!re_download) {
+ return callback(null, options.filepath);
+ }
}
+ var dir_path = path.dirname(options.filepath);
+ mkdirp(dir_path, 0755, function(err) {
+ if (err && err.code !== 'EEXIST') {
+ if (env == 'development') console.error('[millstone] could not create directory: ' + dir_path);
+ callback(err);
+ } else {
+ download(url, options, function(err, filepath) {
+ if (err) return callback(err);
+ callback(null, filepath);
+ });
+ }
+ });
});
}
@@ -99,7 +231,7 @@ function cachepath(location) {
.substr(0,8) +
'-' + path.basename(uri.pathname, path.extname(uri.pathname));
var extname = path.extname(uri.pathname);
- return _(['.shp', '.zip']).include(extname.toLowerCase()) ?
+ return _(['.shp', '.zip', '']).include(extname.toLowerCase()) ?
path.join(hash, hash + extname)
: path.join(hash + extname);
}
@@ -110,27 +242,123 @@ function metapath(filepath) {
return path.join(path.dirname(filepath), '.' + path.basename(filepath));
}
+function add_item_to_metafile(metafile,key,item,callback) {
+ existsAsync(metafile, function(exists) {
+ if (exists) {
+ fs.readFile(metafile, 'utf-8', function(err, data) {
+ if (err) return callback(err);
+ var meta;
+ try {
+ meta = JSON.parse(data);
+ } catch (err) {
+ return callback(err);
+ }
+ meta[key] = item;
+ fs.writeFile(metafile, JSON.stringify(meta), 'utf-8', function(err) {
+ if (err) return callback(err);
+ return callback(null);
+ });
+ });
+ } else {
+ var data = {};
+ data[key] = item;
+ fs.writeFile(metafile, JSON.stringify(data), 'utf-8', function(err) {
+ if (err) return callback(err);
+ return callback(null);
+ });
+ }
+ });
+}
+
+function isRelative(loc) {
+ if (process.platform === 'win32') {
+ return loc[0] !== '\\' && loc[0] !== '/' && loc.match(/^[a-zA-Z]:/) === null;
+ } else {
+ return loc[0] !== '/';
+ }
+}
+
+function isValidExt(ext) {
+ if (ext) {
+ var lower_ext = ext.toLowerCase();
+ return lower_ext == '.zip' || valid_marker_extensions[lower_ext] || valid_ds_extensions[lower_ext];
+ }
+ return false;
+}
+
function guessExtension(headers) {
+ if (headers.path) {
+ var ext = path.extname(headers.path);
+ if (isValidExt(ext)) {
+ return ext;
+ }
+ }
if (headers['content-disposition']) {
- // Taken from node-get
- var match = headers['content-disposition'].match(/filename=['"]?([^'";]+)['"]?/);
+ var match = headers['content-disposition'].match(/filename=['"](.*)['"]$/);
+ if (!match) {
+ match = headers['content-disposition'].match(/filename=['"]?([^'";]+)['"]?/);
+ }
if (match) {
var ext = path.extname(match[1]);
- if (ext) {
+ if (isValidExt(ext)) {
return ext;
}
}
- } else if (headers['content-type']) {
- var ext = mime.extension(headers['content-type'].split(';')[0]);
- if (ext) {
- return '.' + ext;
+ }
+ if (headers['content-type']) {
+ if (headers['content-type'].indexOf('subtype=gml') != -1) {
+ return '.gml'; // avoid .xml being detected for gml
+ }
+ var ext_no_dot = mime.extension(headers['content-type'].split(';')[0]);
+ var ext = '.'+ext_no_dot;
+ if (isValidExt(ext)) {
+ return ext;
}
}
- return false;
-};
+ return '';
+}
+
+// Read headers and guess extension
+function readExtension(file, cb) {
+ fs.readFile(metapath(file), 'utf-8', function(err, data) {
+ if (err) {
+ if (err.code === 'ENOENT') return cb(new Error('Metadata file does not exist.'));
+ return cb(err);
+ }
+ try {
+ var ext = guessExtension(JSON.parse(data));
+ if (ext) {
+ if (env == 'development') console.error("[millstone] detected extension of '" + ext + "' for '" + file + "'");
+ }
+ return cb(null, ext);
+ } catch (e) {
+ return cb(e);
+ }
+ });
+}
// Unzip function, geared specifically toward unpacking a shapefile.
function unzip(file, callback) {
+ var metafile = metapath(file);
+ // return cached result of unzipped file
+ // intentionally sync here to avoid race condition unzipping
+ // same file for multiple layers
+ try {
+ var meta_data = fs.readFileSync(metafile);
+ if (meta_data) {
+ var meta = JSON.parse(meta_data);
+ var dest_file = meta['unzipped_file'];
+ if (dest_file && existsSync(dest_file)) {
+ if (env == 'development') console.error('[millstone] found previously unzipped file: ' + dest_file);
+ return callback(null,dest_file);
+ }
+ } else {
+ if (env == 'development') console.error('[millstone] empty meta file for zipfile: ' + metafile);
+ }
+ }
+ catch (err) {
+ if (env == 'development') console.error('[millstone] error opening meta file for zipfile: ' + err.message);
+ }
var zf;
try {
zf = new zipfile.ZipFile(file);
@@ -138,50 +366,99 @@ function unzip(file, callback) {
return callback(err);
}
- var remaining = zf.names.length;
- var shp = _(zf.names).chain()
+ if (zf.names.length < 1) {
+ return callback(new Error("could not find any valid data in zip archive: '" + file + "'"));
+ }
+
+ var remain = zf.names.length;
+ var ds_files = _(zf.names).chain()
+ .reject(function(name) {
+ return (name && (name[0] === '.' || path.basename(name)[0] === '.'));
+ })
.map(function(name) {
- if (path.extname(name).toLowerCase() !== '.shp') return;
- return path.join(
+ if (!valid_ds_extensions[path.extname(name).toLowerCase()]) return;
+ var new_name = path.join(
path.dirname(file),
path.basename(file, path.extname(file)) +
- path.extname(name).toLowerCase()
- );
+ path.extname(name).toLowerCase());
+ return {new_name:new_name,original_name:name};
})
+ .uniq()
.compact()
- .first()
.value();
- if (!shp) return callback(new Error('Shapefile not found in zip ' + file));
+ if (!ds_files || ds_files.length < 1) return callback(new Error("Valid datasource not detected (by extension) in zip: '" + file + "'"));
+
+ var original_name = ds_files[0].original_name;
+ var new_name = ds_files[0].new_name;
+
+ var len = Object.keys(ds_files).length;
+ if (len > 1) {
+ // prefer first .shp
+ for (var i=0;i<len;++i) {
+ var fname = ds_files[i].original_name;
+ if (path.extname(fname) == '.shp') {
+ original_name = fname;
+ new_name = ds_files[i].new_name;
+ break;
+ }
+ }
+ if (env == 'development') {
+ console.warn('[millstone] warning: detected more than one file in zip (by ext) that may be valid: ');
+ for (var i=0;i<len;++i) {
+ console.warn('[millstone] ' + ds_files[i].original_name);
+ }
+ console.warn('[millstone] picked: ' + original_name);
+ }
+ }
+
+ if (!new_name) return callback(new Error("Valid datasource not detected (by extension) in zip: '" + file + "'"));
+
+ if (env == 'development') {
+ console.warn('[millstone] renaming ' + original_name + ' to ' + new_name);
+ }
+ // only unzip files that match our target name
+ // naive '(name.indexOf(search_basename) < 0)' does the trick
+ // yes this is simplistic, but its better than corrupt data: https://github.com/mapbox/millstone/issues/99
+ var search_basename = path.basename(original_name,path.extname(original_name));
zf.names.forEach(function(name) {
// Skip directories, hiddens.
- if (!path.extname(name) || name[0] === '.') {
- remaining--;
- if (!remaining) callback(null, shp);
- }
- // We're brutal in our expectations -- don't support nested
- // directories, and rename any file from `arbitraryName.SHP`
- // to `[hash].shp`.
- var dest = path.join(
- path.dirname(file),
- path.basename(file, path.extname(file)) +
- path.extname(name).toLowerCase()
- );
- zf.readFile(name, function(err, buff) {
- if (err) return callback(err);
- fs.open(dest, 'w', 0644, function(err, fd) {
+ var basename = path.basename(name);
+ if (!path.extname(name) || (name.indexOf(search_basename) < 0) || name[0] === '.' || basename[0] === '.') {
+ remain--;
+ if (!remain) callback(null, new_name);
+ } else {
+ // We're brutal in our expectations -- don't support nested
+ // directories, and rename any file from `arbitraryName.SHP`
+ // to `[hash].shp`.
+ var dest = path.join(
+ path.dirname(file),
+ path.basename(file, path.extname(file)) +
+ path.extname(name).toLowerCase()
+ );
+ zf.readFile(name, function(err, buff) {
if (err) return callback(err);
- fs.write(fd, buff, 0, buff.length, null, function(err) {
+ fs.open(dest, 'w', 0644, function(err, fd) {
if (err) return callback(err);
- fs.close(fd, function(err) {
+ fs.write(fd, buff, 0, buff.length, null, function(err) {
if (err) return callback(err);
- remaining--;
- if (!remaining) callback(null, shp);
+ fs.close(fd, function(err) {
+ if (err) return callback(err);
+ remain--;
+ if (!remain) {
+ add_item_to_metafile(metafile,'unzipped_file',new_name,function(err) {
+ // ignore error from add_item_to_metafile
+ //if (err && env == 'development') console.error('[millstone] ' + err.message);
+ if (err) throw err;
+ callback(null, new_name);
+ });
+ }
+ });
});
});
});
- });
+ }
});
}
@@ -218,7 +495,7 @@ function fixSRS(obj) {
function checkTTL(cache, l) {
var file = l.Datasource.file;
- var ttl = l.Datasource.ttl
+ var ttl = l.Datasource.ttl;
if (!url.parse(file).protocol) return;
@@ -226,9 +503,9 @@ function checkTTL(cache, l) {
fs.stat(filepath, function(err, stats) {
if (err && err.code != 'ENOENT') return console.warn(err);
- var msttl = parseInt(ttl) * 1000;
+ var msttl = parseInt(ttl,10) * 1000;
if (err || Date.now() > (new Date(stats.mtime).getTime() + msttl)) {
- download(file, filepath, function(err, filepath){
+ download(file, {filepath:filepath,name:l.name}, function(err, filepath){
if (err) return console.warn(err);
});
}
@@ -242,14 +519,19 @@ function resolve(options, callback) {
if (!options.mml) return callback(new Error('options.mml is required'));
if (!options.base) return callback(new Error('options.base is required'));
if (!options.cache) return callback(new Error('options.cache is required'));
+ if (typeof options.nosymlink === "undefined") options.nosymlink = false;
+ // respect global no-symlinking preference on windows
+ if (never_link) options.nosymlink = true;
var mml = options.mml,
base = path.resolve(options.base),
cache = path.resolve(options.cache),
- resolved = JSON.parse(JSON.stringify(mml));
+ resolved = JSON.parse(JSON.stringify(mml)),
+ nosymlink = options.nosymlink;
Step(function setup() {
- utils.mkdirP(path.join(base, 'layers'), 0755, this);
+ if (nosymlink) mkdirp(base, 0755, this);
+ else mkdirp(path.join(base, 'layers'), 0755, this);
}, function externals(err) {
if (err && err.code !== 'EEXIST') throw err;
@@ -258,30 +540,36 @@ function resolve(options, callback) {
var next = function(err) {
remaining--;
if (err && err.code !== 'EEXIST') error = err;
- if (!remaining) this(error);
+ if (remaining <= 0) this(error);
}.bind(this);
if (!remaining) return this();
resolved.Stylesheet.forEach(function(s, index) {
- if (typeof s !== 'string') return next();
+ if (typeof s !== 'string') {
+ if (env == 'development') console.error("[millstone] processing style '" + s.id + "'");
+ return localizeCartoURIs(s,next);
+ }
var uri = url.parse(s);
// URL, download.
if (uri.protocol && (uri.protocol == 'http:' || uri.protocol == 'https:')) {
- return (new get(s)).asBuffer(function(err, data) {
+ return request({
+ url: s,
+ proxy: process.env.HTTP_PROXY
+ }, function(err, response, data) {
if (err) return next(err);
resolved.Stylesheet[index] = {
id: path.basename(uri.pathname),
data: data.toString()
};
- next(err);
+ localizeCartoURIs(resolved.Stylesheet[index],next);
});
}
// File, read from disk.
- if (uri.pathname[0] !== path_sep) {
+ if (uri.pathname && isRelative(uri.pathname)) {
uri.pathname = path.join(base, uri.pathname);
}
fs.readFile(uri.pathname, 'utf8', function(err, data) {
@@ -291,79 +579,186 @@ function resolve(options, callback) {
id: s,
data: data
};
- next(err);
+ localizeCartoURIs(resolved.Stylesheet[index],next);
});
});
+ // Handle URIs within the Carto CSS
+ function localizeCartoURIs(s,cb) {
+
+ // Get all unique URIs in stylesheet
+ // TODO - avoid finding non url( uris?
+ var matches = s.data.match(/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi);
+ var URIs = _.uniq(matches || []);
+ var CartoURIs = [];
+ // remove any matched urls that are not clearly
+ // part of a carto style
+ // TODO - consider moving to carto so we can also avoid
+ // downloading commented out code
+ URIs.forEach(function(u) {
+ var idx = s.data.indexOf(u);
+ if (idx > -1) {
+ var pre_url = s.data.slice(idx-5,idx);
+ if (pre_url.indexOf('url(') > -1) {
+ CartoURIs.push(u);
+ // Update number of async calls so that we don't
+ // call this() too soon (before everything is done)
+ remaining += 1;
+ }
+ }
+ });
+
+ CartoURIs.forEach(function(u) {
+ var uri = url.parse(encodeURI(u));
+
+ // URL.
+ if (uri.protocol && (uri.protocol == 'http:' || uri.protocol == 'https:')) {
+ var filepath = path.join(cache, cachepath(u));
+ localize(uri.href, {filepath:filepath,name:s.id}, function(err, file) {
+ if (err) {
+ cb(err);
+ } else {
+ var extname = path.extname(file);
+ if (!extname) {
+ readExtension(file, function(error, ext) {
+ // note - we ignore any readExtension errors
+ if (ext) {
+ var new_filename = file + ext;
+ fs.rename(file, new_filename, function(err) {
+ s.data = s.data.split(u).join(new_filename);
+ cb(err);
+ });
+ } else {
+ s.data = s.data.split(u).join(file);
+ cb(err);
+ }
+ });
+ } else {
+ s.data = s.data.split(u).join(file);
+ cb(err);
+ }
+ }
+ });
+ } else {
+ cb();
+ }
+ });
+ cb();
+ }
+
resolved.Layer.forEach(function(l, index) {
if (!l.Datasource || !l.Datasource.file) return next();
+ if (env == 'development') console.error("[millstone] processing layer '" + l.name + "'");
// TODO find better home for this check.
if (l.Datasource.ttl) checkTTL(cache, l);
- var name = l.name || 'layer-' + index,
- uri = url.parse(encodeURI(l.Datasource.file)),
- pathname = decodeURI(uri.pathname),
- extname = path.extname(pathname);
-
+ var name = l.name || 'layer-' + index;
+ var uri = url.parse(encodeURI(l.Datasource.file));
+ var pathname = decodeURI(uri.pathname);
+ var extname = path.extname(pathname);
+ // unbreak pathname on windows if absolute path is used
+ // to an alternative drive like e:/
+ // https://github.com/mapbox/millstone/issues/81
+ if (process.platform === 'win32') {
+ if (uri.protocol && l.Datasource.file.slice(0,2).match(/^[a-zA-Z]:/)) {
+ pathname = l.Datasource.file;
+ }
+ }
// This function takes (egregious) advantage of scope;
// l, extname, and more is all up-one-level.
//
// `file`: filename to be symlinked in place to l.Datasource.file
- var symlink = function(file, cb) {
+ var processFile = function(file, cb) {
if (!file) return cb();
- switch (extname.toLowerCase()) {
- // Unzip and symlink to directory.
- case '.zip':
- l.Datasource.file =
- path.join(base,
- 'layers',
- name,
- path.basename(file, path.extname(file)) + '.shp');
- path.exists(l.Datasource.file, function(exists) {
- if (exists) return cb();
- unzip(file, function(err, file) {
- if (err) return cb(err);
- utils.forcelink(path.dirname(file),
- path.dirname(l.Datasource.file),
- cb);
- });
- });
- break;
- // Symlink directories
- case '.shp':
- l.Datasource.file =
- path.join(base, 'layers', name, path.basename(file));
- utils.forcelink(
- path.dirname(file),
- path.dirname(l.Datasource.file), cb);
- break;
- // Symlink files
- default:
- l.Datasource.file =
- path.join(base, 'layers', name + extname);
- utils.forcelink( file,
- l.Datasource.file,
- cb);
- break;
- }
+ readExtension(file, function(err, ext) {
+ // ignore errors from extension check
+ //if (err) console.error(err);
+
+ ext = ext || extname;
+
+ switch (ext.toLowerCase()) {
+ // Unzip and symlink to directory.
+ case '.zip':
+ if (nosymlink) {
+ unzip(file, function(err, file) {
+ if (err) return cb(err);
+ l.Datasource.file = file;
+ return cb();
+ });
+ } else {
+ unzip(file, function(err, file_found) {
+ if (err) return cb(err);
+ l.Datasource.file = path.join(base,
+ 'layers',
+ name,
+ path.basename(file_found));
+ return utils.processFiles(file_found,
+ l.Datasource.file,
+ file_linking_method,
+ {cache:cache}, cb);
+ });
+ }
+ break;
+ case '.shp':
+ if (nosymlink) {
+ l.Datasource.file = file;
+ return cb();
+ } else {
+ l.Datasource.file =
+ path.join(base, 'layers', name, path.basename(file));
+ return utils.processFiles(file, l.Datasource.file, file_linking_method, {cache:cache}, cb);
+ }
+ break;
+ default:
+ if (nosymlink) {
+ l.Datasource.file = file;
+ return cb();
+ } else {
+ l.Datasource.file =
+ path.join(base, 'layers', name + ext);
+ return file_linking_method(file, l.Datasource.file, {cache:cache}, cb);
+ }
+ break;
+ }
+ });
};
// URL.
if (uri.protocol && (uri.protocol == 'http:' || uri.protocol == 'https:')) {
var filepath = path.join(cache, cachepath(l.Datasource.file));
- localize(uri.href, filepath, function(err, file) {
+ localize(uri.href, {filepath:filepath,name:l.name}, function(err, file) {
if (err) return next(err);
- symlink(file, next)
+ processFile(file, next);
});
// Absolute path.
- } else if (pathname && pathname[0] === path_sep) {
- symlink(pathname, next);
+ } else if (pathname && !isRelative(pathname)) {
+ existsAsync(pathname, function(exists) {
+ if (!exists && nosymlink) {
+ // throw here before we try to symlink to avoid confusing error message
+ // we only throw here on nosymlink because a tarred/symlink resolved project
+ // may have locally resolved files that exist, see:
+ // https://github.com/mapbox/tilemill/issues/697#issuecomment-6813928
+ return next(new Error("File not found at absolute path: '" + pathname + "'"));
+ } else {
+ processFile(pathname, next);
+ }
+ });
// Local path.
} else {
- l.Datasource.file = path.resolve(path.join(base, pathname));
- next();
+ var local_pathname = path.resolve(path.join(base, pathname));
+ // NOTE : we do not call processFile here to avoid munging the name
+ if (path.extname(local_pathname) === '.zip') {
+ unzip(local_pathname, function(err, file) {
+ if (err) return next(err);
+ l.Datasource.file = file;
+ return next();
+ });
+ } else {
+ l.Datasource.file = local_pathname;
+ return next();
+ }
}
});
}, function processSql(err) {
@@ -389,34 +784,44 @@ function resolve(options, callback) {
var extname = path.extname(pathname);
var alias = dbs[i].split('@').shift();
var name = (l.name || 'layer-' + index) + '-attach-' + alias;
- var index = i;
+ var db_index = i;
- var symlink = function(filepath, cb) {
+ var symlink_db = function(filepath, cb) {
var filename = path.join(base, 'layers', name + extname);
- dbs[index] = alias + '@' + filename;
- utils.forcelink(filepath, filename, cb);
+ dbs[db_index] = alias + '@' + filename;
+ file_linking_method(filepath, filename, {cache:cache}, cb);
};
// URL.
if (file.protocol) {
var filepath = path.join(cache, cachepath(file.href));
- localize(file.href, filepath, function(err) {
+ localize(file.href, {filepath:filepath,name:name}, function(err) {
if (err) return next(err);
- symlink(filepath, next);
+ if (nosymlink) {
+ dbs[db_index] = alias + '@' + filepath;
+ next();
+ } else {
+ symlink_db(filepath, next);
+ }
});
}
// Absolute path.
- else if (pathname[0] === path_sep) {
- symlink(pathname, next);
+ else if (pathname && !isRelative(pathname)) {
+ if (nosymlink) {
+ dbs[db_index] = alias + '@' + pathname;
+ next();
+ } else {
+ symlink_db(pathname, next);
+ }
}
// Local path.
else {
- dbs[index] = alias + '@' + path.join(base, pathname);
+ dbs[db_index] = alias + '@' + path.join(base, pathname);
next();
}
})(group());
}, function(err) {
- if (err) throw err;
+ if (err) return next(err);
d.attachdb = dbs.join(',');
return next(err);
});
@@ -429,102 +834,132 @@ function resolve(options, callback) {
resolved.Layer.forEach(function(l, index) {
var d = l.Datasource;
var next = group();
-
- Step(function() {
- var ext = path.extname(d.file);
- var next = this;
- if (ext) {
- next(null, ext);
+ if (!d.file) return next();
+ existsAsync(d.file, function(exists) {
+ if (!exists) {
+ // https://github.com/mapbox/tilemill/issues/1808
+ // on OS X broken symlinks can be read and resolved but actually
+ // do not "exist" so here we try to resolve in order to avoid
+ // providing a confusing error that says a file does not exist
+ // when it actually does (and is just a broken link)
+ fs.readlink(d.file,function(err,resolvedPath){
+ if (resolvedPath) {
+ return next(new Error("File not found: '" +
+ resolvedPath + "' (broken symlink: '" +
+ d.file + "')"));
+ } else {
+ return next(new Error("File not found: '" + d.file + "'"));
+ }
+ });
} else {
- // This file doesn't have an extension, so we look for a
- // hidden metadata file that will contain headers for the
- // original HTTP request. We looks at the
- // `content-disposition` header to determine the extension.
- fs.readlink(l.Datasource.file, function(err, resolvedPath) {
- var metafile = metapath(resolvedPath);
- path.exists(metafile , function(exists) {
- if (!exists) return next(new Error('Metadata file does not exist.'));
- fs.readFile(metafile, 'utf-8', function(err, data) {
- if (err) return next(err);
+ Step(function() {
+ var ext = path.extname(d.file);
+ var next = this;
+ if (ext) {
+ next(null, ext);
+ } else {
+ // This file doesn't have an extension, so we look for a
+ // hidden metadata file that will contain headers for the
+ // original HTTP request. We look at the
+ // `content-disposition` header to determine the extension.
+ fs.lstat(l.Datasource.file, function(err, stats) {
+ if (err && err.code != 'ENOENT') {
+ next(err);
+ } else {
+ if (!stats.isSymbolicLink()) {
+ readExtension(l.Datasource.file, next);
+ } else {
+ fs.readlink(l.Datasource.file, function(err, resolvedPath) {
+ if (resolvedPath && isRelative(resolvedPath)) {
+ resolvedPath = path.join(path.dirname(l.Datasource.file), resolvedPath);
+ }
+ readExtension(resolvedPath, next);
+ });
+ }
+ }
+ });
+ }
+ }, function(err, ext) {
+ // Ignore errors during extension checks above and let a
+ // missing extension fall through to a missing `type`.
+ var name = l.name || 'layer-' + index;
+ ext = ext || path.extname(d.file);
+ d.type = d.type || valid_ds_extensions[ext.toLowerCase()];
+ switch (ext.toLowerCase()) {
+ case '.vrt':
+ // we default to assuming gdal raster for vrt's
+ // but we need to support OGRVRTLayer as well
+ try {
+ var vrt_file = fs.readFileSync(d.file, 'utf8').toString();
+ if (vrt_file.indexOf('OGRVRTLayer') != -1) {
+ d.type = 'ogr';
+ d.layer_by_index = 0;
+ if (!l.srs) {
+ var match = vrt_file.match(/<LayerSRS>(.+)<\/LayerSRS>/);
+ if (match && match[1]) {
+ var srs_parsed = srs.parse(match[1]);
+ l.srs = srs_parsed.proj4;
+ }
+ }
+ }
+ } catch (e) {
+ if (env == 'development') console.error('failed to open vrt file: ' + e.message);
+ }
+ break;
+ case '.csv':
+ case '.tsv':
+ case '.txt':
+ case '.osm':
+ case '.gpx':
+ l.srs = l.srs || SRS.WGS84;
+ break;
+ case '.geojson':
+ case '.json':
+ d.layer_by_index = 0;
+ try {
+ l.srs = l.srs || srs.parse(d.file).proj4;
+ } catch (e) {
+ next(new Error("Could not parse: '" + d.file + "': error: '" + e.message + "'"));
+ }
+ break;
+ case '.kml':
+ case '.rss':
+ d.layer_by_index = 0;
+ l.srs = SRS.WGS84;
+ break;
+ }
+ // at this point if we do not know the 'type' of mapnik
+ // plugin to dispatch to we are out of luck and there is no
+ // need to check for the projection
+ if (!d.type) {
+ return next(new Error("Could not detect datasource type for: '"+d.file+"'"))
+ }
+ if (l.srs) return next();
+ var error = new Error('Unable to determine SRS for layer "' + name + '" at ' + d.file);
+ if (d.type !== 'shape') {
+ // If we don't have a projection by now, bail out unless we have a shapefile.
+ return next(error);
+ } else {
+ // Special handling that opens .prj files for shapefiles.
+ var prj_path = path.join(
+ path.dirname(d.file),
+ path.basename(d.file, path.extname(d.file)) + '.prj'
+ );
+ fs.readFile(prj_path, 'utf8', function(err, data) {
+ if (err && err.code === 'ENOENT') {
+ return next(error);
+ } else if (err) {
+ return next(err);
+ }
try {
- ext = guessExtension(JSON.parse(data));
- next(null, ext);
+ l.srs = l.srs || srs.parse(data).proj4;
+ l.srs = l.srs || srs.parse('ESRI::' + data).proj4; // See issue #26.
} catch (e) {
next(e);
}
+ next(l.srs ? null : error);
});
- });
- });
- }
- }, function(err, ext) {
- // Ignore errors during extension checks above and let a
- // missing extension fall through to a missing `type`.
-
- var name = l.name || 'layer-' + index;
-
- var ext = ext || path.extname(d.file);
- switch (ext) {
- case '.csv':
- case '.tsv': // google refine uses tsv for tab-delimited
- case '.txt': // resonable assumption that .txt is csv?
- d.quiet = d.quiet || true; // Supress verbose mapnik error reporting by default.
- d.type = d.type || 'csv';
- l.srs = l.srs || SRS.WGS84;
- break;
- case '.shp':
- case '.zip':
- d.type = d.type || 'shape';
- break;
- case '.geotiff':
- case '.geotif':
- case '.vrt':
- case '.tiff':
- case '.tif':
- d.type = d.type || 'gdal';
- break;
- case '.geojson':
- case '.json':
- d.type = d.type || 'ogr';
- d.layer_by_index = 0;
- l.srs = l.srs || srs.parse(d.file).proj4;
- break;
- case '.kml':
- case '.rss':
- d.type = d.type || 'ogr';
- d.layer_by_index = 0;
- // WGS84 is the only valid SRS for KML and RSS so we force
- // it here.
- l.srs = SRS.WGS84;
- break;
- }
-
- if (l.srs) return next();
-
- var error = new Error('Unable to determine SRS for layer "' + name + '" at ' + d.file);
- if (d.type !== 'shape') {
- // If we don't have a projection by now, bail out unless we have a shapefile.
- return next(error);
- } else {
- // Special handling that opens .prj files for shapefiles.
- var prj_path = path.join(
- path.dirname(d.file),
- path.basename(d.file, path.extname(d.file)) + '.prj'
- );
- fs.readFile(prj_path, 'utf8', function(err, data) {
- if (err && err.code === 'ENOENT') {
- return next(error);
- } else if (err) {
- return next(err);
- }
-
- try {
- l.srs = l.srs || srs.parse(data).proj4;
- l.srs = l.srs || srs.parse('ESRI::' + data).proj4; // See issue #26.
- } catch (e) {
- next(e);
}
-
- next(l.srs ? null : error);
});
}
});
@@ -534,8 +969,9 @@ function resolve(options, callback) {
resolved.srs = resolved.srs || SRS['900913'];
fixSRS(resolved);
resolved.Layer.forEach(fixSRS);
-
- callback(err, resolved);
+ if (!err && env == 'development') console.error("[millstone] finished processing '" + options.base + "'");
+ if (Object.keys(downloads).length < 1) clearInterval(download_log_interval);
+ return callback(err, resolved);
});
}
@@ -577,13 +1013,13 @@ function flush(options, callback) {
}
}, function removeCache(err) {
if (err) throw err;
- path.exists(filepath, function(exists) {
+ existsAsync(filepath, function(exists) {
if (!exists) return this();
utils.rm(filepath, this);
}.bind(this));
}, function removeMetafile(err) {
if (err) throw err;
- path.exists(metapath(filepath), function(exists) {
+ existsAsync(metapath(filepath), function(exists) {
if (!exists) return this();
utils.rm(metapath(filepath), this);
}.bind(this));
@@ -592,8 +1028,25 @@ function flush(options, callback) {
});
}
+function drainPool(callback) {
+ clearInterval(download_log_interval);
+ if (pool) {
+ pool.drain(function() {
+ pool.destroyAllNow(function() {
+ return callback();
+ });
+ });
+ }
+}
+
module.exports = {
resolve: resolve,
- flush: flush
+ flush: flush,
+ isRelative: isRelative,
+ guessExtension: guessExtension,
+ downloads: downloads,
+ valid_ds_extensions: valid_ds_extensions,
+ valid_marker_extensions: valid_marker_extensions,
+ drainPool:drainPool
};
diff --git a/lib/util.js b/lib/util.js
index 7732730..3313967 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -1,8 +1,9 @@
var fs = require('fs'),
path = require('path'),
_ = require('underscore'),
- Step = require('step'),
- mkdirP = require('mkdirp');
+ Step = require('step');
+
+var env = process.env.NODE_ENV || 'development';
// Recursive rm.
function rm(filepath, callback) {
@@ -31,34 +32,160 @@ function rm(filepath, callback) {
// Like fs.symlink except that it will overwrite stale symlinks at the
// given path if it exists.
-function forcelink(linkdata, path, callback) {
- fs.lstat(path, function(err, stat) {
+function forcelink(src, dest, options, callback) {
+ if (!options || !options.cache) throw new Error('options.cache not defined!');
+ if (!callback) throw new Error('callback not defined!');
+ // uses relative path if linking to cache dir
+ if (path.relative) {
+ src = path.relative(options.cache, dest).slice(0, 2) !== '..' ? path.relative(path.dirname(dest), src) : src;
+ }
+ fs.lstat(dest, function(err, stat) {
// Error.
- if (err && err.code !== 'ENOENT')
+ if (err && err.code !== 'ENOENT') {
return callback(err);
+ }
// Path does not exist. Symlink.
- if (err && err.code === 'ENOENT')
- return fs.symlink(linkdata, path, callback);
+ if (err && err.code === 'ENOENT') {
+ if (env == 'development') console.error("[millstone] linking '" + dest + "' -> '" + src + "'");
+ return fs.symlink(src, dest, callback);
+ }
// Path exists and is not a symlink. Do nothing.
- if (!stat.isSymbolicLink()) return callback();
+ if (!stat.isSymbolicLink()) {
+ if (env == 'development') console.error("[millstone] skipping re-linking '" + src + "' because '" + dest + " is already an existing file");
+ return callback();
+ }
- // Path exists and is a symlink. Check existing link path and update
- // if necessary.
- fs.readlink(path, function(err, old) {
+ // Path exists and is a symlink. Check existing link path and update if necessary.
+ // NOTE : broken symlinks will pass through this step
+ fs.readlink(dest, function(err, old) {
if (err) return callback(err);
- if (old === linkdata) return callback();
- fs.unlink(path, function(err) {
+ if (old === src) return callback();
+ fs.unlink(dest, function(err) {
if (err) return callback(err);
- fs.symlink(linkdata, path, callback);
+ if (env == 'development') console.error("[millstone] re-linking '" + dest + "' -> '" + src + "'");
+ fs.symlink(src, dest, callback);
});
});
});
}
+function copyStream(src,dest,callback) {
+ var src_stream = fs.createReadStream(src);
+ src_stream.on('error', function(err) {
+ return callback(err);
+ });
+ var dest_stream = fs.createWriteStream(dest);
+ dest_stream.on('error', function(err) {
+ return callback(err);
+ });
+ src_stream.on('end', function() {
+ if (env == 'development') console.error("[millstone] finished copying '" + src + "' to '" + dest + "'");
+ return callback(null);
+ });
+ src_stream.pipe(dest_stream);
+}
+
+function copy(src, dest, options, callback) {
+ if (!options) throw new Error('options not defined!');
+ if (!callback) throw new Error('callback not defined!');
+ fs.lstat(dest, function(err, dest_stat) {
+ // Error.
+ if (err && err.code !== 'ENOENT'){
+ return callback(err);
+ }
+
+ // Dest path does not exist. Copy.
+ if (err && err.code === 'ENOENT') {
+ if (env == 'development') console.error("[millstone] attempting to copy '" + src + "' to '" + dest + "'");
+ return copyStream(src,dest,callback);
+ }
+
+ // Path exists and is a symlink. Do nothing.
+ if (dest_stat.isSymbolicLink()) {
+ if (env == 'development') console.error("[millstone] skipping copying '" + src + "' because '" + dest + " is an existing symlink");
+ return callback();
+ }
+
+ // Dest path exists and is a file. Check if it needs updating
+ fs.lstat(src, function(err,src_stat) {
+ // Error.
+ if (err) {
+ return callback(err);
+ }
+ // NOTE: we intentially do not compare the STAT exactly
+ // because some windows users will be used to editing files
+ // after they are copied. In future releases we should consider
+ // simply doing if (src_stat != dest_stat)
+ // check size to dodge potentially corrupt data
+ // https://github.com/mapbox/tilemill/issues/1674
+ if ((src_stat.size != dest_stat.size && src_stat.mtime < dest_stat.mtime) || src_stat.mtime > dest_stat.mtime) {
+ if (env == 'development') console.error("[millstone] attempting to re-copy '" + src + "' to '" + dest + "'");
+ return copyStream(src,dest,callback);
+ } else {
+ return callback();
+ }
+ });
+ });
+}
+
+
+function processFiles(src, dest, fn, options, callback) {
+ if (!options) throw new Error('options not defined!');
+ if (!fn) throw new Error('link function not defined!');
+ if (!callback) throw new Error('callback not defined!');
+ var basename = path.basename(src, path.extname(src)),
+ srcdir = path.dirname(src),
+ destdir = path.dirname(dest);
+
+ // Group multiple calls
+ var remaining,
+ err;
+ function done(err) {
+ remaining --;
+ if (!remaining) callback(err);
+ }
+
+ function forcemkdir(dir, callback) {
+ fs.lstat(dir, function(err, stat) {
+ if (err && err.code !== 'ENOENT') {
+ return callback(err);
+ } else if (!err && stat.isSymbolicLink()) {
+ return fs.unlink(dir, function(err) {
+ if (err) return callback(err);
+ fs.mkdir(dir, 0755, callback);
+ });
+
+ } else {
+ return fs.mkdir(dir, 0755, callback);
+ }
+ });
+ }
+
+ function processFiltered(err, files) {
+ if (err) return callback(err);
+
+ files = files.filter(function(f) {
+ return path.basename(f, path.extname(f)) === basename;
+ });
+ if (files.length < 1) return callback(err);
+
+ remaining = files.length;
+
+ files.forEach(function(f) {
+ fn(path.join(srcdir, f), path.join(destdir, f), options, done);
+ });
+ }
+
+ forcemkdir(destdir, function() {
+ fs.readdir(srcdir, processFiltered);
+ });
+}
+
module.exports = {
- mkdirP: mkdirP,
forcelink: forcelink,
- rm: rm
+ rm: rm,
+ copy: copy,
+ processFiles: processFiles
};
diff --git a/package.json b/package.json
index af8367d..b4f3328 100644
--- a/package.json
+++ b/package.json
@@ -1,36 +1,39 @@
{
"name": "millstone",
- "version": "0.4.0",
+ "version": "0.6.8",
"main": "./lib/millstone.js",
"description": "Prepares datasources in an MML file for consumption in Mapnik",
"url": "https://github.com/mapbox/millstone",
"licenses": [{ "type": "BSD" }],
- "repositories": [{
+ "repository": {
"type": "git",
"url": "git://github.com/mapbox/millstone.git"
- }],
+ },
"author": {
"name": "MapBox",
"url": "http://mapbox.com/",
"email": "info at mapbox.com"
},
"dependencies": {
- "underscore" : "1.1.x",
- "step": "0.0.x",
- "generic-pool": "1.0.x",
- "get": "~1.1.3",
- "srs": "~0.2.12",
- "zipfile": "0.x",
+ "underscore": "~1.5.1",
+ "step": "~0.0.5",
+ "generic-pool": "~2.0.3",
+ "request": "~2.26.0",
+ "srs": "~0.3.6",
+ "zipfile": "~0.4.2",
"sqlite3": "2.x",
- "mime": ">= 0.0.1",
- "mkdirp": "~0.3.0"
+ "mime": "~1.2.9",
+ "mkdirp": "~0.3.3",
+ "optimist": "~0.6.0"
},
"devDependencies": {
- "expresso": "0.9.x"
+ "mocha": "*"
+ },
+ "bin": {
+ "millstone": "./bin/millstone"
},
"scripts": {
- "pretest": "which expresso || npm install --dev",
- "test": "which expresso | sh"
+ "test": "mocha -R spec --timeout 10000"
},
- "engines": { "node": "0.4 || 0.6" }
+ "engines": { "node": "0.4 || 0.6 || 0.8 || 0.10" }
}
diff --git a/test/UPPERCASE_EXT/project.mml b/test/UPPERCASE_EXT/project.mml
new file mode 100644
index 0000000..4646e93
--- /dev/null
+++ b/test/UPPERCASE_EXT/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "uppercase-ext",
+ "Datasource": {
+ "file": "test1.CSV"
+ }
+ }
+ ]
+}
diff --git a/test/UPPERCASE_EXT/style.mss b/test/UPPERCASE_EXT/style.mss
new file mode 100644
index 0000000..230a97c
--- /dev/null
+++ b/test/UPPERCASE_EXT/style.mss
@@ -0,0 +1 @@
+#polygon { }
\ No newline at end of file
diff --git a/test/UPPERCASE_EXT/test1.CSV b/test/UPPERCASE_EXT/test1.CSV
new file mode 100644
index 0000000..86e3501
--- /dev/null
+++ b/test/UPPERCASE_EXT/test1.CSV
@@ -0,0 +1,8 @@
+{ "type": "FeatureCollection",
+ "features": [
+ { "type": "Feature",
+ "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
+ "properties": {"prop0": "value0"}
+ },
+ ]
+}
\ No newline at end of file
diff --git a/test/cache/cache.mml b/test/cache/cache.mml
index 2801fcd..e143761 100644
--- a/test/cache/cache.mml
+++ b/test/cache/cache.mml
@@ -2,7 +2,7 @@
"Stylesheet": [
{
"id": "cache-inline.mss",
- "data": "Map { backgroound-color:#fff }"
+ "data": "Map { background-color:#fff }"
},
"cache-local.mss",
"http://mapbox.github.com/millstone/test/cache-url.mss"
@@ -52,7 +52,8 @@
"file": "layers/countries.sqlite",
"type": "sqlite",
"table": "countries"
- }
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
},
{
"name": "sqlite-attach",
@@ -61,6 +62,13 @@
"type": "sqlite",
"table": "countries",
"attachdb": "data at layers/data.sqlite"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ },
+ {
+ "name": "zip-no-ext",
+ "Datasource": {
+ "file": "http://fakeurl.com/zip_no_ext"
}
}
]
diff --git a/test/corrupt-zip.test.js b/test/corrupt-zip.test.js
new file mode 100644
index 0000000..ff6b211
--- /dev/null
+++ b/test/corrupt-zip.test.js
@@ -0,0 +1,57 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+
+// NOTE: watch out, this zip has both a csv and shape in it and uses
+// non-ascii characters - idea being to be the basis for other tests
+// https://github.com/mapbox/millstone/issues/85
+it('correctly handles re-downloading a zip that is invalid in its cached state', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'corrupt-zip/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'corrupt-zip'),
+ cache: cache
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ // write bogus data over the zip archive to simulate a corrupt cache
+ if (!existsSync('/tmp/millstone-test/29f2b277-Cle%CC%81ment/')) fs.mkdirSync('/tmp/millstone-test/29f2b277-Cle%CC%81ment/')
+ fs.writeFileSync('/tmp/millstone-test/29f2b277-Cle%CC%81ment/29f2b277-Cle%CC%81ment.zip','');
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.equal(resolved.Stylesheet[0].id, 'style.mss');
+ assert.equal(resolved.Stylesheet[0].data, '#polygon { }');
+ var expected = [
+ {
+ "name": "corrupt-zip",
+ "Datasource": {
+ "file": path.join(__dirname, 'corrupt-zip/layers/corrupt-zip/29f2b277-Cle%CC%81ment.shp'),
+ "type": "shape"
+ },
+ "srs": '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ }
+ ];
+ assert.deepEqual(resolved.Layer, expected);
+ done();
+ });
+});
diff --git a/test/corrupt-zip/project.mml b/test/corrupt-zip/project.mml
new file mode 100644
index 0000000..5418861
--- /dev/null
+++ b/test/corrupt-zip/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "corrupt-zip",
+ "Datasource": {
+ "file": "https://github.com/mapbox/millstone/raw/gh-pages/test/Cle%CC%81ment.zip"
+ }
+ }
+ ]
+}
diff --git a/test/corrupt-zip/style.mss b/test/corrupt-zip/style.mss
new file mode 100644
index 0000000..230a97c
--- /dev/null
+++ b/test/corrupt-zip/style.mss
@@ -0,0 +1 @@
+#polygon { }
\ No newline at end of file
diff --git a/test/data/9368bdd9-zip_no_ext/.9368bdd9-zip_no_ext b/test/data/9368bdd9-zip_no_ext/.9368bdd9-zip_no_ext
new file mode 100644
index 0000000..693a9ae
--- /dev/null
+++ b/test/data/9368bdd9-zip_no_ext/.9368bdd9-zip_no_ext
@@ -0,0 +1 @@
+{"content-disposition":"attachment; filename=places_by_lon_lat.zip"}
diff --git a/test/data/9368bdd9-zip_no_ext/9368bdd9-zip_no_ext b/test/data/9368bdd9-zip_no_ext/9368bdd9-zip_no_ext
new file mode 100644
index 0000000..a55016d
Binary files /dev/null and b/test/data/9368bdd9-zip_no_ext/9368bdd9-zip_no_ext differ
diff --git a/test/data/ne_10m_admin_0_boundary_lines_disputed_areas.zip b/test/data/ne_10m_admin_0_boundary_lines_disputed_areas.zip
new file mode 100644
index 0000000..4e8705c
Binary files /dev/null and b/test/data/ne_10m_admin_0_boundary_lines_disputed_areas.zip differ
diff --git a/test/data/snow-cover.tif b/test/data/snow-cover.tif
new file mode 100644
index 0000000..239fad9
Binary files /dev/null and b/test/data/snow-cover.tif differ
diff --git a/test/error.test.js b/test/error.test.js
new file mode 100644
index 0000000..8fba15f
--- /dev/null
+++ b/test/error.test.js
@@ -0,0 +1,64 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+it('correctly handles invalid json', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'invalid-json/project.mml')));
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'invalid-json'),
+ cache: '/tmp/millstone-test'
+ };
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.ok(err.message.search("error: 'Unexpected token ]'") != -1);
+ done();
+ });
+});
+
+it('correctly handles missing shapefile at relative path', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'missing-file-relative/project.mml')));
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'missing-file-relative'),
+ cache: '/tmp/millstone-test'
+ };
+
+ millstone.resolve(options, function(err, resolved) {
+ var err_expected = err.message.search("File not found:") != -1 || err.message.search("Can't open") != -1;
+ assert.ok(err_expected);
+ done();
+ });
+});
+
+
+it('correctly handles missing shapefile at absolute path', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'missing-file-absolute/project.mml')));
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'missing-file-absolute'),
+ cache: '/tmp/millstone-test'
+ };
+
+ millstone.resolve(options, function(err, resolved) {
+ var err_expected = err.message.search("File not found:") != -1 || err.message.search("Can't open") != -1;
+ assert.ok(err_expected);
+ done();
+ });
+});
\ No newline at end of file
diff --git a/test/image-noext.test.js b/test/image-noext.test.js
new file mode 100644
index 0000000..0fe4416
--- /dev/null
+++ b/test/image-noext.test.js
@@ -0,0 +1,39 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+it('correctly handles images with no extension', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'image-noext/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'image-noext'),
+ cache: cache
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.equal(resolved.Stylesheet[0].id, 'style.mss');
+ assert.equal(resolved.Stylesheet[0].data, "Map {background-image: url('/tmp/millstone-test/2b2cf79a-images/2b2cf79a-images.jpeg');}");
+ assert.ok(existsSync('/tmp/millstone-test/2b2cf79a-images/2b2cf79a-images.jpeg'));
+ done();
+ });
+});
diff --git a/test/image-noext/project.mml b/test/image-noext/project.mml
new file mode 100644
index 0000000..ca6f060
--- /dev/null
+++ b/test/image-noext/project.mml
@@ -0,0 +1,14 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "background-image-noext",
+ "Datasource": {
+ "type": "csv",
+ "inline": "x,y\n0,0"
+ }
+ }
+ ]
+}
diff --git a/test/image-noext/style.mss b/test/image-noext/style.mss
new file mode 100644
index 0000000..21b83db
--- /dev/null
+++ b/test/image-noext/style.mss
@@ -0,0 +1 @@
+Map {background-image: url('http://t2.gstatic.com/images?q=tbn:ANd9GcQu_OWjgK-0EgHE2kBp9aaGbPMuQ77tuyhbMOybMZ_FFY6TrUAL3wzh0xw7Yw');}
\ No newline at end of file
diff --git a/test/invalid-json/broken.json b/test/invalid-json/broken.json
new file mode 100644
index 0000000..86e3501
--- /dev/null
+++ b/test/invalid-json/broken.json
@@ -0,0 +1,8 @@
+{ "type": "FeatureCollection",
+ "features": [
+ { "type": "Feature",
+ "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
+ "properties": {"prop0": "value0"}
+ },
+ ]
+}
\ No newline at end of file
diff --git a/test/invalid-json/project.mml b/test/invalid-json/project.mml
new file mode 100644
index 0000000..f9b6585
--- /dev/null
+++ b/test/invalid-json/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "polygons-zipped",
+ "Datasource": {
+ "file": "broken.json"
+ }
+ }
+ ]
+}
diff --git a/test/invalid-json/style.mss b/test/invalid-json/style.mss
new file mode 100644
index 0000000..230a97c
--- /dev/null
+++ b/test/invalid-json/style.mss
@@ -0,0 +1 @@
+#polygon { }
\ No newline at end of file
diff --git a/test/macosx-zipped.test.js b/test/macosx-zipped.test.js
new file mode 100644
index 0000000..231eff9
--- /dev/null
+++ b/test/macosx-zipped.test.js
@@ -0,0 +1,41 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+before(function(){
+ rm('/tmp/millstone-test');
+});
+
+it('correctly handles mac os x zipped archives with the lame __MACOSX/ subfolder', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'macosx-zipped/project.mml')));
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'macosx-zipped'),
+ cache: '/tmp/millstone-test'
+ };
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.deepEqual(resolved.Layer, [
+ {
+ "name": "points",
+ "Datasource": {
+ "file": path.join(__dirname, 'macosx-zipped/layers/points/9afe4795-crashes_2007_2009.shp'),
+ "type": "shape"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ }
+ ]);
+ done();
+ });
+});
\ No newline at end of file
diff --git a/test/macosx-zipped/project.mml b/test/macosx-zipped/project.mml
new file mode 100644
index 0000000..4464d23
--- /dev/null
+++ b/test/macosx-zipped/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "points",
+ "Datasource": {
+ "file": "http://cartodb.s3.amazonaws.com/static/crashes_2007_2009.zip"
+ }
+ }
+ ]
+}
diff --git a/test/macosx-zipped/style.mss b/test/macosx-zipped/style.mss
new file mode 100644
index 0000000..c713d24
--- /dev/null
+++ b/test/macosx-zipped/style.mss
@@ -0,0 +1 @@
+#points { }
\ No newline at end of file
diff --git a/test/markers.test.js b/test/markers.test.js
new file mode 100644
index 0000000..db66c91
--- /dev/null
+++ b/test/markers.test.js
@@ -0,0 +1,73 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+before(function(){
+ rm('/tmp/millstone-test');
+});
+
+
+it('correctly localizes remote image/svg files', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'markers/project.mml')));
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'markers'),
+ cache: '/tmp/millstone-test'
+ };
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.equal(resolved.Stylesheet[0].id, 'style.mss');
+ assert.equal(resolved.Stylesheet[0].data, '// a url like https:example.com in the comments\n#points { one/marker-file: url(\'/tmp/millstone-test/e33af80e-Cup_of_coffee.svg\'); two/marker-file: url(\'/tmp/millstone-test/e33af80e-Cup_of_coffee.svg\'); four/marker-file: url("/tmp/millstone-test/c953e0d1-pin-m-fast-food+AA0000.png"); five/marker-file:url("/tmp/millstone-test/7b9b9979-fff&text=x/7b9b9979-fff&text=x.png"); }\n');
+ assert.deepEqual(resolved.Layer, [
+ {
+ "name": "points",
+ "Datasource": {
+ "file": path.join(__dirname, 'markers/layers/points.csv'),
+ "type": "csv"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ }
+ ]);
+ done();
+ });
+});
+
+it('correctly localizes zipped json', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'zipped-json/project.mml')));
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'zipped-json'),
+ cache: '/tmp/millstone-test'
+ };
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.equal(resolved.Stylesheet[0].id, 'style.mss');
+ assert.equal(resolved.Stylesheet[0].data, '#polygon { }');
+ var expected = [
+ {
+ "name": "polygons-zipped",
+ "Datasource": {
+ "file": path.join(__dirname, 'zipped-json/layers/polygons-zipped/7e482cc8-polygons.json.json'),
+ "type": "ogr",
+ "layer_by_index": 0
+ },
+ "srs": '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ }
+ ];
+ assert.deepEqual(resolved.Layer, expected);
+ done();
+ });
+});
diff --git a/test/markers/layers/points.csv b/test/markers/layers/points.csv
new file mode 100644
index 0000000..51cb66f
--- /dev/null
+++ b/test/markers/layers/points.csv
@@ -0,0 +1,2 @@
+x,y,name
+0,0,"null island"
\ No newline at end of file
diff --git a/test/markers/project.mml b/test/markers/project.mml
new file mode 100644
index 0000000..ea5f18c
--- /dev/null
+++ b/test/markers/project.mml
@@ -0,0 +1,14 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "points",
+ "Datasource": {
+ "type": "csv",
+ "file": "layers/points.csv"
+ }
+ }
+ ]
+}
diff --git a/test/markers/style.mss b/test/markers/style.mss
new file mode 100644
index 0000000..4e1e67d
--- /dev/null
+++ b/test/markers/style.mss
@@ -0,0 +1,2 @@
+// a url like https:example.com in the comments
+#points { one/marker-file: url('http://upload.wikimedia.org/wikipedia/commons/7/72/Cup_of_coffee.svg'); two/marker-file: url('http://upload.wikimedia.org/wikipedia/commons/7/72/Cup_of_coffee.svg'); four/marker-file: url("http://a.tiles.mapbox.com/v3/marker/pin-m-fast-food+AA0000.png"); five/marker-file:url("http://dummyimage.com/16x16/000/fff&text=x"); }
diff --git a/test/missing-file-absolute/project.mml b/test/missing-file-absolute/project.mml
new file mode 100644
index 0000000..1ddf11b
--- /dev/null
+++ b/test/missing-file-absolute/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "missing-file-relative",
+ "Datasource": {
+ "file": "/missing.shp"
+ }
+ }
+ ]
+}
diff --git a/test/missing-file-absolute/style.mss b/test/missing-file-absolute/style.mss
new file mode 100644
index 0000000..230a97c
--- /dev/null
+++ b/test/missing-file-absolute/style.mss
@@ -0,0 +1 @@
+#polygon { }
\ No newline at end of file
diff --git a/test/missing-file-relative/project.mml b/test/missing-file-relative/project.mml
new file mode 100644
index 0000000..7d49cc8
--- /dev/null
+++ b/test/missing-file-relative/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "missing-file-relative",
+ "Datasource": {
+ "file": "missing.shp"
+ }
+ }
+ ]
+}
diff --git a/test/missing-file-relative/style.mss b/test/missing-file-relative/style.mss
new file mode 100644
index 0000000..230a97c
--- /dev/null
+++ b/test/missing-file-relative/style.mss
@@ -0,0 +1 @@
+#polygon { }
\ No newline at end of file
diff --git a/test/multi-shape-zip.test.js b/test/multi-shape-zip.test.js
new file mode 100644
index 0000000..20d3789
--- /dev/null
+++ b/test/multi-shape-zip.test.js
@@ -0,0 +1,48 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+// https://github.com/mapbox/millstone/issues/99
+it('correctly handles a zipfile containing multiple shapefiles without corrupting data', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'multi-shape-zip/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'multi-shape-zip'),
+ cache: cache
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ var expected = [
+ {
+ "name": "multi-shape-zip",
+ "Datasource": {
+ "file": path.join(__dirname, 'multi-shape-zip/layers/multi-shape-zip/134ecf39-PLATES_PlateBoundary_ArcGIS.shp'),
+ "type": "shape"
+ },
+ "srs": '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ }
+ ];
+ assert.deepEqual(resolved.Layer, expected);
+ done();
+ });
+});
diff --git a/test/multi-shape-zip/project.mml b/test/multi-shape-zip/project.mml
new file mode 100644
index 0000000..889b579
--- /dev/null
+++ b/test/multi-shape-zip/project.mml
@@ -0,0 +1,11 @@
+{
+ "Stylesheet": [],
+ "Layer": [
+ {
+ "name": "multi-shape-zip",
+ "Datasource": {
+ "file": "https://github.com/mapbox/millstone/raw/gh-pages/test/PLATES_PlateBoundary_ArcGIS.zip"
+ }
+ }
+ ]
+}
diff --git a/test/nosymlink.test.js b/test/nosymlink.test.js
new file mode 100644
index 0000000..e57b5cf
--- /dev/null
+++ b/test/nosymlink.test.js
@@ -0,0 +1,102 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm('/tmp/millstone-test');
+});
+
+
+it('correctly handles files without symlinking', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'nosymlink/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ mml.Layer[4].Datasource.file = path.join(cache, "pshape.zip");
+
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'nosymlink'),
+ cache: cache,
+ nosymlink:true
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ try {
+ var newFile = fs.createWriteStream(path.join(options.cache, 'pshape.zip'));
+ var oldFile = fs.createReadStream(path.join(__dirname, 'nosymlink/pshape.zip'));
+ oldFile.pipe(newFile);
+ } catch (e) {console.log(e)}
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.equal(resolved.Stylesheet[0].id, 'style.mss');
+ var expected = [
+ {
+ "name": "one",
+ "Datasource": {
+ "file": path.join(__dirname, 'nosymlink/points.csv'),
+ "type": "csv"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ },
+ {
+ "name": "two",
+ "Datasource": {
+ "file": path.join(__dirname, "nosymlink/pshape.shp"),
+ "type": "shape"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ },
+ {
+ "name": "three",
+ "Datasource": {
+ "file": path.join(__dirname, "nosymlink/pshape.shp"),
+ "type": "shape"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ },
+ {
+ "name": "four",
+ "Datasource": {
+ "file": path.join(__dirname, "nosymlink/points.vrt"),
+ "type": "ogr",
+ "layer_by_index": 0
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ },
+ {
+ "name": "five",
+ "Datasource": {
+ "file": path.join(options.cache, "pshape.shp"),
+ "type": "shape"
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ }
+ ];
+ for (var i=0;i<=expected.length;i++) {
+ assert.deepEqual(resolved.Layer[i], expected[i]);
+ }
+ done();
+ });
+});
+
+after(function() {
+ // cleanup
+ rm(path.join(__dirname, 'nosymlink','pshape.shp'));
+ rm(path.join(__dirname, 'nosymlink','pshape.dbf'));
+ rm(path.join(__dirname, 'nosymlink','pshape.prj'));
+ rm(path.join(__dirname, 'nosymlink','pshape.shx'));
+ rm(path.join(__dirname, 'nosymlink','.pshape.zip'));
+})
diff --git a/test/nosymlink/points.csv b/test/nosymlink/points.csv
new file mode 100644
index 0000000..51cb66f
--- /dev/null
+++ b/test/nosymlink/points.csv
@@ -0,0 +1,2 @@
+x,y,name
+0,0,"null island"
\ No newline at end of file
diff --git a/test/nosymlink/points.dbf b/test/nosymlink/points.dbf
new file mode 100644
index 0000000..b07eb81
Binary files /dev/null and b/test/nosymlink/points.dbf differ
diff --git a/test/nosymlink/points.prj b/test/nosymlink/points.prj
new file mode 100644
index 0000000..a30c00a
--- /dev/null
+++ b/test/nosymlink/points.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file
diff --git a/test/nosymlink/points.shp b/test/nosymlink/points.shp
new file mode 100644
index 0000000..b3346b7
Binary files /dev/null and b/test/nosymlink/points.shp differ
diff --git a/test/nosymlink/points.shx b/test/nosymlink/points.shx
new file mode 100644
index 0000000..b3346b7
Binary files /dev/null and b/test/nosymlink/points.shx differ
diff --git a/test/nosymlink/points.vrt b/test/nosymlink/points.vrt
new file mode 100644
index 0000000..3df8850
--- /dev/null
+++ b/test/nosymlink/points.vrt
@@ -0,0 +1,9 @@
+<OGRVRTDataSource>
+ <OGRVRTLayer name="points">
+ <relativeToVRT>1</relativeToVRT>
+ <SrcDataSource>points.csv</SrcDataSource>
+ <GeometryType>wkbPoint</GeometryType>
+ <LayerSRS>WGS84</LayerSRS>
+ <GeometryField encoding="PointFromColumns" x="x" y="y"/>
+ </OGRVRTLayer>
+</OGRVRTDataSource>
\ No newline at end of file
diff --git a/test/nosymlink/project.mml b/test/nosymlink/project.mml
new file mode 100644
index 0000000..8cfd84f
--- /dev/null
+++ b/test/nosymlink/project.mml
@@ -0,0 +1,37 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "one",
+ "Datasource": {
+ "file": "points.csv"
+ }
+ },
+ {
+ "name": "two",
+ "Datasource": {
+ "file": "pshape.shp"
+ }
+ },
+ {
+ "name": "three",
+ "Datasource": {
+ "file": "pshape.zip"
+ }
+ },
+ {
+ "name": "four",
+ "Datasource": {
+ "file": "points.vrt"
+ }
+ },
+ {
+ "name": "five",
+ "Datasource": {
+ "file": "DYNAMICALLY FILED IN"
+ }
+ }
+ ]
+}
diff --git a/test/nosymlink/pshape.zip b/test/nosymlink/pshape.zip
new file mode 100644
index 0000000..6cc3f9a
Binary files /dev/null and b/test/nosymlink/pshape.zip differ
diff --git a/test/nosymlink/style.mss b/test/nosymlink/style.mss
new file mode 100644
index 0000000..a71aeca
--- /dev/null
+++ b/test/nosymlink/style.mss
@@ -0,0 +1 @@
+#points { marker-width:6; }
\ No newline at end of file
diff --git a/test/raster-linking.test.js b/test/raster-linking.test.js
new file mode 100644
index 0000000..a7271bc
--- /dev/null
+++ b/test/raster-linking.test.js
@@ -0,0 +1,48 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+// https://github.com/mapbox/millstone/issues/99
+it('correctly handles a zipfile containing multiple shapefiles without corrupting data', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'raster-linking/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'raster-linking'),
+ cache: cache
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ var expected = [
+ {
+ "name": "raster-linking",
+ "Datasource": {
+ "file": path.join(__dirname,"/data/snow-cover.tif"),
+ "type": "gdal"
+ },
+ "srs": "+init=epsg:3857"
+ }
+ ];
+ assert.deepEqual(resolved.Layer, expected);
+ done();
+ });
+});
diff --git a/test/raster-linking/project.mml b/test/raster-linking/project.mml
new file mode 100644
index 0000000..7ad1ce0
--- /dev/null
+++ b/test/raster-linking/project.mml
@@ -0,0 +1,12 @@
+{
+ "Stylesheet": [],
+ "Layer": [
+ {
+ "name": "raster-linking",
+ "Datasource": {
+ "file": "../data/snow-cover.tif"
+ },
+ "srs":"+init=epsg:3857"
+ }
+ ]
+}
diff --git a/test/support.js b/test/support.js
new file mode 100644
index 0000000..542e03f
--- /dev/null
+++ b/test/support.js
@@ -0,0 +1,28 @@
+var fs = require('fs');
+var path = require('path');
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+// Recursive, synchronous rm.
+exports.rm = rm = function(filepath) {
+ if (existsSync(filepath)) {
+ var stat;
+ var files;
+
+ try { stat = fs.lstatSync(filepath); } catch(e) { throw e; }
+
+ // File.
+ if (stat.isFile() || stat.isSymbolicLink()) {
+ return fs.unlinkSync(filepath);
+ // Directory.
+ } else if (stat.isDirectory()) {
+ try { files = fs.readdirSync(filepath); } catch(e) { throw e; }
+ files.forEach(function(file) {
+ try { rm(path.join(filepath, file)); } catch(e) { throw e; }
+ });
+ try { fs.rmdirSync(filepath); } catch(e) { throw e; }
+ // Other?
+ } else {
+ throw new Error('Unrecognized file.');
+ }
+ }
+}
diff --git a/test/test.js b/test/test.js
index c3b7f17..ba6c9c9 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,33 +1,138 @@
var fs = require('fs');
var path = require('path');
var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+
+var utils = require('../lib/util.js');
var millstone = require('../lib/millstone');
var tests = module.exports = {};
+var rm = require('./support.js').rm;
-// Recursive, synchronous rm.
-function rm(filepath) {
- var stat;
- var files;
-
- try { stat = fs.lstatSync(filepath); } catch(e) { throw e };
-
- // File.
- if (stat.isFile() || stat.isSymbolicLink()) {
- return fs.unlinkSync(filepath);
- // Directory.
- } else if (stat.isDirectory()) {
- try { files = fs.readdirSync(filepath); } catch(e) { throw e };
- files.forEach(function(file) {
- try { rm(path.join(filepath, file)); } catch(e) { throw e };
- });
- try { fs.rmdirSync(filepath); } catch(e) { throw e };
- // Other?
- } else {
- throw new Error('Unrecognized file.')
- }
-};
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+
+beforeEach(function(){
+ rm(path.join(__dirname, 'tmp'));
+})
+
+it('correctly detects content-disposition from kml', function() {
+ // https://github.com/mapbox/millstone/issues/37
+ var header = {
+ 'content-disposition':'attachment; filename="New York City\'s Solidarity Economy.kml"'
+ };
+ var res = millstone.guessExtension(header)
+ assert.equal(res,'.kml');
+});
+
+it('correctly detects content-disposition from google docs csv', function() {
+ // google docs
+ var header = {
+ 'content-disposition':'attachment; filename="Untitledspreadsheet.csv"'
+ };
+ var res = millstone.guessExtension(header)
+ assert.equal(res,'.csv');
+});
+
+it('correctly detects content-disposition from geoserver', function() {
+ // https://github.com/mapbox/millstone/issues/27
+ // geoserver
+ var header = {
+ 'content-disposition':"attachment; filename=foo.csv"
+ };
+ var res = millstone.guessExtension(header)
+ assert.equal(res,'.csv');
+});
+
+it('correctly detects content-disposition from cartodb', function() {
+ // cartodb
+ var header = {
+ 'content-disposition':'inline; filename=cartodb-query.geojson; modification-date="Thu, 10 Nov 2011 19:53:40 GMT";'
+ };
+ var res = millstone.guessExtension(header)
+ assert.equal(res,'.geojson');
+});
+
+it('correctly detects content-type bin', function() {
+ var header = {
+ 'content-type':'application/octet-stream'
+ };
+ var res = millstone.guessExtension(header)
+ assert.equal(res,'');
+});
+
+it('correctly detects datacouch csv content-type', function() {
+ var header = {
+ 'content-type':'text/csv; charset=UTF-8'
+ };
+ var res = millstone.guessExtension(header)
+ assert.equal(res,'.csv');
+});
+
+// http://horn.rcmrd.org/data/geonode:Eth_Region_Boundary
+it('correctly detects geonode content-types', function() {
+ assert.equal('.gml',millstone.guessExtension({'content-type':'text/xml; subtype=gml/2.1.2'}));
+ assert.equal('.zip',millstone.guessExtension({'content-type':'application/zip'}));
+ assert.equal('.kml',millstone.guessExtension({'content-type':'application/vnd.google-earth.kml+xml'}));
+ assert.equal('.json',millstone.guessExtension({'content-type':'application/json'}));
+});
+
+describe('isRelative', function() {
+
+ var realPlatform = process.platform; // Store real platform
+
+ afterEach(function() {
+ process.platform = realPlatform;
+ });
+
+
+ it('detects C:\\ as an absolute path on Windows', function() {
+ process.platform = 'win32';
+ var path = 'C:\\some\\path';
+ var res = millstone.isRelative(path);
+ assert.equal(res, false);
+ });
+
+ it('detects C:\\ as relative path on non-Windows', function() {
+ process.platform = 'linux';
+ var path = 'C:\\some\\path';
+ var res = millstone.isRelative(path);
+ assert.equal(res, true);
+ });
+
+ it('detects paths starting with \\ as absolute on Windows', function() {
+ process.platform = 'win32';
+ var path = '\\some\\path';
+ var res = millstone.isRelative(path);
+ assert.equal(res, false);
+ });
+
+ it('detects paths starting with \\ as relative on non-Windows', function() {
+ process.platform = 'linux';
+ var path = '\\some\\path';
+ var res = millstone.isRelative(path);
+ assert.equal(res, true);
+ });
+
+ it('detects paths starting with / as absolute on non-Windows', function() {
+ process.platform = 'linux';
+ var path = '/some/path';
+ var res = millstone.isRelative(path);
+ assert.equal(res, false);
+ });
+
+ it('detects paths starting with / as absolute on Windows', function() {
+ process.platform = 'win32';
+ var path = '/some/path';
+ var res = millstone.isRelative(path);
+ assert.equal(res, false);
+ });
-tests['cache'] = function() {
+});
+
+
+it('correctly caches remote files', function(done) {
var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'cache/cache.mml')));
// Set absolute paths dynamically at test time.
@@ -39,14 +144,36 @@ tests['cache'] = function() {
base: path.join(__dirname, 'cache'),
cache: path.join(__dirname, 'tmp')
};
+
+ // Cleanup from old test runs
+ try {
+ fs.unlinkSync(path.join(__dirname, 'cache/layers/absolute-json.json'));
+ rm(path.join(__dirname, 'cache/layers/absolute-shp'));
+ fs.unlinkSync(path.join(__dirname, 'cache/layers/polygons.json'));
+ fs.unlinkSync(path.join(__dirname, 'cache/layers/csv.csv'));
+ rm(path.join(__dirname, 'cache/layers/zip-no-ext'));
+ } catch (e) {}
+
+ // Copy "cached" files to mock request headers
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ fs.mkdirSync(path.join(options.cache, '9368bdd9-zip_no_ext'),0777);
+ } catch(e) { console.log("mkdirSync failed with: " + e ); }
+ var files = ['9368bdd9-zip_no_ext/9368bdd9-zip_no_ext', '9368bdd9-zip_no_ext/.9368bdd9-zip_no_ext'];
+ for (var i = 0; i < files.length; i++) {
+ var newFile = fs.createWriteStream(path.join(options.cache, files[i]));
+ var oldFile = fs.createReadStream(path.join(__dirname, 'data', files[i]));
+ oldFile.pipe(newFile);
+ }
+
millstone.resolve(options, function(err, resolved) {
- assert.equal(err.message, "Unable to determine SRS for layer \"sqlite-attach\" at " + path.join(__dirname, "cache/layers/countries.sqlite"));
+ if (err) throw err;
assert.deepEqual(resolved.Stylesheet, [
- { id:'cache-inline.mss', data:'Map { backgroound-color:#fff }' },
+ { id:'cache-inline.mss', data:'Map { background-color:#fff }' },
{ id:'cache-local.mss', data: '#world { polygon-fill: #fff }\n' },
{ id:'cache-url.mss', data:'#world { line-width:1; }\n' }
]);
- assert.deepEqual(resolved.Layer, [
+ var expected = [
{
"name": "local-json",
"Datasource": {
@@ -54,7 +181,7 @@ tests['cache'] = function() {
"type": "ogr",
"layer_by_index": 0
},
- "srs": "+proj=longlat +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +no_defs"
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
},
{
"name": "local-shp",
@@ -71,7 +198,7 @@ tests['cache'] = function() {
"type": "ogr",
"layer_by_index": 0
},
- "srs": "+proj=longlat +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +no_defs"
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
},
{
"name": "absolute-shp",
@@ -88,7 +215,7 @@ tests['cache'] = function() {
"type": "ogr",
"layer_by_index": 0
},
- "srs": "+proj=longlat +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +no_defs"
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
},
{
"name": "stations",
@@ -101,8 +228,7 @@ tests['cache'] = function() {
{
"name": "csv",
"Datasource": {
- "file": path.join(__dirname, 'cache/layers/csv'),
- "quiet": true,
+ "file": path.join(__dirname, 'cache/layers/csv.csv'),
"type": "csv"
},
"srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
@@ -113,7 +239,8 @@ tests['cache'] = function() {
"file": path.join(__dirname, 'cache/layers/countries.sqlite'),
"type": 'sqlite',
"table": 'countries',
- }
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
},
{
"name": 'sqlite-attach',
@@ -122,15 +249,27 @@ tests['cache'] = function() {
"type": 'sqlite',
"table": 'countries',
"attachdb": 'data@' + path.join(__dirname, 'cache/layers/data.sqlite'),
- }
+ },
+ "srs": "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+ },
+ {
+ "name": 'zip-no-ext',
+ "Datasource": {
+ "file": path.join(__dirname, 'cache/layers/zip-no-ext/9368bdd9-zip_no_ext.shp'),
+ "type": 'shape'
+ },
+ "srs": '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
}
- ]);
+ ];
+ for (var i=0;i<=10;i++) {
+ assert.deepEqual(resolved.Layer[i], expected[i]);
+ }
// Check that URLs are downloaded and symlinked.
- assert.ok(path.existsSync(path.join(__dirname, 'tmp/5c505ff4-polygons.json')));
- assert.ok(path.existsSync(path.join(__dirname, 'tmp/87c0c757-stations/87c0c757-stations.shp')));
+ assert.ok(existsSync(path.join(__dirname, 'tmp/5c505ff4-polygons.json')));
+ assert.ok(existsSync(path.join(__dirname, 'tmp/87c0c757-stations/87c0c757-stations.shp')));
assert.ok(fs.lstatSync(path.join(__dirname, 'cache/layers/polygons.json')).isSymbolicLink());
- assert.ok(fs.lstatSync(path.join(__dirname, 'cache/layers/stations')).isSymbolicLink());
+ assert.ok(fs.lstatSync(path.join(__dirname, 'cache/layers/stations')).isDirectory());
assert.equal(
fs.readFileSync(path.join(__dirname, 'tmp/5c505ff4-polygons.json'), 'utf8'),
fs.readFileSync(path.join(__dirname, 'cache/layers/polygons.json'), 'utf8')
@@ -142,7 +281,7 @@ tests['cache'] = function() {
// Check that absolute paths are symlinked correctly.
assert.ok(fs.lstatSync(path.join(__dirname, 'cache/layers/absolute-json.json')).isSymbolicLink());
- assert.ok(fs.lstatSync(path.join(__dirname, 'cache/layers/absolute-shp')).isSymbolicLink());
+ assert.ok(fs.lstatSync(path.join(__dirname, 'cache/layers/absolute-shp')).isDirectory());
assert.equal(
fs.readFileSync(path.join(__dirname, 'cache/layers/absolute-json.json'), 'utf8'),
fs.readFileSync(path.join(__dirname, 'data/absolute.json'), 'utf8')
@@ -161,20 +300,54 @@ tests['cache'] = function() {
assert.equal(err, undefined);
// Polygons layer and cache should still exist.
- assert.ok(path.existsSync(path.join(__dirname, 'cache/layers/polygons.json')));
- assert.ok(path.existsSync(path.join(__dirname, 'tmp/5c505ff4-polygons.json')));
+ assert.ok(existsSync(path.join(__dirname, 'cache/layers/polygons.json')));
+ assert.ok(existsSync(path.join(__dirname, 'tmp/5c505ff4-polygons.json')));
// Stations layer and cache should be gone.
- assert.ok(!path.existsSync(path.join(__dirname, 'layers/stations')));
- assert.ok(!path.existsSync(path.join(__dirname, 'tmp/87c0c757-stations')));
-
- // Cleanup.
- rm(path.join(__dirname, 'tmp'));
- fs.unlinkSync(path.join(__dirname, 'cache/layers/absolute-json.json'));
- fs.unlinkSync(path.join(__dirname, 'cache/layers/absolute-shp'));
- fs.unlinkSync(path.join(__dirname, 'cache/layers/polygons.json'));
- fs.unlinkSync(path.join(__dirname, 'cache/layers/csv'));
+ assert.ok(!existsSync(path.join(__dirname, 'layers/stations')));
+ assert.ok(!existsSync(path.join(__dirname, 'tmp/87c0c757-stations')));
+
+ done();
+ });
+ });
+});
+
+describe('util', function() {
+
+ var copypath = path.join(__dirname, 'copypath');
+ var cache = path.join(__dirname, 'tmp');
+
+ beforeEach(function() {
+ if (!existsSync(copypath)) fs.mkdirSync(copypath,0777);
+ });
+
+ afterEach(function() {
+ rm(copypath);
+ });
+
+ it('copies all files from shapefiles (and no extras)', function(done) {
+
+ utils.processFiles(path.join(__dirname, 'data/absolute/absolute.shp'), path.join(copypath, 'absolute/absolute.shp'), utils.copy, {cache:cache}, function(err) {
+ assert.ok(!err);
+
+ assert.ok(existsSync(path.join(copypath, 'absolute/absolute.shp')));
+ assert.ok(existsSync(path.join(copypath, 'absolute/absolute.dbf')));
+ assert.ok(existsSync(path.join(copypath, 'absolute/absolute.shx')));
+ assert.ok(existsSync(path.join(copypath, 'absolute/absolute.prj')));
+ assert.ok(existsSync(path.join(copypath, 'absolute/absolute.index')));
+ assert.ok(!existsSync(path.join(copypath, 'absolute/othername.shp')));
+
+ done();
+ });
+
+ });
+
+ it('copies single files correctly', function(done) {
+ utils.copy(path.join(__dirname, 'data/absolute.json'), path.join(copypath, 'absolute.json'), {cache:cache}, function(err) {
+ assert.equal(err, undefined);
+ assert.ok(existsSync(path.join(copypath, 'absolute.json')));
+ done();
});
});
-};
+});
diff --git a/test/uppercase-ext.test.js b/test/uppercase-ext.test.js
new file mode 100644
index 0000000..bb3f601
--- /dev/null
+++ b/test/uppercase-ext.test.js
@@ -0,0 +1,49 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+it('correctly handles datasources with uppercase extensions', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'UPPERCASE_EXT/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'UPPERCASE_EXT'),
+ cache: cache
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.equal(resolved.Stylesheet[0].id, 'style.mss');
+ assert.equal(resolved.Stylesheet[0].data, '#polygon { }');
+ var expected = [
+ {
+ "name": "uppercase-ext",
+ "Datasource": {
+ "file": path.join(__dirname, 'UPPERCASE_EXT/test1.CSV'),
+ "type": "csv"
+ },
+ "srs": '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ }
+ ];
+ assert.deepEqual(resolved.Layer, expected);
+ done();
+ });
+});
diff --git a/test/zip-with-shapefile-and-txt.test.js b/test/zip-with-shapefile-and-txt.test.js
new file mode 100644
index 0000000..60f5a19
--- /dev/null
+++ b/test/zip-with-shapefile-and-txt.test.js
@@ -0,0 +1,38 @@
+var fs = require('fs');
+var path = require('path');
+var assert = require('assert');
+
+// switch to 'development' for more verbose logging
+process.env.NODE_ENV = 'production'
+var utils = require('../lib/util.js');
+var millstone = require('../lib/millstone');
+var tests = module.exports = {};
+var rm = require('./support.js').rm;
+
+var existsSync = require('fs').existsSync || require('path').existsSync;
+
+beforeEach(function(){
+ rm(path.join(__dirname, '/tmp/millstone-test'));
+})
+
+// https://github.com/mapbox/millstone/issues/101
+it('correctly prefers (for back compatibility) shapefiles over .txt files in zip archive', function(done) {
+ var mml = JSON.parse(fs.readFileSync(path.join(__dirname, 'zip-with-shapefile-and-txt/project.mml')));
+
+ var cache = '/tmp/millstone-test';
+ var options = {
+ mml: mml,
+ base: path.join(__dirname, 'zip-with-shapefile-and-txt'),
+ cache: cache
+ };
+
+ try {
+ fs.mkdirSync(options.cache, 0777);
+ } catch (e) {}
+
+ millstone.resolve(options, function(err, resolved) {
+ assert.equal(err,undefined,err);
+ assert.deepEqual(resolved.Layer[0].Datasource.type, 'shape');
+ done();
+ });
+});
diff --git a/test/zip-with-shapefile-and-txt/project.mml b/test/zip-with-shapefile-and-txt/project.mml
new file mode 100644
index 0000000..0517d5f
--- /dev/null
+++ b/test/zip-with-shapefile-and-txt/project.mml
@@ -0,0 +1,11 @@
+{
+ "Stylesheet": [ ],
+ "Layer": [
+ {
+ "name": "zip-with-shapefile-and-txt",
+ "Datasource": {
+ "file": "../data/ne_10m_admin_0_boundary_lines_disputed_areas.zip"
+ }
+ }
+ ]
+}
diff --git a/test/zipped-json/project.mml b/test/zipped-json/project.mml
new file mode 100644
index 0000000..636e559
--- /dev/null
+++ b/test/zipped-json/project.mml
@@ -0,0 +1,13 @@
+{
+ "Stylesheet": [
+ "style.mss"
+ ],
+ "Layer": [
+ {
+ "name": "polygons-zipped",
+ "Datasource": {
+ "file": "http://mapbox.github.com/millstone/test/polygons.json.zip"
+ }
+ }
+ ]
+}
diff --git a/test/zipped-json/style.mss b/test/zipped-json/style.mss
new file mode 100644
index 0000000..230a97c
--- /dev/null
+++ b/test/zipped-json/style.mss
@@ -0,0 +1 @@
+#polygon { }
\ No newline at end of file
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/collab-maint/node-millstone.git
More information about the Pkg-javascript-commits
mailing list