https://github.com/tilemill-project/tilemill
Tip revision: a0ac3a68a3335111f055049d40a785bd05c2b3b4 authored by Young Hahn on 10 February 2011, 15:38:07 UTC
Revert "require node-compress for tilelive interaction branch" for next tag.
Revert "require node-compress for tilelive interaction branch" for next tag.
Tip revision: a0ac3a6
models-server.js
// Server-side overrides for the Backbone models defined in `shared/models.js`.
// Provides model-specific storage overrides.
var _ = require('underscore'),
Backbone = require('backbone-dirty'),
settings = require('settings'),
fs = require('fs'),
Step = require('step'),
path = require('path'),
models = require('models');
// Project
// -------
// Implement custom sync method for Project model. Writes projects to
// individual directories and splits out Stylesheets from the main project
// MML JSON file.
models.ProjectList.prototype.sync =
models.Project.prototype.sync = function(method, model, success, error) {
switch (method) {
case 'read':
if (model.id) {
loadProject(model, function(err, model) {
return err ? error(err) : success(model);
});
} else {
loadProjectAll(model, function(err, model) {
return err ? error(err) : success(model);
});
}
break;
case 'create':
case 'update':
saveProject(model, function(err, model) {
return err ? error(err) : success(model);
});
break;
case 'delete':
destroyProject(model, function(err, model) {
return err ? error(err) : success(model);
});
break;
}
};
// Load a single project model.
function loadProject(model, callback) {
var modelPath = path.join(settings.files, 'project', model.id);
fs.readFile(path.join(modelPath, model.id) + '.mml', 'utf-8',
function(err, data) {
if (err || !data) {
return callback('Error reading model file.');
}
var object = JSON.parse(data);
// Set the object ID explicitly for multiple-load scenarios where
// model parse()/set() is bypassed.
object.id = model.id;
if (object.Stylesheet && object.Stylesheet.length > 0) {
Step(
function() {
var group = this.group();
_.each(object.Stylesheet, function(filename, index) {
fs.readFile(
path.join(modelPath, filename),
'utf-8',
group()
);
});
},
function(err, files) {
object.Stylesheet = _.reduce(
object.Stylesheet,
function(memo, filename, index) {
if (typeof files[index] !== 'undefined') {
memo.push({
id: filename,
data: files[index]
});
}
return memo;
},
[]
);
return callback(null, object);
}
);
} else {
return callback(null, object);
}
});
};
// Load all projects into an array.
function loadProjectAll(model, callback) {
var basepath = path.join(settings.files, 'project');
Step(
function() {
path.exists(basepath, this);
},
function(exists) {
if (!exists) {
fs.mkdir(basepath, 0777, this);
} else {
this();
}
},
function() {
fs.readdir(basepath, this);
},
function(err, files) {
if (err) {
return this('Error reading model directory.');
}
else if (files.length === 0) {
return this();
}
var group = this.group();
for (var i = 0; i < files.length; i++) {
var id = files[i];
loadProject({ id: id }, group());
}
},
function(err, models) {
// Ignore errors from loading individual models (e.g.
// don't let one bad apple spoil the collection).
models = _.select(models, function(model) {
return (typeof model === 'object');
});
return callback(null, models);
}
);
};
// Destroy a project. `rm -rf` equivalent for the project directory.
function destroyProject(model, callback) {
var rm = function(basePath, callback) {
var killswitch = false;
Step(
function() {
fs.stat(basePath, this);
},
function(err, stat) {
if (stat.isDirectory()) {
this();
} else if (stat.isFile()) {
killswitch = true;
fs.unlink(basePath, this);
} else {
killswitch = true;
this();
}
},
// The next steps apply only when basePath refers to a directory.
function(err) {
if (killswitch) return this();
fs.readdir(basePath, this);
},
function(err, files) {
if (killswitch) return this();
if (files.length === 0) {
this();
} else {
var group = this.group();
for (var i = 0; i < files.length; i++) {
rm(path.join(basePath, files[i]), group());
}
}
},
function(err) {
if (killswitch) return callback();
fs.rmdir(basePath, callback);
}
);
};
var modelPath = path.join(settings.files, 'project', model.id);
rm(modelPath, callback);
}
// Save a project. Creates a subdirectory per project and splits out
// stylesheets into separate files.
function saveProject(model, callback) {
var basePath = path.join(settings.files, 'project');
var modelPath = path.join(settings.files, 'project', model.id);
Step(
function() {
path.exists(basePath, this);
},
function(exists) {
if (!exists) {
fs.mkdir(basePath, 0777, this);
} else {
this();
}
},
function() {
path.exists(modelPath, this);
},
function(exists) {
if (!exists) {
fs.mkdir(modelPath, 0777, this);
} else {
this();
}
},
function() {
fs.readdir(modelPath, this);
},
function(err, files) {
// Remove any stale files in the project directory.
var group = this.group();
var stylesheets = model.get('Stylesheet') || [];
var stale = _.select(files, function(filename) {
if (filename === (model.id + '.mml')) {
return false;
} else if (_.pluck(stylesheets, 'id').indexOf(filename) !== -1) {
return false;
}
return true;
});
if (stale.length) {
for (var i = 0; i < stale.length; i++) {
fs.unlink(path.join(modelPath, stale[i]), group());
}
}
else {
group()();
}
},
function() {
// Hard clone the model JSON before doing adjustments to the data
// based on writing separate stylesheets.
var data = JSON.parse(JSON.stringify(model.toJSON()));
var files = [];
if (data.id) {
delete data.id;
}
if (data.Stylesheet) {
_.each(data.Stylesheet, function(stylesheet, key) {
if (stylesheet.id) {
files.push({
filename: stylesheet.id,
data: stylesheet.data
});
data.Stylesheet[key] = stylesheet.id;
}
});
}
files.push({
filename: model.id + '.mml',
data: JSON.stringify(data)
});
var group = this.group();
for (var i = 0; i < files.length; i++) {
fs.writeFile(
path.join(modelPath, files[i].filename),
files[i].data,
group()
);
}
},
function() {
callback(null, model);
}
);
}
// Export
// ------
// Implement custom sync method for Export model. Removes any files associated
// with the export model at `filename` when a model is destroyed.
models.Export.prototype.sync = function(method, model, success, error) {
switch (method) {
case 'delete':
var filepath;
Step(
function() {
Backbone.sync('read', model, this, this);
},
function(data) {
if (data && data.filename) {
filepath = path.join(settings.export_dir, data.filename);
path.exists(filepath, this);
} else {
this(false);
}
},
function(remove) {
if (remove) {
fs.unlink(filepath, this);
} else {
this();
}
},
function() {
Backbone.sync(method, model, success, error);
}
);
break;
default:
Backbone.sync(method, model, success, error);
break;
}
};
// Cache
// -----
// Provides a model instance cache for the server. Used to store and retrieve a
// model instance in memory such that the same model is referenced in separate
// requests as well as in other long-running processes.
//
// The main use-case in TileMill for this instance cache is triggering a model
// `delete` event when a DELETE request is received. In the case of Exports,
// this event is used to terminate and worker processes associated with the
// Export model being deleted.
var Cache = function() {
this.cache = {};
};
Cache.prototype.get = function(type, id) {
if (this.cache[type] && this.cache[type][id]) {
return this.cache[type][id];
}
this.cache[type] = this.cache[type] || {}
this.set(type, id, new models[type]({id: id}));
return this.cache[type][id];
};
Cache.prototype.set = function(type, id, model) {
this.cache[type] = this.cache[type] || {}
this.cache[type][id] = model;
return this.cache[type][id];
};
Cache.prototype.del = function(type, id) {
if (this.cache[type][id]) {
delete this.cache[type][id];
}
};
module.exports = _.extend({ cache: new Cache() }, models);