add fetchdb task ported for commonplace

This commit is contained in:
Davor Spasovski 2014-04-22 12:35:42 -04:00
Родитель 1f102b70c7
Коммит 975cb3cf16
10 изменённых файлов: 382 добавлений и 1 удалений

4
.gitignore поставляемый
Просмотреть файл

@ -40,3 +40,7 @@ hearth/media/include.css
hearth/media/include.js
hearth/media/js/include.js
hearth/templates.js
hearth/db/**/*.json
hearth/downloads/**/*.gif
hearth/downloads/**/*.jp*
hearth/downloads/**/*.png

39
Gruntfile.js Normal file
Просмотреть файл

@ -0,0 +1,39 @@
var db = require('./lib/db');
var utils = require('./lib/utils');
var settings = require('./settings');
module.exports = function(grunt) {
grunt.initConfig({
fetchdb: {
options: {
data_dest: settings.db_dir + '/latest.json'
}
}
});
// Always show stack traces when Grunt prints out an uncaught exception.
grunt.option('stack', true);
grunt.registerTask('fetchdb', 'Fetches JSON from API, downloads ' +
'icons/screenshots, and transforms data to ' +
'static JSON file to disk', function() {
var done = this.async();
var options = this.options();
db.fetchLatest(options.data_dest).then(function() {
grunt.log.writeln(
'File ' + utils.color('cyan', options.data_dest) + ' created.');
done();
}, function(err) {
grunt.log.writeln(utils.color('red',
'File ' + options.file_dest + ' failed to be created: ' + err));
done();
}).catch(function(err) {
grunt.log.writeln(utils.color('red', 'lib/db failed: ' + err));
done();
});
});
grunt.registerTask('default', ['fetchdb']);
};

Просмотреть файл

Просмотреть файл

Просмотреть файл

151
lib/db-transformer.js Normal file
Просмотреть файл

@ -0,0 +1,151 @@
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var request = require('request');
var Promise = require('es6-promise').Promise;
var utils = require('../lib/utils');
function generateFilename(url) {
// Generates a pretty filename from a remote URL, turning
// `97310.png?modified=1366438278` to `97310.png`.
return path.basename(url).split('?')[0];
}
module.exports = function(settings, data) {
return new Promise(function(resolveDB, rejectDB) {
var images = {};
data = data.apps.map(function(app) {
app._id = app.id;
app.icon = app.icons['64'];
// Collect a list of image URLs (to later download to disk).
// key = URL, value = directory name
images[app.icon] = 'icons';
if (app.previews.length > 0) {
images[app.previews[0].thumbnail_url] = 'screenshots-thumbs';
}
// Flatten object of localised name to one key for easy searching.
app.name_search = [];
Object.keys(app.name).forEach(function(locale) {
app.name_search.push(app.name[locale]);
});
app.name_search = app.name_search.join(' ').replace(/\(|\)/g, '');
// Flatten object of localised name to one key for easy searching.
app.description_search = [];
Object.keys(app.description).forEach(function(locale) {
app.description_search.push(app.description[locale]);
});
app.description_search = app.description_search.join(' ').replace(/\(|\)/g, '');
// Transform from Zamboni categories to Tarako categories.
// TODO: figure out a way to use transformMap from src/media/js/categories.js
// so we don't need to dupe this constant.
var categoryMap = {
'games': 'games',
'utilities': 'tools',
'reference': 'tools',
'productivity': 'tools',
'education': 'tools',
'business': 'tools',
'travel': 'lifestyle',
'sports': 'lifestyle',
'social': 'lifestyle',
'shopping': 'lifestyle',
'photo-video': 'lifestyle',
'news-weather': 'lifestyle',
'music': 'lifestyle',
'maps-navigation': 'lifestyle',
'lifestyle': 'lifestyle',
'health-fitness': 'lifestyle',
'entertainment': 'lifestyle',
'books': 'lifestyle'
};
var zcategories = app.categories;
app.categories = [];
_.forEach(zcategories, function (zcat) {
var dcat = categoryMap[zcat];
if (app.categories.indexOf(dcat) === -1) {
app.categories.push(dcat);
}
});
return _.pick(app, [
'_id',
'author',
'categories',
'content_ratings',
'description',
'description_search',
'icon',
'is_packaged',
'homepage',
'manifest_url',
'name',
'name_search',
'previews',
'privacy_policy',
'ratings',
'slug',
'status',
'support_email',
'support_url',
]);
});
console.log('Transformed data');
console.log('Fetching images to save to disk');
var promises = [];
_.uniq(Object.keys(images)).forEach(function(url) {
promises.push(new Promise(function(resolve, reject) {
console.log('Fetching', url);
var fn = path.join(settings.downloads_dir, images[url], generateFilename(url));
// Update filename.
images[url] = path.relative(settings.frontend_dir, fn);
var req = request(url).pipe(fs.createWriteStream(fn));
var body = '';
req.on('data', function (data) {
body += data;
});
req.on('close', function() {
// Cachebust these URLs with `?h=<contenthash>`.
var hash = utils.computeHash(body);
images[url] = utils.cachebust(images[url], hash);
resolve();
}).on('error', function() {
reject();
});
}));
});
Promise.all(promises).then(function() {
data = data.map(function (app) {
app.icon = path.join('/', images[app.icon]);
if (app.previews.length > 0) {
app.previews[0].thumbnail_url = path.join('/', images[app.previews[0].thumbnail_url]);
}
return app;
});
fs.writeFile(settings.appcache_media,
JSON.stringify(_.values(images).sort(), null, 2));
console.log('Successfully saved all images to disk');
resolveDB(data);
}, function(err) {
console.error('Failed to save images to disk:', err);
rejectDB(err);
});
});
};

129
lib/db.js Normal file
Просмотреть файл

@ -0,0 +1,129 @@
var fs = require('fs');
var path = require('path');
var Promise = require('es6-promise').Promise;
var request = require('request');
var settings = require('../settings');
var utils = require('../lib/utils');
var db_dir = path.join(__dirname, '..', settings.db_dir);
module.exports.fetch = fetch = function fetch(dest, preloaded) {
return new Promise(function (resolve, reject) {
var now = Date.now();
var fnOriginal = path.join(db_dir, 'original.json');
var fnTransformed = dest;
var fnArchivedOriginal = path.join(db_dir, 'archives', now + '-original.json');
var fnPreloadedHash = path.join(db_dir, 'preloaded-hash.json');
request(settings.db_url, function (err, res, body) {
if (err) {
console.error(err);
reject(err);
return;
}
fs.writeFile(fnOriginal, body);
fs.writeFile(fnArchivedOriginal, body);
var bodyJSON = JSON.parse(body);
settings.db_transformer(bodyJSON).then(function success(data) {
var bodyTransformed = JSON.stringify(data);
var hash = utils.computeHash(bodyTransformed);
var fnArchivedTransformed = path.join(db_dir, 'archives', hash + '.json');
var promises = [
new Promise(function (resolveFile) {
fs.writeFile(fnTransformed, bodyTransformed, function () {
console.log('Successfully wrote database to disk', fnTransformed);
resolveFile();
})
}),
new Promise(function (resolveFile) {
fs.writeFile(fnArchivedTransformed, bodyTransformed, function () {
console.log('Successfully wrote archived database to disk', fnArchivedTransformed);
resolveFile();
});
})
];
if (preloaded) {
promises.push(new Promise(function (resolveFile) {
fs.writeFile(fnPreloadedHash, hash, function () {
console.log('Successfully wrote database hash to disk', fnPreloadedHash);
resolveFile();
});
}));
}
Promise.all(promises).then(function () {
console.log('Successfully wrote all database files to disk');
resolve(data);
});
}, function error(err) {
console.error('settings.db_transformer rejected:', err);
reject(err);
}).catch(function (err) {
console.error('settings.db_transformer errored:', err);
reject(err);
});
});
});
};
module.exports.fetchPreloaded = function fetchPreloaded(dest) {
return fetch(dest || path.join(db_dir, 'preloaded.json'), true);
};
module.exports.fetchLatest = function fetchLatest(dest) {
return new Promise(function (resolve, reject) {
var latest = fetch(dest || path.join(db_dir, 'latest.json'));
latest.then(function (latestDocs) {
// (1) Look up the hash of last DB file that was included in the appcache.
// (2) Find the archived DB file that matches that hash.
// (3) Read the file and remove the apps that were already present.
// (4) Write that file to disk: `latest-since-<previousHash>.json`.
// (5) The front end uses this static JSON file to fetch.
fs.readFile(path.join(db_dir, 'preloaded-hash.json'), function (err, hash) {
if (err) {
console.error(err);
reject(err);
return;
}
var fnPreviousDB = path.join(db_dir, 'archives', hash + '.json');
fs.readFile(fnPreviousDB, function (err, previousDB) {
if (err) {
console.error(err);
reject(err);
return;
}
var oldDocs = JSON.parse(previousDB);
var idsToExclude = oldDocs.map(function (doc) {
return doc._id.toString();
});
// Build an array of objects that weren't in the last DB that we appcached.
var newDocs = latestDocs.filter(function (doc) {
if (idsToExclude.indexOf((doc._id || '').toString()) === -1) {
return doc;
}
});
var fnLatestSince = path.join(db_dir,
'latest-since-' + hash + '.json');
fs.writeFile(fnLatestSince, JSON.stringify(newDocs), function () {
resolve(newDocs);
});
});
});
}, function (err) {
console.log('reject');
reject(err);
});
});
};

33
lib/utils.js Normal file
Просмотреть файл

@ -0,0 +1,33 @@
var crypto = require('crypto');
var colors = {
cyan: '\x1B[36m',
red: '\x1B[31m'
};
module.exports.color = function color(whichColor, text) {
return colors[whichColor] + text + '\x1B[39m';
};
module.exports.cachebust = function cachebust(fn, hash) {
var extPos = fn.lastIndexOf('.');
return fn.substr(0, extPos) + '.hash_' + hash + fn.substr(extPos);
};
module.exports.computeHash = function computeHash(contents) {
var hasher = crypto.createHash('sha256');
hasher.update(contents, 'binary');
return hasher.digest('hex').substr(0, 7);
};
module.exports.baseurl = function baseurl(url) {
return url.split('?')[0];
};
module.exports.urlparams = function urlparams(url, qs) {
if (url.indexOf('?') === -1) {
return url + '?' + qs;
}
return url + '&' + qs;
};

Просмотреть файл

@ -13,5 +13,12 @@
"node": ">= 0.10.x",
"npm": ">= 1.1.x"
},
"dependencies": {}
"dependencies": {
"request": "~2.34.0",
"es6-promise": "~0.1.1",
"lodash": "~2.4.1"
},
"devDependencies": {
"grunt": "~0.4.4"
}
}

18
settings.js Normal file
Просмотреть файл

@ -0,0 +1,18 @@
var dbTransformer = require('./lib/db-transformer');
var settings = {
debug: true,
db_url: 'https://marketplace-dev.allizom.org/api/v1/fireplace/collection/curated/?region=restofworld',
frontend_dir: 'hearth',
use_data_uris: true
};
settings.db_dir = settings.frontend_dir + '/db';
settings.downloads_dir = settings.frontend_dir + '/downloads';
settings.db_transformer = function(data) {
return dbTransformer(settings, data);
};
module.exports = settings;