From 3c445ce5165009fa98dab965eb88d0312493f92f Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 6 Aug 2015 10:34:41 -0400 Subject: [PATCH 001/144] Initial commit --- .gitignore | 27 +++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..123ae94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..42a1fbb --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# oam-uploader-api +The OAM Uploader API server From f952aa67d09ee928c4e7b1a7fe5ffe3e48772a66 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 6 Aug 2015 18:02:57 -0400 Subject: [PATCH 002/144] initial commit --- .build_scripts/docs.sh | 18 +++++++++ .gitignore | 18 +++++++++ .jshintrc | 66 +++++++++++++++++++++++++++++++ .travis.yml | 27 +++++++++++++ CONTRIBUTING.md | 73 +++++++++++++++++++++++++++++++++++ Procfile | 1 + README.md | 41 ++++++++++++++++++++ app.json | 38 ++++++++++++++++++ config.js | 22 +++++++++++ index.js | 46 ++++++++++++++++++++++ newrelic.js | 24 ++++++++++++ package.json | 62 +++++++++++++++++++++++++++++ plugins/mongodb.js | 18 +++++++++ routes/root.js | 11 ++++++ routes/uploads.js | 36 +++++++++++++++++ services/s3.js | 88 ++++++++++++++++++++++++++++++++++++++++++ 16 files changed, 589 insertions(+) create mode 100644 .build_scripts/docs.sh create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 Procfile create mode 100644 README.md create mode 100644 app.json create mode 100644 config.js create mode 100644 index.js create mode 100644 newrelic.js create mode 100644 package.json create mode 100644 plugins/mongodb.js create mode 100644 routes/root.js create mode 100644 routes/uploads.js create mode 100644 services/s3.js diff --git a/.build_scripts/docs.sh b/.build_scripts/docs.sh new file mode 100644 index 0000000..84d2d8f --- /dev/null +++ b/.build_scripts/docs.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -e # halt script on error + +# Build docs and push to gh-pages +if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = ${PRODUCTION_BRANCH} ]; then + echo "Get ready, we're pushing to gh-pages!" + npm run docs + cd docs + echo "docs.openaerialmap.org" > CNAME + git init + git config user.name "Travis-CI" + git config user.email "travis@somewhere.com" + git add . + git commit -m "CI deploy to gh-pages" + git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages +else + echo "Not a publishable branch so we're all done here" +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72d63a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +node_modules +bower_components +.tmp +*.log +*.out +*.pid +npm-debug.log +*~ +*# +.DS_STORE +.netbeans +.env +.idea +.node_history +temp +tmp +scratch.js +docs diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..82c239b --- /dev/null +++ b/.jshintrc @@ -0,0 +1,66 @@ +{ + /* + * ENVIRONMENTS + * ================= + */ + + // Define globals exposed by modern browsers. + "browser": true, + + // Define globals exposed by jQuery. + "jquery": true, + + // Define globals exposed by Node.js. + "node": true, + + // Define globals exposed by Mocha + "mocha": true, + + "globals": { + "server": true + }, + + /* + * ENFORCING OPTIONS + * ================= + */ + + // Force all variable names to use either camelCase style or UPPER_CASE + // with underscores. + "camelcase": false, + + // Prohibit use of == and != in favor of === and !==. + "eqeqeq": true, + + // Enforce tab width of 2 spaces. + "indent": 2, + + // Prohibit use of a variable before it is defined. + "latedef": true, + + // Enforce line length to 80 characters + "maxlen": 119, + + // Require capitalized names for constructor functions. + "newcap": true, + + // Enforce use of single quotation marks for strings. + "quotmark": "single", + + // Enforce placing 'use strict' at the top function scope + "strict": true, + + // Prohibit use of explicitly undeclared variables. + "undef": true, + + // Warn when variables are defined but never used. + "unused": true, + + /* + * RELAXING OPTIONS + * ================= + */ + + // Suppress warnings about == null comparisons. + "eqnull": true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..48cc654 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: node_js + +node_js: +- '0.12' + +env: + global: + - GH_REF=github.com/hotosm/oam-catalog.git + - PRODUCTION_BRANCH=master + - secure: AhiKIqAYPeXH1HjhYza/VdMJrp8wCri5WNRY+Kdkb4piYEK20dAQPVgcVT2EM1tvlyZL5Q0ZU4rtnATkJOU3k9DQHI99xVjk4MjnqfpkrM9iZtif6o9xDZwf9iWwBeuu9MmKH0tsvKhktxrJkzZ7vAThurZVz89ZPoCAXuudyqk= + +services: mongodb + +before_install: +- chmod +x ./.build_scripts/docs.sh + +after_success: +- "./.build_scripts/docs.sh" + +deploy: + provider: heroku + api_key: + secure: iZGqyYrx4i7+MPDa0Lopoe/TULu9DNWc2UsYqB8+f39xNhpRNHqIR0lz5VFWl6oQugj0XeZnHpN+f4W+2jfy6ThyTUs42uFAE43HThVipPhf28NdeuCJVje5Qu2ZjMv9spRskalxBrJjdJ8G9C5Vxwx1+LmnjVcyvsk0ycSA38o= + app: oam-catalog + on: + repo: hotosm/oam-catalog + branch: master diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c68a37e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +#Contributing guidelines + +There are many ways to contribute to a project, below are some examples: + +- Report bugs, ideas, requests for features by creating “Issues” in the project repository. +- Fork the code and play with it, whether you later choose to make a pull request or not. +- Create pull requests of changes that you think are laudatory. From typos to major design flaws, you will find a target-rich environment for improvements. + +## Issues + +When creating a task through the issue tracker, please include the following where applicable: + +* A summary of identified tasks related to the issue; and +* Any dependencies related to completion of the task (include links to tickets with the dependency). + +### Design and feature request issues should include: +* What the goal of the task being accomplished is; and +* The user need being addressed. + +### Development issues should include: +* Unknowns tasks or dependencies that need investigation. + +Use checklists (via `- [ ]`) to keep track of sub-items wherever possible. + +## Coding style + +When writing code it is generally a good idea to try and match your +formatting to that of any existing code in the same file, or to other +similar files if you are writing new code. Consistency of layout is +far more important that the layout itself as it makes reading code +much easier. + +One golden rule of formatting -- please don't use tabs in your code +as they will cause the file to be formatted differently for different +people depending on how they have their editor configured. + +## Comments + +Sometimes it's not apparent from the code itself what it does, or, +more importantly, **why** it does that. Good comments help your fellow +developers to read the code and satisfy themselves that it's doing the +right thing. + +When developing, you should: + +* Comment your code - don't go overboard, but explain the bits which +might be difficult to understand what the code does, why it does it +and why it should be the way it is. +* Check existing comments to ensure that they are not misleading. + +## Committing + +When you submit patches, the project maintainer has to read them and +understand them. This is difficult enough at the best of times, and +misunderstanding patches can lead to them being more difficult to +merge. To help with this, when submitting you should: + +* Split up large patches into smaller units of functionality. +* Keep your commit messages relevant to the changes in each individual +unit. + +When writing commit messages please try and stick to the same style as +other commits, namely: + +* A one line summary, starting with a capital and with no full stop. +* A blank line. +* Full description, as proper sentences with capitals and full stops. + +For simple commits the one line summary is often enough and the body +of the commit message can be left out. + +If you have forked on GitHub then the best way to submit your patches is to +push your changes back to GitHub and then send a "pull request" on GitHub. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1da0cd6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node index.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..21e30f1 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# OAM Uploader API [![Build Status](https://travis-ci.org/hotosm/oam-uploader-api.svg)](https://travis-ci.org/hotosm/oam-uploader-api) + +[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) + +API Documentation: TBD + +## Installation and Usage + +The steps below will walk you through setting up your own instance of the oam-uploader-api. + +### Install Project Dependencies + +- [MongoDB](https://www.mongodb.org/) +- [Node.js](https://nodejs.org/) + +### Install Application Dependencies + + $ npm install + +### Usage + +#### Starting the database: + + $ mongod + +The database is responsible for storing metadata about the imagery and analytics. + +#### Starting the API: + + $ node index.js + +The API exposes endpoints used to access information form the system via a RESTful interface. + +### Environment Variables + +- `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN buckets +- `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN buckets +- `DBURI` - MongoDB connection url + +### Docs Deployment +Changes to `master` branch are automatically deployed via Travis to https://oam-uploader-api.herokuapp.com. diff --git a/app.json b/app.json new file mode 100644 index 0000000..7ad8c5c --- /dev/null +++ b/app.json @@ -0,0 +1,38 @@ +{ + "name": "oam-uploader-api", + "description": "An uploader API for OpenAerialMap imagery", + "repository": "https://github.com/hotosm/oam-uploader-api", + "keywords": [ + "node", + "REST", + "satellite", + "imagery" + ], + "addons": [ + "mongolab", + "newrelic" + ], + "env": { + "OAM_DEBUG": { + "description": "Debug mode true or false (default)", + "required": false + }, + "AWS_SECRET_KEY_ID": { + "description": "AWS secret key id for reading OIN buckets", + "required": true + }, + "AWS_SECRET_ACCESS_KEY": { + "description": "AWS secret access key for reading OIN buckets", + "required": true + }, + "DBURI": { + "description": "MongoDB connection url", + "required": true + }, + "SECRET_TOKEN": { + "description": "The token used for post requests to /tms endpoint", + "required": true, + "generator": "secret" + } + } +} diff --git a/config.js b/config.js new file mode 100644 index 0000000..39fb4a9 --- /dev/null +++ b/config.js @@ -0,0 +1,22 @@ +var local = {}; +try { + local = require('./local.js'); +} catch(e) {} + +module.exports = { + port: process.env.PORT || 3000, + dbUri: process.env.DBURI || 'mongodb://localhost/oam-uploader', + logOptions: local.logOptions || { + opsInterval: 3000, + reporters: [{ + reporter: require('good-console'), + events: { + request: '*', + error: '*', + response: '*', + info: '*', + log: '*', + } + }] + } +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..a891a3a --- /dev/null +++ b/index.js @@ -0,0 +1,46 @@ +'use strict'; + +require('newrelic'); +var Hapi = require('hapi'); +var config = require('./config.js'); + +var hapi = new Hapi.Server({ + connections: { + routes: { + cors: true + }, + router: { + stripTrailingSlash: true + } + } +}); + +hapi.connection({ port: config.port }); + +// Register plugins +hapi.register([ + { + register: require('good'), + options: config.logOptions + }, + require('./plugins/mongodb') +], function (err) { + if (err) throw err; +}); + +// Register routes +hapi.register([ + { + register: require('hapi-router'), + options: { + routes: './routes/*.js' + } + } +], function (err) { + if (err) throw err; +}); + +hapi.start(function () { + hapi.log(['info'], 'Server running at:' + hapi.info.uri); + hapi.log(['debug'], 'Config: ' + JSON.stringify(config)); +}); diff --git a/newrelic.js b/newrelic.js new file mode 100644 index 0000000..6972202 --- /dev/null +++ b/newrelic.js @@ -0,0 +1,24 @@ +/** + * New Relic agent configuration. + * + * See lib/config.defaults.js in the agent distribution for a more complete + * description of configuration variables and their potential values. + */ +exports.config = { + /** + * Array of application names. + */ + app_name: ['oam-uploader'], + /** + * Your New Relic license key. + */ + license_key: 'license key here', + logging: { + /** + * Level at which to log. 'trace' is most useful to New Relic when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level: 'info' + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..cf988b3 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "oam-uploader-api", + "version": "1.0.0", + "description": "An uploader API for Open Aerial Map Imagery", + "main": "index.js", + "scripts": { + "test": "./node_modules/semistandard/bin/cmd.js && ./node_modules/mocha/bin/mocha", + "docs": "apidoc -i routes/ -o docs/" + }, + "repository": { + "type": "git", + "url": "https://github.com/hotosm/oam-uploader-api.git" + }, + "author": "Development Seed", + "license": "CC01", + "bugs": { + "url": "https://github.com/hotosm/oam-uploader-api/issues" + }, + "homepage": "https://github.com/hotosm/oam-uploader-api", + "dependencies": { + "async": "^1.3.0", + "boom": "^2.7.1", + "envloader": "0.0.2", + "good": "^6.3.0", + "good-console": "^5.0.2", + "hapi": "^8.4.0", + "hapi-router": "^3.0.1", + "lodash": "^3.8.0", + "mongodb": "^2.0.40", + "newrelic": "^1.20.0", + "request": "^2.55.0", + "s3": "^4.4.0", + "wellknown": "^0.3.1" + }, + "devDependencies": { + "apidoc": "^0.13.1", + "chai": "^2.3.0", + "mocha": "^2.2.4", + "semistandard": "^4.1.4" + }, + "engines": { + "node": "0.12.2" + }, + "apidoc": { + "title": "OAM API", + "name": "OpenAerialMap Uploader API", + "description": "", + "url": "https://oam-uploader-api.herokuapp.com", + "template": { + "withCompare": false + }, + "header": { + "title": "Getting Started", + "filename": "about.md" + }, + "order": [ + "Meta", + "TMS", + "Analytics" + ] + } +} diff --git a/plugins/mongodb.js b/plugins/mongodb.js new file mode 100644 index 0000000..d950dfa --- /dev/null +++ b/plugins/mongodb.js @@ -0,0 +1,18 @@ +'use strict'; + +var MongoClient = require('mongodb').MongoClient; +var dbUri = require('../config').dbUri; + +/** + * Exposes the mongodb connection as server.plugins.db.connection + */ +module.exports = function register (server, options, next) { + server.log(['debug'], 'Attempting db connection: ' + dbUri); + MongoClient.connect(dbUri, function (err, db) { + server.log(['debug'], 'Successful db connection.'); + server.expose('connection', db); + next(err); + }); +}; + +module.exports.attributes = { name: 'db' }; diff --git a/routes/root.js b/routes/root.js new file mode 100644 index 0000000..87510bf --- /dev/null +++ b/routes/root.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = [ + { + method: 'GET', + path: '/', + handler: function (request, reply) { + reply('Hello, world!'); + } + } +]; diff --git a/routes/uploads.js b/routes/uploads.js new file mode 100644 index 0000000..ae81cf6 --- /dev/null +++ b/routes/uploads.js @@ -0,0 +1,36 @@ +'use strict'; + +var Boom = require('boom'); + +module.exports = [ + { + method: 'GET', + path: '/uploads', + handler: function (request, reply) { + var db = request.server.plugins.db.connection; + db.collection('uploads').find({}).toArray(function (err, uploads) { + if (err) { return reply(Boom.wrap(err)); } + reply({ results: uploads }); + }); + } + }, + { + method: 'POST', + path: '/uploads', + config: { + payload: { + output: 'data', + parse: true + } + }, + handler: function (request, reply) { + var db = request.server.plugins.db.connection; + var uploads = db.collection('uploads'); + console.log('uploads', uploads); + uploads.insert([request.payload], function (err, result) { + if (err) { return reply(Boom.wrap(err)); } + reply('Success'); + }); + } + } +]; diff --git a/services/s3.js b/services/s3.js new file mode 100644 index 0000000..4c2f961 --- /dev/null +++ b/services/s3.js @@ -0,0 +1,88 @@ +'use strict'; + +var s3 = require('s3'); +var meta = require('../controllers/meta.js'); + +/** +* S3 Constructor that handles intractions with S3 +* +* @constructor +* @param {String} secretId (optional) - AWS secret key id. Can be set by AWS_SECRET_KEY_ID env var. +* @param {String} secretKey (optional) - AWS secret access key. Can be set by AWS_SECRET_ACCESS_KEY env var. +* @param {String} bucket (optional) - S3 Bucket name. Can be set by S3_BUCKET_NAME env var. +*/ +var S3 = function (secretId, secretKey, bucket) { + this.client = s3.createClient({ + maxAsyncS3: 20, // this is the default + s3RetryCount: 3, // this is the default + s3RetryDelay: 1000, // this is the default + multipartUploadThreshold: 20971520, // this is the default (20 MB) + multipartUploadSize: 15728640, // this is the default (15 MB) + s3Options: { + accessKeyId: secretId || process.env.AWS_SECRET_KEY_ID, + secretAccessKey: secretKey || process.env.AWS_SECRET_ACCESS_KEY + } + }); + + this.params = { + s3Params: { + Bucket: bucket || process.env.S3_BUCKET_NAME /* required */ + } + }; +}; + +/** +* Read bucket method for S3. It reads the S3 bucket and adds all the `.json` metadata to Meta model +* +* @param {responseCallback} cb - The callback that handles the response +* @param {finishedCallback} finished - The callback that handles when reading is done +*/ +S3.prototype.readBucket = function (lastSystemUpdate, cb, done) { + var self = this; + var images = this.client.listObjects(this.params); + + images.on('error', function (err) { + cb(err); + }); + + images.on('data', function (data) { + for (var i = 0; i < data.Contents.length; i++) { + var format = data.Contents[i].Key.split('.'); + format = format[format.length - 1]; + + if (format === 'json') { + // Get the last time the metadata file was modified so we can determine + // if we need to update it. + var lastModified = data.Contents[i].LastModified; + var url = s3.getPublicUrlHttp(self.params.s3Params.Bucket, data.Contents[i].Key); + meta.addRemoteMeta(url, lastModified, lastSystemUpdate, function (err, msg) { + if (err) { + return cb(err); + } + cb(err, msg); + }); + } + } + }); + + images.on('end', function () { + done(null); + }); +}; + +module.exports = S3; + +/** + * The response callback returns the error and success message. + * + * @callback responseCallback + * @param {error} err - The error message + * @param {string} msg - The success message + */ + + /** + * The finished callback just calls back to the worker to let it know there is + * no more data coming. + * + * @callback finishedCallback + */ From 0d137fa202cd8170a110789074e73a2ca3fb3a58 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 08:22:43 -0400 Subject: [PATCH 003/144] Update travis --- .travis.yml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48cc654..83cacda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,21 @@ language: node_js - node_js: - '0.12' - env: global: - - GH_REF=github.com/hotosm/oam-catalog.git + - GH_REF=github.com/hotosm/oam-uploader-api.git - PRODUCTION_BRANCH=master - - secure: AhiKIqAYPeXH1HjhYza/VdMJrp8wCri5WNRY+Kdkb4piYEK20dAQPVgcVT2EM1tvlyZL5Q0ZU4rtnATkJOU3k9DQHI99xVjk4MjnqfpkrM9iZtif6o9xDZwf9iWwBeuu9MmKH0tsvKhktxrJkzZ7vAThurZVz89ZPoCAXuudyqk= - + - secure: W0eGs5Lg+fZOIEYKLIFJZPsw7qFUNFTAUtPS/2UmlWwlQtzvYCYnNu4dd62kZKivzCuWZiwdwsm58l2r6tB6zRv7/3S4mhkiA1SrYFC3p34CXXV9XaJeEgOQ1SXoJuAW1ThSmq7CJu9dsk3Aix+KA4MggzBgTPG2RXyYhgr3Qzhcil1BX6KNqW4TP7cPaJTUCEhoDcyVLNvmjvUDQNwwJjCu1hifDptlyZt88jDshIVPi4wd2ITy10O22dkwvVaes79A7yYrpRyBJyAhb8w/fPZsCwT+9wQz28x1Q47at/eERrpeAVGOUbXE+5cla/dCCVG3rMlyJnaRP0HagaUQK71GJMYe/nnOEirmF3vtpYzRTmzbvaahEtWr6EbdVWqXB8WJCBShgEF2NICwQAYwgIsYyzIAI3wDvVyrdhbSqqARlUqRwEN4NUFAXfVYkXssYWiTz/s0Eaktg3I/Z0wL2lgBy0hz2JN9BcSCjSdzzj9FWaoWjFSDamb/M5U1R6h9rTGe2bgoDGsZbo4TV/QCXIsKvqjX5lC/W5GFyoUOyTrqtGyPIpOdkTg8Nf1V2ZPuAznln6YYchMdO64eABy7w5tKErM6JP8N4pHFgkgIPJI4gZdAlUELFe+mgqp/kl1GEuSd8TtlIdZ/ErvsmhH5ubYifIdBkpSZfUMRM/vueXI= services: mongodb - before_install: - chmod +x ./.build_scripts/docs.sh - after_success: - "./.build_scripts/docs.sh" - deploy: provider: heroku - api_key: - secure: iZGqyYrx4i7+MPDa0Lopoe/TULu9DNWc2UsYqB8+f39xNhpRNHqIR0lz5VFWl6oQugj0XeZnHpN+f4W+2jfy6ThyTUs42uFAE43HThVipPhf28NdeuCJVje5Qu2ZjMv9spRskalxBrJjdJ8G9C5Vxwx1+LmnjVcyvsk0ycSA38o= - app: oam-catalog + app: oam-uploader-api on: - repo: hotosm/oam-catalog + repo: hotosm/oam-uploader-api branch: master + api_key: + secure: VHwJSkfvBAAXl1BPcgV2RvoQFSV62B5gJJ+RK9fiBv++IO6pJpJErStljwxv/LTedT0lp3NHWlK12NpJc6fWQg/hQo4KzxHP6NEqsZx/lNQ5te4KDQNZcOQjFV6miSArWm1VbVv2DvJoAOATuPcR7+daleWNd/H/EDZ+692hqAp4zcQ/ek6q7+TezxKFdUY43f3hN77u+umTbHd7mqwJEuSkUR29oq/fsh9Ik1+6UR3jlfs8mn47yUXbD74g81/5oSogt8uBzVNsfuhEWdpJ7GTdS4dXM5fci5PsvVZfc9lrcsex7vItu+LmggFLOkr2HI4+Hnvd0v5b9ML0khMxFtEY+lb0vDqMch4O5aT1pA1bi73g8UluuB8K/PTbhVsuVA2AXJ5PEznlNUZPpgZg3jNDV1Xrcc0Nmoi7s6dGJyCcWtat4aqi7Lo6zF9tzDlRS2vWrNeWG/oTGnSZhWozOCxavJZ6Z9eKVE2MIenCqYrkhL+E952mQwS++5IhpE5ibvPq/84FfAzjUgGRCu1r3LyYPISLKFrpyfqT7PEMOK1YqgOH6tAIClrxagcB+80i40NG9hu3mo3Sg3ckGMBsxaXR13VH8gHcCAKxv79a6jwzMKdlsOlGLjIUL2adewUToN7ssVQtCRwXKM+ZW92kMgtLxImbMwySshCkRK5vNYc= From 84874c617d7720a1023dfe53ca18ab0d66ef000a Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 08:32:18 -0400 Subject: [PATCH 004/144] Fix linting --- .eslintrc | 189 ++++++++++++++++++++++++++++++++++++++++++++++ .jshintrc | 66 ---------------- config.js | 2 +- package.json | 6 +- routes/uploads.js | 1 + 5 files changed, 194 insertions(+), 70 deletions(-) create mode 100644 .eslintrc delete mode 100644 .jshintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..71bd916 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,189 @@ +{ + "ecmaFeatures": { + "globalReturn": true, + "jsx": true, + "modules": true + }, + + "env": { + "browser": false, + "es6": true, + "node": true, + }, + + "globals": { + "document": false, + "escape": false, + "navigator": false, + "unescape": false, + "window": false + }, + + "plugins": [ + "react" + ], + + "rules": { + "block-scoped-var": 0, + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": 0, + "comma-dangle": [2, "never"], + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "complexity": 0, + "consistent-return": 0, + "consistent-this": 0, + "curly": [2, "multi-line"], + "default-case": 0, + "dot-notation": 0, + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + "func-names": 0, + "func-style": [0, "declaration"], + "generator-star": [2, "middle"], + "guard-for-in": 0, + "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], + "indent": [2, 2], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + "max-depth": 0, + "max-len": 0, + "max-nested-callbacks": 0, + "max-params": 0, + "max-statements": 0, + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "no-alert": 0, + "no-array-constructor": 2, + "no-bitwise": 0, + "no-caller": 2, + "no-catch-shadow": 0, + "no-cond-assign": 2, + "no-console": 0, + "no-constant-condition": 0, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 0, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-else-return": 0, + "no-empty": 0, + "no-empty-class": 2, + "no-empty-label": 2, + "no-eq-null": 0, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 0, + "no-extra-strict": 0, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inline-comments": 0, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 0, + "no-lonely-if": 0, + "no-loop-func": 0, + "no-mixed-requires": [0, false], + "no-mixed-spaces-and-tabs": [2, false], + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 0, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 0, + "no-plusplus": 0, + "no-process-env": 0, + "no-process-exit": 0, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 0, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 0, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-sync": 0, + "no-ternary": 0, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unreachable": 2, + "no-unused-expressions": 0, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-use-before-define": 0, + "no-var": 0, + "no-void": 0, + "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], + "no-with": 2, + "no-wrap-func": 2, + "one-var": 0, + "operator-assignment": [0, "always"], + "padded-blocks": [2, "never"], + "quote-props": 0, + "quotes": [2, "single", "avoid-escape"], + "radix": 2, + "react/display-name": 2, + "react/jsx-boolean-value": 2, + "react/jsx-quotes": [2, "single", "avoid-escape"], + "react/jsx-no-undef": 2, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/no-did-mount-set-state": 2, + "react/no-did-update-set-state": 2, + "react/no-multi-comp": 2, + "react/no-unknown-property": 2, + "react/prop-types": 2, + "react/react-in-jsx-scope": 2, + "react/self-closing-comp": 2, + "react/wrap-multilines": 2, + "semi": [2, "always"], + "semi-spacing": 0, + "sort-vars": 0, + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "space-in-brackets": 0, + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-line-comment": [2, "always"], + "strict": 0, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": [2, "any"], + "wrap-regex": 0, + "yoda": [2, "never"] + } +} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 82c239b..0000000 --- a/.jshintrc +++ /dev/null @@ -1,66 +0,0 @@ -{ - /* - * ENVIRONMENTS - * ================= - */ - - // Define globals exposed by modern browsers. - "browser": true, - - // Define globals exposed by jQuery. - "jquery": true, - - // Define globals exposed by Node.js. - "node": true, - - // Define globals exposed by Mocha - "mocha": true, - - "globals": { - "server": true - }, - - /* - * ENFORCING OPTIONS - * ================= - */ - - // Force all variable names to use either camelCase style or UPPER_CASE - // with underscores. - "camelcase": false, - - // Prohibit use of == and != in favor of === and !==. - "eqeqeq": true, - - // Enforce tab width of 2 spaces. - "indent": 2, - - // Prohibit use of a variable before it is defined. - "latedef": true, - - // Enforce line length to 80 characters - "maxlen": 119, - - // Require capitalized names for constructor functions. - "newcap": true, - - // Enforce use of single quotation marks for strings. - "quotmark": "single", - - // Enforce placing 'use strict' at the top function scope - "strict": true, - - // Prohibit use of explicitly undeclared variables. - "undef": true, - - // Warn when variables are defined but never used. - "unused": true, - - /* - * RELAXING OPTIONS - * ================= - */ - - // Suppress warnings about == null comparisons. - "eqnull": true -} diff --git a/config.js b/config.js index 39fb4a9..61eff8e 100644 --- a/config.js +++ b/config.js @@ -15,7 +15,7 @@ module.exports = { error: '*', response: '*', info: '*', - log: '*', + log: '*' } }] } diff --git a/package.json b/package.json index cf988b3..282db2c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "An uploader API for Open Aerial Map Imagery", "main": "index.js", "scripts": { - "test": "./node_modules/semistandard/bin/cmd.js && ./node_modules/mocha/bin/mocha", + "test": "semistandard && tape test/*.js", "docs": "apidoc -i routes/ -o docs/" }, "repository": { @@ -35,8 +35,8 @@ "devDependencies": { "apidoc": "^0.13.1", "chai": "^2.3.0", - "mocha": "^2.2.4", - "semistandard": "^4.1.4" + "semistandard": "^4.1.4", + "tape": "^4.0.3" }, "engines": { "node": "0.12.2" diff --git a/routes/uploads.js b/routes/uploads.js index ae81cf6..8a24474 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -28,6 +28,7 @@ module.exports = [ var uploads = db.collection('uploads'); console.log('uploads', uploads); uploads.insert([request.payload], function (err, result) { + request.log(['debug'], result); if (err) { return reply(Boom.wrap(err)); } reply('Success'); }); From 7282b14a2c1f65ec8bf52e00611d5a0f36801651 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 13:20:18 -0400 Subject: [PATCH 005/144] Add initial docs --- about.md | 0 local.js | 0 routes/uploads.js | 24 +++++++++++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 about.md create mode 100644 local.js diff --git a/about.md b/about.md new file mode 100644 index 0000000..e69de29 diff --git a/local.js b/local.js new file mode 100644 index 0000000..e69de29 diff --git a/routes/uploads.js b/routes/uploads.js index 8a24474..e288ef3 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -3,6 +3,11 @@ var Boom = require('boom'); module.exports = [ + /** + * @api {get} /uploads List uploads of currently authenticated user. + * @apiGroup uploads + * @apiSuccess {Object[]} results + */ { method: 'GET', path: '/uploads', @@ -14,6 +19,20 @@ module.exports = [ }); } }, + /** + * @api {post} /uploads Add an upload to the queue + * @apiGroup uploads + * + * @apiParam {Object} uploaderInfo + * @pariParam {string} uploaderInfo.name + * @pariParam {string} uploaderInfo.email + * @apiParam {Object} contactInfo + * @pariParam {string} contactInfo.name + * @pariParam {string} contactInfo.email + * @apiParam {Object[]} scenes + * @apiParam {Object} scenes.metadata The OAM metadata + * @apiParam {string[]} scenes.urls The image URLs + */ { method: 'POST', path: '/uploads', @@ -24,12 +43,15 @@ module.exports = [ } }, handler: function (request, reply) { + // TODO: validate request + var db = request.server.plugins.db.connection; var uploads = db.collection('uploads'); - console.log('uploads', uploads); uploads.insert([request.payload], function (err, result) { request.log(['debug'], result); if (err) { return reply(Boom.wrap(err)); } + // TODO: kick off the job + reply('Success'); }); } From 082f200830640141505ffc2574100dbb1dfbcc6a Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 14:14:22 -0400 Subject: [PATCH 006/144] Add stub for token authentication. --- index.js | 39 ++++++++++++++++++++++---------------- package.json | 1 + routes/uploads.js | 4 ++++ services/validate-token.js | 20 +++++++++++++++++++ 4 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 services/validate-token.js diff --git a/index.js b/index.js index a891a3a..3dd5e0b 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,8 @@ require('newrelic'); var Hapi = require('hapi'); -var config = require('./config.js'); +var config = require('./config'); +var validateToken = require('./services/validate-token'); var hapi = new Hapi.Server({ connections: { @@ -19,25 +20,31 @@ hapi.connection({ port: config.port }); // Register plugins hapi.register([ - { - register: require('good'), - options: config.logOptions - }, - require('./plugins/mongodb') + { register: require('good'), options: config.logOptions }, // logging + require('./plugins/mongodb'), // exports the db as plugins.db.connection + require('hapi-auth-bearer-token') // adds bearer-access-token scheme ], function (err) { if (err) throw err; -}); -// Register routes -hapi.register([ - { - register: require('hapi-router'), - options: { - routes: './routes/*.js' + // Set up API token auth strategy, accepting ?access_token=... or an HTTP + // bearer authorization header. + // Use on a route by setting config.auth: 'api-token'. + hapi.auth.strategy('api-token', 'bearer-access-token', { + accessTokenName: 'access_token', + validateFunc: validateToken(hapi.plugins.db.connection) + }); + + // Register routes + hapi.register([ + { + register: require('hapi-router'), + options: { + routes: './routes/*.js' + } } - } -], function (err) { - if (err) throw err; + ], function (err) { + if (err) throw err; + }); }); hapi.start(function () { diff --git a/package.json b/package.json index 282db2c..0123532 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", + "hapi-auth-bearer-token": "^3.1.1", "hapi-router": "^3.0.1", "lodash": "^3.8.0", "mongodb": "^2.0.40", diff --git a/routes/uploads.js b/routes/uploads.js index e288ef3..d218f3e 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -11,6 +11,9 @@ module.exports = [ { method: 'GET', path: '/uploads', + config: { + auth: 'api-token' + }, handler: function (request, reply) { var db = request.server.plugins.db.connection; db.collection('uploads').find({}).toArray(function (err, uploads) { @@ -37,6 +40,7 @@ module.exports = [ method: 'POST', path: '/uploads', config: { + auth: 'api-token', payload: { output: 'data', parse: true diff --git a/services/validate-token.js b/services/validate-token.js new file mode 100644 index 0000000..b260236 --- /dev/null +++ b/services/validate-token.js @@ -0,0 +1,20 @@ +/** + * Given a connection to the db, create a token validator function + * @param {Object} db A connection to the database + * @return {Function} A validation function that takes (token, callback) and calls the callback with (error, isValid, credentialsObject) + */ +module.exports = function (db) { + return function (token, callback) { + // TODO: replace the following with real auth (hit the db, etc.) + if (token === 'usertoken') { + // successful authentication + callback(null, true, { + user: 'Some Body', + token: token + }); + } else { + // bad token + callback(null, false, { token: token }); + } + }; +}; From 4ff973c5ea9019b6bd06a7a638a0044164282bb6 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 15:50:56 -0400 Subject: [PATCH 007/144] Validate incoming post and attach user id --- models/upload.js | 18 ++++++++++++++++++ package.json | 1 + routes/uploads.js | 26 +++++++++++++++++--------- services/validate-token.js | 5 ++++- 4 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 models/upload.js diff --git a/models/upload.js b/models/upload.js new file mode 100644 index 0000000..b262a93 --- /dev/null +++ b/models/upload.js @@ -0,0 +1,18 @@ +var Joi = require('joi'); + +var infoSchema = Joi.object().keys({ + name: Joi.string().min(3).max(30).required(), + email: Joi.string().email() +}); + +var sceneSchema = Joi.object().keys({ + metadata: Joi.object().required(), + urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https']})) + .min(1).required() +}); + +module.exports = Joi.object().keys({ + uploaderInfo: infoSchema.required(), + contactInfo: infoSchema.required(), + scenes: Joi.array().items(sceneSchema).min(1).required() +}); diff --git a/package.json b/package.json index 0123532..371d845 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "hapi": "^8.4.0", "hapi-auth-bearer-token": "^3.1.1", "hapi-router": "^3.0.1", + "joi": "^6.6.1", "lodash": "^3.8.0", "mongodb": "^2.0.40", "newrelic": "^1.20.0", diff --git a/routes/uploads.js b/routes/uploads.js index d218f3e..8a36bca 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -1,6 +1,8 @@ 'use strict'; var Boom = require('boom'); +var Joi = require('joi'); +var uploadSchema = require('../models/upload'); module.exports = [ /** @@ -15,8 +17,10 @@ module.exports = [ auth: 'api-token' }, handler: function (request, reply) { + let user = request.auth.credentials.user.id; var db = request.server.plugins.db.connection; - db.collection('uploads').find({}).toArray(function (err, uploads) { + db.collection('uploads').find({ user: user }) + .toArray(function (err, uploads) { if (err) { return reply(Boom.wrap(err)); } reply({ results: uploads }); }); @@ -42,21 +46,25 @@ module.exports = [ config: { auth: 'api-token', payload: { + allow: 'application/json', output: 'data', parse: true } }, handler: function (request, reply) { - // TODO: validate request + Joi.validate(request.payload, uploadSchema, function (err, data) { + if (err) { return reply(Boom.badRequest(err)); } - var db = request.server.plugins.db.connection; - var uploads = db.collection('uploads'); - uploads.insert([request.payload], function (err, result) { - request.log(['debug'], result); - if (err) { return reply(Boom.wrap(err)); } - // TODO: kick off the job + data.user = request.auth.credentials.user.id; + var db = request.server.plugins.db.connection; + var uploads = db.collection('uploads'); + uploads.insert([data], function (err, result) { + request.log(['debug'], result); + if (err) { return reply(Boom.wrap(err)); } + // TODO: kick off the job - reply('Success'); + reply('Success'); + }); }); } } diff --git a/services/validate-token.js b/services/validate-token.js index b260236..cde425e 100644 --- a/services/validate-token.js +++ b/services/validate-token.js @@ -9,7 +9,10 @@ module.exports = function (db) { if (token === 'usertoken') { // successful authentication callback(null, true, { - user: 'Some Body', + user: { + id: 1, // <- can be anything as long as it's unique; used for associations w uploads + name: 'Some Body' + }, token: token }); } else { From 24b9b59a4c583f9d355f6d67dab57d1bc15aeb32 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 23:36:20 -0400 Subject: [PATCH 008/144] Add worker job queue processing flow --- .build_scripts/reset-db.js | 30 ++++++++++ local.js | 0 package.json | 1 + plugins/mongodb.js | 4 +- routes/uploads.js | 34 ++++++++--- worker/index.js | 113 +++++++++++++++++++++++++++++++++++++ worker/process-upload.js | 7 +++ 7 files changed, 179 insertions(+), 10 deletions(-) create mode 100755 .build_scripts/reset-db.js delete mode 100644 local.js create mode 100644 worker/index.js create mode 100644 worker/process-upload.js diff --git a/.build_scripts/reset-db.js b/.build_scripts/reset-db.js new file mode 100755 index 0000000..4faffcc --- /dev/null +++ b/.build_scripts/reset-db.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node +'use strict'; +var readline = require('readline'); +var MongoClient = require('mongodb').MongoClient; +var dbUri = require('../config').dbUri; + +var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +rl.question("Type 'yes' if you really want to clear the database.", function (answer) { + if (answer === 'yes') { + console.log('Okay, doing it!'); + MongoClient.connect(dbUri, function (err, connection) { + if (err) throw err; + connection.collection('workers').deleteMany({}) + .then(function () { + return connection.collection('uploads').deleteMany({}); + }) + .then(function () { + console.log('Done.'); + connection.close(); + }); + }); + } else { + console.log('Abort! Whew!'); + } + rl.close(); +}); diff --git a/local.js b/local.js deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 371d845..c301ca5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "async": "^1.3.0", "boom": "^2.7.1", "envloader": "0.0.2", + "es6-promise": "^2.3.0", "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", diff --git a/plugins/mongodb.js b/plugins/mongodb.js index d950dfa..7624e47 100644 --- a/plugins/mongodb.js +++ b/plugins/mongodb.js @@ -7,9 +7,9 @@ var dbUri = require('../config').dbUri; * Exposes the mongodb connection as server.plugins.db.connection */ module.exports = function register (server, options, next) { - server.log(['debug'], 'Attempting db connection: ' + dbUri); + server.log(['info'], 'Attempting db connection: ' + dbUri); MongoClient.connect(dbUri, function (err, db) { - server.log(['debug'], 'Successful db connection.'); + server.log(['info'], 'Successful db connection.'); server.expose('connection', db); next(err); }); diff --git a/routes/uploads.js b/routes/uploads.js index 8a36bca..a59d7c1 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -1,5 +1,7 @@ 'use strict'; +var path = require('path'); +var fork = require('child_process').fork; var Boom = require('boom'); var Joi = require('joi'); var uploadSchema = require('../models/upload'); @@ -56,15 +58,31 @@ module.exports = [ if (err) { return reply(Boom.badRequest(err)); } data.user = request.auth.credentials.user.id; - var db = request.server.plugins.db.connection; - var uploads = db.collection('uploads'); - uploads.insert([data], function (err, result) { - request.log(['debug'], result); - if (err) { return reply(Boom.wrap(err)); } - // TODO: kick off the job + data.status = 'initial'; - reply('Success'); - }); + var workers = request.server.plugins.db.connection.collection('workers'); + var uploads = request.server.plugins.db.connection.collection('uploads'); + + workers.findOneAndUpdate({ state: 'working' }, { + $set: { state: 'paused' } + }) + .then(function (result) { + return uploads.insertOne(data) + .then(function () { + if (result.value) { + // we already have a worker - unpause it. + return workers.updateOne(result.value, { + $set: { state: 'working' } + }); + } else { + // spawn a worker + fork(path.join(__dirname, '../worker')); + return; + } + }); + }) + .then(function () { reply('Success'); }) + .catch(function (err) { reply(Boom.wrap(err)); }); }); } } diff --git a/worker/index.js b/worker/index.js new file mode 100644 index 0000000..88d3e40 --- /dev/null +++ b/worker/index.js @@ -0,0 +1,113 @@ +'use strict'; + +/* + * A worker that queries the db for new uploads and process 'em. + */ + +var MongoClient = require('mongodb').MongoClient; +var dbUri = require('../config').dbUri; +var processUpload = require('./process-upload'); + +var db; +var workers; +var uploads; +var workerId; +MongoClient.connect(dbUri, function (err, connection) { + if (err) { throw err; } + db = connection; + workers = db.collection('workers'); + uploads = db.collection('uploads'); + + process.on('SIGINT', cleanup); + + workers.insertOne({ state: 'working' }) + .then(function (result) { + workerId = result.ops[0]._id; + log('Started.'); + return mainloop(); + }) + .catch(cleanup); +}); + +// main loop +function mainloop () { + return dequeue() + .then(function (result) { + if (!result.value) { + // no jobs left; try to shut down. + // avoid race condition by making sure our state wasn't changed from + // 'working' to something else (by the server) before we actually quit. + return workers.updateOne({ _id: workerId, state: 'working' }, { + $set: { state: 'stopping' } + }) + .then(function (result) { + if (result.modifiedCount === 0) { return mainloop(); } + return cleanup(); + }); + } else { + // we got a job! + log('Processing job', result.value); + return processUpload(result.value) + .then(function (processedResult) { + return uploads.findOneAndUpdate(result.value, { + $set: { status: 'finished' }, + $unset: { _workerId: '' }, + $currentDate: { finishedAt: true } + }); + }) + .then(function (result) { + return mainloop(); + }); + } + }); +} + +// claim an upload for this worker to process +function dequeue () { + return uploads.findOneAndUpdate({status: 'initial'}, { + $set: { + status: 'pending', + _workerId: workerId + }, + $currentDate: { startedAt: true } + }, { returnOriginal: false }); +} + +function cleanup (err) { + log('Cleaning up.'); + if (err) { logError(err); } + if (db) { + if (workerId) { + workers.deleteOne({ _id: workerId }) + .then(function () { + return uploads.updateMany({ _workerId: workerId, status: 'pending' }, { + $set: { status: 'initial' }, + $unset: { _workerId: '', startedAt: '' } + }); + }) + .then(function () { + db.close(); + process.exit(err ? 1 : 0); + }) + .catch(function (error) { + err = error; + logError('Error cleaning up worker ' + workerId + '; bad news.'); + logError(err); + db.close(); + process.exit(1); + }); + } + } else { + process.exit(err ? 1 : 0); + } +} + +function log () { + var args = Array.prototype.slice.call(arguments); + console.log.apply(console, ['[ Worker ' + workerId + ' ]'].concat(args)); +} + +function logError () { + var args = Array.prototype.slice.call(arguments); + console.error.apply(console, ['[ Worker ' + workerId + ' ]'].concat(args)); +} diff --git a/worker/process-upload.js b/worker/process-upload.js new file mode 100644 index 0000000..c6ba5a6 --- /dev/null +++ b/worker/process-upload.js @@ -0,0 +1,7 @@ +var Promise = require('es6-promise').Promise; + +module.exports = function processUpload (upload) { + return new Promise(function (resolve, reject) { + setTimeout(resolve.bind(null, upload), 60000); + }); +}; From 2d0bc5de01f91afe759328da4e3bca7f1ee7bc5d Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 23:47:22 -0400 Subject: [PATCH 009/144] Add post example --- routes/uploads.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/routes/uploads.js b/routes/uploads.js index a59d7c1..1f257f5 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -41,6 +41,27 @@ module.exports = [ * @apiParam {Object[]} scenes * @apiParam {Object} scenes.metadata The OAM metadata * @apiParam {string[]} scenes.urls The image URLs + * + * @apiExample {js} Example post + * { + * "uploaderInfo": { + * "name": "Anand", + * "email": "me@foo.com" + * }, + * "contactInfo": { + * "name": "Anand", + * "email": "me@foo.com" + * }, + * "scenes": [ + * { + * "metadata": {}, + * "urls": [ + * "http://myimagery.com/image01.tif", + * "http://myimagery.com/image02.tif" + * ] + * } + * ] + * } */ { method: 'POST', From 40f279dfe75a080feb481a87f22fb65648e11eee Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 7 Aug 2015 23:53:25 -0400 Subject: [PATCH 010/144] LInk to API docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21e30f1..983c35c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) -API Documentation: TBD +## [API Docs](http://hotosm.github.io/oam-uploader-api/) ## Installation and Usage From b114053d52f5296779f27735183a1dd1afc8990b Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Mon, 10 Aug 2015 11:52:09 +0100 Subject: [PATCH 011/144] Add user login for token management --- README.md | 2 + index.js | 23 +++++++++-- package.json | 1 + routes/uploads.js | 2 +- routes/user.js | 68 ++++++++++++++++++++++++++++++++ services/validate-user-cookie.js | 16 ++++++++ 6 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 routes/user.js create mode 100644 services/validate-user-cookie.js diff --git a/README.md b/README.md index 983c35c..8f5eef1 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ The API exposes endpoints used to access information form the system via a RESTf - `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN buckets - `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN buckets - `DBURI` - MongoDB connection url +- `ADMIN_USERNAME` - Token management Admin username +- `ADMIN_PASSWORD` - Token management Admin password ### Docs Deployment Changes to `master` branch are automatically deployed via Travis to https://oam-uploader-api.herokuapp.com. diff --git a/index.js b/index.js index 3dd5e0b..262cedf 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ require('newrelic'); var Hapi = require('hapi'); var config = require('./config'); var validateToken = require('./services/validate-token'); +var validateUserCookie = require('./services/validate-user-cookie'); var hapi = new Hapi.Server({ connections: { @@ -20,9 +21,14 @@ hapi.connection({ port: config.port }); // Register plugins hapi.register([ - { register: require('good'), options: config.logOptions }, // logging - require('./plugins/mongodb'), // exports the db as plugins.db.connection - require('hapi-auth-bearer-token') // adds bearer-access-token scheme + // logging + { register: require('good'), options: config.logOptions }, + // exports the db as plugins.db.connection + require('./plugins/mongodb'), + // adds bearer-access-token scheme + require('hapi-auth-bearer-token'), + // Cookie auth. + require('hapi-auth-cookie') ], function (err) { if (err) throw err; @@ -34,6 +40,17 @@ hapi.register([ validateFunc: validateToken(hapi.plugins.db.connection) }); + // Setup cookie auth. + // Hapi cookie plugin configuration. + hapi.auth.strategy('session', 'cookie', { + password: '3b296ce42ec560abeabaef57379aee68249a6d7912ac19cf70f10a35021fc9df7453225c4dcd9f6defaf242e701f50bbd3f2b63616029bfcd8ddf53f406079d6', + cookie: 'oam-uploader-api', + redirectTo: false, + // Change for production. + isSecure: false, + validateFunc: validateUserCookie(hapi.plugins.db.connection) + }); + // Register routes hapi.register([ { diff --git a/package.json b/package.json index c301ca5..4b008d8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "good-console": "^5.0.2", "hapi": "^8.4.0", "hapi-auth-bearer-token": "^3.1.1", + "hapi-auth-cookie": "^3.0.1", "hapi-router": "^3.0.1", "joi": "^6.6.1", "lodash": "^3.8.0", diff --git a/routes/uploads.js b/routes/uploads.js index 1f257f5..bc6fbb7 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -19,7 +19,7 @@ module.exports = [ auth: 'api-token' }, handler: function (request, reply) { - let user = request.auth.credentials.user.id; + var user = request.auth.credentials.user.id; var db = request.server.plugins.db.connection; db.collection('uploads').find({ user: user }) .toArray(function (err, uploads) { diff --git a/routes/user.js b/routes/user.js new file mode 100644 index 0000000..7252474 --- /dev/null +++ b/routes/user.js @@ -0,0 +1,68 @@ +'use strict'; + +module.exports = [ + { + method: ['GET', 'POST'], + path: '/login', + config: { + auth: {mode: 'try', strategy: 'session' }, + }, + handler: function (request, reply) { + if (request.auth.isAuthenticated) { + return reply({ + statusCode: 200, + message: 'Welcome back' + }); + } + + if (request.method === 'post') { + var username = request.payload.username; + var password = request.payload.password; + + if (!username || !password) { + return reply({ + statusCode: 400, + message: 'Missing username and/or password' + }).code(400); + } + + if (username != process.env.ADMIN_USERNAME || password != process.env.ADMIN_PASSWORD) { + return reply({ + statusCode: 401, + message: 'Invalid username and/or password' + }).code(401); + } + + request.auth.session.set({ + username: username + }); + + return reply({ + statusCode: 200, + message: 'User logged' + }); + } + else { + return reply({ + statusCode: 401, + message: 'User not logged' + }).code(401); + } + } + }, + + { + method: 'GET', + path: '/logout', + config: { + auth: 'session', + }, + handler: function (request, reply) { + request.auth.session.clear(); + return reply({ + code: 200, + message: 'Goodbye!' + }); + } + } +]; diff --git a/services/validate-user-cookie.js b/services/validate-user-cookie.js new file mode 100644 index 0000000..194cff5 --- /dev/null +++ b/services/validate-user-cookie.js @@ -0,0 +1,16 @@ +/** + * Given a connection to the db, create a token validator function + * @param {Object} db A connection to the database + * @return {Function} A validation function that takes (token, callback) and calls the callback with (error, isValid, credentialsObject) + */ +module.exports = function (db) { + return function (req, session, callback) { + if (session.username == process.env.ADMIN_USERNAME) { + return callback(null, true, session); + } + else { + // Session no longer valid + return callback(null, false); + } + }; +}; From f6c62b5f20103579975f14ebc4e5f55c2a0ef89e Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Mon, 10 Aug 2015 11:54:29 +0100 Subject: [PATCH 012/144] Add routes for token management --- routes/tokens.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 routes/tokens.js diff --git a/routes/tokens.js b/routes/tokens.js new file mode 100644 index 0000000..332c60b --- /dev/null +++ b/routes/tokens.js @@ -0,0 +1,59 @@ +'use strict'; + +module.exports = [ + { + method: 'GET', + path: '/tokens', + config: { + auth: 'session', + }, + handler: function (request, reply) { + return reply({ + code: 200, + message: 'LIST endpoint not implemented yet.' + }); + } + }, + + { + method: 'POST', + path: '/tokens', + config: { + auth: 'session', + }, + handler: function (request, reply) { + return reply({ + code: 200, + message: 'CREATE endpoint not implemented yet.' + }); + } + }, + + { + method: 'PUT', + path: '/tokens/{token_id}', + config: { + auth: 'session', + }, + handler: function (request, reply) { + return reply({ + code: 200, + message: 'UPDATE endpoint not implemented yet.' + }); + } + }, + + { + method: 'DELETE', + path: '/tokens/{token_id}', + config: { + auth: 'session', + }, + handler: function (request, reply) { + return reply({ + code: 200, + message: 'DELETE endpoint not implemented yet.' + }); + } + } +]; From f221d40da632022bb3a3b86547bbdbf091ac6175 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Mon, 10 Aug 2015 14:35:42 +0100 Subject: [PATCH 013/144] Implement token management endpoints --- package.json | 4 +- routes/tokens.js | 123 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 4b008d8..2ff5a5e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "homepage": "https://github.com/hotosm/oam-uploader-api", "dependencies": { "async": "^1.3.0", - "boom": "^2.7.1", + "boom": "^2.8.0", "envloader": "0.0.2", "es6-promise": "^2.3.0", "good": "^6.3.0", @@ -29,7 +29,7 @@ "hapi-auth-cookie": "^3.0.1", "hapi-router": "^3.0.1", "joi": "^6.6.1", - "lodash": "^3.8.0", + "lodash": "^3.10.1", "mongodb": "^2.0.40", "newrelic": "^1.20.0", "request": "^2.55.0", diff --git a/routes/tokens.js b/routes/tokens.js index 332c60b..727b52c 100644 --- a/routes/tokens.js +++ b/routes/tokens.js @@ -1,5 +1,11 @@ 'use strict'; +var Joi = require('joi'); +var Boom = require('boom'); +var ObjectId = require('mongodb').ObjectID; +var crypto = require('crypto'); +var _ = require('lodash'); + module.exports = [ { method: 'GET', @@ -8,9 +14,19 @@ module.exports = [ auth: 'session', }, handler: function (request, reply) { - return reply({ - code: 200, - message: 'LIST endpoint not implemented yet.' + var tokensCol = request.server.plugins.db.connection.collection('tokens'); + + tokensCol.find().toArray(function (err, tokens) { + if (err) { + return reply(Boom.badImplementation()); + } + + return reply({ + code: 200, + message: null, + data: tokens + }); + }); } }, @@ -20,11 +36,38 @@ module.exports = [ path: '/tokens', config: { auth: 'session', + validate: { + payload: { + name: Joi.string().required(), + expiration: Joi.alternatives().try(Joi.date().min('now'), Joi.boolean()).invalid(true).required(), + status: Joi.any().valid('active', 'blocked').required() + } + } }, handler: function (request, reply) { - return reply({ - code: 200, - message: 'CREATE endpoint not implemented yet.' + var tokensCol = request.server.plugins.db.connection.collection('tokens'); + + // Generate a token. + var hmac = crypto.createHmac('sha512', 'oam-upload-key'); + hmac.setEncoding('hex'); + hmac.write(Math.random().toString(36)); + hmac.end(); + + var data = request.payload; + data.token = hmac.read(); + data.created = new Date(); + data.updated = null; + + tokensCol.insert(data, function(err, res) { + if (err) { + return reply(Boom.badImplementation()); + } + + return reply({ + code: 201, + message: null, + data: data + }).code(201); }); } }, @@ -34,11 +77,50 @@ module.exports = [ path: '/tokens/{token_id}', config: { auth: 'session', + validate: { + payload: { + name: Joi.string(), + expiration: Joi.alternatives().try(Joi.date().min('now'), Joi.boolean()).invalid(true), + status: Joi.any().valid('active', 'blocked') + }, + params: { + token_id: Joi.string().hex().length(24).required() + } + } }, handler: function (request, reply) { - return reply({ - code: 200, - message: 'UPDATE endpoint not implemented yet.' + var tokensCol = request.server.plugins.db.connection.collection('tokens'); + + var update = {$set: {}}; + if (request.payload.name !== undefined) { + update.$set.name = request.payload.name; + } + if (request.payload.expiration !== undefined) { + update.$set.expiration = request.payload.expiration; + } + if (request.payload.status !== undefined) { + update.$set.status = request.payload.status; + } + + if (_.isEmpty(update.$set)) { + return reply({ + code: 200, + message: 'Nothing to update', + }); + } + + update.$set.updated = new Date(); + + tokensCol.findAndModify({_id: new ObjectId(request.params.token_id)}, [], update, {new:true}, function(err, res) { + if (err) { + return reply(Boom.badImplementation()); + } + + return reply({ + code: 200, + message: null, + data: res.value + }); }); } }, @@ -48,11 +130,28 @@ module.exports = [ path: '/tokens/{token_id}', config: { auth: 'session', + validate: { + params: { + token_id: Joi.string().hex().length(24).required() + } + } }, handler: function (request, reply) { - return reply({ - code: 200, - message: 'DELETE endpoint not implemented yet.' + var tokensCol = request.server.plugins.db.connection.collection('tokens'); + + tokensCol.remove({_id: new ObjectId(request.params.token_id)}, function (err, res) { + if (err) { + return reply(Boom.badImplementation()); + } + + if (res.result.n == 0) { + return reply(Boom.notFound()); + } + + return reply({ + code: 200, + message: null + }); }); } } From 236a9fa851b90d1938ae78ac4cf063617922977c Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 10 Aug 2015 11:03:08 -0400 Subject: [PATCH 014/144] Switch node version to iojs 2.5.0 --- .travis.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83cacda..9035d99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- '0.12' +- 'iojs-v2.5.0' env: global: - GH_REF=github.com/hotosm/oam-uploader-api.git diff --git a/package.json b/package.json index c301ca5..f0abad2 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "tape": "^4.0.3" }, "engines": { - "node": "0.12.2" + "iojs": "2.5.x" }, "apidoc": { "title": "OAM API", From b600142d8053a42d725b0a4d3ba55ff88fa05718 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Mon, 10 Aug 2015 17:49:36 +0100 Subject: [PATCH 015/144] Apply necessary changes to handle tests. Add tests for login endpoint. Add initial tests for token endpoint. Correct linting issues. --- index.js | 122 +++++++++++++++++------------- package.json | 3 +- routes/tokens.js | 10 +-- routes/user.js | 10 +-- services/validate-user-cookie.js | 5 +- test/test__token_management.js | 103 +++++++++++++++++++++++++ test/test__user_authentication.js | 119 +++++++++++++++++++++++++++++ 7 files changed, 305 insertions(+), 67 deletions(-) create mode 100644 test/test__token_management.js create mode 100644 test/test__user_authentication.js diff --git a/index.js b/index.js index 262cedf..3cdc89d 100644 --- a/index.js +++ b/index.js @@ -6,65 +6,81 @@ var config = require('./config'); var validateToken = require('./services/validate-token'); var validateUserCookie = require('./services/validate-user-cookie'); -var hapi = new Hapi.Server({ - connections: { - routes: { - cors: true - }, - router: { - stripTrailingSlash: true +var OAMUploader = function (readyCb) { + var hapi = new Hapi.Server({ + connections: { + routes: { + cors: true + }, + router: { + stripTrailingSlash: true + } } - } -}); - -hapi.connection({ port: config.port }); - -// Register plugins -hapi.register([ - // logging - { register: require('good'), options: config.logOptions }, - // exports the db as plugins.db.connection - require('./plugins/mongodb'), - // adds bearer-access-token scheme - require('hapi-auth-bearer-token'), - // Cookie auth. - require('hapi-auth-cookie') -], function (err) { - if (err) throw err; - - // Set up API token auth strategy, accepting ?access_token=... or an HTTP - // bearer authorization header. - // Use on a route by setting config.auth: 'api-token'. - hapi.auth.strategy('api-token', 'bearer-access-token', { - accessTokenName: 'access_token', - validateFunc: validateToken(hapi.plugins.db.connection) }); - // Setup cookie auth. - // Hapi cookie plugin configuration. - hapi.auth.strategy('session', 'cookie', { - password: '3b296ce42ec560abeabaef57379aee68249a6d7912ac19cf70f10a35021fc9df7453225c4dcd9f6defaf242e701f50bbd3f2b63616029bfcd8ddf53f406079d6', - cookie: 'oam-uploader-api', - redirectTo: false, - // Change for production. - isSecure: false, - validateFunc: validateUserCookie(hapi.plugins.db.connection) - }); + hapi.connection({port: config.port }); - // Register routes + // Register plugins hapi.register([ - { - register: require('hapi-router'), - options: { - routes: './routes/*.js' - } - } + // logging + { register: require('good'), options: config.logOptions }, + // exports the db as plugins.db.connection + require('./plugins/mongodb'), + // adds bearer-access-token scheme + require('hapi-auth-bearer-token'), + // Cookie auth. + require('hapi-auth-cookie') ], function (err) { if (err) throw err; + // Set up API token auth strategy, accepting ?access_token=... or an HTTP + // bearer authorization header. + // Use on a route by setting config.auth: 'api-token'. + hapi.auth.strategy('api-token', 'bearer-access-token', { + accessTokenName: 'access_token', + validateFunc: validateToken(hapi.plugins.db.connection) + }); + + // Setup cookie auth. + // Hapi cookie plugin configuration. + hapi.auth.strategy('session', 'cookie', { + password: '3b296ce42ec560abeabaef57379aee68249a6d7912ac19cf70f10a35021fc9df7453225c4dcd9f6defaf242e701f50bbd3f2b63616029bfcd8ddf53f406079d6', + cookie: 'oam-uploader-api', + redirectTo: false, + // Change for production. + isSecure: false, + validateFunc: validateUserCookie(hapi.plugins.db.connection) + }); + + // Register routes + hapi.register([ + { + register: require('hapi-router'), + options: { + routes: './routes/*.js' + } + } + ], function (err) { + if (err) throw err; + + readyCb(hapi); + }); + }); + +}; + +// https://medium.com/the-spumko-suite/testing-hapi-services-with-lab-96ac463c490a +// The if (!module.parent) {…} conditional makes sure that if the script is +// being required as a module by another script, we don’t start the server. +// This is done to prevent the server from starting when we’re testing it. +// With Hapi, we don’t need to have the server listening to test it. +if (!module.parent) { + OAMUploader(function (hapi) { + // Start the server. + hapi.start(function () { + hapi.log(['info'], 'Server running at:' + hapi.info.uri); + hapi.log(['debug'], 'Config: ' + JSON.stringify(config)); + }); }); -}); +} -hapi.start(function () { - hapi.log(['info'], 'Server running at:' + hapi.info.uri); - hapi.log(['debug'], 'Config: ' + JSON.stringify(config)); -}); +module.exports = OAMUploader; diff --git a/package.json b/package.json index 2ff5a5e..5137b57 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "An uploader API for Open Aerial Map Imagery", "main": "index.js", "scripts": { - "test": "semistandard && tape test/*.js", + "test": "semistandard && lab test/test__*.js", "docs": "apidoc -i routes/ -o docs/" }, "repository": { @@ -37,6 +37,7 @@ "wellknown": "^0.3.1" }, "devDependencies": { + "lab": "^5.15.0", "apidoc": "^0.13.1", "chai": "^2.3.0", "semistandard": "^4.1.4", diff --git a/routes/tokens.js b/routes/tokens.js index 727b52c..ab85be8 100644 --- a/routes/tokens.js +++ b/routes/tokens.js @@ -11,7 +11,7 @@ module.exports = [ method: 'GET', path: '/tokens', config: { - auth: 'session', + auth: 'session' }, handler: function (request, reply) { var tokensCol = request.server.plugins.db.connection.collection('tokens'); @@ -58,7 +58,7 @@ module.exports = [ data.created = new Date(); data.updated = null; - tokensCol.insert(data, function(err, res) { + tokensCol.insert(data, function (err, res) { if (err) { return reply(Boom.badImplementation()); } @@ -105,13 +105,13 @@ module.exports = [ if (_.isEmpty(update.$set)) { return reply({ code: 200, - message: 'Nothing to update', + message: 'Nothing to update' }); } update.$set.updated = new Date(); - tokensCol.findAndModify({_id: new ObjectId(request.params.token_id)}, [], update, {new:true}, function(err, res) { + tokensCol.findAndModify({_id: new ObjectId(request.params.token_id)}, [], update, {new: true}, function (err, res) { if (err) { return reply(Boom.badImplementation()); } @@ -144,7 +144,7 @@ module.exports = [ return reply(Boom.badImplementation()); } - if (res.result.n == 0) { + if (res.result.n === 0) { return reply(Boom.notFound()); } diff --git a/routes/user.js b/routes/user.js index 7252474..cd4dcf5 100644 --- a/routes/user.js +++ b/routes/user.js @@ -5,7 +5,7 @@ module.exports = [ method: ['GET', 'POST'], path: '/login', config: { - auth: {mode: 'try', strategy: 'session' }, + auth: {mode: 'try', strategy: 'session'} }, handler: function (request, reply) { if (request.auth.isAuthenticated) { @@ -26,7 +26,7 @@ module.exports = [ }).code(400); } - if (username != process.env.ADMIN_USERNAME || password != process.env.ADMIN_PASSWORD) { + if (username !== process.env.ADMIN_USERNAME || password !== process.env.ADMIN_PASSWORD) { return reply({ statusCode: 401, message: 'Invalid username and/or password' @@ -41,8 +41,8 @@ module.exports = [ statusCode: 200, message: 'User logged' }); - } - else { + + } else { return reply({ statusCode: 401, message: 'User not logged' @@ -55,7 +55,7 @@ module.exports = [ method: 'GET', path: '/logout', config: { - auth: 'session', + auth: 'session' }, handler: function (request, reply) { request.auth.session.clear(); diff --git a/services/validate-user-cookie.js b/services/validate-user-cookie.js index 194cff5..8410d8b 100644 --- a/services/validate-user-cookie.js +++ b/services/validate-user-cookie.js @@ -5,10 +5,9 @@ */ module.exports = function (db) { return function (req, session, callback) { - if (session.username == process.env.ADMIN_USERNAME) { + if (session.username === process.env.ADMIN_USERNAME) { return callback(null, true, session); - } - else { + } else { // Session no longer valid return callback(null, false); } diff --git a/test/test__token_management.js b/test/test__token_management.js new file mode 100644 index 0000000..67448fa --- /dev/null +++ b/test/test__token_management.js @@ -0,0 +1,103 @@ +'use strict'; + +var Lab = require('lab'); +var server = require('../'); +var chai = require('chai'); +var MongoClient = require('mongodb').MongoClient; + +var lab = exports.lab = Lab.script(); +var suite = lab.experiment; +var test = lab.test; +var before = lab.before; +var after = lab.after; +var assert = chai.assert; + +var cookie = null; + +suite('test tokens', function () { + + before(function (done) { + // Get a reference to the server. + // Wait for everything to load. + // Change to test db + server(function (hapi) { + server = hapi; + // Prepare db. + // Close current connection. + hapi.plugins.db.connection.close(function () { + // Open connection to test DB. + MongoClient.connect('mongodb://localhost/oam-uploader-test', function (err, db) { + if (err) throw err; + hapi.plugins.db.connection = db; + + // Insert some data. + db.collection('tokens').insert([ + { + name: 'Primary token', + expiration: false, + status: 'active', + token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', + created: '2015-08-10T13:38:48.684Z', + updated: null + }, + { + name: 'Secondary token', + expiration: '2020-08-10T13:38:48.684Z', + status: 'active', + token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', + created: '2015-08-10T13:38:48.684Z', + updated: null + } + ], function (err, res) { + if (err) throw err; + // Cookie + var options = { + method: 'POST', + url: '/login', + payload: { + username: 'admin', + password: 'admin' + } + }; + + server.inject(options, function (response) { + // Store the cookie to use on next requests + cookie = response.headers['set-cookie'][0].split(' ')[0]; + done(); + }); + + }); + }); + }); + }); + }); + + after(function (done) { + server.plugins.db.connection.dropDatabase(function () { + done(); + }); + }); + + test('should create token', function (done) { + var options = { + method: 'POST', + url: '/tokens', + headers: { + Cookie: cookie + }, + payload: { + name: 'Test token', + expiration: false, + status: 'active' + } + }; + + server.inject(options, function (response) { + // var result = response.result; + assert.equal(response.statusCode, 201); + done(); + }); + }); + +}); + diff --git a/test/test__user_authentication.js b/test/test__user_authentication.js new file mode 100644 index 0000000..571dac6 --- /dev/null +++ b/test/test__user_authentication.js @@ -0,0 +1,119 @@ +'use strict'; + +var Lab = require('lab'); +var server = require('../'); +var chai = require('chai'); + +var lab = exports.lab = Lab.script(); +var suite = lab.experiment; +var test = lab.test; +var before = lab.before; +// var after = lab.after; +var assert = chai.assert; + +var cookie = null; + +suite('test authentication', function () { + + before(function (done) { + // Get a reference to the server. + // Wait for everything to load. + server(function (hapi) { + server = hapi; + done(); + }); + }); + + test('should fail authentication with missing username and password', function (done) { + var options = { + method: 'POST', + url: '/login' + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 400); + done(); + }); + }); + + test('should fail authentication with missing password', function (done) { + var options = { + method: 'POST', + url: '/login', + payload: { + username: 'admin' + } + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 400); + done(); + }); + }); + + test('should fail authentication with missing username', function (done) { + var options = { + method: 'POST', + url: '/login', + payload: { + password: 'admin' + } + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 400); + done(); + }); + }); + + test('should fail authentication with invalid credentials', function (done) { + var options = { + method: 'POST', + url: '/login', + payload: { + username: 'not the username', + password: 'not the password' + } + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 401); + done(); + }); + }); + + test('should authenticate using user and password', function (done) { + var options = { + method: 'POST', + url: '/login', + payload: { + username: 'admin', + password: 'admin' + } + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 200); + + // Store the cookie to use on next requests + cookie = response.headers['set-cookie'][0].split(' ')[0]; + done(); + }); + }); + + test('should be authenticated using cookie from prev request', function (done) { + var options = { + method: 'GET', + headers: { + Cookie: cookie + }, + url: '/login' + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 200); + done(); + }); + }); +}); + From 340d488b7b1b5f8c0c9b9e43a52f28c694100588 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 10 Aug 2015 21:45:48 -0400 Subject: [PATCH 016/144] Ignore local.js --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 72d63a6..2aeae07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +local.js node_modules bower_components .tmp From e948b5fc532294c7c0f5497a94972d0ef17414d1 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 10 Aug 2015 22:26:44 -0400 Subject: [PATCH 017/144] Send back error when admin login is not configured --- config.js | 2 ++ routes/user.js | 10 ++++++++-- services/validate-user-cookie.js | 5 +++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/config.js b/config.js index 61eff8e..95c2ddb 100644 --- a/config.js +++ b/config.js @@ -6,6 +6,8 @@ try { module.exports = { port: process.env.PORT || 3000, dbUri: process.env.DBURI || 'mongodb://localhost/oam-uploader', + adminPassword: process.env.ADMIN_PASSWORD || null, + adminUsername: process.env.ADMIN_USERNAME || null, logOptions: local.logOptions || { opsInterval: 3000, reporters: [{ diff --git a/routes/user.js b/routes/user.js index cd4dcf5..bbb4c9a 100644 --- a/routes/user.js +++ b/routes/user.js @@ -1,5 +1,8 @@ 'use strict'; +var Boom = require('boom'); +var config = require('../config'); + module.exports = [ { method: ['GET', 'POST'], @@ -15,6 +18,10 @@ module.exports = [ }); } + if (!config.adminUsername || !config.adminPassword) { + return reply(Boom.badImplementation('Admin username and password are not configured')); + } + if (request.method === 'post') { var username = request.payload.username; var password = request.payload.password; @@ -26,7 +33,7 @@ module.exports = [ }).code(400); } - if (username !== process.env.ADMIN_USERNAME || password !== process.env.ADMIN_PASSWORD) { + if (username !== config.adminUsername || password !== config.adminPassword) { return reply({ statusCode: 401, message: 'Invalid username and/or password' @@ -41,7 +48,6 @@ module.exports = [ statusCode: 200, message: 'User logged' }); - } else { return reply({ statusCode: 401, diff --git a/services/validate-user-cookie.js b/services/validate-user-cookie.js index 8410d8b..ca8ebe5 100644 --- a/services/validate-user-cookie.js +++ b/services/validate-user-cookie.js @@ -1,11 +1,12 @@ +var config = require('../config'); /** * Given a connection to the db, create a token validator function * @param {Object} db A connection to the database - * @return {Function} A validation function that takes (token, callback) and calls the callback with (error, isValid, credentialsObject) + * @return {Function} A validation function that takes (req, session, callback) and calls the callback with (error, isValid, credentialsObject) */ module.exports = function (db) { return function (req, session, callback) { - if (session.username === process.env.ADMIN_USERNAME) { + if (session.username === config.adminUsername) { return callback(null, true, session); } else { // Session no longer valid From 5cff541927dd3df9f430230dfd25d786fe58c71f Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Tue, 11 Aug 2015 09:27:06 +0100 Subject: [PATCH 018/144] Wrap errors --- routes/tokens.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routes/tokens.js b/routes/tokens.js index ab85be8..f29c2de 100644 --- a/routes/tokens.js +++ b/routes/tokens.js @@ -18,7 +18,7 @@ module.exports = [ tokensCol.find().toArray(function (err, tokens) { if (err) { - return reply(Boom.badImplementation()); + return reply(Boom.wrap(err)); } return reply({ @@ -60,7 +60,7 @@ module.exports = [ tokensCol.insert(data, function (err, res) { if (err) { - return reply(Boom.badImplementation()); + return reply(Boom.wrap(err)); } return reply({ @@ -113,7 +113,7 @@ module.exports = [ tokensCol.findAndModify({_id: new ObjectId(request.params.token_id)}, [], update, {new: true}, function (err, res) { if (err) { - return reply(Boom.badImplementation()); + return reply(Boom.wrap(err)); } return reply({ @@ -141,7 +141,7 @@ module.exports = [ tokensCol.remove({_id: new ObjectId(request.params.token_id)}, function (err, res) { if (err) { - return reply(Boom.badImplementation()); + return reply(Boom.wrap(err)); } if (res.result.n === 0) { From 6d1bd28ec952c624105fe57c70d25e16288f1aa3 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Tue, 11 Aug 2015 12:08:16 +0100 Subject: [PATCH 019/144] Add more tests for the tokens --- package.json | 1 + routes/tokens.js | 2 +- test/test__token_management.js | 343 ++++++++++++++++++++++++++++++++- 3 files changed, 340 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9abdaf7..b028fff 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "test": "semistandard && lab test/test__*.js", + "lab": "lab test/test__*.js", "docs": "apidoc -i routes/ -o docs/" }, "repository": { diff --git a/routes/tokens.js b/routes/tokens.js index f29c2de..46c6bd4 100644 --- a/routes/tokens.js +++ b/routes/tokens.js @@ -96,7 +96,7 @@ module.exports = [ update.$set.name = request.payload.name; } if (request.payload.expiration !== undefined) { - update.$set.expiration = request.payload.expiration; + update.$set.expiration = request.payload.expiration === false ? false : new Date(request.payload.expiration); } if (request.payload.status !== undefined) { update.$set.status = request.payload.status; diff --git a/test/test__token_management.js b/test/test__token_management.js index 67448fa..0791796 100644 --- a/test/test__token_management.js +++ b/test/test__token_management.js @@ -4,6 +4,7 @@ var Lab = require('lab'); var server = require('../'); var chai = require('chai'); var MongoClient = require('mongodb').MongoClient; +var ObjectId = require('mongodb').ObjectID; var lab = exports.lab = Lab.script(); var suite = lab.experiment; @@ -33,19 +34,21 @@ suite('test tokens', function () { // Insert some data. db.collection('tokens').insert([ { + _id: new ObjectId('55c88dddd2f727d042b097b4'), name: 'Primary token', expiration: false, status: 'active', token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', - created: '2015-08-10T13:38:48.684Z', + created: new Date('2015-08-10T13:38:48.684Z'), updated: null }, { + _id: new ObjectId('55c88eeee2f727d042b097b5'), name: 'Secondary token', - expiration: '2020-08-10T13:38:48.684Z', + expiration: new Date('2020-08-10T13:38:48.684Z'), status: 'active', token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', - created: '2015-08-10T13:38:48.684Z', + created: new Date('2015-08-10T13:38:48.684Z'), updated: null } ], function (err, res) { @@ -78,10 +81,174 @@ suite('test tokens', function () { }); }); + test('should list tokens', function (done) { + var options = { + method: 'GET', + url: '/tokens', + headers: { + Cookie: cookie + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 200); + + assert.lengthOf(result.data, 2); + assert.equal(result.data[0].name, 'Primary token'); + assert.equal(result.data[1].name, 'Secondary token'); + done(); + }); + }); + + test('should fail token creation when missing payload', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + } + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 400); + done(); + }); + }); + + test('should fail token creation when missing name', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + }, + payload: { + expiration: false, + status: 'active' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"name" fails'); + done(); + }); + }); + + test('should fail token creation when missing expiration', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + }, + payload: { + name: 'Test token', + status: 'active' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"expiration" fails'); + done(); + }); + }); + + test('should fail token creation when missing status', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + }, + payload: { + name: 'Test token', + expiration: false + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"status" fails'); + done(); + }); + }); + + test('should fail token creation with invalid boolean expiration', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + }, + payload: { + name: 'Test token', + expiration: true, + status: 'active' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"expiration" fails'); + done(); + }); + }); + + test('should fail token creation with invalid expiration', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + }, + payload: { + name: 'Test token', + expiration: '2015-20-20 25:62:00', + status: 'active' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"expiration" fails'); + done(); + }); + }); + + test('should fail token creation with invalid status', function (done) { + var options = { + method: 'POST', + url: '/tokens/', + headers: { + Cookie: cookie + }, + payload: { + name: 'Test token', + expiration: false, + status: 'none' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"status" fails'); + done(); + }); + }); + test('should create token', function (done) { var options = { method: 'POST', - url: '/tokens', + url: '/tokens/', headers: { Cookie: cookie }, @@ -93,11 +260,177 @@ suite('test tokens', function () { }; server.inject(options, function (response) { - // var result = response.result; + var result = response.result; assert.equal(response.statusCode, 201); + + assert.isDefined(result.data._id); + done(); + }); + }); + + test('should fail token update with invalid boolean expiration', function (done) { + var options = { + method: 'PUT', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + }, + payload: { + expiration: true + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"expiration" fails'); + done(); + }); + }); + + test('should fail token update with invalid date expiration', function (done) { + var options = { + method: 'PUT', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + }, + payload: { + expiration: '2015-20-20 25:62:00' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 400); + assert.include(result.message, '"expiration" fails'); + done(); + }); + }); + + test('should update token name', function (done) { + var options = { + method: 'PUT', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + }, + payload: { + name: 'Secondary token modified' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 200); + + assert.equal(result.data.name, 'Secondary token modified'); + assert.equal(result.data.status, 'active'); + assert.equal(result.data.expiration, 'Mon Aug 10 2020 14:38:48 GMT+0100 (WEST)'); + done(); + }); + }); + + test('should remove token expiration date', function (done) { + var options = { + method: 'PUT', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + }, + payload: { + expiration: false + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 200); + + assert.equal(result.data.name, 'Secondary token modified'); + assert.equal(result.data.status, 'active'); + assert.equal(result.data.expiration, false); + done(); + }); + }); + + test('should update token status', function (done) { + var options = { + method: 'PUT', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + }, + payload: { + status: 'blocked' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 200); + + assert.equal(result.data.name, 'Secondary token modified'); + assert.equal(result.data.status, 'blocked'); + assert.equal(result.data.expiration, false); done(); }); }); + test('should update all token values', function (done) { + var options = { + method: 'PUT', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + }, + payload: { + name: 'Secondary token', + status: 'active', + expiration: '2020-08-10T13:38:48.684Z' + } + }; + + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 200); + + assert.equal(result.data.name, 'Secondary token'); + assert.equal(result.data.status, 'active'); + assert.equal(result.data.expiration, 'Mon Aug 10 2020 14:38:48 GMT+0100 (WEST)'); + done(); + }); + }); + + test('should delete token', function (done) { + var options = { + method: 'DELETE', + url: '/tokens/55c88eeee2f727d042b097b5', + headers: { + Cookie: cookie + } + }; + + server.inject(options, function (response) { + assert.equal(response.statusCode, 200); + + // List tokens to confirm it was removed. + var options = { + method: 'GET', + url: '/tokens', + headers: { + Cookie: cookie + } + }; + server.inject(options, function (response) { + var result = response.result; + assert.equal(response.statusCode, 200); + assert.lengthOf(result.data, 2); + done(); + }); + + }); + }); + }); From 020c2b8f1db5b51f0a3dfe6f9ab377321bb6cf97 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Tue, 11 Aug 2015 12:17:19 +0100 Subject: [PATCH 020/144] Fix date errors --- test/test__token_management.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test__token_management.js b/test/test__token_management.js index 0791796..ae7a09e 100644 --- a/test/test__token_management.js +++ b/test/test__token_management.js @@ -326,7 +326,7 @@ suite('test tokens', function () { assert.equal(result.data.name, 'Secondary token modified'); assert.equal(result.data.status, 'active'); - assert.equal(result.data.expiration, 'Mon Aug 10 2020 14:38:48 GMT+0100 (WEST)'); + assert.equal(new Date(result.data.expiration).toISOString(), '2020-08-10T13:38:48.684Z'); done(); }); }); @@ -397,7 +397,7 @@ suite('test tokens', function () { assert.equal(result.data.name, 'Secondary token'); assert.equal(result.data.status, 'active'); - assert.equal(result.data.expiration, 'Mon Aug 10 2020 14:38:48 GMT+0100 (WEST)'); + assert.equal(new Date(result.data.expiration).toISOString(), '2020-08-10T13:38:48.684Z'); done(); }); }); From 2ca5ea65a5ecbded092efd33a9efcef325443353 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 08:14:33 -0400 Subject: [PATCH 021/144] Update semistandard version, fix lint error --- .eslintrc | 190 +-------------------------------------------------- index.js | 2 +- package.json | 1 + 3 files changed, 4 insertions(+), 189 deletions(-) diff --git a/.eslintrc b/.eslintrc index 71bd916..3ddfe3e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,189 +1,3 @@ { - "ecmaFeatures": { - "globalReturn": true, - "jsx": true, - "modules": true - }, - - "env": { - "browser": false, - "es6": true, - "node": true, - }, - - "globals": { - "document": false, - "escape": false, - "navigator": false, - "unescape": false, - "window": false - }, - - "plugins": [ - "react" - ], - - "rules": { - "block-scoped-var": 0, - "brace-style": [2, "1tbs", { "allowSingleLine": true }], - "camelcase": 0, - "comma-dangle": [2, "never"], - "comma-spacing": [2, { "before": false, "after": true }], - "comma-style": [2, "last"], - "complexity": 0, - "consistent-return": 0, - "consistent-this": 0, - "curly": [2, "multi-line"], - "default-case": 0, - "dot-notation": 0, - "eol-last": 2, - "eqeqeq": [2, "allow-null"], - "func-names": 0, - "func-style": [0, "declaration"], - "generator-star": [2, "middle"], - "guard-for-in": 0, - "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], - "indent": [2, 2], - "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - "max-depth": 0, - "max-len": 0, - "max-nested-callbacks": 0, - "max-params": 0, - "max-statements": 0, - "new-cap": [2, { "newIsCap": true, "capIsNew": false }], - "new-parens": 2, - "no-alert": 0, - "no-array-constructor": 2, - "no-bitwise": 0, - "no-caller": 2, - "no-catch-shadow": 0, - "no-cond-assign": 2, - "no-console": 0, - "no-constant-condition": 0, - "no-control-regex": 2, - "no-debugger": 2, - "no-delete-var": 2, - "no-div-regex": 0, - "no-dupe-args": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-else-return": 0, - "no-empty": 0, - "no-empty-class": 2, - "no-empty-label": 2, - "no-eq-null": 0, - "no-eval": 2, - "no-ex-assign": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-boolean-cast": 2, - "no-extra-parens": 0, - "no-extra-semi": 0, - "no-extra-strict": 0, - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-func-assign": 2, - "no-implied-eval": 2, - "no-inline-comments": 0, - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-label-var": 2, - "no-labels": 2, - "no-lone-blocks": 0, - "no-lonely-if": 0, - "no-loop-func": 0, - "no-mixed-requires": [0, false], - "no-mixed-spaces-and-tabs": [2, false], - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-multiple-empty-lines": [2, { "max": 1 }], - "no-native-reassign": 2, - "no-negated-in-lhs": 2, - "no-nested-ternary": 0, - "no-new": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-require": 2, - "no-new-wrappers": 2, - "no-obj-calls": 2, - "no-octal": 2, - "no-octal-escape": 2, - "no-path-concat": 0, - "no-plusplus": 0, - "no-process-env": 0, - "no-process-exit": 0, - "no-proto": 2, - "no-redeclare": 2, - "no-regex-spaces": 2, - "no-reserved-keys": 0, - "no-restricted-modules": 0, - "no-return-assign": 2, - "no-script-url": 0, - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow": 0, - "no-shadow-restricted-names": 2, - "no-spaced-func": 2, - "no-sparse-arrays": 2, - "no-sync": 0, - "no-ternary": 0, - "no-throw-literal": 2, - "no-trailing-spaces": 2, - "no-undef": 2, - "no-undef-init": 2, - "no-undefined": 0, - "no-underscore-dangle": 0, - "no-unreachable": 2, - "no-unused-expressions": 0, - "no-unused-vars": [2, { "vars": "all", "args": "none" }], - "no-use-before-define": 0, - "no-var": 0, - "no-void": 0, - "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], - "no-with": 2, - "no-wrap-func": 2, - "one-var": 0, - "operator-assignment": [0, "always"], - "padded-blocks": [2, "never"], - "quote-props": 0, - "quotes": [2, "single", "avoid-escape"], - "radix": 2, - "react/display-name": 2, - "react/jsx-boolean-value": 2, - "react/jsx-quotes": [2, "single", "avoid-escape"], - "react/jsx-no-undef": 2, - "react/jsx-sort-props": 0, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - "react/no-did-mount-set-state": 2, - "react/no-did-update-set-state": 2, - "react/no-multi-comp": 2, - "react/no-unknown-property": 2, - "react/prop-types": 2, - "react/react-in-jsx-scope": 2, - "react/self-closing-comp": 2, - "react/wrap-multilines": 2, - "semi": [2, "always"], - "semi-spacing": 0, - "sort-vars": 0, - "space-after-keywords": [2, "always"], - "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, "always"], - "space-in-brackets": 0, - "space-in-parens": [2, "never"], - "space-infix-ops": 2, - "space-return-throw-case": 2, - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-line-comment": [2, "always"], - "strict": 0, - "use-isnan": 2, - "valid-jsdoc": 0, - "valid-typeof": 2, - "vars-on-top": 0, - "wrap-iife": [2, "any"], - "wrap-regex": 0, - "yoda": [2, "never"] - } -} \ No newline at end of file + "extends": ["semistandard", "standard-react"] +} diff --git a/index.js b/index.js index 3cdc89d..82cc6eb 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ var OAMUploader = function (readyCb) { } }); - hapi.connection({port: config.port }); + hapi.connection({ port: config.port }); // Register plugins hapi.register([ diff --git a/package.json b/package.json index b028fff..9da2e88 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "newrelic": "^1.20.0", "request": "^2.55.0", "s3": "^4.4.0", + "semistandard": "^7.0.2", "wellknown": "^0.3.1" }, "devDependencies": { From 376592a0b31d847f10de1b5f10ec756d8e64469e Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 08:37:12 -0400 Subject: [PATCH 022/144] Unify test db --- config.js | 3 +- package.json | 2 +- test/test__token_management.js | 63 ++++++++++++++-------------------- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/config.js b/config.js index 95c2ddb..c469f8d 100644 --- a/config.js +++ b/config.js @@ -5,7 +5,8 @@ try { module.exports = { port: process.env.PORT || 3000, - dbUri: process.env.DBURI || 'mongodb://localhost/oam-uploader', + dbUri: process.env.DBURI || process.env.OAM_TEST ? + 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', adminPassword: process.env.ADMIN_PASSWORD || null, adminUsername: process.env.ADMIN_USERNAME || null, logOptions: local.logOptions || { diff --git a/package.json b/package.json index 9da2e88..cb0fa34 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "An uploader API for Open Aerial Map Imagery", "main": "index.js", "scripts": { - "test": "semistandard && lab test/test__*.js", + "test": "semistandard && OAM_TEST=1 lab test/test__*.js", "lab": "lab test/test__*.js", "docs": "apidoc -i routes/ -o docs/" }, diff --git a/test/test__token_management.js b/test/test__token_management.js index ae7a09e..622b7e2 100644 --- a/test/test__token_management.js +++ b/test/test__token_management.js @@ -3,14 +3,12 @@ var Lab = require('lab'); var server = require('../'); var chai = require('chai'); -var MongoClient = require('mongodb').MongoClient; var ObjectId = require('mongodb').ObjectID; var lab = exports.lab = Lab.script(); var suite = lab.experiment; var test = lab.test; var before = lab.before; -var after = lab.after; var assert = chai.assert; var cookie = null; @@ -24,34 +22,31 @@ suite('test tokens', function () { server(function (hapi) { server = hapi; // Prepare db. - // Close current connection. - hapi.plugins.db.connection.close(function () { - // Open connection to test DB. - MongoClient.connect('mongodb://localhost/oam-uploader-test', function (err, db) { - if (err) throw err; - hapi.plugins.db.connection = db; - - // Insert some data. - db.collection('tokens').insert([ - { - _id: new ObjectId('55c88dddd2f727d042b097b4'), - name: 'Primary token', - expiration: false, - status: 'active', - token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', - created: new Date('2015-08-10T13:38:48.684Z'), - updated: null - }, - { - _id: new ObjectId('55c88eeee2f727d042b097b5'), - name: 'Secondary token', - expiration: new Date('2020-08-10T13:38:48.684Z'), - status: 'active', - token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', - created: new Date('2015-08-10T13:38:48.684Z'), - updated: null - } - ], function (err, res) { + var db = hapi.plugins.db.connection; + db.collection('tokens').deleteMany({}, function (err) { + if (err) { throw err; } + + // Insert some data. + db.collection('tokens').insert([ + { + _id: new ObjectId('55c88dddd2f727d042b097b4'), + name: 'Primary token', + expiration: false, + status: 'active', + token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', + created: new Date('2015-08-10T13:38:48.684Z'), + updated: null + }, + { + _id: new ObjectId('55c88eeee2f727d042b097b5'), + name: 'Secondary token', + expiration: new Date('2020-08-10T13:38:48.684Z'), + status: 'active', + token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', + created: new Date('2015-08-10T13:38:48.684Z'), + updated: null + } + ], function (err, res) { if (err) throw err; // Cookie var options = { @@ -68,19 +63,11 @@ suite('test tokens', function () { cookie = response.headers['set-cookie'][0].split(' ')[0]; done(); }); - }); - }); }); }); }); - after(function (done) { - server.plugins.db.connection.dropDatabase(function () { - done(); - }); - }); - test('should list tokens', function (done) { var options = { method: 'GET', From 9e2de5af1318c935e0afa0d96b9ba89b6dc0aa84 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 08:53:40 -0400 Subject: [PATCH 023/144] Move semistandard to dev dependencies --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cb0fa34..999eb83 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,13 @@ "newrelic": "^1.20.0", "request": "^2.55.0", "s3": "^4.4.0", - "semistandard": "^7.0.2", "wellknown": "^0.3.1" }, "devDependencies": { - "lab": "^5.15.0", "apidoc": "^0.13.1", "chai": "^2.3.0", - "semistandard": "^4.1.4", + "lab": "^5.15.0", + "semistandard": "^7.0.2", "tape": "^4.0.3" }, "engines": { From af37d365cc2300bb88e3394dbcecb2df827c5cf4 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 10 Aug 2015 11:31:51 -0400 Subject: [PATCH 024/144] Add AWS keys to config --- config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.js b/config.js index c469f8d..07eb4ec 100644 --- a/config.js +++ b/config.js @@ -9,6 +9,8 @@ module.exports = { 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', adminPassword: process.env.ADMIN_PASSWORD || null, adminUsername: process.env.ADMIN_USERNAME || null, + awsKeyId: process.env.AWS_SECRET_KEY_ID || local.awsKeyId, + awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY || local.awsAccessKey, logOptions: local.logOptions || { opsInterval: 3000, reporters: [{ From d22db3746dabbd676819b087e6bcd2c626fa9743 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 10:53:37 -0400 Subject: [PATCH 025/144] Swith back to 0.12 for gdal compatibility --- .travis.yml | 2 +- package.json | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9035d99..83cacda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- 'iojs-v2.5.0' +- '0.12' env: global: - GH_REF=github.com/hotosm/oam-uploader-api.git diff --git a/package.json b/package.json index 999eb83..1597fcb 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ "homepage": "https://github.com/hotosm/oam-uploader-api", "dependencies": { "async": "^1.3.0", + "aws-sdk": "^2.1.44", "boom": "^2.8.0", "envloader": "0.0.2", "es6-promise": "^2.3.0", + "gdalinfo-json": "^0.5.1", "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", @@ -33,9 +35,13 @@ "lodash": "^3.10.1", "mongodb": "^2.0.40", "newrelic": "^1.20.0", - "request": "^2.55.0", + "oam-meta-generator": "openimagerynetwork/oin-meta-generator#c84bf8b035ad24ed464320b527b99b838d8ee601", + "queue-async": "^1.0.7", + "request": "^2.60.0", "s3": "^4.4.0", - "wellknown": "^0.3.1" + "tmp": "0.0.26", + "wellknown": "^0.3.1", + "xtend": "^4.0.0" }, "devDependencies": { "apidoc": "^0.13.1", @@ -45,7 +51,7 @@ "tape": "^4.0.3" }, "engines": { - "iojs": "2.5.x" + "node": "0.12.x" }, "apidoc": { "title": "OAM API", From 2ba04e533a9821b6cad31e11c0e1653c3b7207fc Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 10:54:05 -0400 Subject: [PATCH 026/144] Update uploads/ endpoint object schema --- models/upload.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/models/upload.js b/models/upload.js index b262a93..f0fcee3 100644 --- a/models/upload.js +++ b/models/upload.js @@ -1,18 +1,24 @@ var Joi = require('joi'); var infoSchema = Joi.object().keys({ - name: Joi.string().min(3).max(30).required(), + name: Joi.string().min(1).max(30).required(), email: Joi.string().email() }); var sceneSchema = Joi.object().keys({ - metadata: Joi.object().required(), + contact: infoSchema.required(), + title: Joi.string().min(1).required(), + provider: Joi.string(), + platform: Joi.any().allow('satellite', 'aircraft', 'UAV', 'balloon', 'kite').required(), + sensor: Joi.string(), + acquisition_start: Joi.date().required(), + acquisition_end: Joi.date().required(), + tms: Joi.string().uri(), urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https']})) .min(1).required() }); module.exports = Joi.object().keys({ - uploaderInfo: infoSchema.required(), - contactInfo: infoSchema.required(), + uploader: infoSchema.required(), scenes: Joi.array().items(sceneSchema).min(1).required() }); From 82e36b24a9b3da1641d7bc84a9d5b8322b1888e9 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 10:55:08 -0400 Subject: [PATCH 027/144] Download each image and generate its metadata --- config.js | 2 + routes/uploads.js | 7 ++- worker/index.js | 27 ++++------ worker/log.js | 13 +++++ worker/process-upload.js | 107 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 worker/log.js diff --git a/config.js b/config.js index 07eb4ec..7aee01a 100644 --- a/config.js +++ b/config.js @@ -11,6 +11,8 @@ module.exports = { adminUsername: process.env.ADMIN_USERNAME || null, awsKeyId: process.env.AWS_SECRET_KEY_ID || local.awsKeyId, awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY || local.awsAccessKey, + awsRegion: process.env.AWS_REGION || local.awsRegion || 'us-west-2', + tempBucket: 'oam-uploader', logOptions: local.logOptions || { opsInterval: 3000, reporters: [{ diff --git a/routes/uploads.js b/routes/uploads.js index bc6fbb7..560b993 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -91,13 +91,18 @@ module.exports = [ return uploads.insertOne(data) .then(function () { if (result.value) { + request.log(['debug'], 'Resuming worker'); // we already have a worker - unpause it. return workers.updateOne(result.value, { $set: { state: 'working' } }); } else { // spawn a worker - fork(path.join(__dirname, '../worker')); + request.log(['debug'], 'Spawning worker'); + fork(path.join(__dirname, '../worker')) + .on('message', function (msg) { + request.log(['worker'].concat(msg.tags), msg.message); + }); return; } }); diff --git a/worker/index.js b/worker/index.js index 88d3e40..90d770f 100644 --- a/worker/index.js +++ b/worker/index.js @@ -5,14 +5,16 @@ */ var MongoClient = require('mongodb').MongoClient; -var dbUri = require('../config').dbUri; var processUpload = require('./process-upload'); +var log = require('./log'); +var config = require('../config'); var db; var workers; var uploads; var workerId; -MongoClient.connect(dbUri, function (err, connection) { + +MongoClient.connect(config.dbUri, function (err, connection) { if (err) { throw err; } db = connection; workers = db.collection('workers'); @@ -22,7 +24,7 @@ MongoClient.connect(dbUri, function (err, connection) { workers.insertOne({ state: 'working' }) .then(function (result) { - workerId = result.ops[0]._id; + log.workerId = workerId = result.ops[0]._id; log('Started.'); return mainloop(); }) @@ -46,9 +48,10 @@ function mainloop () { }); } else { // we got a job! - log('Processing job', result.value); + log(['info'], 'Processing job', result.value); return processUpload(result.value) .then(function (processedResult) { + log(['debug'], 'Done processing job', processedResult); return uploads.findOneAndUpdate(result.value, { $set: { status: 'finished' }, $unset: { _workerId: '' }, @@ -75,7 +78,7 @@ function dequeue () { function cleanup (err) { log('Cleaning up.'); - if (err) { logError(err); } + if (err) { log(['error'], err); } if (db) { if (workerId) { workers.deleteOne({ _id: workerId }) @@ -90,9 +93,8 @@ function cleanup (err) { process.exit(err ? 1 : 0); }) .catch(function (error) { - err = error; - logError('Error cleaning up worker ' + workerId + '; bad news.'); - logError(err); + log(['error'], 'Error cleaning up. Bad news.'); + log(['error'], error); db.close(); process.exit(1); }); @@ -102,12 +104,3 @@ function cleanup (err) { } } -function log () { - var args = Array.prototype.slice.call(arguments); - console.log.apply(console, ['[ Worker ' + workerId + ' ]'].concat(args)); -} - -function logError () { - var args = Array.prototype.slice.call(arguments); - console.error.apply(console, ['[ Worker ' + workerId + ' ]'].concat(args)); -} diff --git a/worker/log.js b/worker/log.js new file mode 100644 index 0000000..2354350 --- /dev/null +++ b/worker/log.js @@ -0,0 +1,13 @@ +module.exports = log; +module.exports.workerId = -1; + +function log () { + var args = Array.prototype.slice.call(arguments); + var tags = args.length > 1 ? [args.shift()] : []; + tags.unshift('Worker ' + module.exports.workerId); + if (typeof process.send === 'function') { + process.send({ tags: tags, message: args }); + } else { + console.log.apply(console, [tags].concat(args)); + } +} diff --git a/worker/process-upload.js b/worker/process-upload.js index c6ba5a6..0e3b6dc 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -1,7 +1,112 @@ +'use strict'; + +var fs = require('fs'); var Promise = require('es6-promise').Promise; +var tmp = require('tmp'); +var request = require('request'); +var queue = require('queue-async'); +var AWS = require('aws-sdk'); +var gdalinfo = require('gdalinfo-json'); +var applyGdalinfo = require('oam-meta-generator/lib/apply-gdalinfo'); +var log = require('./log'); +var config = require('../config'); + +AWS.config = { + accessKeyId: config.awsKeyId, + secretAccessKey: config.awsAccessKey, + region: config.awsRegion, + sslEnabled: true +}; + +var s3 = new AWS.S3(); +var s3bucket = config.oinBucket; module.exports = function processUpload (upload) { return new Promise(function (resolve, reject) { - setTimeout(resolve.bind(null, upload), 60000); + var q = queue(1); + upload.scenes.forEach(function (scene, i) { + scene.urls.forEach(function (url, j) { + var filename = url.split('/').pop() || ''; + q.defer(function (cb) { + var key = [ + 'oam-upload', upload._id, 'scene', i, j + '-' + filename + ].join('/'); + processUrl(upload, scene, url, key, cb); + }); + }); + }); + + q.awaitAll(function (err, results) { + if (err) { return reject(err); } + log(['debug'], results); + resolve(); + }); }); }; + +function processUrl (upload, scene, url, key, callback) { + tmp.file(function (err, path, fd, cleanup) { + if (err) { return callback(err); } + log(['debug'], 'Downloading ' + url + ' to ' + path); + request(url).pipe(fs.createWriteStream(path)) + .on('finish', function () { + fs.stat(path, function (err, stat) { + if (err) { return callback(err); } + // we've successfully downloaded the file. now do stuff with it. + log(['debug'], 'Finished downloading, now generating metadata.'); + + var metadata = { + uuid: publicUrl(s3bucket, key), + title: scene.title, + projection: null, + bbox: null, + footprint: null, + gsd: null, + file_size: stat.size, + acquisition_start: scene.acquisition_start, + acquisition_end: scene.acquisition_end, + platform: scene.platform, + provider: scene.provider, + contact: scene.contact, + properties: { + tms: scene.tms, + sensor: scene.sensor, + thumbnail: 'TBD' + } + }; + + // TODO: generate thumbnail + + gdalinfo.local(path, function (err, gdaldata) { + if (err) { return callback(err); } + applyGdalinfo(metadata, gdaldata); + log(['debug'], 'Generated metadata: ' + JSON.stringify(metadata)); + log(['debug'], 'Uploading image to s3'); + s3.putObject({ + Body: fs.createReadStream(path), + Bucket: s3bucket, + Key: key + }, function (err, data) { + cleanup(); // delete tempfile + if (err) { + log(['error'], 'Error uploading ' + key); + return callback(err); + } + log(['debug'], 'Uploading metadata to s3'); + s3.putObject({ + Body: JSON.stringify(metadata), + Bucket: s3bucket, + Key: key + '_meta.json' + }); + }); + }); + }); + }) + .on('error', callback); + }); +} + +function publicUrl (bucketName, key) { + return 'http://' + bucketName + '.s3.amazonaws.com/' + key; +} + From 7cac463a2e8fcd787f1b4c5d89632dbfd5bc839a Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 12:00:12 -0400 Subject: [PATCH 028/144] Clean up config, and ensure api keys aren't logged --- config.js | 44 ++++++++++++++++++++++++++++++++++---------- index.js | 1 - 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/config.js b/config.js index 7aee01a..04d5f9b 100644 --- a/config.js +++ b/config.js @@ -1,19 +1,19 @@ +var xtend = require('xtend'); + var local = {}; try { local = require('./local.js'); } catch(e) {} -module.exports = { - port: process.env.PORT || 3000, - dbUri: process.env.DBURI || process.env.OAM_TEST ? +var defaults = { + port: 3000, + oinBucket: 'oam-uploader', + dbUri: process.env.OAM_TEST ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', - adminPassword: process.env.ADMIN_PASSWORD || null, - adminUsername: process.env.ADMIN_USERNAME || null, - awsKeyId: process.env.AWS_SECRET_KEY_ID || local.awsKeyId, - awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY || local.awsAccessKey, - awsRegion: process.env.AWS_REGION || local.awsRegion || 'us-west-2', - tempBucket: 'oam-uploader', - logOptions: local.logOptions || { + adminPassword: null, + adminUsername: null, + awsRegion: 'us-west-2', + logOptions: { opsInterval: 3000, reporters: [{ reporter: require('good-console'), @@ -27,3 +27,27 @@ module.exports = { }] } }; + +var environment = { + port: process.env.PORT, + oinBucket: process.env.OIN_BUCKET, + dbUri: process.env.DBURI, + adminPassword: process.env.ADMIN_PASSWORD, + adminUsername: process.env.ADMIN_USERNAME, + awsKeyId: process.env.AWS_SECRET_KEY_ID, + awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + awsRegion: process.env.AWS_REGION +}; + +var config = xtend(defaults, local); +for (var k in environment) { + if (typeof environment[k] !== 'undefined') { + config[k] = environment[k]; + } +} + +// override json.stringify behavior so we don't accidentally log keys +config.toJSON = function () { + return '[ hidden ]'; +}; +module.exports = config; diff --git a/index.js b/index.js index 82cc6eb..989ad58 100644 --- a/index.js +++ b/index.js @@ -78,7 +78,6 @@ if (!module.parent) { // Start the server. hapi.start(function () { hapi.log(['info'], 'Server running at:' + hapi.info.uri); - hapi.log(['debug'], 'Config: ' + JSON.stringify(config)); }); }); } From d1de59dd584cd13bc5987abe7a37aefc8cc01479 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 14:20:25 -0400 Subject: [PATCH 029/144] Simplify worker management --- index.js | 2 ++ plugins/workers.js | 67 ++++++++++++++++++++++++++++++++++++++++ routes/uploads.js | 28 ++--------------- worker/index.js | 43 ++++++++++++++++---------- worker/log.js | 6 +++- worker/process-upload.js | 2 ++ 6 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 plugins/workers.js diff --git a/index.js b/index.js index 989ad58..0a8ae16 100644 --- a/index.js +++ b/index.js @@ -26,6 +26,8 @@ var OAMUploader = function (readyCb) { { register: require('good'), options: config.logOptions }, // exports the db as plugins.db.connection require('./plugins/mongodb'), + // exports plugins.workers.spawn + require('./plugins/workers'), // adds bearer-access-token scheme require('hapi-auth-bearer-token'), // Cookie auth. diff --git a/plugins/workers.js b/plugins/workers.js new file mode 100644 index 0000000..70c49e6 --- /dev/null +++ b/plugins/workers.js @@ -0,0 +1,67 @@ +'use strict'; + +var fork = require('child_process').fork; +var path = require('path'); +var ObjectID = require('mongodb').ObjectID; + +module.exports = function register (server, options, next) { + var myWorkers = {}; + var myProcesses = {}; + + server.expose({ spawn: spawn }); + + next(); + + function spawn () { + var available = Object.keys(myWorkers); + var workers = server.plugins.db.connection.collection('workers'); + server.log(['debug'], 'spawnIfNecessary; available: ' + available); + if (!available.length) { + spawnWorker(); + } else { + var id = available[0]; + server.log(['debug'], 'Attempting to pause ' + id); + id = new ObjectID(id); + // we think we have a worker available already, but there's a potential + // race condition between the worker checking for new jobs and deciding + // to shut down. we avoid it by setting the worker's state to 'paused', + // from which it will not allow itself to shut down. + return workers.findOneAndUpdate({ _id: id, state: 'working' }, { + $set: { state: 'paused' } + }) + .then(function (result) { + server.log(['debug'], result); + if (result.value) { + // we successfully switched the state to 'paused', so we can safely + // unpause it and know that it will try to look for another job. + server.log(['debug'], 'Successfully paused; resuming.'); + return workers.updateOne({ _id: id, state: 'paused' }, { + $set: { state: 'working' } + }); + } else { + // our worker must have shut itself down before we were able to + // pause it, so let's spawn a new one + server.log(['debug'], 'Could not pause; spawning.'); + return spawnWorker(); + } + }); + } + } + + function spawnWorker () { + server.log(['debug'], 'spawnWorker'); + var cp = fork(path.join(__dirname, '../worker')) + .on('message', function (msg) { + myWorkers[msg.workerId] = cp.pid; + myProcesses[cp.pid] = msg.workerId; + server.log(['worker'].concat(msg.tags), msg.message); + }) + .on('exit', function (info) { + server.log(['debug'], 'Worker exited: ' + JSON.stringify(info)); + delete myWorkers[myProcesses[cp.pid]]; + delete myProcesses[cp.pid]; + }); + } +}; + +module.exports.attributes = { name: 'workers' }; diff --git a/routes/uploads.js b/routes/uploads.js index 560b993..1135bbc 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -1,7 +1,5 @@ 'use strict'; -var path = require('path'); -var fork = require('child_process').fork; var Boom = require('boom'); var Joi = require('joi'); var uploadSchema = require('../models/upload'); @@ -81,32 +79,10 @@ module.exports = [ data.user = request.auth.credentials.user.id; data.status = 'initial'; - var workers = request.server.plugins.db.connection.collection('workers'); var uploads = request.server.plugins.db.connection.collection('uploads'); - workers.findOneAndUpdate({ state: 'working' }, { - $set: { state: 'paused' } - }) - .then(function (result) { - return uploads.insertOne(data) - .then(function () { - if (result.value) { - request.log(['debug'], 'Resuming worker'); - // we already have a worker - unpause it. - return workers.updateOne(result.value, { - $set: { state: 'working' } - }); - } else { - // spawn a worker - request.log(['debug'], 'Spawning worker'); - fork(path.join(__dirname, '../worker')) - .on('message', function (msg) { - request.log(['worker'].concat(msg.tags), msg.message); - }); - return; - } - }); - }) + uploads.insertOne(data) + .then(request.server.plugins.workers.spawn) .then(function () { reply('Success'); }) .catch(function (err) { reply(Boom.wrap(err)); }); }); diff --git a/worker/index.js b/worker/index.js index 90d770f..c3e7506 100644 --- a/worker/index.js +++ b/worker/index.js @@ -14,6 +14,20 @@ var workers; var uploads; var workerId; +// mongodb queries and updates +var myself = { _id: 'none', state: 'working' }; +var lastJobTimestamp = { $currentDate: { lastJobTimestamp: true } }; +var stopping = { $set: { state: 'stopping' } }; +var jobClaimed = { + $set: { status: 'pending', _workerId: 'none' }, + $currentDate: { startedAt: true } +}; +var jobFinished = { + $set: { status: 'finished' }, + $unset: { _workerId: '' }, + $currentDate: { finishedAt: true } +}; + MongoClient.connect(config.dbUri, function (err, connection) { if (err) { throw err; } db = connection; @@ -24,7 +38,11 @@ MongoClient.connect(config.dbUri, function (err, connection) { workers.insertOne({ state: 'working' }) .then(function (result) { - log.workerId = workerId = result.ops[0]._id; + workerId = result.ops[0]._id; + jobClaimed['$set']['_workerId'] = workerId; + myself['_id'] = workerId; + log.workerId = workerId; + log('Started.'); return mainloop(); }) @@ -39,9 +57,7 @@ function mainloop () { // no jobs left; try to shut down. // avoid race condition by making sure our state wasn't changed from // 'working' to something else (by the server) before we actually quit. - return workers.updateOne({ _id: workerId, state: 'working' }, { - $set: { state: 'stopping' } - }) + return workers.updateOne(myself, stopping) .then(function (result) { if (result.modifiedCount === 0) { return mainloop(); } return cleanup(); @@ -52,14 +68,11 @@ function mainloop () { return processUpload(result.value) .then(function (processedResult) { log(['debug'], 'Done processing job', processedResult); - return uploads.findOneAndUpdate(result.value, { - $set: { status: 'finished' }, - $unset: { _workerId: '' }, - $currentDate: { finishedAt: true } - }); + return uploads.findOneAndUpdate(result.value, jobFinished); }) .then(function (result) { - return mainloop(); + return workers.updateOne(myself, lastJobTimestamp) + .then(mainloop); }); } }); @@ -67,13 +80,9 @@ function mainloop () { // claim an upload for this worker to process function dequeue () { - return uploads.findOneAndUpdate({status: 'initial'}, { - $set: { - status: 'pending', - _workerId: workerId - }, - $currentDate: { startedAt: true } - }, { returnOriginal: false }); + return uploads.findOneAndUpdate({status: 'initial'}, jobClaimed, { + returnOriginal: false + }); } function cleanup (err) { diff --git a/worker/log.js b/worker/log.js index 2354350..8cde370 100644 --- a/worker/log.js +++ b/worker/log.js @@ -6,7 +6,11 @@ function log () { var tags = args.length > 1 ? [args.shift()] : []; tags.unshift('Worker ' + module.exports.workerId); if (typeof process.send === 'function') { - process.send({ tags: tags, message: args }); + process.send({ + tags: tags, + message: args, + workerId: module.exports.workerId + }); } else { console.log.apply(console, [tags].concat(args)); } diff --git a/worker/process-upload.js b/worker/process-upload.js index 0e3b6dc..277068a 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -97,6 +97,8 @@ function processUrl (upload, scene, url, key, callback) { Body: JSON.stringify(metadata), Bucket: s3bucket, Key: key + '_meta.json' + }, function (err, data) { + callback(err, data); }); }); }); From 604830082cbf953c989aa97df758d0d3e7d1c699 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 11 Aug 2015 18:21:50 -0400 Subject: [PATCH 030/144] Fix upload timeout, slight refactor for thumbnails --- worker/index.js | 11 ++-- worker/process-upload.js | 108 +++++++++++++++++++++++---------------- 2 files changed, 71 insertions(+), 48 deletions(-) diff --git a/worker/index.js b/worker/index.js index c3e7506..e25ea10 100644 --- a/worker/index.js +++ b/worker/index.js @@ -61,18 +61,23 @@ function mainloop () { .then(function (result) { if (result.modifiedCount === 0) { return mainloop(); } return cleanup(); - }); + }) + .catch(cleanup); } else { // we got a job! - log(['info'], 'Processing job', result.value); return processUpload(result.value) .then(function (processedResult) { - log(['debug'], 'Done processing job', processedResult); return uploads.findOneAndUpdate(result.value, jobFinished); }) .then(function (result) { return workers.updateOne(myself, lastJobTimestamp) .then(mainloop); + }) + .catch(function (error) { + log(['error'], error); + // TODO: save error data/status on the upload + return workers.updateOne(myself, lastJobTimestamp) + .then(mainloop); }); } }); diff --git a/worker/process-upload.js b/worker/process-upload.js index 277068a..cef53de 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -22,6 +22,7 @@ var s3 = new AWS.S3(); var s3bucket = config.oinBucket; module.exports = function processUpload (upload) { + log(['info'], 'Processing job', upload); return new Promise(function (resolve, reject) { var q = queue(1); upload.scenes.forEach(function (scene, i) { @@ -38,7 +39,7 @@ module.exports = function processUpload (upload) { q.awaitAll(function (err, results) { if (err) { return reject(err); } - log(['debug'], results); + log(['debug'], 'Done processing job', results); resolve(); }); }); @@ -47,59 +48,38 @@ module.exports = function processUpload (upload) { function processUrl (upload, scene, url, key, callback) { tmp.file(function (err, path, fd, cleanup) { if (err) { return callback(err); } + log(['debug'], 'Downloading ' + url + ' to ' + path); + request(url).pipe(fs.createWriteStream(path)) .on('finish', function () { - fs.stat(path, function (err, stat) { + // we've successfully downloaded the file. now do stuff with it. + var thumbKey = key + '.thumb.jpg'; + generateMetadata(scene, path, key, thumbKey, function (err, metadata) { if (err) { return callback(err); } - // we've successfully downloaded the file. now do stuff with it. - log(['debug'], 'Finished downloading, now generating metadata.'); - - var metadata = { - uuid: publicUrl(s3bucket, key), - title: scene.title, - projection: null, - bbox: null, - footprint: null, - gsd: null, - file_size: stat.size, - acquisition_start: scene.acquisition_start, - acquisition_end: scene.acquisition_end, - platform: scene.platform, - provider: scene.provider, - contact: scene.contact, - properties: { - tms: scene.tms, - sensor: scene.sensor, - thumbnail: 'TBD' - } - }; - - // TODO: generate thumbnail - - gdalinfo.local(path, function (err, gdaldata) { + makeThumbnail(path, function (err, thumbPath) { if (err) { return callback(err); } - applyGdalinfo(metadata, gdaldata); - log(['debug'], 'Generated metadata: ' + JSON.stringify(metadata)); - log(['debug'], 'Uploading image to s3'); - s3.putObject({ + var q = queue(); + q.defer(s3.upload.bind(s3), { Body: fs.createReadStream(path), Bucket: s3bucket, Key: key - }, function (err, data) { + }); + q.defer(s3.upload.bind(s3), { + Body: fs.createReadStream(thumbPath), + Bucket: s3bucket, + Key: thumbKey + }); + q.defer(s3.upload.bind(s3), { + Body: JSON.stringify(metadata), + Bucket: s3bucket, + Key: key + '_meta.json' + }); + log(['debug'], 'Uploading to s3; bucket=' + s3bucket + ' key=' + key); + q.awaitAll(function (err, data) { cleanup(); // delete tempfile - if (err) { - log(['error'], 'Error uploading ' + key); - return callback(err); - } - log(['debug'], 'Uploading metadata to s3'); - s3.putObject({ - Body: JSON.stringify(metadata), - Bucket: s3bucket, - Key: key + '_meta.json' - }, function (err, data) { - callback(err, data); - }); + log(['debug'], 'Uploaded', data); + callback(err, data); }); }); }); @@ -108,6 +88,44 @@ function processUrl (upload, scene, url, key, callback) { }); } +function generateMetadata (scene, path, key, thumbKey, callback) { + log(['debug'], 'Generating metadata.'); + fs.stat(path, function (err, stat) { + if (err) { return callback(err); } + + var metadata = { + uuid: null, + title: scene.title, + projection: null, + bbox: null, + footprint: null, + gsd: null, + file_size: stat.size, + acquisition_start: scene.acquisition_start, + acquisition_end: scene.acquisition_end, + platform: scene.platform, + provider: scene.provider, + contact: scene.contact, + properties: { + tms: scene.tms, + sensor: scene.sensor, + thumbnail: 'TBD' + } + }; + + gdalinfo.local(path, function (err, gdaldata) { + if (err) { return callback(err); } + applyGdalinfo(metadata, gdaldata); + metadata.uuid = publicUrl(s3bucket, key); + log(['debug'], 'Generated metadata: ' + JSON.stringify(metadata)); + }); + }); +} + +function makeThumbnail (imagePath, callback) { + callback(new Error('Not implemented becase apparently making thumbnails is harder than any of the rest of this crap.')); +} + function publicUrl (bucketName, key) { return 'http://' + bucketName + '.s3.amazonaws.com/' + key; } From 38145c109106e33feba6c800323d44d5f2d78336 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Wed, 12 Aug 2015 08:31:11 -0400 Subject: [PATCH 031/144] Implement thumbnail generation --- .buildpacks | 2 ++ README.md | 7 +++++-- package.json | 2 ++ worker/index.js | 13 ++++++++++--- worker/process-upload.js | 33 ++++++++++++++++++++++++++------- 5 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 .buildpacks diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 0000000..cb5a052 --- /dev/null +++ b/.buildpacks @@ -0,0 +1,2 @@ +https://github.com/heroku/heroku-buildpack-nodejs.git#v79 +https://github.com/mcollina/heroku-buildpack-graphicsmagick diff --git a/README.md b/README.md index 8f5eef1..c55721d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The steps below will walk you through setting up your own instance of the oam-up - [MongoDB](https://www.mongodb.org/) - [Node.js](https://nodejs.org/) +- [GraphicsMagick](http://www.graphicsmagick.org/) (**NOTE:** must be installed with libtiff support.) ### Install Application Dependencies @@ -33,8 +34,10 @@ The API exposes endpoints used to access information form the system via a RESTf ### Environment Variables -- `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN buckets -- `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN buckets +- `OIN_BUCKET` - The OIN bucket that will receive the uploads +- `AWS_REGION` - AWS region of OIN_BUCKET +- `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN bucket +- `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN bucket - `DBURI` - MongoDB connection url - `ADMIN_USERNAME` - Token management Admin username - `ADMIN_PASSWORD` - Token management Admin password diff --git a/package.json b/package.json index 1597fcb..0a5713b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "envloader": "0.0.2", "es6-promise": "^2.3.0", "gdalinfo-json": "^0.5.1", + "gm": "^1.18.1", "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", @@ -33,6 +34,7 @@ "hapi-router": "^3.0.1", "joi": "^6.6.1", "lodash": "^3.10.1", + "moment": "^2.10.6", "mongodb": "^2.0.40", "newrelic": "^1.20.0", "oam-meta-generator": "openimagerynetwork/oin-meta-generator#c84bf8b035ad24ed464320b527b99b838d8ee601", diff --git a/worker/index.js b/worker/index.js index e25ea10..d31e5ed 100644 --- a/worker/index.js +++ b/worker/index.js @@ -25,8 +25,15 @@ var jobClaimed = { var jobFinished = { $set: { status: 'finished' }, $unset: { _workerId: '' }, - $currentDate: { finishedAt: true } + $currentDate: { stoppedAt: true } }; +function jobErrored (error) { + return { + $set: { status: 'errored', error: error }, + $unset: { _workerId: '' }, + $currentDate: { stoppedAt: true } + }; +} MongoClient.connect(config.dbUri, function (err, connection) { if (err) { throw err; } @@ -75,8 +82,8 @@ function mainloop () { }) .catch(function (error) { log(['error'], error); - // TODO: save error data/status on the upload - return workers.updateOne(myself, lastJobTimestamp) + return uploads.findOneAndUpdate(result.value, jobErrored(error)) + .then(workers.updateOne.bind(workers, myself, lastJobTimestamp)) .then(mainloop); }); } diff --git a/worker/process-upload.js b/worker/process-upload.js index cef53de..388720d 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -3,11 +3,13 @@ var fs = require('fs'); var Promise = require('es6-promise').Promise; var tmp = require('tmp'); +var moment = require('moment'); var request = require('request'); var queue = require('queue-async'); var AWS = require('aws-sdk'); var gdalinfo = require('gdalinfo-json'); var applyGdalinfo = require('oam-meta-generator/lib/apply-gdalinfo'); +var gm = require('gm'); var log = require('./log'); var config = require('../config'); @@ -23,15 +25,14 @@ var s3bucket = config.oinBucket; module.exports = function processUpload (upload) { log(['info'], 'Processing job', upload); + var now = moment().format('YYYY-MM-DD'); return new Promise(function (resolve, reject) { var q = queue(1); upload.scenes.forEach(function (scene, i) { scene.urls.forEach(function (url, j) { - var filename = url.split('/').pop() || ''; + var filename = j + (url.split('/').pop() || ''); q.defer(function (cb) { - var key = [ - 'oam-upload', upload._id, 'scene', i, j + '-' + filename - ].join('/'); + var key = ['uploads', now, upload._id, 'scene', i, filename].join('/'); processUrl(upload, scene, url, key, cb); }); }); @@ -46,7 +47,7 @@ module.exports = function processUpload (upload) { }; function processUrl (upload, scene, url, key, callback) { - tmp.file(function (err, path, fd, cleanup) { + tmp.file({ postfix: '.tif' }, function (err, path, fd, cleanup) { if (err) { return callback(err); } log(['debug'], 'Downloading ' + url + ' to ' + path); @@ -109,21 +110,39 @@ function generateMetadata (scene, path, key, thumbKey, callback) { properties: { tms: scene.tms, sensor: scene.sensor, - thumbnail: 'TBD' + thumbnail: publicUrl(s3bucket, thumbKey) } }; gdalinfo.local(path, function (err, gdaldata) { if (err) { return callback(err); } applyGdalinfo(metadata, gdaldata); + // set uuid after doing applyGdalinfo because it actually sets it to + // gdaldata.url, which for us is blank since we used gdalinfo.local metadata.uuid = publicUrl(s3bucket, key); log(['debug'], 'Generated metadata: ' + JSON.stringify(metadata)); + callback(null, metadata); }); }); } function makeThumbnail (imagePath, callback) { - callback(new Error('Not implemented becase apparently making thumbnails is harder than any of the rest of this crap.')); + tmp.file({ postfix: '.jpg' }, function (err, path, fd, cleanup) { + if (err) { return callback(err); } + log(['debug'], 'Generating thumbnail', path); + gm(imagePath) + .resize(5, 5, '%') + .write(path, function (err, stdout, stderr, command) { + if (err) { + // the error object itself isn't useful, so let's pass along the + // stderr output from the gm command + var msg = 'Error generating thumbnail with "' + command + '":\n'; + return callback(new Error(msg + stderr)); + } + log(['debug'], 'Finished generating thumbnail'); + callback(null, path); + }); + }); } function publicUrl (bucketName, key) { From dafc5682155799163b63afc78cf4f6ded4d2a4a6 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Thu, 13 Aug 2015 16:02:11 +0100 Subject: [PATCH 032/144] Add hostname, fix cors and user login response --- config.js | 1 + index.js | 6 ++++-- routes/user.js | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index 07eb4ec..1b9819a 100644 --- a/config.js +++ b/config.js @@ -5,6 +5,7 @@ try { module.exports = { port: process.env.PORT || 3000, + host: process.env.HOST || '0.0.0.0', dbUri: process.env.DBURI || process.env.OAM_TEST ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', adminPassword: process.env.ADMIN_PASSWORD || null, diff --git a/index.js b/index.js index 82cc6eb..5d56ea4 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,9 @@ var OAMUploader = function (readyCb) { var hapi = new Hapi.Server({ connections: { routes: { - cors: true + cors: { + credentials: true + } }, router: { stripTrailingSlash: true @@ -18,7 +20,7 @@ var OAMUploader = function (readyCb) { } }); - hapi.connection({ port: config.port }); + hapi.connection({host: config.host, port: config.port }); // Register plugins hapi.register([ diff --git a/routes/user.js b/routes/user.js index bbb4c9a..134006e 100644 --- a/routes/user.js +++ b/routes/user.js @@ -14,7 +14,10 @@ module.exports = [ if (request.auth.isAuthenticated) { return reply({ statusCode: 200, - message: 'Welcome back' + message: 'Welcome back', + data: { + username: request.auth.artifacts.username + } }); } From a54d6ff4d4ed27d0013bbe71748909898acdd939 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 13 Aug 2015 08:50:22 -0400 Subject: [PATCH 033/144] Use sharp for faster, better thumbnails --- .buildpacks | 2 +- config.js | 1 + package.json | 2 +- sharp.js | 20 +++++++++++ worker/index.js | 4 +++ worker/log.js | 8 +++++ worker/process-upload.js | 76 ++++++++++++++++++++++++++-------------- 7 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 sharp.js diff --git a/.buildpacks b/.buildpacks index cb5a052..87df4f5 100644 --- a/.buildpacks +++ b/.buildpacks @@ -1,2 +1,2 @@ +https://github.com/alex88/heroku-buildpack-vips.git#7.40.11.0 https://github.com/heroku/heroku-buildpack-nodejs.git#v79 -https://github.com/mcollina/heroku-buildpack-graphicsmagick diff --git a/config.js b/config.js index 04d5f9b..1d95528 100644 --- a/config.js +++ b/config.js @@ -13,6 +13,7 @@ var defaults = { adminPassword: null, adminUsername: null, awsRegion: 'us-west-2', + thumbnailSize: 300, // thumbnail size, in kilobytes logOptions: { opsInterval: 3000, reporters: [{ diff --git a/package.json b/package.json index 0a5713b..73429a7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "envloader": "0.0.2", "es6-promise": "^2.3.0", "gdalinfo-json": "^0.5.1", - "gm": "^1.18.1", "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", @@ -41,6 +40,7 @@ "queue-async": "^1.0.7", "request": "^2.60.0", "s3": "^4.4.0", + "sharp": "^0.11.1", "tmp": "0.0.26", "wellknown": "^0.3.1", "xtend": "^4.0.0" diff --git a/sharp.js b/sharp.js new file mode 100644 index 0000000..06021de --- /dev/null +++ b/sharp.js @@ -0,0 +1,20 @@ +var sharp = require('sharp') + +var original = sharp(process.argv[2]) + .limitInputPixels(Math.pow(2, 31) - 1); +original + .metadata() + .then(function (metadata) { + console.log(metadata); + var pixelArea = metadata.width * metadata.height; + var ratio = 0.05; + console.log(pixelArea, original.options.limitInputPixels); + return original + .resize(Math.round(ratio * metadata.width)) + .toFile('/tmp/thumb.png') + }) + .then(function () { + console.log('Finished generating thumbnail'); + }) + .catch(console.error.bind(console)); + diff --git a/worker/index.js b/worker/index.js index d31e5ed..ac45fdd 100644 --- a/worker/index.js +++ b/worker/index.js @@ -28,6 +28,10 @@ var jobFinished = { $currentDate: { stoppedAt: true } }; function jobErrored (error) { + error = { + message: error.message, + data: JSON.stringify(error) + }; return { $set: { status: 'errored', error: error }, $unset: { _workerId: '' }, diff --git a/worker/log.js b/worker/log.js index 8cde370..8569d8d 100644 --- a/worker/log.js +++ b/worker/log.js @@ -6,6 +6,14 @@ function log () { var tags = args.length > 1 ? [args.shift()] : []; tags.unshift('Worker ' + module.exports.workerId); if (typeof process.send === 'function') { + // hack to allow error info to get passed back to main thread + args = args.map(function (a) { + if (a instanceof Error) { + return { message: a.message, stack: a.stack }; + } else { + return a; + } + }); process.send({ tags: tags, message: args, diff --git a/worker/process-upload.js b/worker/process-upload.js index 388720d..2048a68 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -9,10 +9,13 @@ var queue = require('queue-async'); var AWS = require('aws-sdk'); var gdalinfo = require('gdalinfo-json'); var applyGdalinfo = require('oam-meta-generator/lib/apply-gdalinfo'); -var gm = require('gm'); +var sharp = require('sharp'); var log = require('./log'); var config = require('../config'); +// desired size in kilobytes * 1000 bytes/kb / (~.75 byte/pixel) +var targetPixelArea = config.thumbnailSize * 1000 / 0.75; + AWS.config = { accessKeyId: config.awsKeyId, secretAccessKey: config.awsAccessKey, @@ -41,11 +44,15 @@ module.exports = function processUpload (upload) { q.awaitAll(function (err, results) { if (err) { return reject(err); } log(['debug'], 'Done processing job', results); - resolve(); + resolve(Array.prototype.concat.apply([], results)); }); }); }; +/** + * Fully process one URL. + * Callback called with (err, { metadata, messages }) + */ function processUrl (upload, scene, url, key, callback) { tmp.file({ postfix: '.tif' }, function (err, path, fd, cleanup) { if (err) { return callback(err); } @@ -55,32 +62,43 @@ function processUrl (upload, scene, url, key, callback) { request(url).pipe(fs.createWriteStream(path)) .on('finish', function () { // we've successfully downloaded the file. now do stuff with it. - var thumbKey = key + '.thumb.jpg'; - generateMetadata(scene, path, key, thumbKey, function (err, metadata) { + generateMetadata(scene, path, key, function (err, metadata) { if (err) { return callback(err); } - makeThumbnail(path, function (err, thumbPath) { - if (err) { return callback(err); } + makeThumbnail(path, function (thumbErr, thumbPath) { + var messages = []; var q = queue(); + + // upload image q.defer(s3.upload.bind(s3), { Body: fs.createReadStream(path), Bucket: s3bucket, Key: key }); - q.defer(s3.upload.bind(s3), { - Body: fs.createReadStream(thumbPath), - Bucket: s3bucket, - Key: thumbKey - }); + + // upload thumbnail, if it worked + if (!thumbErr) { + q.defer(s3.upload.bind(s3), { + Body: fs.createReadStream(thumbPath), + Bucket: s3bucket, + Key: key + '.thumb.png' + }); + metadata.properties.thumbnail = publicUrl(s3bucket, key + '.thumb.png'); + } else { + messages.push('Could not generate thumbnail: ' + thumbErr.message); + } + + // upload metadata q.defer(s3.upload.bind(s3), { Body: JSON.stringify(metadata), Bucket: s3bucket, Key: key + '_meta.json' }); + log(['debug'], 'Uploading to s3; bucket=' + s3bucket + ' key=' + key); q.awaitAll(function (err, data) { cleanup(); // delete tempfile log(['debug'], 'Uploaded', data); - callback(err, data); + callback(err, { metadata: metadata, messages: messages }); }); }); }); @@ -89,7 +107,7 @@ function processUrl (upload, scene, url, key, callback) { }); } -function generateMetadata (scene, path, key, thumbKey, callback) { +function generateMetadata (scene, path, key, callback) { log(['debug'], 'Generating metadata.'); fs.stat(path, function (err, stat) { if (err) { return callback(err); } @@ -109,8 +127,7 @@ function generateMetadata (scene, path, key, thumbKey, callback) { contact: scene.contact, properties: { tms: scene.tms, - sensor: scene.sensor, - thumbnail: publicUrl(s3bucket, thumbKey) + sensor: scene.sensor } }; @@ -120,28 +137,35 @@ function generateMetadata (scene, path, key, thumbKey, callback) { // set uuid after doing applyGdalinfo because it actually sets it to // gdaldata.url, which for us is blank since we used gdalinfo.local metadata.uuid = publicUrl(s3bucket, key); - log(['debug'], 'Generated metadata: ' + JSON.stringify(metadata)); + log(['debug'], 'Generated metadata: ', metadata); callback(null, metadata); }); }); } function makeThumbnail (imagePath, callback) { - tmp.file({ postfix: '.jpg' }, function (err, path, fd, cleanup) { + tmp.file({ postfix: '.png' }, function (err, path, fd, cleanup) { if (err) { return callback(err); } log(['debug'], 'Generating thumbnail', path); - gm(imagePath) - .resize(5, 5, '%') - .write(path, function (err, stdout, stderr, command) { - if (err) { - // the error object itself isn't useful, so let's pass along the - // stderr output from the gm command - var msg = 'Error generating thumbnail with "' + command + '":\n'; - return callback(new Error(msg + stderr)); - } + + var original = sharp(imagePath) + // upstream: https://github.com/lovell/sharp/issues/250 + .limitInputPixels(2147483647); + original + .metadata() + .then(function (metadata) { + var pixelArea = metadata.width * metadata.height; + var ratio = Math.sqrt(targetPixelArea / pixelArea); + log(['debug'], 'Generating thumbnail, targetPixelArea=' + targetPixelArea); + original + .resize(Math.round(ratio * metadata.width)) + .toFile(path) + .then(function () { log(['debug'], 'Finished generating thumbnail'); callback(null, path); }); + }) + .catch(callback); }); } From 5ba3c3c2c7c1aded2f58d1a6008b1ba93c8693af Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 13 Aug 2015 10:39:43 -0400 Subject: [PATCH 034/144] Travis libvips-dev dependency --- .travis.yml | 10 ++++++++++ sharp.js | 8 +++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83cacda..be09c5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,11 @@ +sudo: required language: node_js node_js: - '0.12' +cache: + apt: true + directories: + - node_modules env: global: - GH_REF=github.com/hotosm/oam-uploader-api.git @@ -8,6 +13,11 @@ env: - secure: W0eGs5Lg+fZOIEYKLIFJZPsw7qFUNFTAUtPS/2UmlWwlQtzvYCYnNu4dd62kZKivzCuWZiwdwsm58l2r6tB6zRv7/3S4mhkiA1SrYFC3p34CXXV9XaJeEgOQ1SXoJuAW1ThSmq7CJu9dsk3Aix+KA4MggzBgTPG2RXyYhgr3Qzhcil1BX6KNqW4TP7cPaJTUCEhoDcyVLNvmjvUDQNwwJjCu1hifDptlyZt88jDshIVPi4wd2ITy10O22dkwvVaes79A7yYrpRyBJyAhb8w/fPZsCwT+9wQz28x1Q47at/eERrpeAVGOUbXE+5cla/dCCVG3rMlyJnaRP0HagaUQK71GJMYe/nnOEirmF3vtpYzRTmzbvaahEtWr6EbdVWqXB8WJCBShgEF2NICwQAYwgIsYyzIAI3wDvVyrdhbSqqARlUqRwEN4NUFAXfVYkXssYWiTz/s0Eaktg3I/Z0wL2lgBy0hz2JN9BcSCjSdzzj9FWaoWjFSDamb/M5U1R6h9rTGe2bgoDGsZbo4TV/QCXIsKvqjX5lC/W5GFyoUOyTrqtGyPIpOdkTg8Nf1V2ZPuAznln6YYchMdO64eABy7w5tKErM6JP8N4pHFgkgIPJI4gZdAlUELFe+mgqp/kl1GEuSd8TtlIdZ/ErvsmhH5ubYifIdBkpSZfUMRM/vueXI= services: mongodb before_install: +- echo "Installing libvips..." +- sudo add-apt-repository -y ppa:lovell/precise-backport-vips +- sudo apt-get update +- sudo apt-get install -y libvips-dev +- echo "Completed libvips installation." - chmod +x ./.build_scripts/docs.sh after_success: - "./.build_scripts/docs.sh" diff --git a/sharp.js b/sharp.js index 06021de..56b049d 100644 --- a/sharp.js +++ b/sharp.js @@ -1,4 +1,6 @@ -var sharp = require('sharp') +var sharp = require('sharp'); + +var targetPixelArea = 500000; var original = sharp(process.argv[2]) .limitInputPixels(Math.pow(2, 31) - 1); @@ -7,11 +9,11 @@ original .then(function (metadata) { console.log(metadata); var pixelArea = metadata.width * metadata.height; - var ratio = 0.05; + var ratio = Math.sqrt(targetPixelArea / pixelArea); console.log(pixelArea, original.options.limitInputPixels); return original .resize(Math.round(ratio * metadata.width)) - .toFile('/tmp/thumb.png') + .toFile('/tmp/thumb.png'); }) .then(function () { console.log('Finished generating thumbnail'); From d7ea2a389ab0e2f49dc354db092d5002db9c761c Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 13 Aug 2015 13:55:01 -0400 Subject: [PATCH 035/144] Fix lint error --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 5d56ea4..6e1d22a 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,7 @@ var OAMUploader = function (readyCb) { } }); - hapi.connection({host: config.host, port: config.port }); + hapi.connection({ host: config.host, port: config.port }); // Register plugins hapi.register([ From 2f0fc73ffdf5fb91d92aa9d43591327a8ff854cc Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 13 Aug 2015 16:27:44 -0400 Subject: [PATCH 036/144] Attempt to speed vips up a bit --- .buildpacks | 2 +- worker/process-upload.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.buildpacks b/.buildpacks index 87df4f5..195b881 100644 --- a/.buildpacks +++ b/.buildpacks @@ -1,2 +1,2 @@ -https://github.com/alex88/heroku-buildpack-vips.git#7.40.11.0 +https://github.com/alex88/heroku-buildpack-vips.git https://github.com/heroku/heroku-buildpack-nodejs.git#v79 diff --git a/worker/process-upload.js b/worker/process-upload.js index 2048a68..9af11ee 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -150,7 +150,8 @@ function makeThumbnail (imagePath, callback) { var original = sharp(imagePath) // upstream: https://github.com/lovell/sharp/issues/250 - .limitInputPixels(2147483647); + .limitInputPixels(2147483647) + .sequentialRead(); original .metadata() .then(function (metadata) { From 9096f94a6cf1950bb72fb7b3010608ee51c498ad Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 13 Aug 2015 17:22:37 -0400 Subject: [PATCH 037/144] Ignore test file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2aeae07..bcd2e04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +sharp.js local.js node_modules bower_components From 2380b01d973e893abab59197b24191d7df0d8120 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 09:49:40 -0400 Subject: [PATCH 038/144] Add a maxWorkers config option --- config.js | 2 ++ plugins/workers.js | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index 74036f2..05b6ae5 100644 --- a/config.js +++ b/config.js @@ -11,6 +11,7 @@ var defaults = { oinBucket: 'oam-uploader', dbUri: process.env.OAM_TEST ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', + maxWorkers: 1, adminPassword: null, adminUsername: null, awsRegion: 'us-west-2', @@ -35,6 +36,7 @@ var environment = { host: process.env.HOST, oinBucket: process.env.OIN_BUCKET, dbUri: process.env.DBURI, + maxWorkers: process.env.MAX_WORKERS, adminPassword: process.env.ADMIN_PASSWORD, adminUsername: process.env.ADMIN_USERNAME, awsKeyId: process.env.AWS_SECRET_KEY_ID, diff --git a/plugins/workers.js b/plugins/workers.js index 70c49e6..68293a8 100644 --- a/plugins/workers.js +++ b/plugins/workers.js @@ -3,6 +3,7 @@ var fork = require('child_process').fork; var path = require('path'); var ObjectID = require('mongodb').ObjectID; +var config = require('../config'); module.exports = function register (server, options, next) { var myWorkers = {}; @@ -15,10 +16,11 @@ module.exports = function register (server, options, next) { function spawn () { var available = Object.keys(myWorkers); var workers = server.plugins.db.connection.collection('workers'); - server.log(['debug'], 'spawnIfNecessary; available: ' + available); - if (!available.length) { + server.log(['debug'], 'maybe spawn... available: ' + available + + ' max: ' + config.maxWorkers); + if (available.length < config.maxWorkers) { spawnWorker(); - } else { + } else if (config.maxWorkers > 0) { var id = available[0]; server.log(['debug'], 'Attempting to pause ' + id); id = new ObjectID(id); From 7a3a397b4cc82fabb20f955159ca52a48b3822cb Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 13:10:48 -0400 Subject: [PATCH 039/144] Use exit-hook for worker cleanup --- package.json | 1 + worker/index.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 73429a7..0d1eceb 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "boom": "^2.8.0", "envloader": "0.0.2", "es6-promise": "^2.3.0", + "exit-hook": "^1.1.1", "gdalinfo-json": "^0.5.1", "good": "^6.3.0", "good-console": "^5.0.2", diff --git a/worker/index.js b/worker/index.js index ac45fdd..db945f1 100644 --- a/worker/index.js +++ b/worker/index.js @@ -4,6 +4,7 @@ * A worker that queries the db for new uploads and process 'em. */ +var onExit = require('exit-hook'); var MongoClient = require('mongodb').MongoClient; var processUpload = require('./process-upload'); var log = require('./log'); @@ -45,7 +46,8 @@ MongoClient.connect(config.dbUri, function (err, connection) { workers = db.collection('workers'); uploads = db.collection('uploads'); - process.on('SIGINT', cleanup); + // clean up + onExit(cleanup); workers.insertOne({ state: 'working' }) .then(function (result) { @@ -103,6 +105,8 @@ function dequeue () { function cleanup (err) { log('Cleaning up.'); + process.removeAllListeners(); + if (err) { log(['error'], err); } if (db) { if (workerId) { From 3364416351083ed65a18780b340dcaaefb64413d Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 13:16:51 -0400 Subject: [PATCH 040/144] Enable newrelic on worker --- newrelic.js | 2 +- worker/index.js | 2 ++ worker/newrelic.js | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 worker/newrelic.js diff --git a/newrelic.js b/newrelic.js index 6972202..f4c4268 100644 --- a/newrelic.js +++ b/newrelic.js @@ -8,7 +8,7 @@ exports.config = { /** * Array of application names. */ - app_name: ['oam-uploader'], + app_name: ['oam-uploader', 'server'], /** * Your New Relic license key. */ diff --git a/worker/index.js b/worker/index.js index db945f1..9998379 100644 --- a/worker/index.js +++ b/worker/index.js @@ -1,5 +1,7 @@ 'use strict'; +require('newrelic'); + /* * A worker that queries the db for new uploads and process 'em. */ diff --git a/worker/newrelic.js b/worker/newrelic.js new file mode 100644 index 0000000..eb08403 --- /dev/null +++ b/worker/newrelic.js @@ -0,0 +1,24 @@ +/** + * New Relic agent configuration. + * + * See lib/config.defaults.js in the agent distribution for a more complete + * description of configuration variables and their potential values. + */ +exports.config = { + /** + * Array of application names. + */ + app_name: ['oam-uploader', 'worker'], + /** + * Your New Relic license key. + */ + license_key: 'license key here', + logging: { + /** + * Level at which to log. 'trace' is most useful to New Relic when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level: 'info' + } +}; From 4c321c59937cc84bae5dd2abc18a4381d76d7586 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 16:55:38 -0400 Subject: [PATCH 041/144] Remove old example file --- sharp.js | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 sharp.js diff --git a/sharp.js b/sharp.js deleted file mode 100644 index 56b049d..0000000 --- a/sharp.js +++ /dev/null @@ -1,22 +0,0 @@ -var sharp = require('sharp'); - -var targetPixelArea = 500000; - -var original = sharp(process.argv[2]) - .limitInputPixels(Math.pow(2, 31) - 1); -original - .metadata() - .then(function (metadata) { - console.log(metadata); - var pixelArea = metadata.width * metadata.height; - var ratio = Math.sqrt(targetPixelArea / pixelArea); - console.log(pixelArea, original.options.limitInputPixels); - return original - .resize(Math.round(ratio * metadata.width)) - .toFile('/tmp/thumb.png'); - }) - .then(function () { - console.log('Finished generating thumbnail'); - }) - .catch(console.error.bind(console)); - From 341f36cc094d92c7d38155a531098988b8e3637e Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 19:05:42 -0400 Subject: [PATCH 042/144] Dockerize --- .build_scripts/docker/Dockerfile | 34 ++++++++++++++++++++++++++++++++ .build_scripts/docker/env.sh | 4 ++++ .build_scripts/docker/install.sh | 9 +++++++++ .build_scripts/docker/run.sh | 3 +++ .build_scripts/docker/start.sh | 9 +++++++++ .build_scripts/docker/test.sh | 9 +++++++++ .build_scripts/docs.sh | 1 - .gitignore | 1 + .travis.yml | 8 -------- README.md | 4 +--- config.js | 7 +------ local.sample.env | 7 +++++++ package.json | 5 +++++ plugins/mongodb.js | 6 ++++-- 14 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 .build_scripts/docker/Dockerfile create mode 100755 .build_scripts/docker/env.sh create mode 100755 .build_scripts/docker/install.sh create mode 100755 .build_scripts/docker/run.sh create mode 100755 .build_scripts/docker/start.sh create mode 100755 .build_scripts/docker/test.sh create mode 100644 local.sample.env diff --git a/.build_scripts/docker/Dockerfile b/.build_scripts/docker/Dockerfile new file mode 100644 index 0000000..043a0cf --- /dev/null +++ b/.build_scripts/docker/Dockerfile @@ -0,0 +1,34 @@ +# This base adds libvips, needed by the sharp library, to the ubuntu:14.04 image +FROM marcbachmann/libvips:8.0.2 + +# Replace shell with bash so we can source files +RUN rm /bin/sh && ln -s /bin/bash /bin/sh + +# Set debconf to run non-interactively +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections + +# Install base dependencies +RUN apt-get update && apt-get install -y -q --no-install-recommends \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + git \ + libssl-dev \ + python \ + rsync \ + && rm -rf /var/lib/apt/lists/* + +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 0.12 + +# Install nvm with node and npm +# http://stackoverflow.com/questions/25899912/install-nvm-in-docker +RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | bash \ + && source $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default + +ENV PATH $NVM_BIN:$PATH + diff --git a/.build_scripts/docker/env.sh b/.build_scripts/docker/env.sh new file mode 100755 index 0000000..235df39 --- /dev/null +++ b/.build_scripts/docker/env.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo $PATH +echo $NVM_DIR diff --git a/.build_scripts/docker/install.sh b/.build_scripts/docker/install.sh new file mode 100755 index 0000000..1280ef1 --- /dev/null +++ b/.build_scripts/docker/install.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Run the tests inside docker. +# Assumes that the base directory of this repo has been mounted as /local, e.g.: +# docker run -Pit -v $(pwd):/local oam-uploader-api /local/.build_scripts/docker/start.sh + +cd /local +source $NVM_DIR/nvm.sh +npm install diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh new file mode 100755 index 0000000..a024db5 --- /dev/null +++ b/.build_scripts/docker/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +touch local.env && docker run -Pit --env-file=local.env --net=\"host\" -v $(pwd):/local oam-uploader-api $1 diff --git a/.build_scripts/docker/start.sh b/.build_scripts/docker/start.sh new file mode 100755 index 0000000..c25ed66 --- /dev/null +++ b/.build_scripts/docker/start.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Run the server inside docker. +# Assumes that the base directory of this repo has been mounted as /local, e.g.: +# docker run -Pit -v $(pwd):/local oam-uploader-api /local/.build_scripts/docker/start.sh + +cd /local +source $NVM_DIR/nvm.sh +npm start diff --git a/.build_scripts/docker/test.sh b/.build_scripts/docker/test.sh new file mode 100755 index 0000000..7f9e625 --- /dev/null +++ b/.build_scripts/docker/test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Run the tests inside docker. +# Assumes that the base directory of this repo has been mounted as /local, e.g.: +# docker run -Pit -v $(pwd):/local oam-uploader-api /local/.build_scripts/docker/start.sh + +cd /local +source $NVM_DIR/nvm.sh +npm test diff --git a/.build_scripts/docs.sh b/.build_scripts/docs.sh index 84d2d8f..96a5ee6 100644 --- a/.build_scripts/docs.sh +++ b/.build_scripts/docs.sh @@ -6,7 +6,6 @@ if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = ${PRODUCTION_BRANCH} echo "Get ready, we're pushing to gh-pages!" npm run docs cd docs - echo "docs.openaerialmap.org" > CNAME git init git config user.name "Travis-CI" git config user.email "travis@somewhere.com" diff --git a/.gitignore b/.gitignore index bcd2e04..75de4f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ sharp.js local.js +local.env node_modules bower_components .tmp diff --git a/.travis.yml b/.travis.yml index be09c5d..9591c7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,11 +21,3 @@ before_install: - chmod +x ./.build_scripts/docs.sh after_success: - "./.build_scripts/docs.sh" -deploy: - provider: heroku - app: oam-uploader-api - on: - repo: hotosm/oam-uploader-api - branch: master - api_key: - secure: VHwJSkfvBAAXl1BPcgV2RvoQFSV62B5gJJ+RK9fiBv++IO6pJpJErStljwxv/LTedT0lp3NHWlK12NpJc6fWQg/hQo4KzxHP6NEqsZx/lNQ5te4KDQNZcOQjFV6miSArWm1VbVv2DvJoAOATuPcR7+daleWNd/H/EDZ+692hqAp4zcQ/ek6q7+TezxKFdUY43f3hN77u+umTbHd7mqwJEuSkUR29oq/fsh9Ik1+6UR3jlfs8mn47yUXbD74g81/5oSogt8uBzVNsfuhEWdpJ7GTdS4dXM5fci5PsvVZfc9lrcsex7vItu+LmggFLOkr2HI4+Hnvd0v5b9ML0khMxFtEY+lb0vDqMch4O5aT1pA1bi73g8UluuB8K/PTbhVsuVA2AXJ5PEznlNUZPpgZg3jNDV1Xrcc0Nmoi7s6dGJyCcWtat4aqi7Lo6zF9tzDlRS2vWrNeWG/oTGnSZhWozOCxavJZ6Z9eKVE2MIenCqYrkhL+E952mQwS++5IhpE5ibvPq/84FfAzjUgGRCu1r3LyYPISLKFrpyfqT7PEMOK1YqgOH6tAIClrxagcB+80i40NG9hu3mo3Sg3ckGMBsxaXR13VH8gHcCAKxv79a6jwzMKdlsOlGLjIUL2adewUToN7ssVQtCRwXKM+ZW92kMgtLxImbMwySshCkRK5vNYc= diff --git a/README.md b/README.md index c55721d..c857e01 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # OAM Uploader API [![Build Status](https://travis-ci.org/hotosm/oam-uploader-api.svg)](https://travis-ci.org/hotosm/oam-uploader-api) -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) - ## [API Docs](http://hotosm.github.io/oam-uploader-api/) ## Installation and Usage @@ -12,7 +10,7 @@ The steps below will walk you through setting up your own instance of the oam-up - [MongoDB](https://www.mongodb.org/) - [Node.js](https://nodejs.org/) -- [GraphicsMagick](http://www.graphicsmagick.org/) (**NOTE:** must be installed with libtiff support.) +- [libvips](https://github.com/jcupitt/libvips) ### Install Application Dependencies diff --git a/config.js b/config.js index 05b6ae5..9013742 100644 --- a/config.js +++ b/config.js @@ -1,10 +1,5 @@ var xtend = require('xtend'); -var local = {}; -try { - local = require('./local.js'); -} catch(e) {} - var defaults = { host: '0.0.0.0', port: 3000, @@ -44,7 +39,7 @@ var environment = { awsRegion: process.env.AWS_REGION }; -var config = xtend(defaults, local); +var config = xtend(defaults); for (var k in environment) { if (typeof environment[k] !== 'undefined') { config[k] = environment[k]; diff --git a/local.sample.env b/local.sample.env new file mode 100644 index 0000000..0febd1e --- /dev/null +++ b/local.sample.env @@ -0,0 +1,7 @@ +export AWS_SECRET_KEY_ID="Your Key Id" +export AWS_SECRET_ACCESS_KEY="Your Access Key" +export AWS_REGION="us-east-1" +export OIN_BUCKET="bucket-name" +export DBURI="mongodb://localhost/oam-uploader" +# export ADMIN_USERNAME="" +# export ADMIN_PASSWORD="" diff --git a/package.json b/package.json index 0d1eceb..261d04e 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,11 @@ "description": "An uploader API for Open Aerial Map Imagery", "main": "index.js", "scripts": { + "start": "node index.js", + "docker-install": ".build_scripts/docker/run.sh /local/.build_scripts/docker/install.sh", + "docker-test": ".build_scripts/docker/run.sh /local/.build_scripts/docker/test.sh", + "docker-start": ".build_scripts/docker/run.sh /local/.build_scripts/docker/start.sh", + "build-docker": "docker build -t oam-uploader-api .build_scripts/docker", "test": "semistandard && OAM_TEST=1 lab test/test__*.js", "lab": "lab test/test__*.js", "docs": "apidoc -i routes/ -o docs/" diff --git a/plugins/mongodb.js b/plugins/mongodb.js index 7624e47..73087e6 100644 --- a/plugins/mongodb.js +++ b/plugins/mongodb.js @@ -9,8 +9,10 @@ var dbUri = require('../config').dbUri; module.exports = function register (server, options, next) { server.log(['info'], 'Attempting db connection: ' + dbUri); MongoClient.connect(dbUri, function (err, db) { - server.log(['info'], 'Successful db connection.'); - server.expose('connection', db); + if (!err) { + server.log(['info'], 'Successful db connection.'); + server.expose('connection', db); + } next(err); }); }; From c9689b9819102bc0d3b2a9b4701a39c6860cb5eb Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 22:40:49 -0400 Subject: [PATCH 043/144] Fix .env file syntax --- local.sample.env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/local.sample.env b/local.sample.env index 0febd1e..1ceae82 100644 --- a/local.sample.env +++ b/local.sample.env @@ -1,7 +1,7 @@ -export AWS_SECRET_KEY_ID="Your Key Id" -export AWS_SECRET_ACCESS_KEY="Your Access Key" -export AWS_REGION="us-east-1" -export OIN_BUCKET="bucket-name" -export DBURI="mongodb://localhost/oam-uploader" -# export ADMIN_USERNAME="" -# export ADMIN_PASSWORD="" +AWS_SECRET_KEY_ID=your-id +AWS_SECRET_ACCESS_KEY=your-key +AWS_REGION=us-east-1 +OIN_BUCKET=bucket-name +DBURI=mongodb://localhost/oam-uploader +# export ADMIN_USERNAME=admin +# export ADMIN_PASSWORD= From 3c6575ae061f0224ca902e89b8c3a0d8510cfbf2 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 23:09:13 -0400 Subject: [PATCH 044/144] Simplify scripts --- .build_scripts/docker/Dockerfile | 1 + .build_scripts/docker/env.sh | 4 ---- package.json | 6 +++--- 3 files changed, 4 insertions(+), 7 deletions(-) delete mode 100755 .build_scripts/docker/env.sh diff --git a/.build_scripts/docker/Dockerfile b/.build_scripts/docker/Dockerfile index 043a0cf..e9c80a4 100644 --- a/.build_scripts/docker/Dockerfile +++ b/.build_scripts/docker/Dockerfile @@ -32,3 +32,4 @@ RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | b ENV PATH $NVM_BIN:$PATH +COPY *.sh / diff --git a/.build_scripts/docker/env.sh b/.build_scripts/docker/env.sh deleted file mode 100755 index 235df39..0000000 --- a/.build_scripts/docker/env.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo $PATH -echo $NVM_DIR diff --git a/package.json b/package.json index 261d04e..d285ac0 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "main": "index.js", "scripts": { "start": "node index.js", - "docker-install": ".build_scripts/docker/run.sh /local/.build_scripts/docker/install.sh", - "docker-test": ".build_scripts/docker/run.sh /local/.build_scripts/docker/test.sh", - "docker-start": ".build_scripts/docker/run.sh /local/.build_scripts/docker/start.sh", + "docker-install": ".build_scripts/docker/run.sh /install.sh", + "docker-test": ".build_scripts/docker/run.sh /test.sh", + "docker-start": ".build_scripts/docker/run.sh /start.sh", "build-docker": "docker build -t oam-uploader-api .build_scripts/docker", "test": "semistandard && OAM_TEST=1 lab test/test__*.js", "lab": "lab test/test__*.js", From 39a81e66d20addf21c49fc356690acbc1fa06d48 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 14 Aug 2015 23:57:23 -0400 Subject: [PATCH 045/144] Add docker docs, expose port --- .build_scripts/docker/run.sh | 2 +- README.md | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index a024db5..d3160c6 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -1,3 +1,3 @@ #!/bin/bash -touch local.env && docker run -Pit --env-file=local.env --net=\"host\" -v $(pwd):/local oam-uploader-api $1 +touch local.env && source local.env && docker run -it -p $PORT --env-file=local.env --net=\"host\" -v $(pwd):/local oam-uploader-api $1 diff --git a/README.md b/README.md index c857e01..09a18a8 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,6 @@ The steps below will walk you through setting up your own instance of the oam-up ### Usage -#### Starting the database: - - $ mongod - -The database is responsible for storing metadata about the imagery and analytics. - #### Starting the API: $ node index.js @@ -40,5 +34,35 @@ The API exposes endpoints used to access information form the system via a RESTf - `ADMIN_USERNAME` - Token management Admin username - `ADMIN_PASSWORD` - Token management Admin password +### Install via Docker + +Alternatively, if you've got a mongo instance running elsewhere, install and +run on a fresh instance using docker as follows: + +[Install Docker](https://docs.docker.com/installation/) + +```sh +# install nvm and node +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | bash && source ~/.nvm/nvm.sh +nvm install 0.12 + +# clone the repo +git clone https://github.com/hotosm/oam-uploader-api + +# build the docker image +cd oam-uploader-api +npm run build-docker + +# set up environment vars: +cp local.sample.env local.env +nano local.env +``` + +Now, for each deployment: + +```sh +npm run docker-install && npm run docker-start +``` + ### Docs Deployment Changes to `master` branch are automatically deployed via Travis to https://oam-uploader-api.herokuapp.com. From dabe3638438084044c3f3aee2e178aa72a0f911a Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Sat, 15 Aug 2015 00:04:05 -0400 Subject: [PATCH 046/144] Add default port --- .build_scripts/docker/run.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index d3160c6..a2e7c79 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -1,3 +1,6 @@ #!/bin/bash -touch local.env && source local.env && docker run -it -p $PORT --env-file=local.env --net=\"host\" -v $(pwd):/local oam-uploader-api $1 +touch local.env +source local.env +PORT=${PORT:-3000} +docker run -it -p $PORT --env-file=local.env --net=\"host\" -v $(pwd):/local oam-uploader-api $1 From 84849547423f2f09944cd088dab8a522f14e76e3 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Sat, 15 Aug 2015 09:05:21 -0400 Subject: [PATCH 047/144] Handle download error --- worker/process-upload.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/worker/process-upload.js b/worker/process-upload.js index 9af11ee..f5a65ea 100644 --- a/worker/process-upload.js +++ b/worker/process-upload.js @@ -59,8 +59,17 @@ function processUrl (upload, scene, url, key, callback) { log(['debug'], 'Downloading ' + url + ' to ' + path); - request(url).pipe(fs.createWriteStream(path)) + var downloadStatus; + request(url) + .on('response', function (response) { + downloadStatus = response.statusCode; + }) + .pipe(fs.createWriteStream(path)) .on('finish', function () { + if (downloadStatus < 200 || downloadStatus >= 400) { + return callback(new Error('Could not download ' + url + + '; server responded with status code ' + downloadStatus)); + } // we've successfully downloaded the file. now do stuff with it. generateMetadata(scene, path, key, function (err, metadata) { if (err) { return callback(err); } From e54fd30b325a9cf52ed629a08e69a4a06fd037c6 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 17 Aug 2015 19:17:29 -0400 Subject: [PATCH 048/144] Set /tmp as the temp dir. --- .build_scripts/docker/Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.build_scripts/docker/Dockerfile b/.build_scripts/docker/Dockerfile index e9c80a4..f726dfa 100644 --- a/.build_scripts/docker/Dockerfile +++ b/.build_scripts/docker/Dockerfile @@ -19,17 +19,19 @@ RUN apt-get update && apt-get install -y -q --no-install-recommends \ rsync \ && rm -rf /var/lib/apt/lists/* -ENV NVM_DIR /usr/local/nvm -ENV NODE_VERSION 0.12 - # Install nvm with node and npm # http://stackoverflow.com/questions/25899912/install-nvm-in-docker +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 0.12 RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | bash \ && source $NVM_DIR/nvm.sh \ && nvm install $NODE_VERSION \ && nvm alias default $NODE_VERSION \ && nvm use default - ENV PATH $NVM_BIN:$PATH +# Set TMPDIR environment variable +ENV TMPDIR /tmp + +# copy install, test, run, etc. scripts for convenient access COPY *.sh / From 640a40fcf2bb4f7cad7f2f601cde3b582013b4d1 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 17 Aug 2015 19:28:47 -0400 Subject: [PATCH 049/144] Add docs on the docker setup --- .build_scripts/docker/run.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index a2e7c79..2fb6c68 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -1,5 +1,18 @@ #!/bin/bash +# This script runs the docker container for this application, by: +# - passing in the environment variables defined in `local.env` (creating an +# empty one if necessary) +# - exposing the $PORT (default: 3000), on which the app listens +# - setting the container to use the host OS's networking stack (better +# performance, worse isolation) +# - mounting the current directory--which should be the project root--as +# `/local`. +# +# The intent of the last item is that the app can be setup (npm install) and +# run directly from its location on the host OS, but using the container as its +# environment to make dependency management trivial. + touch local.env source local.env PORT=${PORT:-3000} From a7d1261c3fbab54806f1630198af76cbdd687043 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 18 Aug 2015 09:41:04 -0400 Subject: [PATCH 050/144] Change db schema (normalize images) --- .build_scripts/docker/Dockerfile | 3 ++ .buildpacks | 2 - routes/uploads.js | 63 +++++++++++++++++++++++++++++--- worker/index.js | 4 +- 4 files changed, 62 insertions(+), 10 deletions(-) delete mode 100644 .buildpacks diff --git a/.build_scripts/docker/Dockerfile b/.build_scripts/docker/Dockerfile index f726dfa..d0b2694 100644 --- a/.build_scripts/docker/Dockerfile +++ b/.build_scripts/docker/Dockerfile @@ -30,6 +30,9 @@ RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | b && nvm use default ENV PATH $NVM_BIN:$PATH +# Go ahead and install nodemon for convenience while developing +RUN source $NVM_DIR/nvm.sh && npm install -g nodemon + # Set TMPDIR environment variable ENV TMPDIR /tmp diff --git a/.buildpacks b/.buildpacks deleted file mode 100644 index 195b881..0000000 --- a/.buildpacks +++ /dev/null @@ -1,2 +0,0 @@ -https://github.com/alex88/heroku-buildpack-vips.git -https://github.com/heroku/heroku-buildpack-nodejs.git#v79 diff --git a/routes/uploads.js b/routes/uploads.js index 1135bbc..981e38d 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -1,9 +1,39 @@ 'use strict'; +var ObjectID = require('mongodb').ObjectID; +var queue = require('queue-async'); var Boom = require('boom'); var Joi = require('joi'); var uploadSchema = require('../models/upload'); +function insertImages (db, scene, callback) { + var imageIds = []; + db.collection('images').insertMany(scene.urls.map(function (url) { + var id = new ObjectID(); + imageIds.push(id); + return { + _id: id, + url: url, + status: 'initial', + messages: [] + }; + }), callback); + + // replace the urls list with a list of _id's + scene.images = imageIds; + delete scene.urls; +} + +function includeImages (db, scene, callback) { + db.collection('images').find({ + _id: { $in: scene.images } + }) + .toArray(function (err, images) { + scene.images = images; + callback(err, images); + }); +} + module.exports = [ /** * @api {get} /uploads List uploads of currently authenticated user. @@ -22,7 +52,17 @@ module.exports = [ db.collection('uploads').find({ user: user }) .toArray(function (err, uploads) { if (err) { return reply(Boom.wrap(err)); } - reply({ results: uploads }); + var q = queue(); + uploads.forEach(function (upload) { + upload.scenes.forEach(function (scene) { + q.defer(includeImages, db, scene); + }); + }); + + q.awaitAll(function (err) { + if (err) { return reply(Boom.wrap(err)); } + reply({ results: uploads }); + }); }); } }, @@ -76,15 +116,26 @@ module.exports = [ Joi.validate(request.payload, uploadSchema, function (err, data) { if (err) { return reply(Boom.badRequest(err)); } + var db = request.server.plugins.db.connection; + data.user = request.auth.credentials.user.id; data.status = 'initial'; + data.createdAt = new Date(); - var uploads = request.server.plugins.db.connection.collection('uploads'); + // pull out the actual images into their own collection, so it can be + // more easily used as a task queue for the worker(s) + var q = queue(); + data.scenes.forEach(function (scene) { + q.defer(insertImages, db, scene); + }); - uploads.insertOne(data) - .then(request.server.plugins.workers.spawn) - .then(function () { reply('Success'); }) - .catch(function (err) { reply(Boom.wrap(err)); }); + q.awaitAll(function (err) { + if (err) { return reply(Boom.wrap(err)); } + db.collection('uploads').insertOne(data) + .then(request.server.plugins.workers.spawn) + .then(function () { reply('Success'); }) + .catch(function (err) { reply(Boom.wrap(err)); }); + }); }); } } diff --git a/worker/index.js b/worker/index.js index 9998379..f8117aa 100644 --- a/worker/index.js +++ b/worker/index.js @@ -22,7 +22,7 @@ var myself = { _id: 'none', state: 'working' }; var lastJobTimestamp = { $currentDate: { lastJobTimestamp: true } }; var stopping = { $set: { state: 'stopping' } }; var jobClaimed = { - $set: { status: 'pending', _workerId: 'none' }, + $set: { status: 'processing', _workerId: 'none' }, $currentDate: { startedAt: true } }; var jobFinished = { @@ -114,7 +114,7 @@ function cleanup (err) { if (workerId) { workers.deleteOne({ _id: workerId }) .then(function () { - return uploads.updateMany({ _workerId: workerId, status: 'pending' }, { + return uploads.updateMany({ _workerId: workerId, status: 'processing' }, { $set: { status: 'initial' }, $unset: { _workerId: '', startedAt: '' } }); From 85d8bf078c5893afab3a34143f792a0339dd3409 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 18 Aug 2015 18:12:05 -0400 Subject: [PATCH 051/144] Major refactor - Encapsulate worker logic - Add test for worker logic, enabled by above - Make status per-image rather than per-upload-job. Closes #13 --- .build_scripts/docker/run.sh | 32 +++- README.md | 4 +- config.js | 5 +- local.sample.env | 5 +- package.json | 4 +- test/fixture/NE1_50M_SR.input.json | 23 +++ test/fixture/NE1_50M_SR.output.json | 1 + test/fixture/NE1_50M_SR.tif | Bin 0 -> 1149210 bytes test/test__worker.js | 97 +++++++++++ worker/index.js | 142 +++------------ .../{process-upload.js => process-image.js} | 115 +++++------- worker/queue.js | 163 ++++++++++++++++++ 12 files changed, 390 insertions(+), 201 deletions(-) create mode 100644 test/fixture/NE1_50M_SR.input.json create mode 100644 test/fixture/NE1_50M_SR.output.json create mode 100644 test/fixture/NE1_50M_SR.tif create mode 100644 test/test__worker.js rename worker/{process-upload.js => process-image.js} (63%) create mode 100644 worker/queue.js diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index 2fb6c68..6ead9da 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -1,8 +1,9 @@ #!/bin/bash # This script runs the docker container for this application, by: -# - passing in the environment variables defined in `local.env` (creating an -# empty one if necessary) +# - passing in the environment variables defined in local.env, if it exists +# - overriding the above with any relevant variables in the current environment +# (see config.js) # - exposing the $PORT (default: 3000), on which the app listens # - setting the container to use the host OS's networking stack (better # performance, worse isolation) @@ -13,7 +14,28 @@ # run directly from its location on the host OS, but using the container as its # environment to make dependency management trivial. -touch local.env -source local.env +if [ -f local.env ] ; then + ENVFILE="--env-file local.env" +else + ENVFILE="" +fi + PORT=${PORT:-3000} -docker run -it -p $PORT --env-file=local.env --net=\"host\" -v $(pwd):/local oam-uploader-api $1 +docker run -it \ + -p $PORT \ + $ENVFILE \ + -e OAM_TEST \ + -e PORT \ + -e HOST \ + -e OIN_BUCKET \ + -e DBURI \ + -e DBURI_TEST \ + -e MAX_WORKERS \ + -e ADMIN_PASSWORD \ + -e ADMIN_USERNAME \ + -e AWS_SECRET_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -e AWS_REGION \ + --net=\"host\" \ + -v $(pwd):/local \ + oam-uploader-api $1 diff --git a/README.md b/README.md index 09a18a8..d902304 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,11 @@ The API exposes endpoints used to access information form the system via a RESTf - `AWS_REGION` - AWS region of OIN_BUCKET - `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN bucket - `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN bucket -- `DBURI` - MongoDB connection url - `ADMIN_USERNAME` - Token management Admin username - `ADMIN_PASSWORD` - Token management Admin password +- `DBURI` - MongoDB connection url +- `DBURI_TEST` - MongoDB connection to the test database (not needed for + production) ### Install via Docker diff --git a/config.js b/config.js index 9013742..a4c5334 100644 --- a/config.js +++ b/config.js @@ -4,7 +4,7 @@ var defaults = { host: '0.0.0.0', port: 3000, oinBucket: 'oam-uploader', - dbUri: process.env.OAM_TEST ? + dbUri: process.env.NODE_ENV === 'test' ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', maxWorkers: 1, adminPassword: null, @@ -30,7 +30,8 @@ var environment = { port: process.env.PORT, host: process.env.HOST, oinBucket: process.env.OIN_BUCKET, - dbUri: process.env.DBURI, + dbUri: process.env.NODE_ENV === 'test' ? + process.env.DBURI_TEST : process.env.DBURI, maxWorkers: process.env.MAX_WORKERS, adminPassword: process.env.ADMIN_PASSWORD, adminUsername: process.env.ADMIN_USERNAME, diff --git a/local.sample.env b/local.sample.env index 1ceae82..f2343d2 100644 --- a/local.sample.env +++ b/local.sample.env @@ -3,5 +3,6 @@ AWS_SECRET_ACCESS_KEY=your-key AWS_REGION=us-east-1 OIN_BUCKET=bucket-name DBURI=mongodb://localhost/oam-uploader -# export ADMIN_USERNAME=admin -# export ADMIN_PASSWORD= +DBURI_TEST=mongodb://localhost/oam-uploader-test +# ADMIN_USERNAME=admin +# ADMIN_PASSWORD= diff --git a/package.json b/package.json index d285ac0..78e6c7e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "docker-test": ".build_scripts/docker/run.sh /test.sh", "docker-start": ".build_scripts/docker/run.sh /start.sh", "build-docker": "docker build -t oam-uploader-api .build_scripts/docker", - "test": "semistandard && OAM_TEST=1 lab test/test__*.js", + "test": "semistandard && lab test/test__*.js", "lab": "lab test/test__*.js", "docs": "apidoc -i routes/ -o docs/" }, @@ -29,6 +29,7 @@ "boom": "^2.8.0", "envloader": "0.0.2", "es6-promise": "^2.3.0", + "es6-promisify": "^3.0.0", "exit-hook": "^1.1.1", "gdalinfo-json": "^0.5.1", "good": "^6.3.0", @@ -54,6 +55,7 @@ "devDependencies": { "apidoc": "^0.13.1", "chai": "^2.3.0", + "ecstatic": "^0.8.0", "lab": "^5.15.0", "semistandard": "^7.0.2", "tape": "^4.0.3" diff --git a/test/fixture/NE1_50M_SR.input.json b/test/fixture/NE1_50M_SR.input.json new file mode 100644 index 0000000..51808ab --- /dev/null +++ b/test/fixture/NE1_50M_SR.input.json @@ -0,0 +1,23 @@ +{ + "uploader": { + "name": "Lady Stardust", + "email": "lady@stardust.xyz" + }, + "scenes": [ + { + "contact": { + "name": "Ziggy", + "email": "ziggy@bowie.net" + }, + "title": "Natural Earth Image", + "provider": "Natural Earth", + "sensor": "Some Algorithm", + "platform": "satellite", + "acquisition_start": "2015-04-01T00:00:00.000", + "acquisition_end": "2015-04-30T00:00:00.000", + "urls": [ + "http://localhost:8080/NE1_50M_SR.tif" + ] + } + ] +} diff --git a/test/fixture/NE1_50M_SR.output.json b/test/fixture/NE1_50M_SR.output.json new file mode 100644 index 0000000..a1ee0f9 --- /dev/null +++ b/test/fixture/NE1_50M_SR.output.json @@ -0,0 +1 @@ +{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}} \ No newline at end of file diff --git a/test/fixture/NE1_50M_SR.tif b/test/fixture/NE1_50M_SR.tif new file mode 100644 index 0000000000000000000000000000000000000000..512a1e7b26834b47743cba00b836810791c7a8cb GIT binary patch literal 1149210 zcmeFacd%snb>DY409Y&*i-k@0VllIm-EopbJ&-^}bT@-7xG%AY%cUHro(DiH9J)S~gG zr>9@{?R$T}bH3;M{hrf*?XUfq@PZ%+-!BNyJSRNEz2D)-GvB%Y*0X}}Ecc(|@gL;z z=f3l}^1Kgz=RQvnKK{(}!Y+UQGoJYU-+5g5oj?Aa`yb%`zx$o%e1c#8!Dl}3^u1o> z{=fg7`+to4KlHm^|3`n<>wof@XN5n>YdqqGeuDeo$Nd5K{}#_t-lxU=m%s4R)B7)f zP!L{w{Uw1vgdgFD=jL<5C%FF$-1pvmNl=b=m;1h(&p!R+X61?e_s{un^G`|t@1HO5 zuP+EEKP>#)|Nq-_@cX_7+^o6{mh^A@MM3!aUlN3$`ivm_z_B2_7ZrrrP!LjI7X;x_VdE;_?{P5&F6*eo6ifzk9*1TB)p>fknok* zhlJkfLqhl)9}@Pz?*-wve&Pk;Z~xp2!k_!K7lcpTeL;BE{(^8;dO;|jzaUtj`>=5P zKm4%pzyJ9U3;)gk@?qh}fBVD2A3ON4@U`%Vg+cGbLgXtS77kx|QF!&wyeRygzwx5* zGym{K;ZMBrqVT-=Md2d%qEKGHDA@n`i^AO>{)q5v|K&%7pZlLaBK*Yv@e$$sc0M9} z!~YRs)cS}Jd+Q^@@k=iWpZT$ugkSorFA0C~@4qDcNhO--UlOjQmxSv4CBgZvmxTNO z-baOh{AWKZ{P+LEM}?pKCm$95_~u82cikTqCiRaBiANt5)Gxd&eC|hH7Jm6Jy)68t zUw&Emkyr6zrcoctGsE=P3-0!?BY<&O6gx~njJ|_J9fA=xrr+)oo!Vld0nDCzc zW5TTbF(GyFF+uzME5d7k;uYane&!Y7FaMoagdctN72(ChSA-|xD?+3His1d~E5gH% ze_Z&@|McU+FZ|7q3qSq8eVnMO{O>1z{F6WVsZaj+r#|`9pZM|r;!{8Q$xr>~e^&Sl zzw+z9{P#ZbbHDoQzxnI`Hhrx z^XJpR_Dk!qe1E?#-&1>a5nRrui3 zzu*6lKKz3Brt&)%|)}jZg*F5byCjsGx{LHtNeJKYfq&@FFsu5PA&_Jvs`bHE{{_3v``)uio1Enh&88DbDEX9 ziP9jR?~0jrFxw8KT7l{?+L$B?{YarNHmAH}s@RX^dy!&4l4%F>-C(gFZqMS)X?(Ow z&rUP_MY27OcBhf%G+7&G%j0~uFXacRbT<~Rd7TAowj1uurP4539*DCOsWpiOD|UC@ z>MJ=j-B7v{<2*Fx)&5GBXQgC25^sj04S%8;D0V}IZlFDhaa8V-Gu4h(#@X5=pY0{3 zZoD!|bK}@t8KWU^pbH*IE&pIG&o6qji|+WeJvnL3R?WexHacyM&$``ZJ=O5|vc_~f zQW+Pdj_ApnTp2^4WRF%H)nROU-Z^$iMa$C3%>r4dc9Zp(n1d+82*{oW{v%t~nNCye6l`x?%I|#?r=g-BIlNtG!@n z9F>Rch*)(L@6598dA_?SO-?GalgjBu^Xf`2)ZNGKEq(aNTXMP!mT29{>+_LogJ7xc zFLwNIfBE{`pYHwkb9FxKPC~O~bg@p1X7MuHr|!$O{PHN&9*MP%D9OG++0KRYSKXnS zmrI%EqscB`)?^4BC9=l$D4c8ixTbO0)f$KE!(h7UI=NoH-#+R0@_6b!Z@KN2yS~Oa zB(=lQMle|O1ghRdJCPgYqOF)dr9TWDs3IzV%^#9QXUT2Onr%5-upUabqk)>wS+qOz z7E?lZx&vOzq6?HR&s4xDUR})ULw?f&NB7jEx;?w;z# zzV*Se?SV0{XN#$0C2OYc%rsn$QGj3~50jjq>NqtyEiNwF^YbpzEReMrgNN>nDa*(2 z#u8O~JgZM+boI8YH}cCpU%l(?jYE@Fl8i}O_m>>bjLDU^q&uw4kgs5kl&nEX>r80k zvb)grCmZgB?1|Kz#lG)pzz?*?p~XpD9)?nNZ=~c&)x43sE0A_^jcVg`ZIW%w^5v0~ z>VR-kjajiVt2XClPALIDSal~_0kP_H7VHt(9j|+W6^FM(X0fmo{UvL%8Tjhc$p7AZ z?{QfzSp%`1wVG*qq=u(6ij1dGxgCJWp87WJh|+hUiPQ!lH3c} zBC302Kir1XgLAX0h&rfS7=gq6fz01e_i$|mL$D^w!gR|?_a3xiHfo3Pv zX!v~M(T4HPwr3|$afd6;Ov{(7dn&C!V-(7^e36QyJ1aeT`|90yzwz!j-~C>OpnsY7 z>dwmfwyQl3jAqdmXsR8Q`=L@hz|ZzrBy&acUVqBt&AI$#XRz!Dl^tbPNZk=H+nfEs zcpj?{L-B??Qge}oXUkY;9K3$>iA1nv+qkKD zDYs@>Vt!?mtc)|QIf1k` zKX3I`_4=$L&#Jw3v%6}rwUcepQSy+FctIW^M@TJCvI&-RgsSf9q|iTUPtMxobz!`W z^yl1SoP^3SlIzD~t*9uI<06G&rZz3N<~5F!oS5!O@phO9Kz$@O!p@SzkklWG$Ci|i z)m0iw#etM)di_PKHEpy=27lfXDB7a3t2#i&lTZ zmTP+_>vV5HHI$$v%cz&PHNGz!{FP~d3IcBV{=}Zp0!7(?Zrj+^lC^v8LrCQPOrz0pDdp|UU!BG zPxQDk3_W^0Tdk@_^X8oguO4Y`q%+oh$>Z?u>>a;;^Wm#EHeWr^-!c04E#V_yToue3 zi)~MD5*p7$x$iHuy};DLvO5#g!S_&C%rop~-*wYwrmu9V)IHG4~TU&$6IIfz=_ zsW_NL`ZKZI_wy4B94a{@RW~^(-SU?D3DybuF4awO3pXMGIg+N{99QbIdU;#~q92Ek zjv^{uQfC07By^NT|HixT^w$GbLd~+zHoeUuuru7B zL{3)8#Y!5ls-!s%Ug}0XMMt9Q?v6wvKhJDTQvGFZdC@z2#L4gU$KjZyHM@7zu5D4$ z4@cpX^Bl-Fn9v+r?jKn1C$pyYTDrXMTwf1Xr}c8(ABt+MuB}7k9ldQMlF~`CJJk*~ zCYjl(yt?RdP2YTb`sSPSFTK5Z`|ZWmlhJ6Ml8PoWxQ8nhK63bWO@V_*(I&S-p_J8| zv?c29S|?O&2a26Qz8y$ZUGb{B&8R8oA0ZL_wF~pdH#4=A0@hzXm==fCSs!> z&eVOiey~0etL<>1EXJfjvfxu{tFEuq55{CSa4XmFGzOv0Br=@E>Z6d<^wx&K-ZV0r zi*J7E^nLXizsCpDn}o-+NNXHwO{o)O$#$4Hne0SFS@e`##-#2zbOf1W$y(G&ojRe_ zN_wJbv>A;y0z^4eTx-pl3HaWs&t3J$TJc0H0?xO@REF@;j%R1fwYA~cd|d5s%0Ps^EUI9j{Jn<#8og z^XbAzrkEz*i**-;!Kyr1*P63(eVoYx-UbQkcmB$DYc6W?yw+aG{H#r@;ACJh=+2bZ z+Y0vPqQMIm#wD>r08vHqW~t_sN0Iq?dv@MiUbJidKp<_*$j(YXK+K7^LXbibIFW`J ztcQ~wDLa7D$;a9W(v!F1)hG4jEDLo_fvVqRPC|m|oLM6Yv_8ouTS0f$5UM$)w!!++(9Vc^j zPZCg_HTa9ROeahpkCg4Uq~4b^ReQevB-H5ni2SW7d|jTjK09xXPvrVI#m?pD=&Va# zgOr0)I<`J=g>^PbIp?r3{<4iKDOK|jzJ{|%vFBGZY1tvxT%2tj_>w0%tE{zJc zX{kCX=7-tZwAh$en)7OYn2R(5M)CND_SS)K&jP$DSPA&b?^dBitWN6IB0nv9NoUGf z%cD}b9#m1i#x<^jMeYT;wTI!(Al&GM7w6sOWq)$k3RRuPh{~SS!Z3jOy+wOWcD3i( z^<|eczr5@YSEYE#ZWoWV?j2j?C|z~72jSr;A~!wJl-?t%q=I=g5>GFxm)Gs#BI8IK z9HL6^+}7GRjP5N@R9$ZQ0PPU7P=5e`#d&9b*1di_dh%rQ0{EC>+*BgOmHzcM_8rNQ;?XlT ziB6IzcMv+hGi{vay zjK-qcUN(7acg_Nw2~$`g7)Z#SXE_&v3!&mW*c7uJP`H zbt6==KYenW=g-bNvvV+idwSNKoym)J?c%C;{%Cr3HC~)na%ER6;mqa&`GVW-+B(wT z+Bv?dG2a&f=Xs+qa=2%_qjNrlU>~jPR9Irctn=@hLx&-l_MC+`3Kgy3|3Wt?b;5AS z@v1Z5@MUEWsX;8;NZhqaj$5`Hqo|Zxfn3Ym==*Cem)vnveV2!^(K-jMx4xtp$<+G( zJWrUUJ4-p;2_HHhZmMr;9h-r)31|w{L&*l;0{(|%_Z97}QKZ_7K-w#`eBGUDDo^Mw z@>h?$jd`sxC;rRm3ovZmWp%i&3|0lgg2uIVbMLhW`dd&sJReP@pb)}tSHWsvxu%V# zj0xm#&6#v5y;0H~$5r4TRa~o+bbPv6D_Cg-YpqacD$Or|5Up(6huXDgd0+}2P=k=! zLuF^Y=7#U0&QeOPbxodRp&30nlP{wei?$@}G>f5NEy#u&@#!?7cc`Oc2! z4f05#<}NfmwJz7f>q;36!9#0QUF!M4#+*i}>8|#J@*u>mF$zQb6Z!om6F}nY?|l7z z75d(J_Zt^ii{Ug*Wrq$(!UCkVhhlFWA^3ypd$Z_pnW~Rt`7VWwAMLWz_DiytGO*l3 zLGx3Or>Uuz;>C6Io8Nrref8gFPk62NF>z} z^?l3zJEpt$9h*D8z5U>UI&!@0-`@-FdnyhbB>3Wto*o;BHWCU~SBa*FQuZS5^q_?Gp-w&ngq=N539w|HH3 zasG*!JzaDqbRgxFJFWQB6y|WN%#u-j~MZ`m6??m*)-QB=_NO0kmL3TT-VB z9UXc1j)=o)<5B4F(0KpG&gXvn;it91eRz1dl>SK*f(UE|`B1J}z@Gqb=oC>7Qrd`( zDEPcOEmy{QSa+y^qwvvj@DPZ&@7dKsUu29fG}FArl{34tMt4p@?@`ci)|f0?D_tL0 z9bN)iO{^o}hESja71ToyaWC5rCff?1Lr{WtPS&02y0bn>_XtoY&EaVWhKef!Jl?f# z*y9>9U4PviuI0|61m=eo87vD@-Q|-E9ELNYb3~54F}2ROqjqc_**1I`Ba#ORVsY77 zT{cgzT5E+%sg6%;sD2lh-OH=V)uY++Os;prQrcZAg{qB!-M4qByR}1@HQiC$Hk>ha zFryEoG>*toxMX`uF?IV(yBt3cLJ(Gmk=R~&(yx!7{EAE~LMWqYdXOhcYmodD^m z?4f??EUV2~p*Tnu`Y|>jr@C0T7weApz~6(b8OCa(sO8{@bYr4nLdXrj$PfGT%4&yaH3>a)1`g?y- zZcMV62Y?DNS7dFNKnGVaYepc3*zeA={aJo?l52DWPVsolbZ^6Sf8Vw3O=zT&h4YLO z;VId}Xh|K^f53^tp1G6SWB2x^;m)@4-W|;w_tZCbb+@$E&3M*Q@Ax4-kO>kMhbgLd zCv~Zsv$sh06c`++4FXW#C=uOB1kN1VUmga@1uW4*H!?XLeUBs3zu@gw=S{iq9nGWt zS!BA1wI*V=?MJSa`(Y{XA55ZyMXbL_^=5HuQqp>J6rwaJwEV#SeA`#<2B^Z|0EhF) zbd?+~itoJt6pequ$N&BPr{NM!6S*^@$c}K=tyriYbd}uhvM=0>gk{kz8TOqI?-}nA zHjkrf(7sw!sl}ti;Qm4Q2t5Tn@2U7rS@V8y|DJtg)4QunYLiWWs_BP8J&daN#N$Kp zcrUQO>)Soze&m=Jfg}l5J^qTzoX{V8cK4iHs?ec6rg6fG1Fq7@XBKrtwasF7Z|#al zj-uOF!%7r%SG=~I)l+fi`^hBfsVjvWVM27On}&gxXJtG#$k6aq=J2uFyK54UU=<=& zXQJr`EFA?7j=Z~6D`F#5nN$j+619LSZQOS6?6|fJ(PMks$VW!DaF(5pqRUhE!t=*l zB6&&|QJJGEON=7o7`6I1aKJty$fp}_LIfl{;fOMGe8Soj5typ*0J9{5BO)N0TD~vh zQ^G6+-N8ARC&}svR;K{-&|6kY-2efc3IaqrIqM*KkP*tGe7NFJ!S1?us^bi-{`jno zLx~(w7$j!r)yI-TK#d8BNd)D^W$W_s z0DH;Bqv@kJr|0K`Ry$L!h}}+dI;)gwZngQ|gM-%|sBY|IHonWaKDRsq2-JNK-pDHzpd@o8d zPO3tHz{3Lzohg~iEq8Yso}Q$TBN6eJ=f%#Vz?tFTV+|h?31!-`ZDZGbf6udHPG}9{ zu`P0pTPXu6(e*Va$wyBbZ@%4n^rSXF%}!3F)2rO+W&ZT4vN*3IcA)HxPTH+Sm2#;v zpsEUm%MN2ybyIcY)y-FL9=#sSqZ-%(WfHWV>u!(?cy$bE1B;Ny^Bpgt8=0PpfS>`r zFj>TBvsk|$$;m!R_9PVoA=#W4;TsG6xQh*9-wdX-nWmRWPMYu+?S)Qcw5~l(+2zVG zo|3(xyxpD9>4Hbrq|T8s7?X;4k!%K`?jfl2Enl_e2}?RlP_b`OKWf5?q|zM*I+HNv z4Co?U_eq@yu-{X#+ENC6RBefZ!W@{|ov5 z-aGGn<=uC`_T?|1&lVB9h#V?BKT_}FBmrYjwrUR}<+|7(#X8eiXBLOmFZYAoT9YVr zR-xm|xBcud(is#P!JoROKJd#!>`Yg`zv1)`(5vB2X-pKaV{e%)4q`wWlVk+hYLhyG zGp?Hl_RR<8d&j{8d(j@2gOJ-h{{3Co)&Z+4aG(eHVbKX4J#cJ-&%dDhBBad*tBxAM zc;|uX-aW$|NYq<~+c$K#9@saC|3`6+v*ZfYL;iZmQF4(@cDy^Re}n_E5#bi^Br|4pU0!R9nISn^;BPY!i@cUj@68q>7g^9Ln8q5z?_w{EoORpL271t*Lal2+0e z`%x&Y>3KKTiJyj}kQ} zcs^EjVSgL}S+n-jvE4tl=Y1OqI?XDh)?-h>cqsGZqo2-YO)fCrkf}f3d z0p~~VZ66setu=VIb>5vo*4XF;rYq^}s&@Ifb#`4lxyY=~;>**-`BipthPew*ui}Os zuIqy}F*%Y$*00#&1@nP@^V3_O{^HJOAE<9qD*@m!2od(7^vxL)=0Hjb?j&Rv=$9Ca zjUm=>qr!+aN4eu~^~2qP*c*jm1tS%-n^)dRS=ue~VQKW(Ux{V(UjWULBXE2SR?)QaV-q_LGWP9xpm;*cV>`r-FVT-1z7iYT-y)b+bF-rv|bxgZ#W;`)Ze~A`ld7FH|3c^Cg@}tyu;MxU*S?@HIgM>HT|Zj?lLY3cbAU zU0jb~)O}dW6Pi%YM8ue`a;I0FtH;wf-&$Tho?SheTwV=2-Gbam&*s%J6vQYLh^kcj zJNroR>KhPQ7}TY9P^`MZuDMPKy`wbBB$`1V#E7JiXANOVAEXv&06wF|0V1;U@iC}V z{*W&Sw($8NQeu(6U~x)@Xu%YcblhTjqf|7P>h{*aH=0LA%NXQCXP&{P3ng`W)kTfP zC$MYZysNvlZN6s(kqxF~>9zJN^nG+HO^1z8Nu>2=5`6 z)m-u*h$Rvp4`SaS>B0P{y0MxwTyltIT#+7#pm;0H)ggUjzQqpYN@~MAr|zbLfQI4) zNgScFmtG8-GAKGk*=tW5cwbLa7bBjh^q?1C#SPkzHht8UtS)m@qtrD~l`E-3LSh+U z4+xg*D8=C#grT4NWU*-70}bU@3{Ey@ffQp))*RWUyD<#ro1R43>dP3MT%fEmnm6a_ z4wUr%6rodOL2{@e7|5bM-3p?iV;I1_OoGFS&0%v+NYj)axKgP^NMY`x#g;Q7)ulU$ z&a8^~`<1V~_1(weAZ8@{wT2qE9kX7SN1HeJNApP|A7-IGYY z6OjsqTsEG^k$RCoa$SGA5$sP>U|b4Up1ghW z-S{By|JTI5AD6wH4R66>it8vFt?=+Ey)LfNM%72*Lk*RtNKLADXN)XV%siU#kqX?4 z<-X!IWsowD?wap^QTqm_otxTQ8}v0e?5Je;7K{#Z(%R^s%?$*@jBe$F&cf5AT}q-L>AQHZf$(?us7`+EsR<*##?9 z4R%A?jI6xv-MOKCU1#v_VT2oO$SiB#-~mVbQOR5_aU5Gf)+9;3Lg!(7b>o;fDPZp1!EQtRnr?q@O z(jQh9i&l4#_XRX+?cIk5U)a{(_8`!=f~b%Lc`mvQZ%M%}b>j7LHd^<3a^^tRgcKeq zSve2r;MH-yG|Wyw|X*06bF}tnrWpzLf}9M?hO3>vC^J1 zTgApJWa|th{PF@B8s)ml>o}8|Ez1M7f6pG%&?c5CTMM$i)^-!tQVlOO+UZ5^;<|YC zq>6iIdRC+FV{umLuMkZtvvYZNS}gQ}7TPPI?+D$&ePdu>?cPTC#x{w>2^3daP6}3H zCVCU=nJ9-X2a1>*Tt}wiCFlYEA%CbPwF$lA<{~m@Oo1vKd#p0b(iMmk0LfmV`*J3z zKYT%4L39CPGA{@nDzzi14>XlXy;QQ35aj@9gy&=XMCix2V9r?Rjx;9ddC}S=BgrKt zyATMfL$NhW(Bw#Aj`4{yD$xpKUIN>TC2OJM=Vpfp*7=WE? z3}Zv1O)(y08;)j`-j-yn5wx;d@`{i_)kDJK%&@jm zx5|S-rbJ72u|%8m_w+Z(jbJM>*jAm%e!od4VKTIRcc#)JxBBU12X-~q) z!JvH{R_rVcV6@KB4?^%C%);dNFMajNzxhY_1NRDKb}I1*S)PPmwMBsn?ADAChnt1{ z64#hwS{v9ts=`+ZY{P6%f$6KbqOEAG6@ww)_3Yl(-@a$KeaCS7f#p8L2vAueOOrZ? z6kSZ?F1vGsOuQ9=HbEwSdb>wCfXVaIJD>Za`X+sLEUIwT4#IJ#O~4jWjT}qen4$wNo5fMlnzvDm9LLqV zjEOD=6okUCh=0jhcB^1bi!P~0t4`KW7ei79RxHAP&q`o^96=hKe=>ox;JChEy@BV%O)D^cvSg?C2WLt|zU>fghn=v|vsY z&Cm&&w&Ee(a-u$fC8wMg-J+%zY4y;UH zN|&wKDbL4?DEpd^c79zwyUb74wAW#9t@l<+;CVtcA&!8I0|XLSq6?s>_&|JRo1HGjr|hC3X>{ap zrAFz4Lt7^u2-PW8W=qT?lGPHL=VO6IUR1!+R z`Z!6vtxZx?RjIlQ2A)>{-!{e~UOPGmaX=LYseE54TR6A$cQmJIBx6ovw&oNSW0(_K zp3O*|0s}R_J73y_}*8(`e=R8uZd(TLEnO2ooqoino8DkUR~no`VpHaxjm##v+-K0|wLPIB6Jxx z2v8m-YD()?tQk<+Fu^cwk$;7V-gZ7jqTO}xK#XWf7Fnx3ZwJO4M~;u8>W~uOV&;_L zp>^X8&CQ4Qhljv^_s*vIJ|dJBRskPKI}E89!y;a^BI+H4aP@CdKJ5DUSlGc@fW3(_ zyKmdj1op`$rlkHTbo3Bu7?vY&pz!d){lmEW_-Ua5Lj@z`s(5G+qX(J7fvltn9$1q4 zJ>M>L{ek)Zk@-Fb41;?+Te9H_tBrGwxl90|en)p8^-!w7`5<1GM)<(1c#8OF2f;&f z0i9g%8z5Lhu{LF3bVFxR#;*cn<47qgEdvDPNdl_5J7CEP)T z*+eT!r*?H(%?7$UMjNs&XD(M30 zf$>IQEh07CJ~R%*JIXBbF7_VS9%tSx*4>#dX-=^aRK}&kun?BRmXra56I*6D7fB@u z*>nify(LP{V9Md51FjvxmD(I{p=buz{@>6#O? zk7NEfqLjNewgmv%Bo`%Awx!wuq|f3o6H`Ks6U0SztYB%Fe8)q= zKK>lo6;QMm9X7C!p8)!^h|Xs+67v1VX*mx0`9YuxlzL zkM7on<(}HJ?Gz6)MRTL=E4F>v20p!zRC4Y+Hg6ohe)I4P53Khgvgk6yI23C})p0G! zNE=b1z0%#R6hxsz^i%5P%Cr>kr0hj!xb9`!fxa_&^pRclY(pS8wco4x@-YWw6Aw zI1I&vCY92rGCFKIH9~sFovm0xk`_0q*1xCm@2NaHN3Ly)c;v@eUbayhOje2hvP?Gt zItBgRbT@>uhDO^3S{p2ZvPw*YBI8;|7?@DDaW!c{a*Idkr0B?K+55H!w!mJYW-E2P zBv5QZomrxZ5vAt~;eqdq!*!0H2$05J@ZiwDr}ghXgg=ZNHCpyT-;L1}^hkgrh{1{w z?G+xoY9~rRjqN-Rp+Os+ojmeX(eM)VU}=;YCao}J2t^IvfTq$Uh~cUP$wN;!nU3Du zbkPX*rl2D%V<5pslCAk-k~>xMfc6(>HCobI{Q4w6Uey=$P_JrY&HHU)hc+=K#gy%y zyk)$S7~?}X*8Hs9o+;y%s0SGd0wGiA#bS*BJB#5SSk&06bbK^t2yWLD^-ho}%pU4kHjXxHRb|pcaFF;G7WiA>1Hm zcxz%fY!$5s)vhATqVP`VNm%`F&u!qZ!+C6dnp&=tvsHYyj4xNI*)j>kUmIblh*wAl z)7a^0X0nVCZCD1>EKogkbx{-1?Zr<{lsxb~dFziB;V+MXZDPC?2!bcOO0Ab@g{U;E zLrF0NPBP7T4&AN4svrX~)2uRzPfp8c*Ui;M5lXGp_ImPW*kDRynr)%1)7@yQBP#pr zset9on(^Vl$!uC4K-=OvF2X!d@DZhwEhuQTOU2{OY!>z%<8DA!P#pCB6X+B=Y&d`+fKBap*uB*O}91GR}^B7jLH{ zu2x!ibLRVq(Z)Ln0kjMpdUvgHZ3y=QyO`P*#o4{{hU&)a`>)+r-&7pbj7nf_7oAv) z=wd*j&_p%#`15h7anzwhw7K#)hl>WX$X^XanlZW7n7ZMaa@K? z;>H9C=0#A*5YH(;=@CPEW_FxCqH@L5*t{85FjzN8zV3pVGk~gYN@x-Fh&c>iSYFVL z)#V99s$=IS@%AaL-J4(ZnTkf2=*4yKieApQAoJ&s-{j}?y!bx>7sdhzG?zqYV!;cEk@DP!3UD6Pw^dPU>h_;9+ zjPAj7N<~QwkH~)dx%#tczUA@cEObLK&58Ve=-kp_PmQVV2t$sCTjqQCh+2I`q=W1N z#VLO5g*Y4snlu8y@vbuRf_bCS zSWI^Uu7VwC0o2KK7+T^d$)d{_hso*`Y)^-8HcJ!VI8Q&gJY#fH1*WdI$Y4M%wLPJX z!5Z9;qB|=CBEXG7pfUr*nuBmM=WPuWv`(B|ch?u~<(blx)ti-0&)dU!Nd(Wp>f>uL<24F(Pnp88Zq!B+&CZ65BDFBDpgxKu8-D6ySSgO5Ht?s-(P;(D_Z5qy z+;RmIDqHvv*&Rv6l{N*%#iC~^aCZI>A+r!c)DS)-L=FIojCHG+wJHorW3B~;C}n0H@;BTs zZdgr92TTeuw2;XO{cLkmdh07nx9s_o^L#g{bbQOM?wH2gFkK{#4v{&^z&ac|Q1Obk zUUx$Bd>i(|(kM30nFup8m_~4)PUrZ1lHD;~d+6^WxsQE1SZ*T(#3u6?k)K3BAqgc| z8AU+|<$jnZWtw@J7O1cXWd}7KEhk*B+6b9I3I5>!V#hOC*Wdf21p8?k&9#I1UJ(0z zn=>{Dd&|yjJ5(7&AaaR6;EhBZk4gaE(x10dTDnTKec_F_Vsr^*O<2XcLq9_rN!i#u zh~9V`=r_THL(?vJhK^lWAXVVdpEaWHRYy{5mM<~Z^6AJ#mc3YUM#?tEPbq`QYJM7~ zLUI6dRTtH0qAGZPMuS3Q^43p#ZLHu)nr9tJU7}>Ayya3dNR!p*&0CN`A>KVDXS6|o zq981m}Cxy9%S>REqH zF>+(Gfpeh5z{SLE$FsX@+uU>S*mE}KykU1y=3O*Hsjgy1g$QCx60VtL%3ff`DU~(k zOnb?Uoi3von766OC~*zi92R~0bF1SV!zAgwt@K09c^=Xgv<$f&fMh7!*|TIl(kRn@ zMyE|k?CEK(In8*|rX%M=w`5Sz{+SZ_r*Qkr;%HqW_(RuRKOR1MJe-{ryck7X+fv$+ zFWNl8y(9e{t?{nMw_7SYo6T@KZF9NzEZ*(uD*ptZ=jkw4aGD$&54E>$9)D36*u&!o z56>(k9OlV_CB+G;(3I-RQi*rGm8Ls^4#-%Ev_4yRa^0!WNqMPOC|0Lm!^3BX-6D|U zDyPM1&%UXTYC+UgZ7}38y3iN>5&gWZ89U^mIiW)f&$ zQ$sNhG=MKe%$9`^>L)kBg>QA>ZF)& z2U&qia?BX9chW&d{A8nbrWliuCrYJ($ezN83%SRRD76%43%~}c`rB#|nm%WWNV=nSHbK74za z2l=E-WTJpVdJ!nbHhDS)@s)Cz4jM zvNR?+Sgd^88>9O&ZStiJysRr{B@MO^YE0%~ z*2WS{EHn#TE>4wW?;bUR4nf(yy=U757ZpbmtQ5=-(2<{&NdelYG|cg8%ym(mNO>DI zJL)#v4iYf$1?*G>_Ep|J2y@y_;l0uOXcMDgfwyEs5$!GEL$!00K7HWvxA`2kZx^2o z;F-#cIhK4bf&)=8SaB+9CZj5z4&=cMU#JDJG>@lo=2;c0&S+e3K+ZUPU2D1e9ud%IQ^=_OA71 z?&48xby0=Wc4u)in7}iM251DX7`scF*=|^>80(s9`5`^XR^Gg#0;^+cc%(z$K23ad zg{uMw$)c&%_KYUslMCtUapCehcYc|xrnrar@8+M+qc;DFc9^<_uhAN zO2119hzyIb27C(#PF&>%u@I{|Xi&m{QOC4UXQsL(nm5q`gO`u}#kfdoOhacSSX3Q6 z&@(Y0ZJ-kqr3Obqz3(rzeH3(vLtCLr;BC8z))$%re1npBbY(gs2bbp$>}78n7Xq+p1s6)IBE2qN8ix;aY&N}2S+wr$Ug z&1nwmnBpQv-61h2Q%_R>fCRv1<*#@Ym79U>%vc6|l@!#ZIr1IuBm6lacIM=RTIoV9 zcRWmBNR})#ywkE8&YO5&rH3ngM9G686G@BbH)pBNJkL&N(hi~k-akFFHw#umIwOW? zfg~O#J27}vM@mPFB?nOMCtFzPXJr&m0=`(pr)Oi33v+(7zP)4D7NV_-$wf)6HLf8) zBJ)AMF=_*D!AlogTvO|XlwrSDV>DbcNNFfW(t2O;IA3BcZK%+2!;qNl503S>)P_4Q z_f9%z6eXQYR6*1>)zBE&u#X{DNjE@>bZ^-E`2XAFhnKAI5575fUTICgq#%z_Ns+^iQ^OtP`qNp+`@{I2Z7-{x;~d?C&`QJ?AaAP0y4^JeL_=k2=$7& zF4T$gz~7mP^sx7r1;!&Y{GmI?KZEJ9$nb|s*ISUCc#{;ml4e_ECeMzdvorb)CA;>C?QRuLI7Xz{^ zC>@?hozwz&uwH^zA9=I12LEAsQYA7=ls~HAfgM{j^~aIwIF}(^QTjP5F8V zAVJGR9g%Oinc0gOV>k`VeP6BTD-FVo5ukOQL0*a>8BIzt`!V9i8ZOwvC3G%Qyy7B; z{DiTgO!)SsFX~EaSeVaX!NOhuc4D97U6?G2!x<(s#R%no0t;NWmxwiKy0pPBu}7#D z?L~*Z;Ph8P9d`N@u%=VL#H)7VXJL?JPBQZ_GqTHxDJ8BzbiQ}Mn9$M%jQ$0K5s-A* zx&y`ls~F>g%OgDY*<{m4lm=1oRW;B8Kz(dE6f*S*C@V=q(UKZsYG2h2YQj_T^ov)b zq!OrF_$bs&avx!VyhHAw-HG7ljH-x)^w;q9A1GXmmg>ytMaopiSwsYcMFd}Yk|8^L zGc;$>E@YdYlus_|(^Xa~I2@imjp>0(ncw>$U9?g8QWwW^CaZs!j#p1q73703j;w40 zId|r%{tBC4X?jvwE~OkQ3B9^mW2+w;OhqL?bv&(hSZ;@V%Ji_pa$UqE$y5R8HD^N2 z9A*M9%zUb98%hTPG#U+;BUJTZ03+j{pfOW(7+|2IesJ zB2l&jE!rdM#4ujLHoJA0BKeYq)qz88@F0*g(tpJ2VzT0R9>I(UD+|D0U!=#YJQOru zED{ZJKh&H(a_r9;=^#V+!&Nm}7Z&GCNGqLRSC~^o@`6+(;GkMkBvuDvf1cz^ALbX$ z>nHuwO8{^Kdn$G{)ERCJ6^T`xG>fs?m0?OXr_!7fJ|bnaenNRmwc{ZfDXzjv) z8)omYn5jwG1qvsh^Y{`@#%Q253>JmSx-wc<8{@nr`;!%Cv}orO0aU<6nAReV5MMCC z7)E4Y(hZILBtCOohu0ckeRobB7o`vet4nondl2m~=@E*i;vnfDKpT<<+&@Aj z3xpy}u%i_V5EeUq%BW8pP=vVbsuCe$VVP4Uz`MW!$=2ZApaqOYC5Ic0Y zQ81E3m?sbI!}}_cpCNS&DH>rD@*>TnG7qN_)Uo$li2g1e4_};wd_(z`X!xNJWo42> zQVFLW!YK+-h`~_w`cVjw8X(W<;GoK5G$PR$x&~5_z0ZlIyh1f=&~h>f_ohJ_!vS)X z4D9Yw%Y#M(l{#HZy>YPCbft@ybP1a`rG!PIzG(R>y-2l>;m1uISD@r#wJ3d{RXc?P z0UQMuH(ILdAneeZ(VHiG^EA#G93A-Ki7k8)2K+gSA#FRFh%yC~A`u$rsqtRI0^uvz z=$*uFL_aC14QMCN3JSuM-FOjofkR_d6N2qe8=1p~HHenjKth8Ho!yO>i3vIKyxO5O zUez<52rPkEcY^D>YlXpv8Aiip(Z-PJH5>l?%GHF}2>TP%UBBVS=#kb)w@+Bx~VkL`I zHisp3rfiv?NUQV8koH@8>gRbTGc)6ep&yK-BmSp*>Ea0OKBA4Oc0D^-O2CbZ2Mo_Y zv05_3%6JqNZ4MeR-V7#N5kf9WQx`bEqf%`-SwgIw;exdhf|G~uouAU1SpZZs0o79L-zTBN?vkXK$&CH7zhWRK4zw!Nji=;*+o8V|JgEfL2|=~CJnuhxGFe{^ z>6@U|i?`zpmF0?{+S5J^>L3~`bpaO20fSg!W|(-RI4TQmcUM8$vFR#$dXYN6DpFJM zjX$N)|4rU~^){AnX@dUYgPxkI8NkMV%y}q^H^_qN$(YY?TNu1O%rb8Ow*oYp+aWcu~t5RLIWD^X{)1n4gayBk=wlZcJf-i~(r z*&c`6Mhy2=wb$6_*);#f%dN!e3Sfu-$+)bI%#?x;QA#v@CNB(X zZ(}$qhSqJ2h*QI@kW-`6OL(swg}4V^p|WqRa40w?SgM9Bq_Aja2SlFmzl5;MP>*G? z2fL%$Jl{U3?XuVv&ShnWSYqYscCuPhwd^|Ai>sc^K;tPb^Mo&el#YU5mKU+sM|w5?aJLD&RcChW^%C3b|sqB;7(OaXA>BL0a60 zZe5F4ca(Z7u9LF!JbTeGvEma-jT#gB21uaF2n{KTosD5r9keIvB0|XLvv7QF| zXf?S~Sn~QQc;jFb5#1{S@!|?6OmOaR4v?OAbrP^_%!gUEdYSHhv3-w?$@bwy<%ggy zWamF@e~h23QJ{40YlrP49C99U%ee(BX(mFD)$&zZ^(LXR)n(xDe)W~|j)2Ycxps{& zaU}C4dA`fISic0`8C`x3M=E!7?fc68m&?bWTD{PSqJNi#ixK{yhH_%Tw4B~~pD1rY zR4%&d#QxOS?Dr!x-@KmqYHZ;LF&|9893MjjDG~;mt#=!#DZQ%;blFP>VEmrDLWY9l zlrcL;OFfpEp7`Z>b}v7FdLHSt;~uE{PbD0Z&ApM=bKkG*yqjKq z{(j-vdUoz2q!|WJR23s}y1`f=Ro*bL^Cqg_1Zu89%zQK%c{?3>%S0lMF^IJi8ym&B zO#^;;!Ym8XQ2Bk(M_cP-Tg470bH({wd4;h<_g0ZKaBU5Btfao#@Sfb1DaP0urR@Xg;SV*;If|0A z$Wnem1MD?q3X8SYc5e{vTt}N%+q}YhgSa7tL~wSS_7z@7*y2Nm@yx>+^=X>L;8CYDm!kHnvTg7Q`k9#?=1A z5pZ=KNTHqH&aZ#_Um~$3S=mIxIJit;V;UsE@2li{L#4J&oP51Q+w#nN{WbjIk%^hKedA)Wk*)&UZR@7qmHuf3k_i$`G^QMZ3wF zg*I->7i)~c!>6ia?zEGF#m&Y>AP@JC z`TqS8Bd=I;4DIvH$R9tB|7C9R`RAfS=WN@Z3*1oeKQ~=s#Skk=4djoFTEJ{%Wb^H2YFuhS`&wHYy}pUoJ5rni zl@!9HG__gFxs`%g5sWHP0vFBc5!SndP2OXbxZTju>qJXm~tPzI0_O%9zHJ*;@S z$>@Opyp;r6MXkHne>l3i%T|zNYMbyk&w{Dk82f(no!vB@7YlK1cB8v@&H!QPp;5e8 zw}6KEKz@u8oXl~U`63dLSx(VkFf+kAPF_E+@ve0+FcWMW zb@h(0VZs8sU<41xJFldZajE&eLHhQ7|7Nh?>~e403mk(EbML)NQ7qSd=K^$-y@-V9 zt}dox)C)r`Hc~kv&_t`d`_KRKzXY)#aWPY4 zL7xdA`5E_~1w2gI)`XV0`or#h)0j+{Pinykg&T(a@%C+w^Q8L?jNiZ8W0z_Mao*Sbk^)zC7zD0;k``!phi$5}AnQ6os76o19gjG#<`h^fky;qi-~VQN}~W+f>|wJ6i!u(?^B)l#RWFk4pJo+Jy28^rR>#fV}o#jsH9h|San;ntui$`Ci6povJo+%1OQdr(zOxcUI+ zm$MYjGfs-DhhUXo#>{LU@Gt>|QLd4Xsl6;TK|K;4{S@yRO=F}V%;f7*&kpu-E$Y)zRE1Wf%pne51AC(q4(yJJ^uq_iK9ZvBNJS0Q#TJ4!ZF~| z;Iov*1}!ZVZfSSK+7WWnygAs6eVmy7>g|Vr{5bl@x%ux_HeS$GA>@?XH1a!2({I~^ zt@M;5JKwZ$&o_F>5~%iYdST=P)>0490# zgaH)P;-#eWy>SS+Ka?Wj>^2mTMYyK6Gj3GDzBEv%YYrn^vX84T*AgGI#Rbv8{k!;e zKgM?&knR*C9C%Rk?=|O#?K^!w)d|;#^5xAL-U5?+KAD8Wr~*WYW?17YfN?ex;(7&W z&SjWC%z35|8-V8~0=Q`ZQIlWnKc32RUbZAd-ul@qe?tgG86j;lZQuh`IDt; zbK6cN?1D3)jj?oAs7*SxMqs=fK!PpL7vJjeYC!0O{l2T3HQHO{%5oyaD@=f%@S;KJ zCcfpN?AMC<^dj(%GUjf(1HP<02CE5dG;A&lEPP|F{$hS{hOJy-x!lf37SJyx&UV&| zTYORG^NSX{^bu&@M;1L-DR4a$-3!|;E~W}Xc~;ia^Hz15NzehoEQ7UDFjcc-jhxq` z*Zt)6K%Dzzuao9pG_Zr%K5XUc^0`o# z*|IT6Tv`rhQ%fe(I9RfEL}S|@>=7ImB7p0I z6Nsf@e*`Ba*u^YRA*uOH%)lK}@lgYa`apd~HTGiq8&j%)twKAfcpy$nIvSM&01tmXK?5p zvqeQ9iR5NY(?~lZ)bb{CdA-+vI(_`<^8WMr?Nj+ys?<$xN7UfZ6D^ykwO1#uo?(X-WzEms_A;9IzYAiiIv;Jy+{pEV} zL%O&qsQ0wC4p`Vbn$J~MEt=IIt*JYTgzBslPnK8JNezGgG`NTWRpezm9LWrio58_` zH%05N7V?NYN9@Jyr`!)#39@uyZvCgaJWZ2YvwvjLj2=usUwHPXkAGwj0(^5`OF9S% zYs~{Z*zUb7=4?pG-2Jc;`xxBhvUBevZ>h#sq94)Nj;gB~7&YJeuiQo(&OZHAym>6p zt?S0=a2q$c_otK;@O-9i#EEg1fI(0&Zl%I|!B|S(sK<6k>2(1qVI9^MdKCzS0I?9K z)sV>^ys2CLgLZ$vdzv&KIfi3?bA54y3w1P=mYTX0V)k_(~ozER#$)97a$%5XVvyBPZYkb1VdlwStmuxDqJNVhe6 zFu%<2s}ha1=*GnY$MGBC3~yMYlsT!al&WhCV?%jw z@93cN)iuvJ618L0CWV2y$&T{678p};NZsHeZ7J3U`1kiZgQU*ce20FfQ$pQIovql& zY?U^1R35Eu_XgUwrCY9^iUki7?mC(7U7>S(>>%Jn!T_z>n;+N^3`{z}R-F;ZnKN=ZQJoiQsHM z!L%gU=J)ms)%CT#vANhsJsWe*;NEk2K)sc19I)}6iH{-bLrU3~9W`^iCu;~Qtg2tk z|M*dOS$@ugHjWA&dHZ%~Ya>w?{`_|N1=TPo_Jdl~`Na)exWel;r1b~iMoF*NCRAoy!kNuEm|p~;OA3cfB)eRau_W> z?mkzqWES679>3K3kCn?_?i`hZnPev|M@;f>qkjU*TZw<1T6rP2*T?G4JM=Ba(Z^7Q z%8J%HTV4e*!Ts7dXw~bang(`_1c?@5EHY2FVxhCaf>`CZu)`!lh{YEb#Ve4P0-Cgz z?1Ijy*eR8-Lk7LgNZ@!J&u_*@UP*TQ@P|LV{)c~j^M5!}`EP_pe=TKiWkqGUV>EPF z-v#^6?Y{rG@{;HqY;G^lUz8VgB0J^iF-{^RpLdWpledrg+ozMyzg#?g@v~R^4;KUW zdbj5t0|;!t9%q!dom+@;Gazi-pP~kZA*4@Mk)NU8K+7$4G8dG>!WvpPshj)k&Et`c z`Dtg5T_jAY%wP=>PCS-jxxAb@nmgjgcE0Z6s$NE*ZK&`1jn&lD2Gy+|#xqr3-chjyK*QRgP*-Gd&8TIG|g0RdtjD z%3QkC`gX8`kf1T-aH>B}-ebg3#{Ruj|2Ea=CW**Gyh|qp^#<1H@D%tj|N85HytMwE ze+TuGjo3-|M3cOho;)nf^UDH@vAG6ccRZ2jaeF zFRnq_udGo4Xw>!pn2?$}{liJ1Y9~czK;dk%+Wn;Lj9NEpjzbP?4^n&O4WSa#1mnT^ zX91#!KEyu7Y|Epm*-JzC8R4!LHmIU7ya=s$?1c9?__SBVO4u?fPVoj1X-_ zn4}chL;fQ=6N811t1mw8el!Ri)c1WLgiy!ZkDzOU+&@V1&di{-cr$s(JY!5M+I#)A z=P`M*gwrz3H7+{Bkp@pmH^N1kU|}ALTiME8CP8h45<$G?X)P?-wkNC6&HSvgFG1kl z%1hsYC|-ES*w(xE%g+&zcz6u8?vhx1P)Jz*^fg%;-0zOS}IdgD%eTN!x^zf-<^EWwhL*oV2zHy;+BZSIZgyPy!V0N`Lh z|JwZYi}c>I^bp^C4e{^$yY%u{Ym_rC;h&uEAn}uiWFGv!=_0Aw~zO$81O+${; z=o`92AbX~MXGf)z)>KsMoAOrl%$SP$4JsSG8Tp8f8tkw5aeih`zds;Z)-xh!~LIgMZ@leVo=dwAR2A4-~+JY;FH! zaV{acYkD<3lVs3c-C$3Yt*-Mi_P2vO8_ZbyYMTt9-D73Bq2b@j%p0_suNqHar(#@! z`O@kOnF!COzZ2~Bdg(`J0iU0s0uD09muOmI^+5Mna33kzgCcJp%^Tx7azqF}vvVY0 za+ZzHT6SSucEr)j=D~tf!*<Ny85gqNlj%1|2R!NJxRi(5^Ahi4`v$8S9QVpUGPQ z+pvhnc9T;4sd1g}s6ND5TzXplJ=IC;Si-nI-$Xl+9Rt>r<0*Cx=Ags(=k|sc2Nwpn zUP4!Iv1=)O){dXnVj6z@0c#PIwot^baeHhEqUWt`_J)7wb^Aa5@>o8?Ka&@GqtQ{Z zQ!9y4MZR^-@08Vv)u&*%b)4#wEmD)}Ven*yEFHv>4K3qeT)cCL0=&l9LC`8l-3pw=IxvKg6auB#>OOqDZI#OtFpx_+tZ0L zzAmgm5iAvdw{EPg;a_+cdG}`R6*8fO^mI)$OdJ*jW<(PLrTRjxxQB%^Q z&HStY6zqEP5P}POUJAQnBavpy#s-!o#0v(|pV~i0q}7mMD~5JSyZL#7KK{iVQ!f8@w|Ns=*baaFYNU6$e^h(F7pue z?=yFg+1_nN@o!#nLfAYg1e{O<1Hv_>CaSwAmlTeSDT1xUUGip_z4-Ly@TXr+KYckw zu|5yvM*1eNA>pAeov)Q^tEKW1f4o9@Idi;_U>}_|zP@=Sggy6@w@YM}MfaL_?&s%e-6^h11 zD|&hvW3UW?cUKQ@@=&f>$L!a3<(9T@Qdc+W?jY@kf#rgL_{?+$sH%ISe*z<&pe#sq zcbyc$(as2bYSqYb!dxlsdQ-Kx#bS(mCgni; zv4v6Ohm}7-J>EmZGDQ+A-SaDSf~bMZV%<1k%)jdUe;@)ef_A3hEkFP)A_IXFttdAe z(8a_!eqCViDEP_Ah0K&`lvSqr9alj+_lAYq_Im*?kVe)bn!_+z7dONKeApe46vKP} z>&dTu8Zp;gZk{v0u&@ssZx#6v*f9h)kx85dDz-9Wrf_Qb|IGd@O54oA92ukNE+qWv z*a&-kk(5&AAv_MsiRAcm9ns>C&t|@RyY`B2z$A`322OG!kd`yk5@g`UZ$eiYUhs(G zv;7|SMcAbk4+=`_$ot@mX%WnEB3TKEiWE9$om_C{XZT;=gB6d-AXs>YljFT|Z%bNH zRV1*V_d<-L>P_>a#JS;-8y(z0v-|FO}%yKMK)NMmelqX#qlr|WWfjmGrUxC?$Xr;M-Jiw8dw7(l6mtk7`CDFiLu@zl_27(6-}>$3H(!ta z1&sgo$3Fpf{`~fzewh6F!}=@Z)$5fPd^Q{+2nNi}TxDDxZX48IR29%>{XasHs2wAr z0nlQXc!VT`3fmjISJ`#!VgbRC$~^_Ya{sCI^rdsnY^`4^B7+syqP|ahdJ%nh`@?E% zY%e>TI-HZAm^)oXytJt0a7bdDt?dBiX1Na~L={uL@ok<1&eNU~w!7g$ALiL|H$w#j zg-{Q7k5{;yZRKZKe2hijOj$Y&fhJKK)Z1dF8kG1=FT!OUrMIHmdYX*nvYBne$ z=Xn&c^hZKQe*$=t<>J;N!ukFUxX>W+(@*)o{mbP)|E>PhUu~1|NcVD=aij<&H_Nz( z$E5DJ+}djN;*U?6+q=~5tLijG}FU~UIRTMU2c{7Tse6mP|Bw1>W+P71}` zT7Q^Zz1O`>ckh(_j9hhA(poi~B~xT~uus?HwjcIF=EPO(BR#)`ofDim#RgfOS7eNn z;&;^|8%cwJe8%p80!8%XmZ2lW5D(EF(2xN{O~bPtJXHVVP4(aTckrN1rKdyuR^+{; zEhn|E1BF2MVwvbUtzX%FYcsY@i=nZ<4kg{LR@gBDKQhcoBj)_o&)cU|cpueqI2kD6 zV$tF`j@MvEpdL+*Tiq#NZOi7~bVaNNtb4*JaS_%%s6jb4yxhNs0xr%!lAAErjQ>0( zEOUu~thd<2h3LC4X z=fX=K{>uK`_kb6&Ulb0GMqpA$39|?Yl4F9{U#rQu(z-S-EU#jfd8CkD$eqEf-wS@2AUd&Y^5vRPsI5i$cwcF*{oh##k2dc>lEgAehq>+J?~+M;^%AyR#KPk zKuVIh&o4HU*AF#de*LC+a1z-&TGo_C4`xxSI(H|WQPHdR3M+`^=s>r@##tIWDrxoc zb7k;!lsjGeIQ#vZ$!~r0$DiBxUs`A;6k!=Qh6byI%X@Fa`PSmr=;GGLwcU}W?e`O+XOiO;DCiIi4HR)MZBL>k z&jjK?>U8*swadwI9aOQIf;QuKtux&Ae_e-FQF$vE#9cC8V9}3Y5W3>*>3UB5L16*^ z3P?_Kibu_J@~9Du94;g0Sigcez=`e$kC_07i_?3%-z5U z`AEoQ=~&Gml-6yoaeG?5IYUbj{SZHsVX?)XNyBqcPn}*J*r{1^@ogY;z(&jPG$!lI zj#q1Icw$z)%D&xe2yBgxx#vA8LNvUS>BHI0)M(^jDtAuoxs1z7;YHFF(h$h6DjUb4 z@<$X0O=IZ29Y#YdHr*g(0d?k_fM6-^Sb>Xe|c2iMo~|ncMp?CUMd^R1(q*~aW35I1AT;Ytzifd%^(4y z$K-KtvN|jTjd_x4=>k^VrK{azc5S^IK6)|7MEoq+p{x9c1$Vw96*bpHU|HaOM%5l_ z_G30;rRM)_#x=y0cymQSM)WddzdBXeU63+Sb#aX-F6(?ErHH5$?aeS_0=sP4j)}F@ zNMckyw>8jpxCiWgR!C7YGrUK>k>$PeLd{8DB$>USg(waqyFsW9LBeE4C(s1SSbx6w zT;KA6H`g#$>^&D9nZP_i>}}6>-1EAfNm;!+AH7S&CMf;DA4}?xTow<)0p1a?Q}rBF zCGvnKn%h8-gIxhzNOu$YdQ6FiVNzm3>;$k{sVo%d0iDS(|PmrQCjL z-+pP4xca9>GJX1`^X0D{J^A%*wp`mf5xtF!XCPX6zOwnge_QY_5stSmP}EVx@p=$=N7DK}$ene|qFv}x|l zrNpRcMG5Asdt){xtNTJ8A=Q$$){g7=$?5&D&y9>jB%;AsRfrpu&xs6ZLRnExgA@@i zou3m{t5%dc$K(Kr&AMMW&*@d3V!GNb^zTmEc1+2sndO&nCcb$#@~3y>UwxSRW@Pq< zx%JoUJnc)Ene2vgAhph*d_&*CjlYvAUAm={x$3r{a~6W_dy2f%{qmYuKDG63&cR`+ zdN9k?oLd6gKloA4;BN6V%6XBia?z4JdrX0o(BT*lm(J@E zR@DvAYn3Ont;C5tfzQ8|wbDxYkw>=Pi^)SXa6pI9uEKiqA}z&+6wWnbg9%516CzP_ zy~B4EAz_mu>4scazoZL#wej{F|A(Q86h3P2pINU z49bETU~v`AK5+l8cJTLxB|Y$SXq_sZ6zA1YyP6>?uG!yj^ipOh7c_iYpKc3{DsHUX zEG+3XhAVXvY2!fPc1qk9odc$O_?++EXN+A2H0O@BvPPFMgu=^9s^0_xXR(zu#1R0Z zLx)t{!)0+usahs}o{CrZw$66lqB2?D@4SC&{0xhq-+n<9-o;Dw6ge_v#AN1Und3O42jmx*$x9k5sk9C zgcO@KG;cu~(@wDnupnS;&ccqV4psw_s&r#jaXXMg_dQA!MpQrK1D*(2lgDrsV!#VM z@_t-k{p8nZ_8SM&c$kdD0qhLo5a5lycx%A1-|wF;Q)Xe|)_Q4%0QWz2d zf4{HCsN?%FQ5kyms2f^bBRp8%8DY5C$V|p~9EDikRr_dDyylRd1wo2~m9;*M&PC_Q zZU4ev_5J!AJ~n)ziGw8C>n$~kxM~pfmPg3CTP+Bd}`&I85ON;B2& z7cQ@P_#9bJ6x%dqTYUdknHR0So{Nr%wAO3UtqcR%XY^WI2Q!X%TsUp2Mreo{6}|s= zPo>^{t}RKNnWVQXF=lBVJzg?BDfo@n7H0{Tl_w20L^k;6JyfniiJMdYY-4oe&9_tE zSaqrVI1I`)wj1(;vhSX`d&pnXe_ckd2WbeNp8{{{ykBvnLf8o2wNJPQ@77;`KlAO2 zg=fZC(f0Cybmsc`AjCla457VqZUgilYX~__W@n?8(VGzvbGe}X7v919sLCzV@M5T3 zhqn(K$SBAQ4sQz3TcUTO{3$OKSkPmgyraPx0f*;Dr*4!lcaAGtcw%};SI~9GFvq9O zovR+}h$sdD`VRYa@SW+vvEr*NcGY~}uyPfH8Z}ipe&da8{0ar9gwi=AoO5i``Rg|I z{OGSYw}=1!-~PWQOa7f#`v3XmVQdmW1;70~pUk<9?DXF8{0V-YEqrK!aBo;ktwZC_ znp?1i%j?8tFNKCDOvD6QY?%TKWh4@=omQVOu1xJXLIP~xrr2fD_ABO2b9)xMEoW}? zCUxEw=e?(^w;%v0;c$|P)o+siiVKHCuxdAR*xZvn;k>Z^TMYYFG48~J82DQoLlp^Q zsPSN>HS%in?d$D#Be7Aa0hPEeKNxa$p@Ljm2 ze{@T)7gjB;-fz4y6pRk%Vhj+`hrl0?z{l8#upx)esBbR$ap$9dC}2DBQAY!eG=G_* z2oU+ozgM|7tE&9A!vbXpRQJx~#deO}rB=;e654=(YJ|7*GGQTv%+if6KiBY6=$rSo z#F^uHJ5b((Mgw98T>bW430%VymAR0$=|wLOm!>A5HPgfMojbeAh`1$lMzjFAukL=V z)+N)O;r2iP8aIb`U)rC3={^3`xw=2471gF*KURB>C&%YoQRgXiYxAw$?sDW!Br$qm zN8C+!`nmJwuJt7{hW>CfH8H;WGM1fh-W)OiQMz}aSf%AlM3>t#W*spsnhp2q&p$Wr zA4@C(_Vy>YQ=_Z#kC~&HgW>}B8;NX%^EFr8ePZLob^u#RH=e8EW+z%Vdu|&(E!IFm zl5^p7SQQ(za=7fRhU6HLK#_&B6}EB^NP&9Rqx6{DrFBVgg?5%k8Gn1cPkAB2)3fE}i z$2arOTqdp~+RpQ?h?cXP`|86do^SPsr}KiUm}W*q?;z$ciB;m+flk=EvRocrf3t*l zke{LKaEE{NzrM8*oz+-{M-#b0AoSD?3uNODOQq$zp z=Xwm=)C!g5c66e} zR9ZgtgeZT)h^evk^N<0By4BeWl>4iA2Zys6D>QZjX|v0dEr$2|o4teH68xnhfkXiZ z!ufu_0bvI01SM$@lVTw2Z&M7P6)Pt!cOY$*TgbWNm~hbOH`x0y)9&76{_B7Jm;bp| z{=R;%fk!aro=O!M5FUzBflAelMnBcPKh(^_-Ve@}PcDG87~^Xm8Mb)*d7%C7UdyRK zthGG}>bWq)1@U-2;yuUbEVo7ZtZN4B3lQ}MKH9=KmpN+%VwE7d%ZMB4yzOh_w@(Mb zLH+TlbC+-Q^DGMKyeym8n|N3-HiMI=1O0eKO4k%pnT&wdm#@}dzg~O&Q8=$sAN}|` z^7f@bz^(W1g>wngjRpG=YzJ2M%KK1_7#y3KMt(i@wNT(UoN)GMr3h*GaWeU9ZI#wB zLOlh1*s%Roeyl%Rt+&B5=Yz0#iSu^5-_3sieC37Sa_2N6nA>D6ZX?jG3*stP0$V9L z!DHFijx(Un&S~>-?AbeSl~y1Ua4M!ffy)Fk-F%DBFSdsE-$W1RA(Xy-I>r6d$?$J4 zwqk`=g8CI^Vueb&QbP^8ISfezA>$?{@m9+DUVa84^Zm?sOFJVBB6^m*fx~$jY$$Fk z4^QncU%F49YXI|$cIx)7^l*RLxlSW*jIX?SH~T%<{MGa~w#ifj2gMaJd1t}@kcMK< zxFH?*=wv;UpC3FFfBv}(%*mdt3THelE_bf?%9q=*{aHlc+I1RQB30_}WNCi$&5Iys z_yJK+xI?M3O3bB4g`?{FP&+7#EguEI$-N>MC8@Ukx$5n942*qfVN~b|Vv^q25p297 z#GnSU14V{BuVZWM28nk!q132CT0Sxp{(Ez~AN&ZCP0X8z^%N^?dy|t^8bde~RX|xM z56#ZjgH!}>cDuAOOp3upoOA55q|1s+;6GVfJ*q`do0^>6A&9}}GkG4YQwxVJ$;&6r z{@K-_T&Lu{IqTmYWu-`i*PLbOwdu*J^*7YnALf5Rk$pMyEzc#HiNI$Ex1I+rD?;kVO zdc1J11}1gCaQ^(++C^>s^la(47;GP0TpCOzZmYPqbf_TDu}i{{n2CN^j(?1#CxfBx zH6mFm1W>ZQijTgZ|Ka7-H!tUYaA4_n#!#S#H!u=4>jvZVPG<6?vZl!MWK;HC5v`pE zL8t9WC;!)$M++gsdm$&?enuT$$;}!l{`0^7P3YA>|Lx(pwhr@AtMNcEPF=@pZKr;f z;NEu12-GjGlQiaFPx(;F`_&-L?~y(WT!X9M^4o!`l|j)-V;)|3Pl8>Ta%BDOH8@XI zFra+^_B-)&M~0xr!Jd;{<2txYnJ$= zr(t#5*(HJTA<2q7dNYr;qajj9YkAfdbx}6`~)&qyuZHeP&SVLWKDO-KZN^IvW zoePICE@8RKisdHf0ep4o*cj1Nx+Y5G@R6MlZw0S!ydj|?chzhwzhc>?@;Sn=fK>iD zoX??l-oFn0TRaGnA5vw32FUPpu@Q{WK+d!v*!|%D>|`127djzg+kI{|7Q#M4@eg93 zjm!*9EBzya5cWmTJf4N02tQnah!+spYbWb)exJkB*vir*WgMncz-9?8=>80|2q1u% z71nB|V2C#dPS1Yya2^r@2DflQUu@r<+Hr$5klq`z9b1W{K_ELVRn{gE3$%HQ=*VVg zA6Q2eu$CI1iM;;6w{7da)SK=@y>)-tysP2BLyEe0)w_pFX@K_+g`3-a|E_pM-^LaBkoK_--g{A&o{>vAf_QOrdOo@Qa(?^$FiB^wa2|MEWkydu;d-1v zcCweOGvdxKB%(t5!1{7FVaL6s6;$_rBMG1lpq)tRWxl<64EEm)cLGzg+6LW?7S8d( zY)bvHRb(P3Edq^)^QyC8|1!f#m2Vq!&SHJk0gVziW77$%+wWIXYLB@|Eu6AWfAgRjU@Gp)(oNRG`A)*Vbc z;&~apTl(>%ZIKMJjn}TD{2Ex+-KQem1cL?eDMu#NRZh@h1?4{3zCEU?OxN}`S3@dw zkgt+G_6DM+;#>uAcp9ypo4eenpQ`syr<{@a8ntC^H9H#95VsR7GDd}No{6=W!5A<$rmEXsMJv0@ z$E)ZQMurIv%%L-6CyofpN+qmi4K<#u-`tp)JivL}^Lxjug_F&#yrsa*Qf_)BJ9+!L zr}X^$fBX0D-Pu_)WY(MMfH|(UPMbwP<7GtsN-I{q+EbKpYgzFS@^jNVZP_O#+P6I3 zW12>AK}&*jS4Lzo3h=y;egyRgd(YD~LkCzdloYA!U2X$$D~8{;$zE`&X%T6wyQj7w zkrr4t1T{g_TER)-7sJXQYj)EA`ftDUo*(@tf3{-VrBQ*kp2KsxQIx7`2)T~ir>9EG zwtKWBr#PIgSlEb(Ka-vy-l7(h+O~u-SjIZuz{SPep*?2~iKtAD;S#vn#18r^Q4RnB z4ZqrG5a!@fX*py=J_5W>3q;vxBM{6#P0hl%L9)dq#WQY?#woY6!#LD;o1uqt--^n@ z?MKW8IGnCHuhBsX0i5{Y6q{N24DyeUu!WQmq$`H&BH)#l!ebSBX~g(dkh{W8FpGz2 zlvv0uFri&9t~&zE4$qc;bQ~-mE=YmjXBSXoBe16sQK~Xg#4|joA1ofsA@2pBTelpf zu^a(kh@j#w4o0vz<0qzT_1btBrvy+oC9~&zH?}iDdwV9?WJy|q?I(D|U263P9JU!` z+OQP;03r)+!`IniEW(20n39yuPAe#L1NaZF!~+2ouO&J6&REU(^ZJ0i6rYOn@%XCG zn(_qOu-QNEJ(M25oPYXq`S4g~zbtz7Sa3jgjvHiv+!0aOt$rfES*g3ZK7Rv~Kpd<0JFS&yas!Ej$m0n&T1^ zFF$`!UZDX|^W9)jT-178{a}gT$?n$@0$6QHUah{QD*@78-R!A*#3SVflJ=I6bVkIy zeeB{5IO)Oy7eO~C2$#mhk_g{V&o5nTjh*`E2qVrzdCZsofESYVa-Zq^lxzx<|OFPJC7mTmdyay!v4#LM!OSt{+b2 zWL@;n3Z3G?Wrpbuk3K9=jA$5zvQL49AJx)}_LBYk!snkVg9i)fj9eHz{D+6k{e5aM z*sE3}^~SbOX4=D$^DGp)c(%G79G0%=$}@>-%raMQQSB-dAUn5^IHA@7heH+Bew=5T zkx)W`M*%*gTOiZFXvQe|&qc4LEfD1i8U_YnTSU z4Tsl!4m-?P?P?EDpjI4X^`JM`wl$gVQW;@rz|f}yN%X^lL&+O-0Trza-9(U-YUver zCO?ISi4qnKROAAOvdH{7XrKL-eneP+;$I^&?PUJ#-+t#kKdGR~FBeHRkkQXa_F^kh z-O1H=HSL@M5~#>SQ|q%iVe z)42?y(ec_jLhwLyrU^ScsG7zv5dJy*G`&pAhsOS#6r)!~I~ly1Sc&c-(1 zUa(%*fCX{_?q?L7M z?8nWdqs?mrU!!>+T<+@RS&8E9hsvKA4L*eZVyTld)gEHeZ~-MFed7O&XD#eC zBcnEN_HUogI@g&*a{T4kpI%S=`T5MZV>=(=Y?!w(TYjklEl}!~ZI0JTBF(B4Z?K^3 zG?S1VYxKpqMalc=#H8aJ_0-dfDv559D2Fs;J-owR%PJ*}%XhE=2N|_MMj8ewF#KDj z=*E~(G3N)x1ac$FQ#UxH5;$~jPp|LJS~of3efrkJss&=gKg|+^5S#gud)}@G7{ZT; z**=Jjn>~3KqAaA&0j({hbBT#I+n-7t&IE_g+;skMYAyD0YW?NgS^UyJ|1j~@)W#bk zT!&Zhu{@;3HeD>vLcS+D#2WTXVJWDwKxCZ^WQ@xo}otFFE`x0fG=@{1z(Yil~_X&7!5>#3}k%dd-q5TIjbGa}5$jK=|8* z!*(Hc!6Pezb_GOY>j8(zQH}9+ zvG>vO9%%{cxC+}Pl(Q-ihakj5*!ztYtgn#!>9Nwbp-hWUJ#D4EU?w>yja}ihg5fOp z`3ItCa^tb0vboUGBj7pXH2+`$Pqe;T$P{N4v-Z%mL2HpB6DIs5x@78tdhOIOx*|^R zou_{D=V8&D3WTDB9T`6dwaCFC9TJjBHJPgFLo8%t>hmdM$-x<+R<(B)uk2V2vsFKC zGkE0ie`G)CZJ`Xp?Ps3m&_vjP=Tq0h?HhwrjGba@kFAQ7#_Cl{c^Bit4!w1Yz*!{+ zfZj`BytvL0$Y|Hs)6=5Lm>P*JDR&Qa#RxN+QU-U;-od5K=~ZfQTloC>;uE0e;jGn3 zoi}1!^l$p5!CmF?bF=?Y+1?Lr{D;+-G7Uqa>twtWeq>0;p(LUv84Zl~%V87tKV`{H#&SVQ0Q} z&Z=*yI5cyt?A5{K4{cMOhNsQUB=)?6P4AW+85R&7w@li6>@vV1jejOdL zS&*CTgIG^e4AM)nm1*5ouc$Wef}0#&K=30T_BWQ$=X_IN?#3-M3&r%mD zcdTZ^yukSMsJw>TDJX|#1N{*UIs))3?K{WXu^!s$DASBAe4LXQZ36N;7kA zEz%b{P_ogm2hq$QwX#|c?$%o+B!G9XWm5D1%+8H7{MnnpRkMj(%4+-P7?lU{JL{q9 z)=xkGKPvxEf4wW!#ayoMAFo;RV$>8WTZM9@b`_`E2Kk1@z#gy>1?FGh$;}M5Jl)%U zAT#VyG!J?wCs>x$ehZ}!7C`l_-)!IV7wDikW5E~E4P&9qlM|Tmn{OmOkAp+@2l4=R zfm=(pzq#WGlz{x`_EouRDEsmA%V?{!|8Ku{oFCdI=cwEY@stWL=*(=z*c=f>>C=ZZ zoJ1Xw?p^JsS*&37aCJtvNSC?vtZF!2vQ|G;4Z2O405ThQ- zXm2>$CwZMtUaM~}@1Jn`SQFS36Yl_pf~O@@IFXJZrpsU=WTJh>z#mjirr?d^z#e``K?t=D)YHd^`8u$l|m2OV2gQ{Hv8#jT{Z{>&0hO zZ-lGsiHR)u%PJ%@8A**r_H8}FHZX*^Rb%x*$Y|P=iP6 zs;Uhy3qghnLLZ<6l4Mf?;BvE@`zFEhkBmB3Ua;EWhOJ4UtHR--W&UycTe{%!tq(kE z-mShglGxyY2MIUSXAFE%EOx{P`1b4~YrEK+W`}f*r*sY&r~wEu78kq3M*tkAH&*;4 z7|tf-$>Cs9W({E(p?T(h_QUkI&u711jeX1!sg`0xV1k`uv|vBj2Z+eGFnbECopK5ZY!q2}T6oH#u`ib3J2!E>`= znGen?fj*(~+r-(gDIm9xH~(YLfBWe=4H;&OR72?r6Ac6&a~Qz`%u;SekwBm{Oea{l zH~T3B5*!0K4|7h9V$g(kV@N5Ae_a+=6KRZ(W-%F zdPrs_INOlDgG+&`DTSAf6{!ED9ina2c9@J3%`wlH`x7i6lngusg9E(`)r`{tbg#|_ ziEcZpnfLr+Yh(L2y1U|fao7DTSVNRt4S?Gqt=R)_Iy;Q^7^kRTp8e4N-6RK($9#Yv zzMS;#_DhXOp}DJd^cuhvVQ{F+dJY_8Sb-DYYJdxt{i#!c4={swdPxS4qQf~iHkkgA zZ!{b?+f=b6Jb+3P<_`>M^GoR<^~&;XTPrNOD1ni|c~b>)SOg&c1S> zWfsV?oCSnh=hs3cBeJM!y#u^WC&gJO3&3<;a2O01XrJq;acU-}XVCbN7uh*rV4`I@ zzbTpJmF`L>31-PUqlbC)spEy{-sJY~*j{?BaI#h5eOitjSEM+tZ$`)8Pk;Y*^4s;j zF_lV$fq&EkxRo6Jas1ERFJDi7^?K&piRI_x%g<*vUe0a5og;IA$=9OoTlNmc!}Ha6 zdVDJ}5=l)kkywqhHJFKM|90L>6XPjCrX!p0Hf~M71>Rmp^D=9AarsU$IrbOMWByqSL zw7=Oe;#g|w*)n_gDAUzIzm-$Ng3O|y%E_ejarwpA`Wu{H^Z-oA_e;-yocfki<;&%l z%D!ek_&?;V!xn*oVw%OcxTENvD%~Pyp+iD2Ict?|6CVi68LgcLEGLRfW;dH$93Cm3 zRt9n;YyIk(te>`HI)v0C#*A*h`)1;6h8PB`z3e3Cob&o-9Uc9~#xrq!A9%UB%e1*h za1xTO%8&EIx~F%$-x(apj&JmG^=lRfNw#L1SZKe>|L~Z4`jiu%XkQi~US{u~n+sxR zX0U2|C4#C{?j9aCBxS^crKDIrR{QAeG z7ux<8tFL@;00=l(X$gti2r!Atv=AB?X{iN zxIQ}K6Hs@CY;5|>V&ptfdR=|kb1CdywS5<{#1HP_9Y(t*wegM*H-?@Q?Znp1hYnwz zm(Lz}aaBf$p}kav2)ojiO^u>Tg|{!_lb#pw3*Lt)0v4LoyPbHY9z8sj3!nVmZ+QLq z=L|Gt)3h!1*1bhcwl(TN^5w%NO_a6%?#U*xQ2jzz4R!WsjiX+wxeibLTLwf`gckY3~_4y3o}t zc2WW9b(A|_>@F{Fa$)sf3u%v7VQQeiLKR*L+od_e9RFf?F)TxeS&(oTaNlOhM86#k zL8A7m5iDzPlSL1V%VTxXJ5Xlz2`r`wDuuT5XV^O3#)4ZrTqbr#Ds%%@LRs@;9NS1TAU$~)QLq+|u> z7qc4sf+0+O-hDWSuthQx2Pec_;rrNtpecgy3gyk}B|C+K%gb!*I@|6ZTsD$<@yD^z z+4Z-0DSCi*|IidR&Axv3T{=6t5n9}jaIkG!1G$IQwS&{;sB^?eVHxj8_m#@ea5LxZrVnaJ zo4fer;(O*2V~9iJT;`G!5p=}lgq_6%Z@SI5_`nOPX{M}v?~$)yeL`%(GJ?r~W?@gX zKOMI7!p9r?$c>WCg?n;5kh*x%Ig;}1K?v${S@xvF7xW!*VgU^U{s7-U&f9QBnfAffyUR5C7R_Yr`T9+$&v>f- zA+a^xo4Bkx!A8}qj1~3?^U_A*H;xJ<+#}W->^(e*6*#)PKfG*35muK0m?#hjb2E%; z<70@jTz$+(jL~#oXlbnbv!@%($xXGKQn=uiy(!#=K)w^C@!p$;s`}+sFQYFZ+Hmc&yaB!L-N2TRM-> z&)U@J37uAgc*OSkKU_=YuXgr~|18=%=4%44I0bF0ZN>~KDRooTkWi$Zp`b=Zg zE*&6F8tg)cAbgYj+58Ta@Q0>;_03c4M8M=!@ZCVYe63}! zuLA6ghs#i}NN=gGg*LviMI0T@>u{b1lAeFUMxe{C&3ZRDFmuvQ3?4F}bqS_SA#s&V zHAHn-wR9w62u95Kb2M)NApWExk5nc$H^f9wqK1i4GyT}+01kA-^wScb2&0DAlzcQnCZinxhLyc)@p|6B@J zO-fos%MU(OvWbVuUrhKZm_Va4uxdBD^imym(LPXcpsBQ4k-%+R-jrLH0m#g42{{e! zi>u9^psQR#cI58pc1(rjWIL;9+o(c#s@Fi*Lm0l478xSV+ai6Xh9CZ)s;r?MYW0b> zJPy%=IT5<)vklwibZJeiHnsi=Ag>zm;TN6)wI+8(^Y(oz*Lf2JsDuy3kTiQVxG(hY z&z`?_YQy%qoGk zNKcY?;jV8c$L2R*Pp!U~gpH`BT=E1FGFwPb+4BFE-I=S$>7WzmN%035v+p_oR-EV);ftjImB#dGX`Sx9{e^r(YOddF7?fZojvwF%Nost$r|h zC@cb3K`!m_5~3qZ&yC`G@v7w80`4q&q9K29k)kkQBlVOaVl6QW;F{n0V2A)L8$&h@ z7bjz5gu{M(zM#~^T755o&o}I6Qbs zzJrsf+uQ3>QrztS?)m(17^h)y&#Oli@Idb&32*OW*FS#BxHR^*8MII06ish_7WizK zg2`*fOj~#?iu@@&XIRzEaZU}N@4Fe*<*?YH$O(e&hch}{YepQwQCuKb@7~NVogo>@ zyYPj6IP^JY*T}>$tU7UZfEN(Fmz6he5A}yRg2A$N68m}-J~3OGF%ul}47Mk(j;EvG z$GHf8_2fCyk&aLYgI-cX`W*5G_NG%HUe$;1K?6L$$f7ok2%o+=IE!$}0PYi{z1WK( z@bGri4f?{s73|oKWy9j*rU;N{v#H4(`nR7+byq`B=1|diivgN6^Sn0`1?}+?G#=+4 z#qp@Rd3?6U9%m);Hn~52!j6()A%_{eM&JXV2eYuyq^}=*I&0q*7(X@#xHmcA7z+h; zDQJ5yHf`Q1t;D0qvxpG4CPxp7%l`HAnGNF(IE zd3pDR{r+pAuyjF~eaMkECQl40O zzMK45EH4SV1@c@C_PTc|&Dd|O4`m-DtMc0oITYNY=1230jR}&KO@YnGri77tM%gci zf*%$ua5fuT3MtogoxWq^%^Qx*I}**_KAZXe>#?u6jMAuTzj?Spr_~nx{PUi@z!5_{ zK@S+kOt}?&27rtC1@{2%P5-HJbz9v#i@4Jm+cM}IDhg-$QCMUw$Hx-n7+&pF4jbR8 zC)q)tZx%oUU|)lWj3F&jYSI$iE+n2#@6c7(q*E!rlR z&~%R$Dd3>EJD7E?w4c{@M!FCK6GM%W({y~)+!uf2eskS9OFI#WR(@Q3#+gT}Ps#XV z`T2{vA70IcKu;!k&la8md}e(nPeLF2Q|w1}3M(wZotu`zv*~HH>Y3CytI{vO-2d|H z;Gz>OI#m@-c%JJNvSo9Za#^cy;i#&p{1}5sfKXo=XK^-}YS{g06jgpc(w06tcEEnW z9|Tzt17M&VMt+UfUGdkw1AiC@lZuF7LsWq55ms|3g6FGsiktG@x%Mwc?axv#AqB|m)ivm zLaxLuxvudoSp7Q!pm5<@^AR#r_=a3oYz8s#Lv%Is_Mx%AkoVKIu&x?*L9l($lR+RO z;oa&M-@6$-3Na1aUbZd3!NrzVTp72k;+?*Ayn=W(8XFUpC~@13QNIa`SIxF|Xqc&8 zFwU3SX+w>J&*X@4UA-wIMI_I6d@E_=!F%m8TIys)pWBp~L3r~*s3e3!kkkR)@^Bj) z-z90+2JZ#Lo0~4|*GLj}xKctAql<8r^dyR`EmbMFaoVTvQ*`oG)Bm4e7qKn_3!S#2 zx{2fiLq=8^eB!HrUTP*CbLNz*hw>ek&eP?P9du+^K*$ur(i$B3lHmzrxyhZkWFiYEuh1DbHA*pnJ(Bo%T3M*K zx7-PaK3JddO?`roMwrKg#NKUYs0Ynq+&Ng;?4Tx+(n>bNN3uv79CuCW{Js>wX zvHQ`Ulxee7simzC_BCb@|L4>(y7k^xNKJ6KOQTcarF5zJC``b4vI5ZDUUe_%vl{Bp ze&goo@k{I1U%Q{b)H8?68`~d|HZgu_z_)S>W%3_9w`-ikeLHbAj@4IP?? zFg#}G3=Z}nz59IYw&<3q_DcAx+S()QGXfMv;{?bOtmBMW z6a<`^#z6Ln6zR2!)LA1=?_+akg7dJ$ zZ7gn}0r=j65pN$-jqBhEX7k7dEYQCx1mZqtmcAp?@@jf#P8= zfjwJX(fMnb1Eq&tcc>gh3~-61ty#zWas{8`x0e1o7=9+N2Kad4uYma)fWXiTPR{V3 zkmu`9$g!Ol_IgebCl2j=q_9jZ43(k;IkmlQZeji#QBA(;2`m>%BfrnxXn*H0LemPz zlko1keW-GpKyTBuho}BBp1?Nbs9+ZvA6<@*h+aMIB;}-N8nuqUHIU+TA^dz9N2s#qkPF#BnS432w8!w#(?>hz|NFbZaJ~?!SP_o&&0mQy`izfd*|n{!3Vpz z>~&Hdk|)payc=11KFSDU_X84siuzJ?#py<_%%d{@@EIZSa`1R@eOGMsDQ=XS9IJf$ z-22as$Di8$PYop<+XuVm?n{RoC-%6h!OFn;xc5}MdAjI41X%$A0^_@Mi8yHAf%5dz zr3Bce`1l0OVfO1cgxX7|Cw_}O6xXR zIz#pmLLR|T`R0*=S?=W9gW~;XR&0%5e!cqgQ$2mS$f8pu@#NA=<`}#CGshQ^&TSFa z$GSn8Md%{RR`$m?-pt3xbr-&;p;DBC1w^qEUbzVo&(kf0#@V{PXw&OmCi177MKDV< z&Ezw8wi!QM+(=H(?T)_Lcuk^Cs4j}a2XQ*uOsX39ZeR4v!sz*ndLM9x4WWD_8vU|# z;LhN*%Le?Rs=QNRQQfkcT059Ns;##NhmAp@ek~B;&||)yocg%>S{u(!oZy|km1tf1 zl#%GDR0}zb_)C!@$w~h1>6m|lDh+S%4A`oKsGVVoLC((N{5oB%t*46f>B78Fr?9rW zil)k|0N9E2Bwhs5^2IcM^5RJY%)%2OY;*K@!J6qRdU2`oO9sAoFM-uLbn{^70mX&V z({lx#gcA z%*)>rLHIPxfwRl);`!Ev!MPJEzxr=qV2)^BF?LS<>+ikt8?x{)fioC5Ar{98jxAqa zHB|Cjvno=YttLq$Ey`VvLMyOLKP6EV;s5kAbYV4-bWK=nqT11HvD|{aC8{oZc-Smu4^49mE|9cEO53P_p0dSsAzu zu=<5u*er1RHRy$rp&jn4lX^J0AmQ>)*AAD!)apOp9ahQe@iJ!*UYaDmRJU<_lEQ)} zTzTjATxfH8JSkok{bIj`{I&P+$lc9O0djl?3kk$G5!k$NuvDaWX%XFD;yLa=2K<3LbX#+mw~MeK4<) z$J44Sb5|%1N`#uPhxCV94(n7nk8Ma;+HyHAEE5%?MXfMaT7C6)>A7zI`^m4r<=8*@ zwck!e-m&+j<0B}D9W7Kl)Us7IzkEfGw1>EYf{v`7Yvx_?V5WKv%s;BqpQ#^Vt)rCDGX7BBSG##MlOfNMb^syVsl*S8=>B zr+#K;unrPcth4-a``Bw13CgaJ6XyBVaVvyHR@%q|wpB%Rf_|K)28+en^JIIq5cv=r6Ha}QpFTNTBvmlq8NOCfw$hV|KS{4i5ILD-o-i#? zk~qCfV`*+t-GZ~z)_-MAnkEZA&pmq{ptSMN=4@&Y-{MusjKA4W-W5@+eV-jv(*h> zcoohI^|0F5sdtjU`<`DF^?nZHR08bd9W%QD>eVG2Di;Xg#<#@#Z|4FTL#-DE3UIsT zCDMx#)B|CZXnlLVM;IPuk*!JdCr%BY-#5Gms6VNytKq;jXS6^BH^Bcz-hDSUmTqaH z|Jrx@PWL&dPMNf%!+URqBtXa^1V}pZj~29W#9+<`Yy$ z5E)q%DL{Mg^{(}-$KWS;pF6I+VJ=|r>GS*>;X-U%yr3Jy8SRto{Xvwdxh|!1%F&ap z#Ni15e5Vy#SKxr{KQEwSZ3WAb`)k+|;`cVbXG+GHkicd+{bJ@wE#!C^$QZDrleJ{k zi4cJp8!B;<2iK@ndb;M{J|BBVj6cLqEkB9snXD$G2keRaKg_(6LKWYgEwzgP@QzNW zt)7(6HYA>~5D$8#GbH?g9l?KBC0NNZa9yDSM@f6$2MT-5S9lDS;@4udX``gXe->+^y9&VHk6sy1ee)YG%-F^G@%qvuC-Z~`7;@zj} zufLst`|aY>Z|6%^k!q(I=LFoNS z;dDc36;J$r=CvQMus?rtx>Bw#4Me+z%Zr;2<{08gwrAE5qRR8ZpE*$M=(7S;X!{m? z)IF!cj~#O(BqgDlsr-R_b;&p#*k}&5ShwO&y8&|hb0|ah2sW@5PxjHlexKhpRo&K< z_7yrUCe5jfAgm5YQ#yvGzdvMa? z=vbgLTFs14VPnR75#22=ck8R!LrNB8hSl_8-0}-5BBLk1i3>UbZaTUu-+etLf(uG* zjN+3hAV~OT{kmWh#Pbx$2-x@LJeO!T{#=5XElF?W!^pzBf%&)7tAnZSnH@}<+G==1 zu7YLF)yKm1Q~BhkAk~*twqsW~z&3DbrN?RO5!gD4ZtX2?9>h+4_xsA}S-#Pdw3xhl zC_Q|t8cePqs|Tm~O?$Du#gK`%GsEG>gFt?0O-#LRe=&nQQJCR>xR~faIa&Pt71L}B zu+tmu5n8b<$4xl(=0xw>V2_+QnY_uNo>&jVTj6Mg5Ao8B-7(W1{Lj&xvd=7iYJ782 zpW_#S=j+Nk9R3jMYG>y$_xQ8c2??!_o7nrDGbx6Y{S>Je+FNyX{m6Caa1u)&hJ-Ih zCHy>jyK!9(4>*2is1G4m-B^vZ6IeQ+B!&;+|LPU9Ua-a-vPCrj6fzwU>MHF2juAOE zYNzXGoz)VJ7(P{~{M@1c6IA}7iL>eOhLC-};x==A3QsfvGmmECW_c{9@=6mY@ehVfLydR78 zpxQHr;DI>heg3=oKL{P2TTFH)q&)_22Xvi8WeqMt%N0 zf4TVb_2%2R%TJ$b<^`#^Uw*y#?YEmRpIg`PsOy3hkH=qIzy5yq?d!$Ir{?X$@eQB( zgUB!o>;Av=5C1$i@nUrGeOQUoJII2c57@r_1s?zmc1?BvT-VEypZ~G<&#&g+EJ{qy zjSpggVA06Q%8cSkVukT%uAi}j7oWO$DPQk#6P)|-6lh&{Zj5_ezTn+t3;}|9qRXA! zX9t9y#DCf@xwUlu zUwkKGY;d+~l+DrFU>3i(7LtjMI*iTWdU~2C1G#=W z&=PZ<+Bsa{G$=gC+2Nw_M3PN2zScjvHpm;QakW=rOOl(~*qOb)E!=-@o;}oQ2kfnc z#aQ+F)pibnGt1l|&ThBhld;_;93(tdd?7hy!>a>pTN9)%3MS+>nxBBA&83IAxajv# zH<@yLSnlCWo7$d;W`-u0-ti-il8u+?~^*F=NgP69HErSnrMhKUzF2ui%Xv=!h)*y5a z+6I#*{MjgNvN&pm&ogSk0XVzcXUKTUYDke7rb7r)J8bUG!@AZRZj+l-`QbbaI-K&9cN#qDjf$a#vgdl& zQl`1a7$4b&l+RgXxo zm!TlmxMc1TP$@tE@hM}sc^lzyL)w}hu0+bq!Wm;y2@Vz_g{kh<{@#^7_cm=l|7Ub- z1drv?(gbA+Kg`1H%;uD#n4uyMlY`3xUMtEwb{v+z+tLmcRwg;C{ZP98cyO)QKAoTw ztaidgI9*p-@$&JKlq(*0Jk>20gI7Gy_Kjf+GG}GuZ2h1fTu)&gL|A-unYPJTSxOBl zyz+CkYJ1iAdEmrCM^W*!sodnkO0O>Q{`1k@=i1|^C_r6c=9meB078%vLHafb<8^}d;THM};!?~VwfFWU2J@+G}0{y)j2 z4w?8u(Qq4pUhVhYLT)qnSQzf^LKYrzpW818b6hxn6o#O z^i+a1foKkw{`ohgSO<+{8|y&CMh(=tGMyfpSngTNjBVn@b>ddzfjH{aMqw}@(qeaHf z2wnc9VEIv{_S7?qY~kd_jgMc15PN*(3tx$>^KEf zY|w<>%+u<c2VDIc&A6}4J1Ord2Iv<&nHT}G~&zPpGcaw=D2h4fp3TAa3GZurn8e%V6-OhHqg# z>G0iqg-W!3n!J7r=_hXEXl?|)T|QpiF3)mB_V8L#*Kc-$T6t%3F7{z1I~Wk5>n!Ub zu&4KymI1>yZzE7y0={h@vx{G{t_p#?>fvRo&C=u~nd0;kd5={H+Xw0V`rGxd-_9G& zWOQv{Ioh`n?TxLCq|%d#wTZ>$fw{R4Gc)gIX5Y=uc27>d85sSgckq|7$*x#@w6M46 zh?$DMn_Yf4KKE*L>c#86KffFNaW?V+h(WR{Zts_opN#f>5)hWVe;WPyFMWS`HvFT? z4}MzCDRd zKTLdd*($#Y$h>xZL3$a>FpmxS9ih8$Srs28y~EM<;Kz;ahw4DXdoCL^WQE|n=IY7d zmQeMrrOFoh9R+5Qo_3eb@+OL=DF8VC`L{E<^FSVc8qNDduROilJ2v&Ycj)Jlv1bDl zKX;A%{A%Q9-1S&ytRSnDMs;@~8TJbTC>II$pgNYVA#Lr7jII#G!VeWDC+YopAuR7_ z-wXjsV!gqKd3T-?1krR-gv4t9@Jhc0go3}mJEPwSUV`~aLMs3EjOoNxxp{qfe05Mh z-z#72@p{^6S)B*TJh5B-)KR!Dy5jVq!{==0fR^5B(qnh5yf*#AZ0tkN7YPPA8^mmk#9-;&V!Q`>DUdqNhx0TS@bLC(MP_=K zI_FXK#1q)puT|EdH+;@_8mkwNh^9Ll72&^?X5e)r|CB;#=b?j^*M00Qg!zN>f9xm$4N02Ff=I}5L$2i-%bWqed9h0nyhFoI5;hF!oKXE14B zzQEUq5x{phCREo6Mem>f_&L0KeynN0jyD|k;!OZbn8MEQH;5e+QrsuoUf{oj)mNTe zW#lv7f7+#?_D|F+gPZajWK#!=i1gTB?oxYRh19;@{9ZmySQapJ1!w~;X|J-3j;m&b zHFoPhEcg!3GeTSJ^m$b7o+h~>Fks{w8fJl3()S9v00K(d0+m80FZk5`<_-zFzfMR{ zC>Lm@ejQ^%J%O0*XJBy;SMT^fZRa(UTz&SLIm8ayVMF8`ajS%LipyxI8&bxC06!r~ zyb{ZvFn-KKL1_nUgjf0VobOoza$WP7bGstot=}H~U0w&3+M1A-yH|@cuW-hhWKjeW?GrWPCnNFx`Sl^xeg0Y>zg_(L`&I2Cg|5>-_RGY=o5i(( z?8bC%b1ES|Y@vI4>h0uI*Yw=`h54ST$#;E2zr5-DVQTtaE;n5)FHS|fd&Ym!>i^T5 z|NGs*UwTG<=D?hSg9Tpqz^mz3zf8V(D>69Nqx0{S{hzz0Uww$YcbKxf^uhZ`(l_*@ z-&wFd%?2}FO|KoTNH*8Ju~%9a-W`~VQwoBTf=DYr?Fb@8R7xYWtU*3-Y(lzgO$JpSlE^b z%)RhP5I_XSUZ&x}C52s$6+`1@|Dc)KttWSz8Fd)x3uxaL?MC3A+!znJ>SDQQuk2Mh zhjag+4iR}-;oTn!swgr>Y08uz1kNQ6A*d-e)474=r2F_|M2JDzW^}N zH7AloxUcy69~Rzq&%UeKxS&|+=1@f1!8o0WCMh!BwQH_*0G|+jr9%VK>TLj? zx<|7=w{2<;A4WV^)-%g-*GYtT5tq1K0V%WJ4>3=n&NGMP#$#Vr!Q~6VTb^RxbPfW@ zj}4K2^-{Xu&8DS*t?mhF8_Zw8daz$k%cLM5b{ter6RnG|-qq^sE;POM^W>4}kk;DW z<9F%VAFtE@-@g%WOoz4BJ=p04DV(#<=pDHCE`b__8#&>PV_gcp9b( z=DgLvquf{WU*)FgX@$Zm^_qw9TsvMnNy;{`uCfijJwC~vvj(*5i(N|F872${1T`Pu zOyj3rSJe#&JfqCHS0A^*!p)m(Fb)dNy$Lg;y5U6_F~N>tdT?ENg?p_qLGYT;gt!w> zKsQcSl9g3EJ1hRZlVt}w52)6iw8x#3%Pl>F1IrUll3-Kjj)fH%jajmk&WhSC#6NA@ zY<5hfm;dZYdXrWIbTnAg!K?XvGqo2PNeoX~!mbR=t`7~&zwz{OGZyiD*jS%j?)@wU;?)hofc68AQ}`MV)JD8u#o`r{5y!rLd7bU6HbhLmvZVw&9utU)p? z)o<|pVI!I5WsccShA*xZ^7N2(uwTK0a8zF_9W9Q{zkWOLL(kZ=)vZZSG339|PUng< z3$eaTZn|>3THIe;rfiz-8W?{*GSihWEY{C5C6#~fb^qv3?}z^S^M`-@F#6+Yya%G9 zTb@M!NDmFK_5;Bn7}JUV@#Xh}%kLcr5C#X@g>mrHWjX0!ebjuk-s+6o0|=n?(i6JAc273EQcD;RA@d)JgkTkE z-*mx)CFU&FKj3#cnu+)*#YCm>Tjt*5py7$V+ujkj$c0Hn3sNP%>z$ha?To3>;rR|c zLBf;_aV|7J>hNb?zg>7Y6z`j2Q1<8>C_yx^cHxpFA9q7xKa%=~#9X7Mn0nkgZV{DMGO20%O30O_jvwjRy!dtq0R_v18Bhf4;pJAeJ6s|n9rc& z9@;h3FjdSf;(Z}MfzPSw4L%aqou?}QAHV0PrK(@4(z0n#=|bm^NTd7G$umq-m$2rf zMTbIqvO=Z=lVRPzQH|rV!@c1Tc|MRWR_eh(L#0K{ZHv5=bkmvL@PBRvhT7-eW%;q7 zP9f?8{EhRwt(8F7I}Wbe*!Z*?iociQ^9B_4#qPkyoeT&nq)lSp2G=J3U3b8`l$wdp zBdg9OW~+;07vz3*Im>|yp@zHb5V{;I9x<`e$p0mdpbZvq$;-#$!=|*tdq$tV9r$bi)Qj2Zhg5#dx`7}U$QlE}@T&hWfA0E+AKw1gx1&E#CHhT0#6Ghk$kQXL1Yr^H7Z?Tqfk_@~46e(;%|pt$ zJ|ATLl8e{W%kkAd)Df+kTEQ!cQL3aBZbFNh(T=x2GlvC<3|Egvp0MiW7U%?&zowVq zyUV*H*np$e={+R0(v@)2y4k`WXQR!Dg??Of;wcVu8AqnQ1iV%+%Be1+|t%6_DAmR($Q%)Uyj5#CdQ*ZZzf+l>m%tQF~Jjl^^fG`P$`8N;(E$(4qu)g#j9%ep_t)NfbTLiEz|f z>v&OO?j_jG(bdX~a|AfV_YUVz>QZJvtf8gZYHiaXuonFIt@isrpZ)f?i^p#c%2xaF zIM>Y26&D7=u}OADo?z?9n@m(`oAEQvMWYHYaNIzVuoGe|*JNt6-&p>fqi7xY2j z6A#4=u-67=s?t}`_yF}#`dj*gf1L>6ToFzoZu@9eB8)WEkd$4TT`taW@a806ZmiHF zYxvFkMlc`BK37i-Ozkw?1{9xhV@n^0lHU2~=)4%RvEs)uKqG|kmycsQd}o%}1(Z(4 zr_@7kHkF^x!`I*be@1ivyHWlF{S77)BoY?_AOe*xaH1}PPAaeqQ!mObX=O7HN(+tI~0 z>@hbh(W{Tg_g|^Ns=1v>T=d<_%9pPvmk&ou_fI{4dOh-!k$>$_oPo{bn84hh`~LF7 zhktr8^3z0S>@a}g`-bto%ej$+2q2h|2Yo9GToIo_Z(;TX;EK2Nzph^4Jc$Iy((e4-dTpSjN@F zKD~TuVcn3n`POfyU;U}?&(Fu7^BjXsu_FVg#P;Ty8kO1OwDWkF^o$}@@&}R3p`Ike zTA2|v*y^aAKN0d(UkCd~vg24iOy>rK7f@vEMQvk2^!2-%bS*g!!L^um&-OGVWbq~ z+5lJxXWhCdtmoX@Iu-|FU}c~2a1dP`JWPU*761hFtt2iifyM|@U08&LPswk!-YRamBWM22xOcqEd;11Ijrr`(nC$ndM-M9TNDUxge4El z(OSn9Ic@}F&zl-es;$GV?yor~i zeL+mwpWc~Q_N5w893Y8t+_KU&J3xBX1W=E~u6FSdak`!^$VCDvw@9>7r79nIBL>-T z(w`@7s7|)Vz^A@OW!#Xrj<3R~xfAsQ1-iQ4a8m_~ehuek=D+>Rw?9yg|3CjKdgJQZ z+CeK2V~k5W_7L|Bi))1o=O73@_HA%Ca*cvWM}2Otu{fjH1-2`vs(t5@Wje5JsP^Q# zkXQ%0hMbYU2i#UJAVMLQJ>h^r^>?G{w7DmVOFPH(d)PGSWdjqyPBym9#k%cdk(D{;}uv06Zk|)g!t=MMvp@0gw;i{*&F^XBQBv_4MRk2o9@aK1Cog*|^ONBX4w>(6R zH!2RyEm*s-CpuZNDOlWH!o}UKF5~mS#to`QNcWG^!TLa5#qA-#+N`IDr{ppa>!ImUBH`%n1r)42)Yj7qC5=d^Zyzu&$O)(YqMd(LIZ zVX3D1$yLwuVtk8r@hB7-yO1K;wTYuBijm75=fg=&TwU*+GBVc==$b`S^~U@9EOR%; zBW0Gh`k(!ahuYagt!jIFR`3SyoNoK6yn8^WkszU6l=f@L>RWuzdSWQrJwSt;8r;~K z;AOfesO2ni_h`;#gpQ06+Is_X=;u3I5}bC-_Kx#d$|n_nqy=kqzFRVX**+LH{TcpV zkFx`Hh@8+`US*iUvQ9OFP`7RSshXQ4V{=7m*K^Q&Cl~8Z$)wP{UQ4CTiE=e|a1uWP zvzHf)5A0Ajo6=rN!PWc#1P8O-DxYrd1W$?J$>|XPzLEZ$DvO(Q0rN0l?Pav}4So2Z{*v=i?&6B2%z@h*XR!AF3Cu*Ivb`ZBqV+cE^JiJhZ( zrLlh0NFFy+#JOmG4GYlZ9GSrBal@$acP*9v3#papwxH6l_5tmsBQfPky{McU<=ycMMek zJFI=>st`z4AAdck`q8FZmU&|Xev%nMw3uG$4FK%n!g6rJBqn9ENL#CwFD(K;1QOR|nxD6YzX%2Wi%FZe#bd6HP!UuHy1 zWKDJ**%JYi-90PV$ZVX*Bp4$@Gkm=m{OezP|McA3kQ|Dam)roFRmWECs`Tx*)33j_ zKYy)%{<3fX?si(=TiRz{S5KgO@p;g(>(#aHwyv{<`3WG{<^(+;^HQv#le?;DEn07T z4f?F^NH^_m)@j&qBJVzaZa#i?5T9N?9EX^gW>%~0A0pvTLya<&XpST}5~3%2@P>J9c7$`-yjgyzNcic1Rd-$QYLGawxfy) zoXNd*Aj#{xp+K%0;6+OxxK+ZA+gx@*H>u-GE~zBF7O$%~8%}%&%AQ!vVAmf6n^h_d zZp^xlOJ63y#kmmEXIiiZvnC`@>Hs8d@+; zn1cLR5&9MCNFb-1yd^D+5R(FQaAA9ugZ;8-@IisALFIYwAaLsY45-lI+@bSDIGGOUO$Yx;p3eO!}BdtUKC+K{K%)P%O*G4%@j+|EZz`qUH=Ymsf8r_KX`HO4PzUi~=w47(#;L!L>F zXUBS0^|WJ)XHE_h=s zvo~jq`c4bcIbgKf=D_pS0R#ir+BVQR4#ZHw+>nM)1wTB&&#o2n7IuuzU9ErKOYXCt8W-a@-;VA{>#_nZ(ok8 zdg+tpMA?Vp^o{6!omiJJDiv{FRt4c^vQgcLfI+cUIOe_ZjQAO$>Y=q`ei;3 zg;`@D0LrF2SBFOC>Q}q1n>}IZyKR23AWD%yDL?JF?10U2b4na*)Pk7NS$cT2G{edR z=82ss^R6>@<8*~4KN!?>CUKrx`hN$3Y7bluWuJ)+!Fvdzih*izHd>%;FsYgS#C@`~ zaX@THXB`w4d}0k&n{_5RX^$J*TdCO_l_ezSN*#57VR~Z>q%*+8HQpzvR3wBY5vZRR z5HatJsO$aZ@|n2y9Z(nn7fm#Y!D4xlq-#KM&-x&tG=JxtJ~hDR8vett{3Rj}3< zD5f*~aIB!B%VzO%EuNh3`C5WE<8}?}ZyorsA|0M2 zUmDcjV%%tX5Rutxy5DaC?X?H?ylG5+s6HC!Ni)$h5ZMk_C4dCo76_VAn>TI0wsL;G z|DUAnI}FrJ7HY;W7bU#&g3Q{9&}-2>*?wNG-+c_tl`dV`N4DH!XMMAQj%9bV+fwJX zWE5C-I>7_B)5!}}PkAJ(0Mfnkk%5>tVl;|GC?FGeAJ3v-t3fY|{p)3*yBT07sx zfPb(Q=v(YV+ZORFuDaGQ;>OE-2)(hi?BD%-_@STX4y;G;-U6w zYk@^x7VDD1BjYT5IY#dF`xAtGbP<9{}kC6<&EiihySpzpHB@fWyYxT z-;e*&&rD|g<@ns2iOBnz^?t@d7=uIeZ(j}m_1(xX6VdMVooO~i-eF`#CXlUb!N)u8 z>(b@@-q}6F_A=GL)yK;D-7f201;M+`+n;ZMlY+kiw1;(D`80!Kfm|9Finr1?z1Ht{ z`Tg&g|NQq$(9p%l!{7e-=KkwBT$dD!cYTbP^_hOPcJXxj>9?yVA75L?7kTRvo-Mm) zWfhMD&yVB3{4nst>*<%1xzW+=*vq-DzYP2pOR#tG9lwi#^f1U`G&4GtpHS%F7|`;= zvrQ+d1QXpd96qosH^asRH@;L^gjsFv%^tVd3kMr0b{scP9Hhlf+V+O~O_OG46#T$D zS6of#bZ&O>>-d2I<-6yQFF?UYwPZu;FJTFL$bNIx$^gN}fM)yLz4)$YuB&_cHQSBp zST`d|`+7c~Tg`lDA5p>{1lQzAFj}jdJJX~3+qpMOD}7;U9_{9gO^&qJ$`xBVh<*P` zf3Qv9PMjP@q!(PWdnQFa)O;W4_cJ@Q%p4GzXc>?M;D@?;kt<8taq}aDoEx*OTLEp_ zek<2_X+aV+p{s-7%zU)>ng=Qjlp%n>%I|Wo9xUq^ZIR-8%bKxrGktTv-MYwN za<-um$ULHW)hj4sCF%nU2ih{}0>IQR}!M ziX9^ziYmJR!5KkX)ge(Q6%)j*Ks)TjsZ&wu`4wW+~GPIc%Y>}9tc(d3%Ib3C;h(L zEOm&W9X@bQ<;NAV7!~MABS0Ns^Q#hevWHNk>28kBA8Y3yn=lQJWCneuyZ~5;6lb`; zu$t*zc*8Y|RePBK_B6S_z1?=22`FvpJ6@k3B zMd}LwpWQR#Qnq%PYg}xeUhiDomD~4w`gxAcmme#k?00e~wgh99W|^dQxbCQ}8{9bA zIFpiXR+g4FMi*E53%g6VPe;H1?dtD;zx({9c6+z~`|mfr-PlTutPQ@Ke=}29w9dZz z)aKap+dtp_?eF)eXW7;DA-gbOln;xHSEIlD$x?Xgr3^}RK`C0VCSLHme>oZQSk)6D z#gjWTqXH{;$lnt1leMFG(LU&yV3zbrR>Fepn^~F2m{CUy#lv}W)PuS`Q7{P!N7Zp$ z_sIQ>10HOR^?`-~@lRzi!72MDcWAKg!jld#3=Lr|nXPXq;G(iX4J>)*3ru6CW_D)g zcV@s%Q`ym;Xg71_4=C<)Z-$n;*&X261L0v5Ww~irVS^c<{>(_46DjXL#-5Q66U!ea z<9!3s-tqMzEE*mHU=Zr0eiHA<2UZEZ@20b3=AGnmQo&dGk8h8mJ(=3ZaCY=9`_sfA zKBqVvDZL0(yqyW+3P&8V;%C=;PhZY&pPEt#H@9bU#d-Qi9{#|Vu#adL#UD5L?@xe6+KIEl*17sZm-!z+rNBbFloZ0qly9d8tT(x3R2 zhEvXLQl#x2#Izk&m+GxJ#G8@6(Tu{81D>Qg*#UlXjk>oE3g;{kGa(ThVVjcC@vXbtAGH2Ht7uN=g5LhF^QhV=4(%a|vDG_4GKL_>1UOkLa{Ay1A zlU070iOqYXvu}ml$h(P5;JNioVh0#N^hbD9v znEbkh^vU%m-uulHvaxQ#l87}#&P#a;`0p`z$An=;d-+hrkf-%7pKfWoS`Q)9+@_C8 zMLfDjiwXJO`7SIPQGqdXPL!UGNP<;LPc&kAR_CqbP^(KX@psQUTn#@-#LlH!1;>j_vXu$$bJaqg~wvI5~Ze#0q$OP>R|!Z zueZ^J_y#DO+%i#RO6{~bcy`3hA2Ru=jQnns!Y8SgR=i9DYR$MvqW)vqik6jsWf7`EK7OfH5b~$ z%QSDjB48_P6670T(5tZ6y2i`0uM8+hhc=w5w((e242+3`D3pe{Rd+{rt;E3};8&n^ zhHO*PPde+Eq2$n52SV4J2k5DU_Mh0OXVHETY4*-bFofVyztSJ2k0{M`YTzuO#%jC6 z8k*JT0~s(_crSB%P8Q__FMBEI7WaE%E0v$V&voB#Qwy%~ZW2<{8||jv(xbwZgibnf z$~74K;r++j!&CF~*H-Opb9tpd5_vBKlVV~dIqWsTVlZtI1ANZR!E)_vD}S&&yV5&C z9+DY4Zp1Eb7zii9JNoBfT6N9YIPiIZHco%M7xrin5-pjx6e@-FG@TC;gV5U-rr}7* z!c6&SUe0fmn6aH)ksVW7fS7IZxZtW?oV5V5g12|ZZncH4U2hQ=GXpg6T26WAIT5*( z8zk^D%kmSEt*O#cq;)RbmgQG$b!RHMJ(iZpoS8VNt}6JPI#dY=6~M}W@>B$w>ZznJ zfq6an`g~71PZ*1PIyKB*-D4;?CxUe2?kH~0b1X8B$UjqdI>r;EVoU&)U#kyaa;1De zR|`Gf;YIQCwsdxxL)}5(Z{OrV*8C-`dV^^L1_+Ih5pOS|cjc_2JEetjO^;id|Nh_q zQy~1`JO5Kzx2@o@Bfk%ony)5?ExI+d{vo9rTrGk%>aoe#QN_6z9rV0MT-f4mUC zh=-Xo@zLg2BDx5$Go0L8>L%3E>*qV8!K%4HWAER31rC{*1-TG#CwO1 z`mePxUmyitf853n@<8p>lUE;$pS~eTP>$mQVY4L3_hkMO{u=>6-`1(P8TkLaIyb6o zjXUuo5^0;FH?-npAQq{XiL_VhIffpXVldVn^pC;1bAEe&e!VaS#p1NgaRRBET-S0_ z@CuA#abt9PePA$1@_Ufjt-5GY*<1B5cTeWV(33bDf0E2%+qc*cN=Ty7MY_ZZbGIBE-TH;B4$GAU>Wi=r)FL)4 zqd$$zyj;(YW;Z66;{!AEZ^stiNQ!jg?2%3%FD(G52Y>GB{!`cBUrI-@>-$orwz3rK zj<5BW4(AH{bLq_yDO#(WBaAo6V^QU+Hw@wJE?dAT9G;=npedkhuIt0X8?s;8t^sl0 zTfsa*11Y_Wz?o%%r!Q=6zKbtT6U(~`OXVe?5L)k%3Ra85`B?$03>{;TPS4*-X56jB z_UbG8Sj-|uj&{)V-YB{xMxD7aK5USjSaH^w&SX+Lr^>+c3-xHLncHmS&?$>&MLTN? zec*g-Zvhc&WVP2nMBuj zYxNsSOcc=l1Q#M(cblx<1l3`fPaW z#f-SarS~5??skOFLu&)w3vbc*2UpF8Lz9c|ho)Z)&b&(IN133uujE<7_Jg?!@YAkS zh3s@nj0!!Zp9cJS?6 z;iMIdZI12i&2AKCw-~1cUu8Q!sUN{aJ*;{|+^rBUNM;tc<2bng$akGS(S=SPncv&N zVi9_ftwcIB9-6fUPPuGyBl|I1b;uaqguF1k3j{C(JZCg|Kzu)HSLJTxg>`S8dI371F%jRHuZXtCZVIDZ+*@P$P>g{vg&mIKjR5**W@9P}| zg^sY=Mi}z17Hw#ugZ?)dsQMNPdl1~UQhIoo*xL8(F2pNi_#lf~2cX<*{*mXoz6joB zVjx&n<=26s@;1E(-2&c+Ojs^H|x%Ibpj}?e@=kBuXK4OHGe1y`zm9 zT>dc9@zjOw&*ybe)69NnwkA*`&Yqh3{L4?LXCE7>YPgb1HF+1&E>zj2RHYdgFe~F2 zV@qiI({6d#8<*nCBUbA5D_-16J&t!Q=HRPH1x*~c1Z&0*I)YFVo8ZKL79!di`f=uW zSHSZ<^{T;Id2W4I*l|bz@qPxLZAkM8gdEFXm5sSXT0QS z0r#@H4$Q|pB1%eC;yh~H8|We$n+5t@7G%2%xJg@$Oi1dyIy}_v-&Xf7%CUoJmZ|LC zlGvT;wZVy{cOy&hZHwR`0Edv1c)f9YU&bZACNyiM=hCAu#-BeQ{_)-93ulB)MDKDp ztNgLd2tcVj^1eIrVRWs3LRPN~+}W;pcKE0kf%D&pt6(qx@9qRyLL@goB;TK^D91(3 zSb+Yl!aL7JD6_XxWz(&*)xE>T%E{tMZK>6conNls-V;2o>hKLg;4<203~I~MgY(?= z(|+T2&n$WNaO94XTq6?B+MU%KKrD%QTbhl-(>BIYZ4LDgpLg$`is%r@{Mi1{Z2N2_ zm7kS5%6HdK=x69SE28me@>wWT*@T=9)gGejDTXr~wEYRZ$^v@>OTQ!SH(&TW|3=!e z?`0ncScm3V50U{nP_)&eP7jtWky$U2!nN*e#_F1x@o4_S%KNr}j*Eho`(T;HKg9z0sV&})t)qnlB|M3Sd`2Xuy(;ImI>9+johC!_n(FJ-n zpy{20@n*XyBqvg0L{_YsH-vm4Ohpwy(s((1U)$ns7cE@LweEWYcXeKngL(mplrE9(afobAbb`(|FvC;H;~aY)YI zaRkl42+USA#D7=UaoR@`1G=fWCRR_CpG2Gp$I4@~a#`lhZkQ5^q38ukb#28BJQ@8k zJl_SXBJyPmFZii_UvWR%Blpd`#0U+}rN*(X*?eJsx5kbmxkvw4nn~`=ZC1$iHV;Lu z?MGtNSkZ1HhNnp|AFoIu;3ltRX+~7X(A4wsnPlIy}uB)KK?VRt{sqb9FE5 z_(;wV6upix+@xs*spj*d!QN5%g}B{6;fr=pKKD){ z>Nu<$w>!qPv{<+DY_d2_?q~HQLB^H@fc9qUCHB7E!Nh96XdYae3A=LQxa1H@-f@KOjPU9 zgUaIB#mf1`>f^`s)8|b@P{VTl0=>=qlQrP5{YNXc!!lY{&VxUaiD2_+aT!chm|ZW; zuhDMo&bsKRToxZ43TP^J5Vuuvaq-i(iem>SOEeedT2$T8)ms|E>uQ(5w85FB|F4_} zvA+(``rcnqZZw&;z0iK!eAvFh-v?Sa(pn&PR^k>Pq+~)~Je!&XPAqOGcmkc8hSO63 zzGo32TR-0>gvfg;eP{%iApAi7gSoBl+w|V~C zb#g)UgPpDZ{`b%St33XJ|Dq2sO4nrthISb0a0ZiKVP}+K>7(gFM|88nRl*2{-ew!y z3VG}FU*RJdZAokln>d^w1pej~?9pOW#%|ab1Rhrid6(@AAd;xEY#fmG8z3ehr)%4~Y804g@lS9nc$Wk0a5WZ)nN%6Xxy z4}PJASzVbxx@2Vq0&kJJeYnJ_2_;hCGQCD_Pp;Pcu@xQlwB}H~HMhd>F-SX$3}(?$ z%_IuP^PwMq>HYJ|(Vr=6!1X-VoICXL#rm4uRqZ{5hvj4a9^?IB|9lfiaW*qN66qd< zr9`_~YqDQcg$QiljQsfL5C8aj_@~v(aOrqymsDA%u-NW)7`1S0+}Ju~PyN{XAYaGD z%?Ta_P8Q$d?6I-eNrBW=W~3GDFOoULa=isYeiF8~`>`bwJXq~0lvlyu|yaiGtUW!so4H5Et{&0W&Q zVf+_ z3wl`$Z)PlL`rYS2L{^+N|ALNr7q0*icBVmmpa8EQ-8X}*?Hr2r4zPWW^+*bnT%hHk)%q~^hJPCV zr%`AsF*vh<+c{yJlpUWnzeqEe+E9&n%5+w!`?@BDPOD77f_f3y7q=*@UnvJ)ai zy~zPDF#L6HHY1I5XF=*h`Q^&eiXAZ6X~wUv*Y6*)U%rS%$XNv6J#03vc`u}5<;9te ziJ`=ROm_3HlIti0gwAJ2I2VbzSuf45ZqFc{!rN(ngLO$WXCvUPL--$9e6x}n)P97Z zQjrab0byZw^e&1kVn_d7dmG5rER^p#GqY*ps}=;9DnG|gg8=C+Qd9ac${%buFw8me zhI}rEyT!c?h=q$&n`#fnh28^a4#dI+yp#z*`r$LgX|LU0-PxHamZpo6vMcdAu^Xn} zNxVVhPt<>r7R6Gk#*ht$(_?r|)<)OYxtrVkO(0NhkUW=a>z}?{|AA}%_g~HRwX&i3 zc3%PF)ONt2#u5kKmhHQBKQDdwKhL!A@jyfq9xi)TV5~I_a|RX)S0D4NI1OhWjKr8Z4kms8$s|+6+8R67{WINK*4}Wl zk%iAxuW|-My+pYlZC|ZlKgcE7#NMX|B>%S~;P$DqVQI>_&YonEHUSZw1V8~=0{+QW zwjO{itK{=Uxw@P`3>pD@Tg39&PNEYsvu|zS6%4`T*;8*|x1L+hjmi^5uI3{L_%#*k zqB%<(UmxNK=>Sy&p{^KZfxeO+9*Tb8l&Fy#Snab@TP{pVjDp<08h_S1*CoEZfJ;nY z))2Y1T=tYzVid~Dg&AP7DxsaGfs&4jGJ&hSf3eGafKW&?M&3(AV+tm$>ul9Sr81hW z>@GHCh5vr>_4fY4I_%DIbzLiuR18v zV70$@wJ-2CVFHa1gR z7kFPH2-758a2svBU4YU$hsv@SQRjK`Kzk;$WBi02yLds-y(I+@&F1@#z-P*5j6ls| z@c*P=isV-K+O3t%ry33b`G&w>?XHAP=4tGV*f@ykw65jbfDOyJTFc5me-x+ddmb^y zk?iBt2-_SEczy{Y)lM-0U$C3QO!eKLx#tF~S;fJ_D3H2Yv+hw7h zw}Hu@afWj02>t4Hni9bhlNY^CTUgZ@xZ~PVAlJ49u18O5kz#4K(TqDpK7HP#tbBah zxVqWM9x=KRjRsNGS}=q(i&@U^&iQgcHBk*v5s=}xu%0mE-0WMXD++W=IVfS>$-T=&7mP;x(F1b%$n{P?taaTBCx9`k5ns(M!V z!(%?db!~rvyS{1+fgzUE=R-5vH{p8eoy4Gg{8CgRs2Ww;+mBl$82FRmh^p!`JE;MC{d*GINzOk8rXQ z-8e~@(@}9mOH)P3H;3)BCC}{7Cow~s2HSNcpNG!^$_wU1Vn)Xe1@2;P9fv9OtSc3?FybKu<0hnWNa z`D0ZBOm*n!;aqxnQ8>Oq>Ij@bYeB?CfrCN-?dA|2-_CWhK^R!+^D!Ln8DHsB9I!Qn zUo31-ZZ&eOeQnLqdqiW~Dd%@*LnF-?!5Xn3`D+*<{MFP;Pmslv*QYq(b_%KlrV=E#_Z8CM=H1cLujdTkyx}{*90%yZ(n4~Odp}Ph=sJ4&>BBKs2S*z& z6Tppm^~V0f*m3}qr`87nwy5K0kB61Z?P6euChE5Zt7J#mUkyH!FM3z|-!8p>F~O_( zJ*m$6af+rD80C!;RY1k*JwXudBtTWzUu@JebX%sXY{K8&xuw~7pc_CX%a+5$B zccyS!P-N!Fsltw6z0}Ulre8!c zp~l6`H{;+Is{Gg{L7TieVXWA!g;(1@+hRSJss`yXbQ<3hmmb4eSaZHkdzdpR5H2m* zU~)7iI_RXClmm~>D4oTJgoOqz!%Z;4wHtK`DEs)Xq&JOgQ*Bs#QKz=Av)2##hfn3Z zkHxBW$h3D!Rz9zS)ojCYBn804J zqU<-rdDpz$c>J{g>2tB(TFGopCGwMSu=D%v^M}IevY(>z2AU)6Qh~?=+4R~tN-#xx9%a8D%tIxt!c$WE|P7U_}%uJi=ubpfyO7Z zX3g3T*n7ttA=A7?s?4`-QCD= zpg5zE9*Fn8o_K+2Kd~{!5S#nNzQqLh^4*r`F}iYWp_uMQ=Vc z`7BnNYx~kGj$>N=Zm}s-FXq$3$W*nPL;LQ=-H`?HMp()(8Z_w6NWF|9UY}>R6=aAB zyMU!c(fO>T9j3YBsk1X> z#2ttn$I@M~#A&W)!4%BogJ+$@VSsRXNwtpz_z1!iVav(M$H+jNK!6+wjV(N6>!<8u zX_lATv(cYNquqYy=IxfblKldT7vBam&hBZ3wNbvFK`{FsP{JqNiErazj7?S($fec? zZbJUgxO29UydwlJq7x<nSXvZH}V`Z5>7SnwDY~A|a`eM@`4fR+NuhYom9su(VKAoP`dwgbbXsWI9}W>PwgMfpz&8~v9qh>m#_IxU-FMnn;l`u z8sr4-kv@v&o9Ufews@E{bZ6m6pp^>qw56pTM&+YSYeiwsLHUv)Vx|upHaUd2+a2p2 z#`4XNJJx~5`ZqZ&4iPecI|y_m#2#pm6`@TZ655K4r_)s;9aIeuFF;s_?=xmv+)t4=Q(mxFy}c2mbRoKtGB>#W6lX?};xZ#@_)ddM zcG@{oti(SP0TV+}-Mhi2a~ONZNl+KQ-~rwIJPJ;@1+HedzZIT3(7fnDR?b8$-sH>tRE8Ki|W zM6Of8qb!&&+3C|;1vJJE7l(??CIeoT$IJ3y)EZfwS zAr)N{Qi)jBNPjyH+7nR%8vfGlQ_-r3wZeD*F8shIxohyo(FPyGVPr|D_LP9PkF2_~ z!D6l>`dcc@m}*(jBoCvdwy}a~$u4Lp81J|^R5kdQ+7lN>@35y`|2zzR>xp+1mO z>ZOq$1@>V|Z0mOR7S%Mt))v9WXBsi36-%vgTRFQstX*suYikO!AsP_9peH;@K;thT z4-09Q+J|705X>B|v)1(Ug}KGev3Pc9t2nW< zJ9FHKI$OnFfBClax4-Wqx*Q#^e)@u7yu+)Fe3;jq*kZEl?K%xLie17!S7QSOVhKx3 z0e5ow77BA?xrs?ZGn*5`=@GhSOPoR_qLX*C!HUgR8`ZFDhVTPeoXhZGQ-Jy(6aiH> z5px7A2)ZFf7if&jO^kr^)_F*^`nobNr9*I8H3qEU6^AKr+IH|xWVpm;|Dd*V(p=*U zp{n>A7EIi&gC03IjvAq`NOg(sM+iN*g`;kIc0zCrbJ1qJd5(4^hh?RHx+aGg=kw-f zfr*dRm&eONdm2%%2Qi`8Omh50`+{_MjkNGfs4rykT7M7 z^Ae|`Qu)OKV&m(k!bs8y=iP6{<;VyyT`5k7F`pG~Q~EZ#eq9-Nw_8`~UBN_XY?ex9 zOvGk3Wvo|lJbD*{|2%~+s1pT_$&c#&Zt*=^EkF6~=Q<~I%x5#cAllK5al8>>*;5aT9`LPm;}psyV;n5whbvr+yqOQ* z+Mj+sd;C%@9xYBTf8dPG#X=3F_T4zqHKmWwc+Zkx7f!DWd#4-rM~p=5vX^!jf(nf9 zUtSv^5^+3G#cH?>nRRnVc6>24lH8dFj^SOvLL1jx8^^BlGTQ;g%9~@!rSjv++&xNL z*udUlLEzvE{xcnVXNT33)J-jJ&+y@YHrw@K{%s;V#+Mkv58?UpYw_!^`;T8rLDSmC z*51@9)ozbH2*zaQ8(J5ZcgT3i4^BpJ{%CrPRVMKN^0Bbp1pX{eZcSp&z+Qlp=F_3I zL9{3jvf7Sj>B9k`nRggDi5yHIIJMnpJg@>dRA9RUyC;v2EwGKD&d|5ho@w>Bt07|b zpvi_k2T#_$JFkNw&)(8}(R;XL=Zj%zf5h18JHtmk2?mVEiB7P>2!(Z21{~QJLoJd+ z6prF-U~TQ#<1bwb0t+XpBV(<4%GfQ*)l;XczJbnaN}t73BXRrr;$Q!`X}sw40C{$r zo}J1iEHS_`GVky+(XObaY+*cxke^_@b}rUrLrHhBv|Bc|upK;q`KMeU%$qJQTQDI1#>0eJp+o-E)T5H2)xW@0@&7O*gw6s>5#8Dl5QuFaY$ z&__cUC@R-7LP!;ACr11>ST|Vd9x|&r1CXNI?egR~|JVTU3(|`0N&?k*Xk{Ud;nw2F zsucwF?=(^%{gc~6gc&sZ!}Ee^-HEdoSj}7P0jDw}?AFu#tWI;7!=v%ufrapB6LVP> zJ5zu#x*ZPc7(qH@f+kN*p_wJ`>W=jYqD353^Phd4a_j&{alh!QOJuv-pVCE%z2Y#MNrxq3vp0Rk(hpCs!{#dNrE`w!U{iekH zM{)`r!*pu<+meK&u3%ri~BRxv(?6Ji3_OX^~kPiRHbH z-*O|NCx5s6;g_*zd>-D6KOdTX)i?Fx?btI>;8Lz8)`z6c(n@#S)bUXzeJ@yue>dL8 zo`nw>C)EuNEd(2mtra=x#CfRvOEgxTx+}}Y2Ct7e<$g#5(E|w1zD!|zrmMuBtM*X* zE+jbXG4z~{Ma-^bCAi{~Y!heAZcdG_d+SE5dZ{@$FYTNx3wVNQ3z_nsZ?(4gttrbW zwdhYBNA5*1h%ErU@ajQ4d9>nd*zp;lIBe%jvop5&@m?O+UXzdg(M92iOWSF#eO+u` z>{L&;8fUp8g<2(G7lsG|XY-)rKDIrLHbXn?#^6j#Nypb>W_4gWJ(?=cZ5=I#U1SX{ zVS_$bD!~aOeW^5o8z1(z-#I+ixQZ(D48qfa_frkICn%kliJjxf($)l?z<8oxELXJ= zzkfso&3yX2_4I|U_m*%&o(85=kez19=9*2D6_`AZ`P?{9C;r>BJG1UO)_n3@AXF;{ zyrBXhtdj|`HQBMz%vi5%g4EFL&NQG>nMFC!&x3S;Jl{pTCKnVwWNg4byw{1q%EM|b zx;M98Udrx8^p@z~usGgL{$#Lo)J#ar>WqPz%@e}pKzj=az+r5@v@o?ZJG~QQPEb4{1OSk5=OcE6j!P4T72*gX>_~gw+8C$29}yeGFS?&{5lpfr0-jh1Vfd#A?(X zXkQ%6fA$wm9KQQWE1@O({QJ`%mE^Gi!xiDla<6ihNP3ZUF-HCi>I1$C|E=E34nn53y2^;g&kzpB3)G|Zc5_`3%O6K9@5Ha<`2TNDF#jRm!7~iW zt;b36wU7`ud@%dco$gHACg?vM3K~UFmA#=QFNBJtrZ5p-$seG9?)BU8=cGySCGjhx zOK+!EdyQ1wn*kJPY&|nC-;F)%o_anO?I8#7>oaYDi%v#+mb2sg=g8*=^_$`qU!e1B z6hnLF#l+IveR|I)VI?*`+b3LfTG8$Dj9s(YWHkC-2F=TfXFm-8^mgL;@Z9SU(=SJY z=}phj(z~g6&s1W7H339Rr;XokRqD)zt8Y{py&}x4Z#NQjQd`~-?H@caECPTwIUm=E ze|N!178}IWiR^6xwZXKLYM!sPK)dHjxVH`v@pAnrs^5^T;p;(;q+SejELaOop7WVB z`xg}qF)7vq2SDIaC|FZ6d?WTZ*%71R4`Y6sy!^D0>}X~e7M(e4ZAet{dEaGP_q#E! z{)ujeKj1#Ygo#x7#6ZUEd$KF3VM<^uy@>QM|-N`X~xBu^2@a z?p}J2Buw3a<_dSxEidoPk^`vxXjjU;ajrYGrKXy#(y=aAr=*X z5n4`xwC@}Acetj!_t(Pse+|kDG1+M3s;d`t-BF0`Ebs#<2mk`QdK-tsx>zwT!5HIK zgrT;$tmJ#D2cFOL_RJKTR&l;VtC;p^_(hz~7Df&x!C@YJbFn@jD708c2b~t|!e4Ya zHL}7Mui*#a87q%_F>DN3P4Zw5YoI8anL);A6xOzN$f?x{Ql(+S^TB@2|?q3 zg?QNr8`v<8(bvHIo1pt!&M+JL_OrWiE{Ni?rcsK~I2kVaU)Y-rb2QcRvI%=3u6u1Q zF-2_vj)L9sendZ}_|xY2i`@;MaKu7AzeTX-8IcB@izcMh7aFF;V8ck+aLg)2r=*QPm%NO3DI(TFb&&4brkrf|YY!eihPE$A` zouI%J?Xx}~inVU(K>icTe)qsiKPt6cCmV$2{fN$RZfkM|)X-_b)O;QVCXt~O#^3}| zQuTWG?4in62UJeaHOe?{@_F~{E80&h37Z1!ai>P9tw*iwoo_}qMtT{njXz^P!^-&N z`rzVYT{Hl|K}pOXE?qrUE}o80cnH^4(%TaYu@8}zp8ewpZU#miIEZwP%TV9q+ZW@{ zu=l%XUUg5t_<7)m|Bt)-{A#n!(nSAHpEGBsx~ksj*Et&un4F^s0t84P36zB-M2;eZ z?KgC&uFySS&hM7j>6taF=fj+?>Y1)uEH7S{jipD=v+upHeTDCYd9{E0tmomI{wG{9 z>D`7G+BtX4B>Q+gvQywniW~3jCi|s~CgQf)g!2=9&~Vwiu~h*>=^1>ljW^95o`>aQ zpeBN>#r2Z^yFZawj%wciRFEnradd^5qipaF)&`qLyx9Evg=A1ax)~@pq zg;cC_A~iId^RF)u4%8b`ayA9!ln&MkdogD*)ep5o0%@>{c`gds(ojX3Hlz%O&2o=Q zV(-vOyw>yRQTM~vg^uiDe(QXTxf6nG|7=@gW90BGe|fW8x3S$An@{xT%8||O=t_2+ zt15{{+r;aZiPzH4-;9gG>F|1N87zQU1y06$U1w$Awvoge3|^|MPBR>;p%dP_$I~@A zy;S*8@mCo5sQ)MD3=o2E8@1OFj?=;H-jUtq^m=Ks)|5X}Mqdg&yiOb< zg0?Vf?*_@w>k{kp8=JHB!)3Nq25LoPxnic9g~2*~x)89}gB))lY)Sg?k<5T50*qY0 zX(_|9$zoz4IS5;WA$UpH7lVnO8A%60g&G!%ZV6ZuCpz4oSa!HS)+LC+V#rb>AVTXy zkOxPA?}t`<8n-8Zm6bmvn``7k=5+|X2FrxD$_NQWC9ep6l`AC-;RZj?uxi01r@1(m zqJ9)kYDa)17X=>z&xp^G&axG5P}s+%VXHNGZkVzqmI-ch|J^WEqFREUv}5u)=Mu8kNyAU!Ie5!Jd%~3yXN|92_3w!Yp8mX9oxF zb3eQN!Y#eYG=R&~9A*m#GYGaC$N)!O!gZv{LYqNa)AT}p`(qzECSJcDc#1q}_rzM? zf(g(Ry4TC}-LtE?;pMe1UkIo`suyD~ z-%Y(^*bda9MSa%)1JZ1T5DSiZQfFD^!!3c}>d~BD^aRcgy-N#X(Z%AhN*<7$Rrc$`D3z z^V(o9^2PL!k%Aw%Xhkbr0RM+2ouk<&<%1($K)lD;KfYlSQbP%N*!kewcVEq|_HpC9 zzNzKQ6N6JNk=TdT{8+LyHP7o~y2ZZRfax+)d1HIB5O)Sa?Wo?M7V}}rHHDE2>U7$c zd7kXd0(m@ln|lOMjBaA**NKa(xG%-u7dj%odXvDvxR3MGTm+P%-w2sO?HlsTPS_q^ zg{-}EPpdsRZSU!Fsygev@QBkJtDhJTwxv_$kF6J*{dfEDWC#wb;<{foUVBRw3pfx+(vCt9L?oP4H zh_MA>k6J|ihe~BcUY@4^_@_Vo^%eiKt5t@mgcUmZTC;#7R|om+XcW1gp>beW#jIG*n{KB_QFlq1%n$p?)p}QOyH1b$mt^Nh#U+QXGd zH{ap*;JdhgU1yfg%!F)n-EeWZUO5iF@tR~APYhmkfK&i;=127YZN<$IGdqwP02!G$ z^y+w$H_*JR4yZ zOywsCeLL}%0U9}2_d>g@Th2n&V>+yqUj2iIvFC?Z-4B^XbA%?^;@B`>7-KqQ7kPTS zyL(YZxwn{8VKz@TU}Cl#a2^LAX#h{@8`0#S4z8^%KOP)hZgSL#dEQwEus%x-gxcQy zy_G?Lq-I*}Te)K~dvO?fRn(~z#}0yl#Sg0mWm76;!a#wNl-)}QJI$h1{$ZHQEKl$w$Yh0}ejKT!kdT_i{pl9CI)U}wy|3G<PFX6AR8O-1QYwG{ zhkwY+Vk<>bIz3sL^0Pu*k;ozb0KoRZ#U;{ollX{ncvwL5aF{Jl_NPbs(eDWr3lm}E zknCqY>fajjYih_5&=>EeM3^f_nDVLTq4G0X4PkR3rj6SJ799E?7XrftZvn?yh>9o& z-xY|fHVkrWMgRm2tp$W?CO0~~J{SNZ#vQ^kJe%7*@#g;F_kaH{Z|AS(8$OwcVf?Y( z9%0Ge?Do<4tyffjPH2W`J->>rAh?V0^8qrdD~5&%)EFL^x+?IV`ae&x<>?O9jCsh} z;^kHFlG3x;@;E!(7{0XeEPCAJZYM37{#@m^(^vKte}*c`LSWYvr<^8WC@53&aGA;A z$nB?Aw_<25*z!(%kjuj6wEwh|l`9P-q&bz%q1AUYEQ9IR2Vxro!Kr?^t0&stI{NBK z*ALISAN2{G-I|xxm_1B+1|=&|AZn^jj(DEr`J!7q*Xg~a5)t2uFI5)eb{2aaA#&;M z#b5{=lE-`O2f-BNC(=L+t${iMVAR#eJ|M~WW7JQ+;ax_m-_GqV+Ahz|Ru@?aApze> zl}p-MZu1V!nqpiEh{62kJOkmTUSQR+=>GKbXu%*t?BYG7K-&2-X6!0&!-e!f7n*(N z_g1Td;JJzcpy*uCX7~uP4?Vcq6%*s~D#$h+mf)7r3J&FhvbK9sxDGTGE8Ua(pN{T6 z?-BOStaL%_ag!u&cP?~%{r>A;z5E-7JRK6=mpgU5=pR^%eX*|AF&@+{G^#e;d@8hX zM7u=I#{A2>ot@(}+Kg+$uTp@r^^IEvWI=mUdN36Ijsd}9^enLr}_|LyVIQKN3HE_ zCj~_{44TJqs|bawRO62dR^K+%Q=z#;dng+ zy4BB$=QpL(i$eV*UpdKhc$35XwD-~9y!@MAJ^$4YAHE++^yPOJSF&Rx)2&l;t+R;b z>7gcRr*n9rgKl?3n_7xc3fM{cl>KT*LBGTnL*O@ohTt|ds#*lL@7V#-W9)0@1u5Fn zBIjJ)uHN0RUf(8gdlY{s0t?NpFqpHk-PjX2!hsq7f3bFbpP{T#jSU62AJ=R)Wgj~w z?VifVYl5__s62fxkzJc$2yaYyt@%} z<1AP!uFJ+_&iMAeT= zr}H9d7*wt+ARspYH8*AVyq9Ff01RyQ+v_UTsyv8HcueSGurViOf-GU3bur^2{td1> z)qpa2me-0S=%55XGzk6`zlnRpWF8G>ro@DQnGswQ1Rfy0N9l1F0Rim$xcJJUHeg2b z90TGWeC=X%Gs-p2GE0sXn3s|<$bRc$zs8l9Xm$)0kTB4b3k4!|S5IQO5fl9of*Bc? z1_OUUY$!HGHPTe&&=Jp35-rwZ1U@XMAo-dooND%uY%2`iOe4Kc6-kZaoetl?wU&*D zJnyh@!_$t*d}$ec3d9D+x*(R_nl#Q8bX$$PJ&`N=^Qq*ZWw&%-+)f^hb}stl(oy>Q z)7Fna9en(>e|C{&4INqOCOzXw@J-9tEo{W>#irgp!q0s3+ZO}R9BKmZJZN?|HV!r# zaQ@!u7T#jYzM)(8r3mBW+wI%WyZBepRR8RX#FN2vX%x0*O`uOOMed!aIcptXhjd8P z0`sP>4_O_{T9+$yJ}ZE7XGYd=o#0ELj}YzPLKDKWtCGU@W9}2!K5du9tAbBvOK&xr z8SiWQGX$3g&fADLE?RmBJ4JKYyV7f)&XP3RH1b>i-0`uc7i9T$;RFbx)yS1Y8ZaF{ThZlAGHT}#wv9I3artIV~4 zZ=%(M(Y+jGl3~)teMf+FNbqTr5ZlUQOFJZ8vC6uiUH?$mQg`cg%gd3Mj|QH0&9*86 z3nk&~y6YhbNsmj-5;lC(8xbF~Ilfk%y8r3=U&@Pbe*cHx9~@WGh3MSs@Y4GD!p8V= zZZgaKXLBykr94y^feV2jZi3TQVs}rhkLbs234yV?bf1eTXBHFY3wmDHFfda1dhNS6 z791SBcO<9dzT)b8F`g8-6B2URA8m0V_D>*_EQU-mTG^$(MEmdNk zL`c4d-*!y54EE>b$?7dc2~;P^VaWe&jgPM~`{&uC%Zyz%_%!%@o`$tAQ-~n?Sm`FM z)w!`jE}w{B=k}9wtkC<*Cjy3mgX9C&PS!X}!)GvXg8%UN8=6f~aq-%M8Cg}wg_y$` zD~-Etf_ZFlJQGZ%#*G7xPW$WXaSBItxh9)ng<*w!TSpk{>Fj<2xet!AJF+jw;lHPgNfEN4^+%MeG&h z9Me|E_{)LF+Zn-C8$*EBrnVwVEdqMF`COq9b`QW|z}X#3?Fivrak>pBhkDIf=zQ^BUP-gr5zHuz zP)(f@J-*t{a|}}@`12Gd^n1Ol0*E;(r0t2Qr$^jETPqX_L7u#~5hMTyIdSS7DmHe4 zb@S@ZDv-x#WKGi+X`yR_gFWqfXxe(ka?{3>uku0%=OWHF04x4}4E7`O9t<(9vNFOl zv~-;Jg*MMA8rM{00C$T{PHtnb`L@9%!|2ppMktBqqti>8g%ECXeG_{7RA7Zoaptk} zMY_Ja%b{YDQ6~cWgvn5*o-Q4i3&%x2obId>_j0+%qX+7@YH=%anX?^h{&`Z4()oS; z=JU>{pY|H}^xeY6^6ok|eSk7_GQz#*qpzM0KDGIMJn$4D)&!^E2Mre#oij$-hIeQ< z)jyc(!^woz9A0k!tA+9U?rbF!olJ~G)1&FqOtLV&RzOu~6)NnfFw>V_`wrdL$6hb%x9bR0c`$72Lu=6>wyrrz$Lq#nHX9;=lT){zLz! zhM#(ut&@DK?MBdWfqJOw^!Z3Wm8O%MYvl?Xd|0XLsGXb4LkPIgv@B$;V)23?`35Ul zQBZxK2*BSTL(z1UyN6v&o3$?=tJ{xCMe|@mtketW!5vbu(MP#M5dWg*X;fsR7%LF@ zSo!JXxjtge>1WsjnnO9nPUk0)ia*SEphlFAlAv-d7YKkovTYP-SIxh;ub$nOkFN?r zF66}D(n-elF4&J3nRw|8`16-BzU6T7{y|LxzNUVhw?RxmT) zE&|~2B6oVbl_*cO_CN0Gf7n0%Vkq)Proe-aZ@+H)_WSN1o^(F=ffTF%v85zv+9!x7 z56$Qy0gk*Knr)d_?lM5Z(v2H*&8Jk8WpMl*XiBcZJ*;mOtQ z`sK~~#m&Yk8tU0fVJm`&8(kkno(%qM9QD`3(`^C7aLu zufhs+ChUOVNCS(KV)_*sK4Wk&KfH7-5r!DbEG#cRN;|;x1DEXe@Jp!&NcePWZXu05 z@)8P@_(cUD)N^&ioGrjpgR28Dv7-iqyObB~p)jB7PaG&;)L5IRWyPP~aBQ{r#qbMD zBA*%sjdlw-W_sl#kw4hjJ};f#%3ImyKKJ?O-M!0_S2$Ie>0a%79}F2<-p{tS&Amq| z(CSMgk}?L^(NROvzSMkY*~+F!#If>G?&inqfB470``z#V{-6K(f0}Iahd=(|_VZPc zpRA9jERmV77AGAsT{I4eI#^YC7)wO!E>I=_P+PPZe6(1pphGi#T{Lgn=A$KWWe@DH zbG=;1Xj9J(cipSKUKmmz6o@N}=bEvnVbHP(!COi%>Z!G|-`Ddy!VG=bCYFg@9D}JT z(k#_dZ6+X5Htgbh>jCQ+TOTMMX8uDL`G57rOW?Zjb(=c5N^YGd&|re8PBUW{H5#WE zOP|NSg&LPO0dOS?GEV7xsJWE&ku*WX{sMA1dL#`<{b=53DKOV6&DB$W9i6W2_1&r+ z-^C4B31QD!14La3KFo%9e9p7*{lPisAG?5n{P6bifYXAh{KgRU`}N?v7zgZ=Bl%Ko zY|50Nu7wU>`qk4MVkb)$WEf?3U#W`lRDq5Q=uxGy7q-6=(ZpZ_p_5T}M-&W|hE2uoU@1DMaw_ z$WP+fHxuHVap9!fWvUhyg#cB*%0}0C5Dx+{H;#B7J~_VjMI=^h-y zXijdnZayDCU5?IE8v_PMbA(IvKBz0_o;Z<~4{=3W@- z8JtQr)4mc!sEyX^>CKSi;Ls1M@H2{l%*jm!&#HQy_P{FjLJ+8#b?mI*b43tEaM zwiE0fP+5>{^#iNh%LcT}!A5Fl4N2w;fQ(b*fDXJAeZ?xS{)7zO5BoU&vaS z=AtJk9omhM0*(iw@8h|VIIUlP49SNy6=04$u)|O*WU0qa&KQhGnX9btdS2G=KUav> zQ*3Mx!Uo(TK&Bp)$!s3Zxcq2WbJiang z1(##as4iXbTWH4bdUi|!7Jn);J||i=a$Nv+I3n%`c;7}Iyzf_esf_pa?V5Sl7MPQy zLkXb_F_@&t6%mRD7yRES#g?V(GIpjJRP)KW%o+mPE^nF8t|<6);*@2Yf4*CrhpXF- zEStn2s9}SkZOaae{9r_eId6_Ouyf|L$5dmH>#RpDH=*1tF$ul^o-9^s!4Yfi;5;Q7 z3CDi7k>XlEMbtyCwbnO0)AFYGhX?TO_g_D1|4vd;&&+#dFkt=<-9J3apn?Egro_` z27Gr_d89`G8e6fsxBJ%mWI`z>%%b~@=S$sMATD7r|H-A!*F6s{5NQ%&v&b-&lZ-RP z3wu5A9;GX-r1GT7hOO6_U&(8Q}xcgL(%`b_3Pih`|450_m8_C;6e4zyS;xI5{gF$WFM^S_MgwK(^9R zrrxk<-sHTE`DuC0(U&TDkZW9KXrnp>Fmx}uL!jK8{0XBJ|5O>vQHYE|a-MA?C62u$ zw9U4iEn9v&k}i)eYz!GLY&Kz!$Cn%hS6uU%{G@|PZ(t?OJTo5>ic>xs72e|v+0(d+ zRvBCATC2{vTii>mr#ZrPEOpKCQVR(Y%T^e~UyfabEA1bc$Ogm@tTotcRH#er4#IHZ zLKXFyQTwJ?Wm+xbDr{vC5TtO>Ba&quk?7?ulW%z0(OmRRx3-ME>YWvKIjUTb23x-o zdm`0a4h3=65#aPJ*0@^EZ4=8RPOgYJ3eL^DpV({UiL*E^0x&W8eFL%{jrUN`y&Hc1 zpyRu5+P}jz#zU}rz$?H;n<>p05KIT0RP%=^j#%09XgW9WX7t6w-bYXSpFSgF9DZ&F z5fjFCn*hp}jn!Ila;1a}Gdh>E=&?c_3EJ317g+3nwG95#KmYURpKppgY421~b%G41 z5h?rqOvRt!8@_JX%H?enM!MJfShTUuh}pl?qPB8EJ#OyTuRrE*@AKDp+3ULv9AK5- z65kvrXALs!4w)&;_*^PN(@u+&gH0JSz-=6l!3+=ep#_ffT zK|x4Xfnp-f-OLFGqwkkfffveh4a7>SARv-WiUTp48`szISLewu&x;S(3RkP+gmR%o z6B>vHMviqWy3J6e!enG-sm6c_fL3n6q$rwvfGT5w@YrG$r>$|B;7OT@JL7n1jxTqS zZfQi>Kl**j0-LGX&>oNSi$K&dTTX^wJ6(q_HExQxA4`W98F9@$J&90_le>=Nz^diT zPA5XWRh`|xC|G}kksVc&xG1~v>Ip8HW`3e@kk~#i*olJ(Y(QQr*GR>?=|p`M^w&1= zhJk~Zfm@;)`s0B(%>m&t=n)3K+&1y`WDm+Ut5UGV- zGds;8>kML$SZCi#_f%mD1e8bX+TXAmu-%?a_HAsAr#Hv?7COI3Q6QY|a~@OUiJqQ# z_lKn}xxf?4Hu&SQLWrAODM%Kc(wRqccBy^I?d1Pz<^S^MfkJzY3XT7w!LtgGrw)h! zd75EuQ8to z%5zS3m7ke;U!tEj&HE+uq#@FtxwSN_d_m3Hivd4e5C-HgW+j1oifz~>TtMy0AxY3= zDk*#OVENO}XaE1+?Ejg6%hpUKH;fZsd+#LGa5m31m^x^~PXZdF2!b?x5Uc`1ony{lM5Xz_Rc{ zyTzd*{PAZFm$G|H)ICsfq(=_Vo<5ov2)vgEAT6IT6~sjc{x4O}iuRbcnc~+~=F!z2 zVsj$aj`3G-`yaQG7fro+KlbW1SAm(9#j?PuEH(wO$g{}~+A*hF^Oj8nXSbExPg|!q z#q)c)isclEXtF<17%>egR$C1#VD8ErW_gW=!aI+$sA$JJnO=^@yDc&I&r_bw>M0O4 z3+Hjhxov!@2B@muFD@|!I-~-g^M1Ou13P-Ti}DK*#nXgbL^OnGVQt5W$lB3eik?BP zYdh>%@uOG|IT<%k*5reP!1t^A=1D%YM|rVye$SqwESW{ae|}p%1NeNb-`{WXHI2kN zAGdz}?aN;Y1w+&%gCh-PW;B6I)a&d_rEY!>Axo0!A+LQ1{Zz;->Xb#BWmgE(+0+Z~dl!;`ORgR4H!8h1k5fdL&3bXnWF@sdpS3RAW-RCq0iuF}94oanE?~T5-_* zQYp9+9i6OQU57q)cYnD-L@D}Kd_1{IDWV^kD^BB*IhYPF*3FU!7im4l!DasHZj&MN z`CXPBJ*1HXhIuw#7{{4k<3_$QKn4!QmAtUsD6_XhBm>`Bkl{tkSn(7fmC=R|r7b~l ztw)Cp!nvTCG7UZMfAT)sW|`%&b%oc;kwk&QYiuGI-S?c{9Q<3#+VB7P`|NI9=kKDS z_lsk|kyeG=Fq%WKzvC6=9J<5->=7N9rCMNL1|K0pu}CtVL^rK0V) zM}G4Z!+gMxqsakxCgo0Zk36oer78103ph6kew{8NSf|=y#X7^b2$_@4EAWD*tsdN0 zHOlCd)j$2|kN=kU`+wBmRf-YD=&j)P)RE`}IGHTK+ERlET^9Q_u zmT;&P{CH(rZ>;qSMnJoc1AkyKR-K8Kr}%5(&p3BN-P;caTyEgNx;e_0PfEfNu^P0d z@~1kdTlp}w6F0#s`@r&VK1{u78Rld5mI9ZN!r|4X3c&qF17>ceCD&(q*o9S9PBtW@ zGvF0>0zP5@;<;9t?a{zTup>RK$KTBfkw(`aU+!UsOx~h1$4klWYemuk4LgOsbG;cJ zbtsa}ZHQG7F%j7sMfosf+fHzZfLE`Z9He%K)vMle2SCt;BVM2sNlWs+J?w>}bp5=z zd+xQ-O|Nd9iJ2~*+y?3n#%AMwe`9xPY^h7U*Ujg;#{2Sq^ZdSg`|04*kB7G(clH|T zfoO-)^y?SD`VCrs=K~2xrQ;$_Y}?e^#~&U%`tX39C7vF1Z~7pW zWhS55jca>W@}!Z`@^Sp&A}O)1a*TQbcVc70@Ocq2;Nq_M^Dp(k`@8+0e%bx>seE#= zwsW#t_edhkxfEix$$od|xeyn(XPE7q%Wgl__s$IqX~NY+c4(tCrg_VIh>-kE#Apnq znVIsutn;y4q+8x}X0Uha&1}5OuKA>y=Sx**xtu`APma&64VMnG|8@s^Yu|(Odb}_f z&)aU#2-Q{g)q!Bzi3(VoCe?J@k7vdWAbJ%B*DS37zf-x8%NF#uG?j+s^NK9B@SklyD}eVYWqun`lr91$}gIhK`v)bt6LbpqRnI@psdHsR=QVr2#p~^OOc26 z1f&{)Kj`b(>@Z*D-Wddqxk)Pap*ytfaV_Z!d->Ei;6r>oXwh|*BARh{H2R_z9ef=YHkJ# zgNXpJEB=HnPnaR}K9roIgHF>mOV&{KrHzAw zEWwe@kc7b_{t2Tcbrn&eHLj8sax0o(se3ZHO`f&}y_bFqTR?+n@8*W2A=TYkY+-L| zL|FhyA2`0QU*7Lsf7-qHsO;}H?g?*?Q0WHeI^yM-t4~M%Q}2C#Tf4cdonGY&I}5S3 zp;nZ7-9j+^Y?9F?>bh5d+y%y*e zr75<|XZO48l%;aWXSaVW9N^@yU)%-yX!+1$J<5XeJ@e=BH{kscI4f9ItV})11+IF! zu6aJZ%%9y>k8Y|4hiGAR0n`xub8PgcsCTUz`H?Pw)7OXGQ4}0DYcUa83|DSg&7T>427_ZIYoXjiQGiT==Nv0=lEP!{k(GZKXs zs^M@pEX1iiie@(%GB&2rmXPVNy~N8)G8zL7el_+lleua72$B>1!;|)JA9X&k)8W@= zPFX2Tj3)+0mibx?x|iV6T1WB$Jp&P(R=Gtu`@|bL#m`5YmKU>a@)snS93JD7Fddo> ze4$zld06A!%!QdYq_FWc8}59p`@`$aFw=dmT->CY zTjsWdi)NU}wq3x~!THAi*(xX-0md$Pr%_P)0^=n=vYhT;*$4!o)9XU%AYseDlIq(i zjl1_KxeEKiQ(81kWT|~T`rc1Kue!cTTwcYIOX=MfvSSPD<1Rei-cxIZ}ov)gVn9#@3M=qwC*8E_6P$) zVR}xD3a;RB!ZgR!cbwayuxZQX$~-=;13iDZ;q2q!5t!BZX@TK66EBUKC2cvq21kGb z1!#gt*+AWXI{tIL<^SzJ=jbXh)v+1PgZQq@894b=<)@#Alv{BRPpMCmreRG6;3a<<4)=FN1-PeXhN*0YO5L@8t^leJE}%n6u$&fIdPj zi$TMH5CWHX76mUJHv1g-K)lBDN1zX%#yQL4Xkc+K@y6#}Tskqg2DuZbCdHv=_Wg^2 zCr^7HWBbgn4@tG4{ZT#S^nuwuDLa>SrP8`*c(u1>^f`w~Sy9>p|0t>_y8~M`7?G(^ z4ipzhIa%!k&))Ss9$09nFyTNg6uC_I&ll|4Go1^=`F znoS^Su%OiTL#O}??j?>2$i_wt{f222i7vU5!VJOp&|o`mEIoB}S`mO`*lrvpVatq^ z87%m`X0jj0;LJ&rG_4wqM) zQyi*GV@2WY`*Zy2sd{%$q*g8R3?Xo)kY&MvF=aYnI0N5~RkhO#EPX^azyz#RD17ZJ z+2SEpQbS->*}sV3A>!8ZDdX_Ku9Y4@f#J)J`1RDgZ{L4IRDQjOpN7fi}i@$CY(PSZ7 ztSr&%`G4IQ!OG)c$%|L^X|;)QBR z=un^s?o1p5`E*;2GJ-ev3`0TK88x5Nwap2MMF>$w35#qmMhgUYT`GlpUY8&`8fT_A4FnFY%yS}-k#{pURckW z7p%)eEY~U0PE1zR7PmK1%ZCKH{0|p!A$V~FO)FP4OGCs_f3eBr^smGD*{n@x11nF$ z7w2c>QmjaRENFFEbi3uA210?DU0z5Yc zC_U8))y#x;Jr!II7}Rg<#4r!LVYbBSOWo{)obNqCvFF;xCf^NDzGL3F6>4%C?VN`# zj(Yp#yTQc|%bCHLQKC5Nwe6qdsONSYX~$f+U7Kk#@J0(e#-y%KkP|`+amLiTtsq2V z@=w>px527+dCLNe>McCoEOF3}bs&H#t-j>~LcZ?KuY7gbEGYicfm!AA*t!4l^rv4= zFLZ8|xoTYiL9Dj5P~Ru>Odp-DrE=quh4y|5BfkBSmggfcAo;IGo_kWXm!?{%{?ndE zimgk8?BVr5nWVr_V>w&R1%ZgjR$^;*GTzxc(^A`>y17}sx=r8SrW+SF5eY{a<@;Oy zlT+{DY@8>9W{UW|Fy*b$^EP>&d5I@JIJ<*dqfk9bqFY;(h5y3BYdSTzk+FI_($fP%( zm&xLUoz@nzdjc3Tb@9fb#Bd&6A7pz##aQX#zv>9!kEHR#_YOc@Fqxm?)`As)C+yz0G6NG1KZ;ZvyGP%o)r|=Be_I$2@8+JC_>$ z>6g!cuEw=mFHT#{G;I@{o;rWLkwB@+jLqkC-P7n0<|)a}^b1JQ9JjReh^98?(WZm6 zw%e1R?O;DG1-3AR!8k34aBHn%=ST+*A`65RG)RXsO za()41Y(^|njf%GLF7m+{jEJQ(&v4Ao5S0!JKqK^++f4 z@OCJrH-Q%l8| z8R}gqu+kynNJlqYx1WwQ_Q#j&J4Y*@J{{bDIykw?Ehc&fhhKJ^Q2HLn;@w+2OIs(Y z)AMAxGCef=erWpLq9BshejCQ`KYTA)jL{rFL+@IW?DM|IN>=$;O)iI4YJc4otDkS0 zL?EA1Fg#Go7ZYammCc=L?7=do<@Q4HfTPhebwA$>h0doydSFWq$;Vy%(w91*Euc9C zeW9>w{v9qMsXX2jFO1qk#v~o6MfFIg$uJ4jaZj0)RY$5kzS^v2JM`_re~+8@927$C z13EX{8}4VB?gT_zGBPMf&Jp(ywz&>aq0p`Qg4p0+0g>oFT4&zNFC!V4&kZ34$4XOc zTmg@gUbA_`+tF-TzFUBrWmuV6TQrTqXFdKpdEUPcWlebTq!eTe>!Wv;65rZtoyIbodIFMmm@oTD}y=sh=ny zu=?$G5g?UW9X?1$QV}hOBR*i4? zknkUm5~#sylB#s8QN;p<)jaKaXyU^YpmN22v>RphCI+i*649bZlVGDKqc9>DX{}#m zL$X{^nfIxhUdvuoA_t^pqi5D7H*{Fl2P~81e=U0!Hk8l)@tvj*mN* zwjV#$owK>E=-G7{csrjNf7SKi(VJhteE+qe5C(3U?P$omA!b?bY8!b;2sVeLmmCDb zeB1Wz^PWdQElx-M(f5p<2WDGw;nWjgzG2GhTz%$j&#t5&&mrE^7HwxVYC-UrN5oS1 z^u`cC%;LizgGe(uY@hGoW?&C%TR}=tKi^X4#XNx{aISngb`}3xqe#INn&C(bP@Rol z=!9YE@!nbT=kMDGIBlhOpY(N0N%4YJTIO=!Dfw`9(*i-m*qNOnxRqC|C2 z6_YD88&9Hkh@iuIC0kGI9+gkd%AbEa{ICD@&zw;!_wefT)vSI)N@giQJ%FY!oj_Jc{e!y zHdh`yy2{cLLLIG6khG}_8+VnP&oy-7p*ZWd?$Jb_8O=DaAM;8;6AG8}#jo;%61mZv z`}057k@vsx=h)stMRraRbpSOX0LwZXk%{~kz8CcMB#;sfcrWl&myWbI=X(weMgTy% z6M@qL$t~gs6JW|*imfA#vge$*!Ypsv(%##q(MTfs=;y^Fl5;BiQJL4}DXp46iSPrE z59S78j}ZL#?7V!q`@z&v-m6ITR5PHk3gf~MFQ=hEnV?-FOb~Du z_Ky^5OK=_mCE_534Z+Pkn?P(nEeK|qhsJqR{BENtVy1GM!yf1Puzf}(Rbr=q^=a?o ze*5Sur^NA<+7fhrn3!1Vd(r;gv)13z7mITu!J{&WB!^Ke0aNnD+ErQ%lW04l{&!@# zUEdSqJLVQS+sNil52SZj(3YH%$T=44c9$XlMLm!AAo%qrdc`b09eM`7=veM1s~O8q ztZXHGVg7J*7bVX}kxZ5Cq{8S+Ws8{IcazKTL|A3)8^nJ6>K?om)GjFa$LgViUWY- zf$}7L!H`88VwY+IGTlV9P@cF0ZJy@vPIdKQpLZiK!sQ)(CD4I!s}s=G@Wnd_sfnB4 zN+r6nB?Ta;#vt|uV#BbUDkQd7MFTfM$cpCE_{lYH^8ooIL10@v+Bmr8zfs@5*m8`X z27yMEpQm*IZz7oI*1^h#nA$*tWN^2X7ecW*Fv>7LH(WbSabx)Dm%Y2sTY|%QTx=i8 zRF%EBZsGg+r=1^vKKlIg_QmD$)=s2;xNvltI=PgCyncL<**{AacVmcB_7Oho&YAZU z$wACUoEgQRiw-r-`Td^_*#DnZyy{7j1rTynOfq#7V>cIvSWE_PxKDM~*(=549Ejc? zP&0$Gy__Gb?Jsln2fLM+{_W2nov#sjon4c(WEwZwi@S}jqXp)Ty9ZGz-h1bHaTR6( zDxv+${Pm~W?T_0hHzl9iJFep}uH|msh-XC0P?4i-OXofuT{!su!~YK~ z&n=C44)0tfZ5BQ3l4J!^-1{b0ob<-6DR+0oR?7+iMc% zy5J!V%uIBP{aKWL@h*UL`Z%a53uyTowh8oExE4+mT{^d}o#UwU1)E61m+b)c?%9nl zC5L{j9KnjI?A`WuYN|@*Ob=g4ZO6LiJG3ROv+vs%IocL>%lcnIt&Z1!d^1FoiN}m zriNm?yVr-=-6VYa*|DLeKt$Iw&$D{lim3bq&O41Y8Y?b68dSXmjqq+`WBW9{bG9Z$ z`tV})^qQnMq>z9n7@Fb6sM`b)2pel$776_K)48Lpen$O=BpXTOOE#m#w&`dD7x6QSW=O>^ao2Ql9SvZvQN5$yo!fdkL>!Lx{hZi?&ZgW6)w9-(ckIxO~YXbZdWoV?VeAE6#Ms2)!y#U_91khK8Uw zGCHeF3hRkfKMyf3mz?Y5&diY??kt>LB^|YHEjL){z{YBWebeP#cIPbZ)L^weU7Q6R zx!F?N3Vw7vL#jW9SA2R&i^0z!tH(gL(e0bR99xb;h*sZ9FLz2CrN^BQzIyxHuW7xz zAIT?v9J5eB z*R*1Q+Trs%1(X!)R|hnZzq#z#u$I*|y&ZYU)r8|y{UpJ_4cuDWjhF8t zev&%A%3gmgp-5qPQXS&Ro!-ECR>25pW*7HcG%42~tB!^V3b?_i*6~4=x!TLNoj(k| znN!alqAU=aajg`O=uGRUX#W9&wL#`I%LNE66StEP z35kf|bCn%jeHt~?U)04F4#KXPRu^5FN>QT=)v277bw}ylgtlk%tSa`7un4~TYVa9& zA4(}tA(-4Qb(;r?UWZ~_(A!0RL-mte5n5Mb$#+9fgPY>mD_cn5C;w;J%FK4$U5FE8 zJM{RWwJ1&!&SUH44zMdXqSD*FUn zJB{Faw0#zcOc!_AtNR>*dE+*7dX+rATH9;D!Gh}0Eq8Ek-;~=w&upJ0EsU^CRXqA! zYf#`VBmtZYr==;t;bF)CLg9NXMg8nf?)kc+mykTjCKUfan(@YM$M09$XeZ zk~mI0wHTt}!+_k;T($a;+qvSn5NGWum`H#!&Gkxlm^T{oJRuS&c*8+_w#cj=W0oCj zu;STW_1EF&Go`ud;xt+y|3)a{G+Un1l=|kWmneGoi7SGWs{n)#VX`Sp)dfyph@CQ< zbn?<^ANM}`>fP7hzW??~-;?Kq&!6H-4Lo~0-P*SB0ns#(8x!Nm2+mQcO9AHa%=Gk=90&^PDHrE9-wq^>X8T(N=zXb_;*->ZQTdpG+LP!~_Kkj_><35C!1G~|w_39$30yei(%Mdn(`n@-rz?;{M1;;? zi{t^TB*))lZ@zJvxVYUoyUs8&RrZN%4mku~FbP!lH)Xcg&yb)ZbebnOx_zvP?X#PW zY-Ms}zTMODa`a_av<-QiinD(OyvdJc8mvuOrtOCw*aFl}3jcN?!~al+|NZ~d`+_zA z5OUkoyaD+bZ<&eCgnrIj%R20^E*A?ELEc+oK}koyXynW)@^!r~y8d!7>g2J^sO^Bk zNMiGF!FEQs0OWyh7_Tm_<7hv%_^iAe{q(7Fa=C$OYb|J@AXpQl#(@PriIk#}OeK2_IM_#i)Kt8C4%x3LnNbAr zG=HApFvEs-t~C;C$LF*VQRI0(bGN7+E;z!tl^Si%u9K(Nsq@>UP>e$cGUxEdWxOVY z&l-7eBM(=>OfU^$rfuvc)g@~twg$DvX7RXW8NGc`cG#7Vg=tNJthW`U4 zb4)YlVmdLp*ul|V-wt#NK>!GQ=>b$#m^m4O!Gz!ly24%mo3~$ej0EI|a*9AZZ17JL zCKOh3`Y1bDqW+*=+ML&ay5{NhD5D%U5>R!8F_6CIDg*{Khq&U;rZG7h{X_j9ZQGh>`-Cb%@*<**~P2X2(IaE z2OrJsdgHDrTcmslUb6`9CJJya_%kT(#u*a?frcF%PEI5}v>5uaG3!y8L72u^e>TqY zU$C-uDVDR`)#fWiI#af>PA*M5uLdaApb0VLF^!o<0RlJoy20Y@r_Jk6j7Afjjo&aD zH5u`IUg?4CarNmJsxsU1diZ(E_-l3(VZkxe`gHI)qCSZ(EK7c_8_b3xy_d4%P^0F8 z!yA*FQnSuCw*%sU*M|Z4PD#iQ<{_s_SNID`=90!)@$DxYH+x(-POqvb7p2O6JQ^S9 zW0AMg?fUCLRnR0-ZYvQXeDrH4(#A`}5Q zsBv7)ty;X1xs@&&>fu<2^aK$wo_dcqo4d}Ho{m_jKkSe7cp_Y}W)2H$9XjH_Tqxo1 z|M)wvc`7RaaU4dc;QJ`u*jz@ZiC>CvhDG!+X=p2T*%NXED!8>W?|5a8rG}WfZJn+f z+&q8>=ke1U*0@P4U<3em9G)F56AO2P_SWgTLtmB9s@?x|`1$8!3Uo)mR%B}FLvQpw z=T&t;L?as|P$WYDnXuqo_%`gzvSSMUU~}s4fA{mhf35!W{)_wog(;}~+F;ACyPA4S!7dY!+Ug2*cCRvnE$@8?rk}-@hx_V<#GN|Ln&A#MA#Rw4YwdRN03o)SFQ=p zJ6a&v0rfzKtWlP#GrI0Ca^Naw{(bG-5`CgS1{4mS2y z7cxU1Mqj+>d&1d_55w_QW$TFLbsarrtwM&;YrH*XfN!vGJA^fOV<~9 zjrL5xa|-xJ5oqa-pY=U?*!2VZwQpK}OLqFG^9NQux+8F0^$4n#ve;7gq1OE8zc5KK zreMpe5E+*ibmxLh7~s6=z#@ALk?~tm))2>Hsv23_=cDVwr=PZ<%0K??@sB^BT;FVb z7Z(G0i>$}kxeG6?1*S$c;hAv;^#h3*}noMzek^fy@ zmBBq=UI;uNth^>EtBdqt;R7h!!2uq0sM+}3e5%}oL_U?T@3Y*52BPgjwK4f-Ks+n6 z%C(;L!myZ?SgLP)g$4xGe9A=G5p6@4>Y9FUuEg5aLYQ1&M6yxf68!<#Q=WGyQx; zS8tTFarXPbAJNo;e?Bu{|dMv^0|vX2*CZxlHFY7B5uwog|%hafAmnluIQFxT_n zeA>=zj*ia0jjViN-=x$tWD=>!{UmC=C;a5otOTMg5TWUPE*96TrQtn#xL*Ibg&3HfvC?oog*R*FSE5n9%zB4 zPMCxLk5qgDXS<491OQ!eH4%pn8nN)mV?W-q1C^;1!j*g$rU{LN1wnp4oHc>I%Qby- z9;rBYz;7H8qJSu39fk}O6ScW9(lOhn`|=b+#j#(wqLFwD~3`t8~w{&cy~T-2ZvK2T^L!*4o1`c`c*x1yf)3x z*{C?1?(ZPloo`!6_7MV*ec2^l+$2HA<)hUMCknID?krCO5p{%GCKMY(T|IUsc7YuS zhQM-d-h0vSLGq2yZdZXXY&LQH){rCtVpp2OKtN?C3B(32*{zF*BcE}6AfS3B2aUqc zVIs9L(l`BXAl5Y(_Qmt+{D+a3UZIoDKN`ynciMFHJ_4sdXSUB}wC{-}I9=)t$HiKV zsFo3sd|_W3`zbJ5{x~%Gcu+h)l_2(aoZA7;tdLG_a@B*mQf=bkbY4UL)8DcS*h0r* z6V$lLA6}LYuWC)-q`K4RKRy31|JSqshqwQCoj*M6e)y#KG18QM8Rn1utO<)QyL@2# z!*C{vy@NJmsft3w({iRbvs7E*Y#GTLdvu7azCJ{v#Q(lO4NUE7W1tRO7J1u>B zlfC`CetEsZ!7Li@?6T&VZW&&Y&eNB#PE_iXsp81gYF{if++=O)- zcK)c+4FWOmvSxng&DAsY)@N#J#?k@ye(^$a!)Wt%&r9M*C9dKCL#;D=f;qCRdIdwU zKsmeHx%@;LxrYQbv(VZ*`Yf{2>7lbe$m@j5KeC3ZI^EOZ^KTihGP94!#1Lep*)i>tX69F^sMCL7lY^h@j!gAa$F{j(MN z-yPvu1i9>V#7UgRLRR~+#2o3pj)UOLiL`TizISDScTBvUSnP;r2R)?4 zdc?wloR+0wNaCREt1l_A{yfeGw!EbthacyTcuhz(e@L0I7gtFn!}&z#>U#f7vOg^M zv!ln?Y=?Q)w>@HI+4FFe{wt#zONiso!$K!e!W?JZ{O}*FC*Vk#z2)LzVq!tQa8~=6eDbsm z=gUFZv6hZj4I1jaxB(7A7}vDJEF)0;QMSa98v*FB7ORU+N#|g_u?f(~?}gQQUbt9g zI;1r*C}{A2jnhiFyXZ?SO^5cW}HqrKCWoq}B#Y*bnEO~Go+p0xMh0#s^ zhfK6Y&XvdGtKG|~KFYWd7>6X;^rnMEu{RN{#?;ycF-JR|ri!y@|4Uo(MMDbmRip)u zO1q)Y4>kY)3t8?*QLq5^I#t6bHpF3 zo)nBr`&Y)L4fL?5T|ac=V`JbWotkGeLlbKu9a4T%Gjh5jbxs3gWHx<6S-D$(aY3;V z`TXCGkGwb}D8SlE8uLN1L%x zjsEFRfB1K(&wsgpA_NochCo>0wskF*j`6v1!^)nSPrz{ERa#4~Zp$h*TD}umL?amp z&w0G@%JE8-6wBf}c<;<*iFr>5b>mzSKtRA8LMI`=0$HpB*s?M31QRW7PIC(KLp)hp zCJk_`Gz|>VnK#?uG?GIzX=sCt#a@iLuFl^>imtzN2@fg5V?oxhM_xYYdWd2-ve+(M zL(&RUCVj6BDNuz>n;Oy7&reL{S>Q&M3sIi9(^Ndml}YP>kZ&lQPF8!Odvd8`DMPzA zCVi5fGMAW`lbOEUbXIo$NJbF3&oa>|bKP#%QTE0e}Y#o0^ z80F;EV7pfS(e>T6F8~_YSw%&~$+6rC=!7GQ_JgCREoilMZ7& zjU{Oqmjjl?g60R-27q|VrsXm&F2=>_O~vNUp-%{A27x<4pYVm2EA!M?>M)Y9UqeBt z%vuc5o6}0{9P?2OIfL_sFS^|seL@J$DbC1H-aN^Y?D<5_?>2w=+kLXl)LwdAgne<+ zO5JNi-Sjwf@=-LR>mKuNhpAFLkses7Oly8XYJoz4y0h9hlNz+(W9$&8@!@h-p2ZN> zjuIBRwzxWs{($_!q@p3&aMK-|#%jZkW0Jz$jH+AaA0+wu*DRwc?aSieu^j>zBWR zhGEp`^MUPje8M9%x?SCCW=0{XkKs~_(y?#_{8!v#E$;- z^U=?LcdYUwbdAou?H+hEHvJ}<9RX=#9n2&LCfyH-K8S%;uM^*m2mL?>fIcZ!=^E0@ z!XN+mAKSg{@@`eFR^)&DDgF7U_1jPDSNF-QyVZ-^wadHY{iigjM^6RP*NFPE20{yw zqJrTAtC4Q(>4Tw#tQMe6J<2tUpfbpN8_N)BxGonst-e#h%D~WMA^?d1g7|;xwZgDm zjVcS;atI!NH6L9@Vd=dj{CRadIhCItTpwZ92SrKMW8)iR$kl?P+QwdeZ-LnV1ltWc zaFPmA(QbxAt?zBjMX3|R9)GK9zj}`*RtL)1#t5z;jyhB`S(V$!=pf5Mx85jPJ!?lp6a_DF?y|8C zordk_(q`nv!1G_f`So`|`>r4C8GA7hmO3qY^{5sQJ<+zF$Xm2ko{%=$RQbDhE*Bf; zx9o-E{Ll&vc^V-nS2=||qhDI4=(LMrp9_dCQ#U;AeIjAdXb+rO^r7r9b0}g4)=yWR zAc`mXSnWj4$TAxoTWM*PH1!BkJPN)R)Xu*6!}m{6%ZeGWi>fwWW4D zg=A?whYnGhscp}eX`0r01{T`7BX5Pda0F72$Kj&INx&C>8$n}s?&w%fknY(wA4fo+ zYEjKFq2hS5AM9zT$rLoTm%`LV-SMw?FZYlnJB4jkxb1Ne{r2sz-?Bhy8el5^{*~sh zp#Hu{cKG_!>0hYq`>_XSxUE*Yff)j7=6n@Gbu%NyW7bbO`+dOO-c?Pnj_N$jM3;B8 zReQ9Jpx!)1SZd2t-kBk;*Go8jmHqNj)?LaIlE=Hw$LeIKv7!DS+-!dg4gbEeF(Hp* z`NP;;D{adbb1zuX1+clgN+ zIWbREiIdC3{pa-kXThe4yL&2c=DK?IbTOgM~jQpus!Qh#09yAHI9s{R7fq=X`Ko&??9_)pS`?`V|jE zyhzK)i-Cl|xOsODH51V#LH+xwcb;YH0N?|bNRT@XKWCNCRz)2X2e!Im%;@=(^~JTVMG%>I_gr-{kq;KWs4Un=ofBbut5?+OyDJ=at&E$9 z%i_J9C7e*csEMVdY0jMslWPO1&4^?=`*gQWunssY9SArECN%6vn#Nxj%&|%B_Q4U( z0@P4k{r$$q{ipwzy8C=;G|ke4{|~?MZcOZMkL~U7AMV^!{!#ra$dOZ0vT`I~_q}XJuoAp65CDIoG+&`psSK=n~lyYCpod z^Sk!P-Xg3Xgw>wuPJxfuYwAqX=)1SB;B*(Ra%DGD-b#s~%+B=;#v5LAKYP;geK_$A z1(|lbKP?28S`24z^1~isMPRi3HG1@d6VZhDb5nwW0QMwkp<3^;zIH- zZ)UG=Wono&291w~hh1B9^u6arFAY$_G8G*QF|d^M;<1Qx<%z}J87BE!bdGA}!{vqT z*{KIGDp0tQkt#_inif=_?;uc)2vq%ED20Fs9aKxQ#lIjjS#bs_dSEfJYgn`gADMOU z^`qAJG9g56Nw%P=^ylfb-ti7?k+(kael*u+`Pdq33Jkr;me-Nj5CaaPOy=h+lySTn z=ebnXPL}RJuHSvEY@IBHb?x~E7agwt`&U)scj{5*ZxY+}|6p*+T<;ZPArb{u*K{?b zePstK-cfjB^*A(3S0CV_`?aH)$xMrc%2$NF!*AaWy$3h*K`(=U;3VFt%sI|RCwPIy~>*7L9F3?lJZb@bi7x*Y{60YX;gdGjm-U$lx6@+rz6(zlTUL1oMIL7y6kbtRBGh=B(Of z9AAFezWuawd0)M_-$V$%yx$a6aQk7`>Q;3M4a{s0CdtPSYo9(T&!m|Gt>u^ zAYG}pv6b=Z%^cGSh${dgbnZct!uPFo`co}DB#kNDsuqy)UTjn zlO{zn9SfDHbtK1Noo&c`Um2<%{$=}io?qqfKFpy>aNWVZhwW(iwf+1#g)N@n%tC(1 z;#B^li`@Ok0(ZnMMo-%@F%80x5Lav`5j!m+O$5s|mUnvo7@j|Q7dr@XX=p-X4veMd z^ob%T+aaFY`SqC1V=vE*(C;Y%b8VS+K2eq|I%mp9fTa3k6S}f7QYuH5%aQ!@0FFJ2 zczStmnGXn0FC!i5LV;^vdMApWrybuvY5eBxz)NWS#?c}_`SxgC>e;P$1aP*=c~oYqB5GnX5H1iZ3GfpJLQDIgFD{CJ+&r3- zro_U|zCb6YgyN;(f!kwhvlV|0Wa~sjcec|JVB@CIs;4?$JaGv2BrJ_ZrgJC=EKg%Plp#D4itYhaw=4S z3{aF>HtZ%AhEPyKslVSRtEtc7lPoKP8eK7js}(~62WvQ|00Fhu z5fpEBL)FOQ=B*B`t`A4^y{(fia)e!3MUVFC{X~6FvYAso^9MCwt@9Jv0}2@037OG? zPIAYmvpXkf;d%SzMJA}{EKWQ2J%c-KqTli^*lX|)fy6fmi=9lJHIha-9 zEqz=p^CPg!A=n*n61KvHsd50g$Q@n=lTQi1a5h+RR;G3^)S95U;lAZGLF8y3tCR3T zEr`p3k-o4#I+1DV7=4RIyRkQ^vxQ)53?hYY)?vTgY4|IA=gZr|E3xY3}B3Hb6aB`J?mTz!sw7&azi`g_Fx^>{f4!6A)vhyq|GzGjQO0Pzn|h^zu2| zPyBqMEl9rAn?uu+NV>%s6S+=G9hN35U2PR&Pk_F+3)me>BKiSj#`LOHp4r5lNpfGW zY)3bDC-#q$4&}*0-;5Ze;y|>}6J>H$8nM#{5aDcd3?}D)+4IbT5{;b8dZ2M`=Rh|s zR!sCVchq44QlgjC_Qx%c6f33pVZX3QRTRhe$+T&+p+NpqFRIf3GvapcT*wxmvEO0& z>tI63hG63f@O?spQeLLGOO-}IMtE@=Do@aZpV&Op-R5KqPK97WIw_}{dH>x|=sC^) zTzb}yR|S6UUaZ^d_U1a8<4x!d?4jhskENU2M&9%ij-^`@Go4Hrk_&@Ks-4-cP`+Y~ zkP4KZc)l~gGFUyy9bA=nFPR!G)9>GX+CRJ5-oL1BUueH;ES+yY?yKbYpAP8yt0(gp zx3&9EM|Yo&POi$S;#ho6{M>L~j!dAvP#QfrE!}+B`|#=T!^gww{>;?uKzgbxmTZ36 z^3Cs_|K_(Z{#si7^X?y>b%)aNI`YQw1-#VP9Tmx6nSs}O> z$#qQlDXT-cuMbMA7BG%80!wXqFS~nQ;Mv7U!M@uCHN3kzxR>mU)r!H{F2j_8OLraW zZM-qPJgDF^C)^fveio%*Qt z5yyCY8MlW!`le2S%QD`7&dkwPVAN`+5i#05S3^}6nB zz9xfd&@JkLva|D$%H9$o0mkvLo1XG(!$1kXA7X~YWzxJL@)1htSQgdTU%a>B&XY2`k%k+`o1^TVAk$SHjhrVC1*P)QjG)Aw{4?!U`C~EVmabn z0(6Wo_Ak`t{(NSo|IvH%3;sL0=IPbU+4b!0U7n%G(OLTVJacj5R%hsuJ3h}Hob%E# zR*Sg_72dk`;yP#9P4p)~*_!NO;Nq<(#hxb*#)VDp6nc;80I=_r{Nmd-wmb}@gTZ2S zSkH|vkKovt+{I3z98mpp54K8E4{ddAtO$E;4H&uGSefi_ErxlgfQa=%i@`dZ2H$Ep zUFK~&4#%d`RDaz0b%U>4CK^Q<;=0@4y|>R|SvlE^^sA)=$OBs*w>@FdF}gUU`@$&_ zwq@A2YqRau8ab6sDKb4)L}9O(J!fBmxZgUl4%z5J(}JRP`yrkl|7LqCkV=sL2qMNMh=iN~kt3a6T* zQ*8^yNO^O7vlho`XHwxibWH|ifJ-edQ(`GBi$nU!zIDl}McDc&2zvAAs&;x+yS&@K z`FMD7w|D#T@c!fB`E_mkh)vlPyTa=ahxgk4^HQanS*#?L{kq}0NVd>96<=LduPG~7!dn}fjnd3sypEkImvGIDPEJ`<%81Rj*vUSm$Q zjwE?F(>BVVO|nv^1Lkkx>x@RZS*i>#Z-zI2ZGVOZO=_XHIa=>?7Am8gI}XkXspj9qit;4{+w%>D*G23rTwL@*?v_wKi{{r6|L>WSWnLu`4)M^~E&w2`^Nw_Q)`JD+qy6yuGfHoSBFgryu2T}|wipjn&k)zZKNvuop% zs@P@4)Fo!K5H_ z$fB$9vo$mY4oonJTxv(M!l$aW@7QcHs&8|6X6)s|~FoB#9w_@`g6^!)p` z-Z)yGSRQcE{l4m|;?J(lf*#*2NJRpNlk{>%@-ioL3vL!A=(TA4ruB@pn(bLLqx&Dk zIy%!$T*C(pv7hyaLxe~W%1Zc&>Bp-?{Sh1Ih_q4xP;VY)mNwiH5uH`fcwNU>UHvHB z@|BDscyC{_2~o=AB|>hFb=yNqm$L6nwDQia#sB$_zsCFX@9)!}`F~F!IY()&{?%3X z{5*YnmN~u1Df@?K0qrM~xxAZ&Zxf}`Td{8<;5s^=n%$f@yU8=;M;k*^;j^J|L3IpV zh&`lUfPH7g-idv#1jaOH8;0sA<-k~Gj524Z1(dA7Y)9$uz{^au?6w*OV@3vT4AfgQ(`NMa?mo_!nO!j zY`4B84I$9i$NcVg)|gQq_KiT@saSceAC)!^3kRo5=g4fAr5)z^o9S{TxwQkwD_KWg ze>4IdonLQMcGA*Lwh5!_-6AkEs&Ny?>^6spsgL{}`3v))tB?S!W$nyPx+qrCYi|&%$$ORexmaz4# zDugkJZDK`!byq#P#7b$ma}2iu5^K=ZjEqjT5>5BU8!Ts;`Ga`{DH?n*KqwH&SMBSO zF?1Kf%WiF4pAXC*nQB=q4{dCZyQTLjtV2f01e*f!$0&|Tv-}1!vml$P^RvR|Ps^tl zGsh>n^Q-xzOK1H84LCv+`2d>>T($h#2m#Ce$CbN}tZ&xtKCDp(xMNd`0}1ZUrC~Oq z8c1&l>G#sg;KusMVzD>NyfAQYnqg?k8yR}A&KfTPfI7b76e zwrsmr>gRv@>Caql{#V~_`(#DWFZ7B(4^D{dKE5!B6djxZ4~rNe1lNP!b|s04TSHS#*{xKU}yPB(_JdOSKT$0a$N!O9}o zs%~X3i?6b=Gd{aK$h6Hxw5#G0(4A}!{{LE2_A;9?D1&9R*XC^U=j5`fWc&~R{Nw-X z7y8TiH)Y&foj)$+3c`Sadqb;EDmtFh%#kAs%mg;VSF%=VR*iFUzM zllaog!t1ejFD&w64Z#pza7Lm@tdiTnXb@sj`D^97)pm3UPW6DGUMTnKgZ~3>s7m5N zO0ckfwyr#W_;LU27C99Ky<5MHu~VeP*(p#7^%TI8{SGOL15vH>wcz*rs6Cz>FS&;`Og6vO>W{p8H~12HgTGBTC&tA=(>;bcPzz7n z5e!?yHgTMREN~9Jbw`)@xAJY7Tjo`R^5F{GwDY?F>5!$P*7!k1SY=g4ZK_mBEi8^^ z3xnm#-~1hH=eN&( z^FzZ|&5>6_GhMji>a&=2h?$vsvK)iXIcrku86ZCZ&GzAe7}M!>4i*M*S~PFv-rkw+HxdmgaSUa2sXW{~@EnNLCO5A%&Qm=#-PS+;wwql>u3hsi*BI_$ z^GiTS2B-(nEfCtzuScs#%tI0j8)J#tp4@C#r5xGckDZ*PRP6EPA*IZnMvKJo&5#In zw}hQS%iNxkcfWNytx;KoRWiLlBK9x1Kj*jH@RO(r_{q@Y^3Ei7Xzi5SYK~zMIYUGz znP^PSbd=Xe5Jj@H-2?H4>}+RceQ18MdvdC2dQRSM;^uz-^dftDmfGKs7nk~mhF`UJ zJ$={pbS&Rh-A^$6csP~DSFH{ol0&Brn>%?*ReLTtq&ZHMFkuM&HCPPa)g9iEZWScW zg^Ab}ZtpCke_`utgL8OYUEE38>S8Z%A5E7wqa*iou?}Zmdp& zVZp(Pj?mle21prMq-#oR*_{T;Qq5rfhRNy09|P7jJyQ*SGr_M@K5BowIp~d$XHS_FtaC zpm<#q6*p?ni_o}pwD2$g@(a4y{h~+6=jvwq;wp>Ab9Frp=S1V&Kg|I756&6X zPl7lgZ=uVoj^j@2Kyn)~)Ish&)q}()mVPkaOh{e#hMNu;t;jZ-DsyZLzQ_C-6inG$ zg=&1fIGkAr_>AE&OWiU$`DBBGNpn&H+G6PFL{{kr!?KCbNxDrj(Fq&AG?V&U8cZ8N zjw_hk)Rz{!k}uq-Y6ysu@I)NaQ^tmo*HBj91ek15%c(%;$p|h4v&zFN%esw1iJbyL zW4ktyrR&seEW?%=CW~fXIK0}tz#l#8rMo)6WIZ#H#){OX zxE0|%UK`4aU%}2|xHueaPrNYMD}l5C&m8h6%>rX~+dB5|iPaH33HT9-RQ~Dm`q;YM z#*HX|IO-*Bv@H(IaqvnyO& zn>aj2+BbAH*R?(R`xk%nsP4Cqo4@Uf)zfi=s<<)6ze4OdP8Y#49>o{(ClFQU?31pi zHsg3KDEfXuJ(d3#K?wR}$;eqK99toXaLGk+flQ0sj1R9jeX`R;WLTr5%k?xny8&y; z$jjEqD=OD`o{Fm5mfR*qr@{Whus<~2$^VV|hGT;^51qRdl5vJB8)Lg-+-nJZ491}- z>)t(MzKH;}80BW;Fz7o(x-j1m_!W{z94KYBA+zKp!X~8n(Ij%3-8;_G=V|5chC-zksYFT+)&8 zUF%~inD%Uk!}pooil%QmlWp1?AS7%=(=`rwFOL1;kAL_@i_e#DzIC+h$LSUM?($?X z=1bKC;M;=1d4TziI7?&iEdh1!v64d}v$#FYz)+|<2_#Q!?~?byuuRyME)I}gPz1Q# z!X4CVGyp|gdzmOrgpxAVK00rv$l|aq?aZJeoZl?DD;Ue06Gx`nY{{8F8#EYdA$YJH zQK@j43GtxQA+uniKd6vc+?0n;E;fFJx9FGn5fmTIab+Xulp^;6U?xTa1*jG-s0Q2hseDI9=F2N>VtE4F1F4>GB4ZAqY&47 zQ{6phR3G&XR2R*5CvrVQlWmzo zWSvB7VX(31>5KZ`eOLc`VFwxMX~8&}oyD??2x|%p!~+z^($RJG=YKr7|6I+k4MTyn zk#;n@mmAj~w@+@i7)_G~VC`Zaq6|GBeASd}<_uAUPdMxBb?Hc&9Zf@fuf5M`d=2!cs>l59FF|@IEo0}*xZ_^Z6#^~dTM&< z#Pn*ggrfZ<<;z!s^ggo|Jvu8~+!aMTIS6z$x)|1jp+^>K7 zasQ{Eb`htS%0r2TzKmrDZt7x0dtWFHTHKFh+U?7(K3Kgfjn%Kf+g2<6e z`AzuH_EYj6U*xWCa)F$)Hd5N<$Q^D83m=utfY3}4`yFZEbw6a(_dPqnbBcSEIDJuX@_y~=} zs$@z488IUGX_S&iiT-rU>%QmoiL+H2y(T*pMWdtz$TH`NG2{&dLxZB1dCAi z0S-2^aP_{qeptQQ8{B%CM9Fz~|9~LYMvt$S$(Lkm2ill=>K?UYaXPpSPff24r{;TE;kiB3Npu}wt}0qTh2`D+fJ{bX z3npjq&pB91!l1I9JwY_oVlg2DMPD~IKOp{+42{zuqh^+~bhuCm_aS*p;JKZxGuy$> zKe^r6v&_9cxcz)`rSg9~LM%VOsh;2N+}clg~@u79C4F_rD=^CFY&tQh82hdYMfylsE{uKNd2>dCF9eZ#v49CoCV zI4MNfn_?D8nQ-mIfEY$6^U zq328DOZHc!6M}i&9(2%7;fdMq$i%ymSlwu_5?G@M6R%Z~8;|5c-1z*fG=NXSuOMm&^9E z60?^XT^UEhw@DF5adtIxd^vyjY5D%+A{)2$T9m%~LXY%m}`b`ux()RC#X-v7tFy$Ar_q(xe>pO)y3j*~1b? zILseTP3=Rk`eF?X<%rnSLF7=KE&5LY&;n(Ac6QV9dpOAlR;l z8sor*YOZ5fx|L`Y{necWAGY#-`tW>u|11}L+qTg-6HpyiKWsCzq;2;Q|WOnai zvJ|}d3{BQ^3VuT1G9OC{>yY`#&0rK$2p4yznsg#)Q9=wfE(dtomoTCfqLl4yVGA=> zN1{avq)6)Uzi)fe5^qkHqdvjTw0ow@P=*`=b+JgrBphFj<4ZWWxxV|0CJM<7bYRkl zIN?!i)T?J}76+-dgj5k_mWX3vJ39%Dd9au$^7=bw>_11tb#zVEx&Z5gMem=aK7Cx+ zJ)R1%cCI70(1(rLJN|xZp@(mCvN$Ly*x=xnlfa0|_{ENx$CqL*Th=m~cK+@7`*w+h z*mTp@m%SQM&Jy|Rln?x+C_4g`*@~)`;Y}@o+d<|j5Y%;cw}0_rA6#z@eR;cebXMBf z%xu=|ZkA

>bXO7+lW|EpMi8?sq?ZKE1rJWea_6JwLo^ebh7dKD+46AI?MD(j6^* zFZw4M24!=x#yVOsi*?4i#0*~Eg>)D@3xT|knjb+49Q~$X)M%cl!7FvPH)w?l1ExHO z+Hf%nOM!}CKnqXP-UMVMQeXT}zydC%Ld5T#ZjSspUz-~#j*ZOqFy5z$h|F}=M}pC4 zL*Ea?3sTRbgd$}J|M?HQA3i(iaXu0I9j}Y2x$dD@U2d*xYj1Mzc&fT9=w#yJX8zNUI~O;r z*2-=M>|EMgCzo^0S&z*Hc(HlEysn&F8P;m0gW0E@Km7jv?|7WLIRS>e{Y{1C!y+zL zjRo=An0RRXf2>jGEPqjr2k?z4hn}C3ONLY={$3g{WP^@Lhu~hqC{_{&vhChp4;NK} zb3~rn@Zzu_%V5Ye)nRsSh`u+Gbp(r^?$Gl;{H0^M{pjWwk6%>BDu+wG`K|}bbOc&! zSXN{dXNE{H8A=nD^#PJCH|Hw|57ck+B0WB;M~|tIXFC4d{pegSteTb5_us7!TFbW zk|Dd~XqwO-_=6l3^jTXIi=b?FhVN&NC%ILMy4gvM3#uRsq7E|QGi+<4hCLapvI9zp?A|@%Z|$fN?aXx}92yYZ8(g55cCytI<4gu7tm4B< z#Es$Yo#@KyK-2JRpiuYdo3_YHT4QX4h02JRAsvRaZS%OWea4dwM4VF?A!fk;_Q|H# zLxHf=KGkNa?YT3muoMP#gG7*CtdG0$QYs?j%TZs5#sEms=hx1wyO*{7tL>YQ2iNzt zO?j-PXqv%Vq^tjROUKhhvi;&pYWBgk&2Kf`J@EQfJLoJyOIUud678a>#&GSM}f201h4XLPUNgLFGr9Bsd#c57}& zd~SIlh8Y`gc+&jn`^K-oevkmTQkp9)S%h=h$LRf9cQ&f>IPm($^4{2fo^m2`rWbqH zH%IsPC-+X$xUUkEEeeT1wIX@Th53jclGPhW5qxh!JYC7wko;?~3R%5_VkjkUyG+?i z)H`g?3%b0y$sQg}?j6Kyd(vBGKmWA#@u$6?|8V$E|Fr-4r_HmQ(%H@G!R36$B7U;5 zd+cpyp_g-YaXUF@O>>wzy_Nd6Qml*(PSm~YdDb1PPp?E&5{Owa&^QV42Zt9M+ox;b zJ{ypxkvG42_Sb*+{BPVsi4tkyvQD0v((YM#_i%A}GeOB^@ac**NX^?iDVFvZc#ped zEzq4NdQ>qJ!X{9NL#wlu1BCJ zA|EtdJwePq-Es2E2k$;q{$lq`N05(+!FqTE04cG>2yui-&=y-*HssbI>5=TtcD){Y z+c@-w=?an_ngP*0wZ6+bN*cK}p~738S9T_>=JB$=7%cPjtL~Fxu_nmi60pqm{u5ST z+-;oSp!a0-0~5LS>Dl(|Qm}%d1h<;BCFeS8$@o6wY@sVN(^jqwme(W8YXcBzcL-m1 zv5)AEV!{AiKubJxR1ljxZJ7N6p#d#(2sWiHqQf0;h%fY^HW_&W5?dSdr<`SfbFv>F>2s_X3ep)uIo)~)Up&aSr>*2dqoK7P~u%|NnsbuVYH zZB1|+HU%52o09`+2GGy`?#&g(c@*9kJ}d;aFZN3L=BEw>*wVxTr$JBkT=(i(OA$}ozKp6!t^m%877 z-}1;C$h%T2j=8~;*_XtY1~6DmPhzAvvYG{0JJ$AMc0-)L%;|?G$*qGVrTPAGdPS~K zaR_`@laG9y!%Tq3Vw^6nQ_)5yP!^M|@Bw$JAHn1y1iH=j0(rt9bHKlS`tW%2=qPb~ z8g5sd*=2DZp*bD-~56uZGWm@(4O9Wr&+_O>%(6_ za15*_mPU*}?Wq=fXPt)KmLX0xu5)8Y>qn+LsN{r>QR4KcTE}FAvXoq(SlSxjl8(og z;b0P7a|e`hnA|<4(aVa%UOUKJ$#>SxC-=YlLkG{b17 zjcIm>YXJ^u7a7Y)X$)wO&}Rw7qg-o%6;rE2*B{US^y~bk{DpJAEiD&KZVU%!Q+FQ< zcOU02ZU}pWQK$cOE}+*cp1^r(N7czYynCEpM6oBg);31w zSNd@dS<2+4H7)eYMjS}DvG%a%?0w$a{(Vc=v$o!sodd6P z^FwD>>zCK-NshrykD7a)F`(A*B6B7rj{pk#oq9C-~r_BQ$#-TrrS-W$Jp*7F1R>^J?dI>+iJr#nXST^^zVJIb=9 zwiuKmA7)oD_$(cj_Rh+C$MXvei>BHoY+BHRV&tA{O%=C|qdayKiS88D_H6eUZf7NO zbUZ07^!O}&aXE!r#xQV8k9|3JdO3T1KDBe4nlB8jl%um-lcq=qhnGsgn_V89F9)0b z8GHQTD9BGQrgPB0d8p_4?!wX!=gGB>P9`hW(W-}n&G=FN>cjfa|G4| z2XQR@xrNS&spiRCODf;SE_G_rb^zy!f9WJ;VsSUG+X1b!P+$~!cFjzqLS)AZ36qdl z$=X42VJoZB0w@M@?HX4+!)e%H$dMmM-SlY)2DUk7SllGGgb2?9pqpb&cGl>fA>sF+ z=n;@s#%qqhAG@UVdeW~GYW`TA4-#Eak5(;&O;@Ki`GboQo0`{lXT8rE`^X6B%5}ML z&nbVIelG{@37FKGrQe_(KD+xhqQ=poyJp&TVqU;3vGY0?_Yr1XxwU9e?9TP^!GKM9 z_JIIj0v$XNv|SzwJOQD5gHt4aJvjUsB8!#b)!I1y3tzRc;oV^upE)?6JHA@Fye*yG zI!|RH=i^J9;fI{^2Nw(Fy~+Gi&|h#vH8*u98iC2YN7_BvjWL*BdyqWpwrzJ+sw;wHhr$IN)0#iVs zNz>!3kL{hLRHytpe++sqR)m@J>)E5r(DQR+t{zRT@24d@19Pl{IsccoW0jp4NMCRU z%R~{kf-))W@=jPu;(~H&>~<$n96|2=)17!zo^)6)VvovCc+1h3upN>E9%sF4zpRb- zIH(MiQ(zqMU|=|Ga_`UE(;@S>0yYKI&h;4&&2%h+^-K2ky;E%(2JVOk1Y$A8M;An1 z)IT4AUBA;Ekx(UDc>+*T^-VT*4!@e1eD4A0;0lh&V45utX6L(Ux+e?W! z?)>t#vBg05vI2(5upqt9{RoCyxQpZl6i-eZH@Wi^0RwMGZN%lK zvNU?vF%mJYXFF}X+~h1Y5%qC+2*C#3ff??yb*+8RddA)Y+oek%U#{(+F5lh@mENmt zrv^vsBhkk4`s7M^qEFCL^W)bIUw00?Xkq3*+OTSY#eL!+KbCCzq5i9PeJ>aS3@?q5 zJU?px{%_v?!+(GJ-~Q{j|Ki_*O-~H9!zWHr)yd{|G95ElB^_t%ezkc5PoJy{z z&_7+9Wh>2r`FQ|#zJ*udmb6>4qeIO1kD-B)CzM9Duhw(b({<;dR@24W;u?LjR^Cd- z*+_C?p(l+sPHm()%fUnB?hR2^#v8}zPxIYqb%_EC{hqmvQC$SufWQbjm^&v~(Y;5P zi-hM#r>PBN{@SP{ZsDOcIN8$3Oes<*Mb@{+=U0bksQdT0&Zdtq^4IqZKu9>hHIp_* zLe}2J;^F1Y)}9=+!2A*3di|mNhd=E8`18(>KO;vKcK0U=#onpumUOmhIMDzZP87PA zv;kESllh~og`KlmR^9?=q_S3m}@|x!#`f_al={FfPkqs~>BEwTHX|^h--y>o9Q= zAaMXV=z}Z~&jHDSzlYj|S}fiOIp1`{pObP#Mw@K{gj{>iIP3j*!{g5HnXJNCz}nVu zo+`_s;w*0G)7~%jJzQrIP%~X z8S*}6DkHn6YyZ(N_@DFNRsPG%3=J2fw$GpSJhR6aQ`a}y>#Ho(6qEV<%EC8gbW*YT zpmKQdxM4esT=8%nTu8*r1xmmFWcV4J+!*)-0X1=i%I{Cpgk>maEmw1GH|C1y{5f3% zTaZ;SKRtc!H%|%|ar-H-kFy^0%A!{Lp*aV|pnrp8LlSfa7{VDQ-v{%j=?HG0T=8e% zHu@!ZEWR$HJ}jEN2whO9^yAj=s55MTW~(9Ul=haI>`U9p`GJy!+m$VnxtQpi9Z>&t zk8Hv8d^b`wO9$>=92M~S#WduW`Xse+>PyV^*f?O3nL0;sLTJNliT(2hnTZ$oRkER7KMGL!@JG6#OI6N|ih5qSlJ z%aS}M}PI-pZ!KoQ|Ba(eIi(+tR}RVje{>mJrWaSr$;fj_C@@$1&*(;PbCep08(yj67j`2)88=_=+;vv~;4i8qo z*l@glsWN2Vga|n_U0k~!tN_J-b$demVzD;4xJ@CLT&Ygz)ho5=_F-}_IK-w8j#6T# zS2xE`k5b1csZIS!NPbW59Or)gyl{P2K;+Y$2U3ifF3?cjHM7~)#| z?z0=I_1*mT!R$&cm1SL(@14kWMq-TvMzb$96JX$7o~xi#7=r!P<+} zsgYz$&%o;*zI;;vnO=f0%nLK! z7@R#9QW!9!7-D#`c(f9>V@Y96KW=}5ysetSTO24R1>}U=4`;tl)yh=Tk-1=v0!zX0 zfuD0RQedHWP^pN>tTzRRMEWAw1ly;G_8f9N7lai}4vayW2tbL;QZ- z|B}7ZV7f)|AA)UKKrdqljnXk0Qv4^;4Q|rik9U7TPCn-sSzSG&s{PaS&Pkerdtqbr z@MIEjdU%%Rx^H}Dg&~&RDWJmJ0w71h^QCv~p3ZEaWToHIxUKAl;Kj{@1gD+aVZxWJ zis;%J53>OA*BZsvh&Rfq!3%M7KNX@3cQg5Fg0_p0N+*N6GO-ML9VcVd!^1snLIWA5 zoV4rFx$tfDFn&TaBmiBla_-X8L&2d21f!z zgU^FTw3`oGx;4fSvcfiJo*e1Z&j%lV+CRDBVbrVj4nD48H=#IUAIrQd9_fxLyh7o-o9pZX$ml_#jB0BO$*`M1~ zIR#?Q@1Cvfoh>a_6CIJa4ed`ldS4`_y36b7qvQF#)8)ik)ScWPYb=V_T#hvLz2F%0 zs^dvhvyl(p7h6>iF1d{u%*%D!T|N$w+&zMTl;;(SE{r<1yzfT4@#@w zzWv?r-u~{pw#RQmV;StNRHPVAi16Y>=+9-JNhcAC1cgpjrY1^p)a!@*M?Rcuur(6h zpuB}5YV3KqW6JolWi;` zn(4I>p;Vf&!uq&1z2qtHtt`d$?dZyw zE+x>+*5c?%mv_aR`(=CE&p++_<3H`4-c@R+i?<(3KmJ&``?QYdV+5F(=@8L4oNf1W zoz6OWd?&6l)N@o$feJhdRx$c;mGPqTz<8>gl6@v_V6a2#boK0la&4}&gajh!QP zT|;j>`(HQrzw!n;1VIpo7kEB)j$5C8k{hynaa-O$m@XiC^}iS%d7U6>Ya3rm_Vdg~*HuB!pCl1L6F7^5SOiUw^Uq-9H^G``^ zqlc5OCy(LDiXmRw$GsP@Sb<4uI7KH@;L5O5Sgh5mA$(c(*^0c#NI0Ybbiu(O#Ih^?bV;9ny#LcL^`HDNC_R-uIazT~zg8R|BaqZW zQ#WOQ;n1`n(YNoNBoTg`0$WE;gseOl)(vbqcaEoo)Syy;_Z4t33tV$Vs}%Z>TyI<$)Y~f z4cXlD^i9(@eZy}Ti^E$x$rIS-`EqVO7Dr-9wpmWK4!wTf_Fdn|Yqr(zIqK`%d!BU- zz8H-+PKz@ujnXe5O#w3%_P=}gyML(v>IW!Up*L3z5p2%V#-(p{aG(4AWN!;FUos*3>9fXXI7a@M{9D*~*y8pIGeYVwB<39qg9}$N+N>+oHwIiAAPH z+;7T51ib4T(K8X5XStn&$_*u8^=?&`K;TlqS!XVTaq|XivuKe(mmi&en-xD!gl2c zVF{9xGltzcW*0m&vpF>rvS0lrZ6?JAPa;)HpERRQcf`G2$!767ZNo6J_d!cooiPxd zT-VNSd~BUvmo-%D+p$@+RdEc_w-fRACZpNn&|)Qu1p&!)+IFF}#2RK+hlHj*1XP`K7*y(m)y7@|WhHp^?{BkfVo7 z;v2|uay2HT#$r*;Nvutb&-d9^>b^8%l){AMV5QBIrNirR#Kac*BwtXq^u_A)#Q}%! zB!3}^jcBEF{rz*jIOM3sPGl%kY^krbJ^P=$;Q#66*;-Z)6I*I~2(y;#oI1I{=M0(Y zYY%C6VitK*8w%h&^0{bxJr}Zi`F8F-M5}M?r#SgQX$)ERUovijA<0f$W;eqte4hQ2 zq@f$yuL+T==5fH5xA_7px*~$BLQG+G3g)9-ae&H_iQW*IVq-WmxIkD(TQSV=Q^p^V96H1tzi#-NCX0^1_LGA&-lMEN2M~}awnAkGbm?Q4++6Xvjz{ zDVkG!+GinX6AYpOQW+(Lxbb>*0muItNpP*m#d@L>DC!2e;DALXJ`#p_w#-Pa_Q5) zXWzDb_f_*ZU$=huUDs3Sz1M6PiGT9EDkaKEiLN~Euv|Qz(hYF1MTYhIp`-8nC7{bn&N-)&=M$r&nzPz2l3)iKAD8^8pr?Z>+|9UEZf9D&H(+wyVP zqQW{VfdZOjew$#EyBL|>Oi*nMO?3=UH1&?vPv&~pYiJnBjonF^2*#+nauo9!$tSVc zeSQDSZhLP(-|F*q(O$gNc)MoLsYRZ?j#Zuc(c)W#y7cN1aXCu^OPgp{StaFND1 z;!idF|LiZC`HQQm{bOX;_|bWqMfv%4?&dB;@$Q~Zaql@eO_KS%OI{?9ItEA2ADP{?Q)%qi{o@ zx_jH+(21aa0)p7Zzhc*%YPA}wLW7FO+E&k2iOXNw>K5C_dUJO2m7Blb$!`?w-)bOx5{=lcme*YP%!&GcUlKfB`7Q6y zTljMIzx=q%d@0LjcJv)z>#yJc{;PMt{pS5w+(x`xH7m~9*@#$`}+i0piV($+I z1W|T;&t66YqsVJ5-Z)ItZ32`5q5S!g-iLDS>7{hE!_8T;A!0^%aJee6r4RQbRuB3MrtX=xrpPOn6fjBWsae=eO3^DqiF>NUAXXLzdM6te!6$kCqG>`^Q z!zM9^+t2ypj3!nk4dzSC#0D49h9U_3YY7u*VJUKd{}({=1nlfktQ-f)gKB^OVNO*S z%fig(;%0_#Ct%;x;n&ml}9~61@ijPj_LO7D^5V;l<%kOn7hd z12!^XpA|9+ADr#sqCpIWfsper4^sEigwJPVPL=68x-0PCno=!%$kFwk1xR1kwG_b^ z!j`ru5W)WI!TE$k0)P9-a)Lt?cL*9Q)M%<1&S#g0tjugAJ@Tw3vFQfp2BeHxD?pqm)L61u({enWR@XG&A)saJaqFUl-HKu@mu$23L?azJ3^wPiYniX(kp_1f2jMNEjA<2hVl2Bb1%MUs@;lKF!A`}z}jI!BAA=} zhvrAjyPo%PdT$~QqLTAsdZxQgB}Ogd32pD(&Rk}5x_^E!#4HZHsz=&)QNp$*BlEFx zyyRT|Qi$leD!WNWYWou%gY$;%5>_? z^#*zj#NfAr8c~Z$?J^w>U)E1M=M_9FNT9Vfs)>GpPx5z7QlXz93A`P8i(2n(aRdbV zcfQA_)h(*QaP^@;7-&eXJm0D(s~4PLKktA3aqI9nIi7B5AFb2I%Qdr1;guwhzzGi3 z0VQpQ(@^Nedc}kvM-R{#bpH^tO2_K3^m!qc_U2?- zZ5$M4&49~8%R(yI#Dxg6nU(EqYPN?co)BYpxqtT{xy+P!eLS^1A`GIkohWWh!2Ek> zx=w$2hUe|a%j61Ign*oz@nI=!d8i&NiP#eIp<3R_cq*Zbgm{M5%*s#(J`a8`kHByg z2M<1yMzDh6e8nrE7-lz3nr{dqc37(+#$?6B*=>k0vY8tC;|tN;D;?*FgP z1ShW6LbM`Klqa)%F09U*ySZE3&ijHV;`kzSc$T~6>T{Dnz0NZpG)*5~WXPh?csy%l zStTrs?Fs68pwLn+VSL^`PIC1D_#pEPht2Fj*b?$Lu4RyXkPx(~N`we<@4Rd;*uDi^ zd|6!Mhy`nGWmL7H-&E+dG}ib2f}G%1H(QM_4`Wh!JRvaZaKMW;)8>dk5Gq;xjfR23 z6%PZ&gij0de_UKDFmhnN4f%)glyU;>$=gJZoA@^LIRt5YWGO|6E`13(ZXusYlYkTi zmIpcoljqqU+g|N16v$b_nAC1ad>f|}CzMx(rGsUvN(XoobgRDN&u+N)+I@oGeDL;)iGZx~DsPW_qAf zxvd!n(I$o0qwk;%9N{<~j7{oN-V34?8Ae7|H}L9F{a240zW%1^o39$a{<`Vgm%?kJ zbw@Gce^wKGKYH6oypCcj%?Nyl%&g}{Gk zCv2Z;%WkAt`mfKM;gVD zJ2T6JjGGuluxKF8l8&m*11Pw!<500oukMEsLvy`}(v7OApeV78vNob9!9(8KP1LsI zwf)KcBPeBtO!YhMx5a09cKra z$kxM$MX(LKD5!_bXz|T6jQ&91s0GL>OV!wXWvnk3Bwf~#PNMhyFWC`O-XX~Og!O~M zd}TaejBM@1cMnlDW_C`BJ13>eP9`_kJL+zi&ywlTdKtPcK;fKj=BbsD8OC>OqhpJG z7r$`N|3CldzpWpYds#NwN)-A+NJnt_=+kcD@`2nr7bWKhxFs6{T}SuZ?)eh8SbHw8 z8?qnkA#twydG-s4Jpoc%jxc}Yn}jUHYLX_XKi>y51_C@>zD2mDm2TJQo5^e&k*~cE znZVXber-3U;CmLnZ+%1@G?MKoR)#ejM_0?c=gX#V_k*@md}=0`Hjf!GlslN46?wWe zc>np=aJ~4yJTgkP@v}=b&goB|=I=i&-G5qQxp{h}?Jwy3v83#C*%cmN<}YvaM;BQU zz4961pDfLF`xF!Uu?=wRQIcYg8m_dHd=LdA1@Y|(>?}2Kx|-&A!)e433A5#67SfZ0 za{}|{s6cb~FwLybWY>V!h0-6aV2~>LcqqSfeG`ijU#J}ER|)ql_>^?pG1qRkD)0qy zR4)94C)!HO^2Jr=&^0{Y7t$5Q9u<1jdce4ytp_;bKqc zq%$noCu%|29phnoy6d9#Xf{GCT86{+*4`W~khM!!yy2_&zoWoa_P=TR_V*26|Gx2S zEj_0o#%nF(??D8psw25}b`#&#fAys4TN-2`UySSsoyrG!&iR(JtA~NfKUq#C=KADL zLiPP#L7^yHbrJ{dybqPPx_L4B~xdY<^=bVEBce*_EK!Qr>r!bn7Jb`84f6gTctD#2+6i z|SJC^N&#aY0yn{cv=#8TlSj@RNKrGM~p z--;uCBeb|ffIMa*TwL=G^d4pfBqt-XCvu-`pB02wP<_gT#pEO#bd8>NK1GiYK7kVr zE+3@M&T)KcIG6y3__h^9R@L60utX+cEKF^O_b;nww;Nc^u)Ftc7SiolUc5opj~1;s zjY~YJ#|z!KQqm{QL7oDO$klz&N0C$C*4ethxQ%h1trv++GQ|joLt#?VrEPQS!!guI zt!xEJ51x{GY&+?c-;M8`Vmi%j94;=@W@gvMk|b2x81D(IK!e^yOK>|6hL-&q-E2ru z9T=ML`K5dQ|M4$>RGcj-uw|45FB@B4FXwlB*q7z9rQz}Et|$WMdW;&*`@et5Q@3z@ zQ{B6&IvqSg-?u+@{@TIWD(1_$Ni3R&i<{$a6&_;Bi|;Rx(Vkbf`~3D4PYl|Vmf<(- zvdjBfZ!d~3A(C~;>;4xOTl_|h(iSMm`Tpuj0gp@nhG?XcxX=z_1uf|Y7OA!#MU3>H zvE>1;>i@DV{|aC3P5|Ag?Ye!=gqOE-jJBkX!tjxKusUnUQ#3MLC!yxw-c6rhXKA>Q z`HjYl;o(KW8g&Ff`Y@JY!$Hz%(f^)`D4gZAzyE zvB5?*5rCBp4m@Uo&xrB*YHWPCiZ&IIl63%*7@m(zD9CMYGgsK24?!X2qzCX>_cK#A zC9gS=R*0dH7wis%q=J(Ustx0M*YyMF$*xwk3LBq7u`joE48t$-O$zVxv35kx^V>K(BV4;E`y_BRUGzya(genyb( z37;hlF=CBC^;AHD*~mh_R@NcT5DBvjA1q9cp{GU19c3Zzjs+L6pVf7^d3-{5E{vJb z0}r^1W#t5xT;9#@oXuR^EYg8G>54lU%y~$=x8lw48#wcq%iW&tz(f*4G`~Ho;L|KG z?Sxc_U{6|#*G>xOw=2ijrQ&X8XtvvU^T_J-Y7Ar0gwnO(!=kYJ>;qx#5`bUInCM~Kx z@H~h_0tIWphS&ZG8ZY=s20ehTdb(UYFV{|&3tK7lh7e3}O8^!j+j-RRHT}3HtOsqT zJhHG6H6cQNNIW#FIACCZH#U(t#@8%;O|D%7(;jb{S{m3oN=t7SRLYV|vZ`HA{aAf# zyd^f@#~TXMn6^%+G3eDp?Ns&Vn+p)R7Ei(~j#nMi&f|N8@+MmvEWmgZ1dl%epH?(|xsx%K?=zZwS z2c#T^qPj2$ptI-w0;fcanzuOfx7EGr&7-{Mabera3+b451RII=?sY$aalBfW-V^5tKv?y>gKn$?FhD$yiG_ZOhV9cC zPoLfrRPNCuwA8m_at;D^yD{=g_)B4XVSFVKaq1V14;(Z7vmJUB&H=b<+;CLLoX$AGL8ZA^t4T z;RDFU-a3eb<(1GNk&{6j^nfXp1HOxrLr=~#`woSzi8U&%i_Gph+CXOgXm;Z`I9Qg* z9L{o#*>&;?oqk|)P3$Aa$U~+EEkJn$w!g~Hb3esPc4mXw&WIFFjfDKT`C3woLZ?G~ zS7Q6Wj~1@~$t-RA{256EA3txcA7*jLEf(wSRa0%$h`}dte$c`l-$3$akR2UZ^vtQ} z`Wf~DJ;`PWn=?9?+Ktx>1pJmye&_dA1L8ILJm?Vgw|H(?$Cd(gD~KqoJF;(rnFF(H ze`<7Ti!h7KsyG1ehThm6Pppg!m;-n!{!SCLB_aY#b|PrHgUb&ro@!fX6%04+im90y zR4_2)eI0M=n{0_9C-VkJ0OS80tJk{%t8KkWXkjJ}3wL{8zU!BEZ~NoF{EJu|d0q{(R=duRWS0foSiFifnvd$u5HX!l&)#>)CJU-T*H z^!i@R*yUJv`rvPC$(*gaENdW;U{kCqxjdpL5mu-P(U72!aiZv*ZomD{jrN~h&+eYh z9G(l+&1&zV`bKFSe$%vkM!yIE2Qzc?aB`_OZr;Szv0m2<;)kaxqrurlh8If7 z+Tc;^-iJ5Vh2yJ3m&fJC^i6>nR`w5T zTyx%+SPDi40s-?c0VCo~ZgbiZ;5&n7gTw7yS7A-^W}G*apQU7RC>82$!JVQjTo_<4 z22FA#`H-=q+P7yqoh()oJq&6d+$4~N1{a2Gidh7+HBhuUPUtqV!tlWWgC-z^RrgZB z6CM_+0j>clD;jxeHGRSeSQsgZ4#(Qrl75d!4gRA~vjhSD_~u~7rU^i9y>7%y?Vl43 zIY72Ru|it=P3w0E`Cm7F1H26xQhUpz#W7~Q;PWrkwARWqeCIq7lzIf0+i%$r62kAQkG*L4Z z2#oB-AvN4%zIm87ik@EziT07*-yu5R-ZRDGO+OkzO>0s3t=!6Xqm>9`CWJA4i$P^`J;Yp%!6&|#H z59Yexe)sj{E1L*AG~6K9hG0I?D%zenI>l7Xo4v&xQig`EOp3^?8^M6@77%H1Cslv9 zK{DWiH<l;1KecFW9ZQgV=M7v3J5m?iB8ANG_WN45@ek@|$CE2D*#ObEM zq)M|Sl(C*{Yfo}V&$l;Ey#WrtnSA99_@t^5*@B#3JCYm5^1gy-u69yjH$-%=<$=h` zeapZ66_p7*!BVtuyT(&4z(SETg$}c`+3DQxc_=KH+~mY*I9>E}aqK!tKK;Y%X01S3u{52W4S^BAgq|uIs_Q* z){|7^`m>@s>u+Lbl0Y4z`OO5E3_>|=bcUJSqSTufC#Mld!^v5ueww>@zxeJ)E(bbp z<3mf>xeRAIWe!tI18dt8hD0#qsvJ#Ibnc#Hl(9RfSyNXGo2wn?7>27-p_(iT-~YJy z;Zw;3uAhEhzWFdyzsNY7_Ad(LpWJ&+-mP4JTEF!4m?m;mkJnl67at_)8W9lWIx!Xv~nY{gcf=KRG|<189+dPP;OBt_R1ap!=Na z8k*_lpMUlX-o>}j)bi=q&aSyXSftzY?Su$YC1*G9^LxY>fUA4 zJ`(TyUH2aMK0vNhM|bl%U+Txs<>Z-N8AM@nijgPqc3|j%jfsJ&FMmt(7+CDbHkp|1 zMC71x10wN4WT-y9J~~sK*gP)kuMwEM?s?FgYFVp?>RIf9DunE4V?<_xbWOl0tFcgJ zgRZtqr+o8oXR&SLX!XAqmVbrc3PJ8=?(!O#lgGxqyyn|hu)oCo`0u+sat=I42_Tqn zn7w!c=ONBYtJw`_@j&Kxq|!S>fhdBN;p~i1G)4X_i?d2X%-0R4=rK{+MljzTm?O5t z)x$T~N;2M;5wV$i$wI-V@Y9)Y@QWaV#z z92Gj6ux)Sn4NjK&(Z79Dg+hruD~vQOF{uf{GV!dUr3?6FimR&&V>h3oY5X;rlEj`t zTx0H`a+2bV?kp!0;}Nk@&y%wH(*luE4hI)=jQ*tM&LH1M`G5vgk7rY>BU->y<$+?L z2mG0NZ_;?C;NidrOu?o5GqXECxiQ`BWzjWRy=B_CPitrNFAX9Ye*5NYwea1J zJG#0k5VzaJpV>7NwCqvhN^j#rkqgbqEXMW;57vRIu)g`(~|#QBYUFog8yrH?5bAYqCW`E2B`^ z>Gt-fn;Dh7?tl1Z_$i5*LAM%4qmGYs3!}V_(U*!@?Xz~2ElrSx9*y1kjj^T8@jNA9 zQ3l)oxgDGT^w&QVW7-0k6~d;CU>y9^+NiNyj0z1Q0`e$H1lZN1g}uwo)0^7)hw9Q^ zn!O1sEG;>%pgyl+2gVCMvs@`Iqt#m(m16|@7qH%_nJ-dDEYQ@ZVSFn0u93N3W{F$p z1~bX_=Y=c|Au4p<;7lW#88-DjB&amGsm|5>U@`dBDCjFAnEWb>DcnD-54?zSoSOkv z%{KnkIw_5O`04$x5XS!te?q!`cA3Aro~nmaJvqtK z#OXSLYN-G;fM%)*1KCvph@DbyDto8Ein4lZj1Qiy&pxdlXE9y^td7a%3x;u6nKX36 zGo6wOG=6P3tu!v&B(92I%w$dl!7D=}4;E0r$tir)&12Gl!zzazA;W8K`x5semu@T+iEV#86*i}6-R7ODQnX173svuvaAXJ)qbmz{u9PJzSAGWW3XXPCnS zOz80~HVFK(ahg_p4a915IRc=;HuzIbRiApc2RT`)AEM6ngl^#W*^+Tf6y*qGBQxFR zSoP%EV2pd0Rfz}xG^w4d!NcVEW*2*;xIN3_a(Fob{5SOQQU9aILr>qbblaE)d?9$Z zjXZnYeINfsamwXhfzuLVhjC}|`TO1a^)@&7;e5w;Z@&Ke^;g|vFETUjZyVP7uLmC^ zOJK1oNt9sXPt1`~8Kqh|(&NivuHwoHp{hmJPf`MPncD&P6Kun#Dct;~c277psnOI& zoSFvtBk9^G3WB1;_G%m1k}8UVBY1HxMKsi4CR8DOHLr^jHZG3n(-@nK+NcW;&T^|q z^NOAEwW;tsiEQ6oIks%`F~Kmku$`*bv#SSbQuLpG zUi$DOmmYR)rT3qf%oRPnDCkDzDoMRjN4AGt-Smg?wFxB>z|C1>`l0ra(q33r#0gpG zLwYU(#pXfg;(f*Vc$qI$|+Z>kG%1oAs*->qzIjwv-ka zdhpCjzlj4elzBbaBQHjuKN)-~U6&~#Fe?w?(j$fxgsQhSHQPOB>fBO4@;}-5!_GUd z8_=8Qt;!B)kE_EUn|j5Su^G)We%|-!$-onRG`R=%4heSG#OvOv=E13E=9bB|At#RQ zNdlm&HM@~8GIg#xJUrhCU;V?sd|nq~IKR*xGA{7Gq1eIDj(!%_eFL-FVS%exGCte2 zcMiqckoIq#&13vh(iurlGA}(-%PZI&mx#^0EaV$jf*cjAU-OZx!}3m2>z~qJm1Mp7 zLl9b>Wz9z|#zKXh&job&zrS?YCpKGp=#o`>6c1G96Jvjh$9`PKy>#s)EBMD;ty;q3aU#$HQ*ZGCa>4b{mJd>NxGZ{jEUV_4 z(yeqcOt2*?Z283jK&}&3w$qksrf`obOQk`3DssF9o`LY9L9Za~w=l#^;w)^sh|Ge2 zRkJ>tzqiv~)X1>7_4*R)PAvfQ_-MgK*iY5s`?EWSA##M6#^+?ng8=`q=YbUx1abgu z`mIrbI;zaToh+(y&%w+$u6>e0G+u-Xu|Y9)7^T<84=)zbiQ%zoLjse{Ss&I8}mDqnt6O%E$nNmW+jxC6<-y zN+H0k7NHvR@SVZm97toSYXmEPb?!_Aw&uMQdDoM%7l{3=-tIFa8hX;4YnAPTG#zb1 zy*-6SrRxVojkDEE1>aXTVS)@{r{i1hZTn8%Q@dYZ-LiB|nsff)j=PuU~%g zvi}h}+-jY9?8eD;MP!q;_D}J|cq#pGig~pOy5K@*S_>XBSsH+=%d7-6_W0N-A}~?R z(r)qi-QK_a`Jev_S>-SK6|Bx%ZVyhfC4^IF$7!0YbD4A2PRJ?WJZisVx>@BYbNSxv zhN8SEvphhp_6GlXz6TfD=)mzpdsA30KZj`LH)2S-Q6M~sv?ezj!vQPx928gAEIy;@ z3`>Up#!y`ieT(G$W?}z0$p*v>HTxX|t%22+cqzi~UX&Q|;fjY&CzvOF@Bzb{CQ!n1 z?a+Sb2%!y9dU^xeHjJb+w9vi0zxa!;!>{(_U~k^!uOi8h*}GTQ`OB-^yX%4!PR~N} ze|?ijs6IK#UY!*@M>Ci5OF3)^OCasF$?*56jIM2utE%K*x*ph%7-&Xs`X+QfY;mJ386M$(qnD!XW=^muz2DDksZu;D-ZGMSIvj-X?AD!-2<4KA#LdZCF5KWpw#- zAXC4q7B!v_DrW>i!oQaX1;S} zcxi25@4yVWFloW}P&lCIJ1nrQqvx_tYOR6`k<`TMC?|)~UPHlmG6%*l?WQIFm1B5I zo*!v)GfhwJmQ5K3>ejEx4~Lz|)&p~IU%2Ur{T2YZkd?o8K2Pk1i|8T~hgw&?3;mf& z*oI5yQU8k4>AHY`QK8vF%&7ORv6s zJ@$eTJ&Pezn+@|Fl3nEAb|s!`@wd-(!3iZr7H7gFVhS4inpI@uVclU^N6Gl4?Qsat zQuvSk{nv>_B7dPW5xXp4Pi8oh|30MLU>6_En476!x%>7z5)~EM*4Zo*VEd7{X0p@^ z+n1`9wkM0_UUL)rCSF3Z0QkAJLC(XVek4zWqIOPl3QzW%F)nZoZK)4{K;J?2FY-W= zAqi%+pc(Let?f-S4S6>s-KJ`d3ab!*%)e0=yqa4HEv_NpokHX7XkZK|R=X$LZsf%;j0` zs$uC2_Q%Q#_b`0F(g2jKR2ey}Gjb7wJ{wjs{t zy16<=doFk4zrpvIH;Z!B3sN;QZD4W&uj`rV9%j3-lE6Ur3UNR)d#}dyMqw8JBsOUy z4$I`(Iw}9B4Bl<@Np)N3xs>g8tMb;Oigr9VEnQ<^rVeJVU1WpF-@@_&K9DfQ@Qre6 zb4qoPtA$fE4Hsn=OE(Zi|Gf7z>`(i<@Lv5s!4uxg&nH)z1_d}cRQJGhVgZaz(Llm4 zp0hi6)c24R*DEUBpd0lF^+Cv)@7E{AF|+-REGg6qe$k_6p&udtaqmMdKhPo%w1xe7 zK0ia{5kg=`gCVL?9j0xhKo}!0r(O&Gj|Lv!2g^fr$6sN1IybdD{UPJgwR$P3{g*AR z)}ba8>bQhf_3^O(p-GFsdGQ5BlEH$bs7m>c>7`y1tPDM*@C_5!hIkfi;^sQm;cRH| zs9%P!tsiic8&dCa&x)DBYI zsvB5MB1T@WC)`STf;4`9CuIaVL^qW^Wnw@^JGV ztQloR*l6#~>fvH0i1{%vB}leuXdw{KnQlR9wd>l59_}n2m&x+l<#Zx$sAJl=ShHNo z^kgz?2Xnh8v#=D)UYSWRhQ6!hv~nyVujXiWj}X1wQ>B(+4NLnLHuyRn;U(FOY{Lk8 z<7mNgoHwUtsXI5cAtxw)#g)Jk8a{_91pKmYYlhMP~W$Sy{l{&Og2wP+Zj zUSl;-OVNo?2zsB&d0E^-ZJddUEeXBHe3L|_7P=>Mts37`vt8xN2sCmDG_g0qG2ZKh zwGNlyZM3~H>~us#>%pn2uOTc%uS|0kFG)OWu$CjUCm2k#ijg(-)+Z_g^F zZB=BskSg0v$0k`(d9*h}Vb>IF6zRE>}XCOIqHV_QeJ&W%$s11kc1 zmir~6`@Qp7vrEoz<`2(#IweI3`WkdEvJPpywGn6`WBi8c6J*9J!lqV2#zgE!zqNWe z9XQWPMhi)=mQYs(;hB0&1cS(C%r?*8QH$q}&a!)T$A=0uLs2dr1><;*bOpWV_%w5T z7SF`by!kM9ay7Gkh@4zdxtmUCrQy|R&d}HX+b+(&1tyzFAS2=b2quw2(d;_2UW$u6TUh>w0-|w>vxYt<1oQ!a~5mj>lAo3Q3&fe zJZu}Z=k?<<5VGN+)VaD}y!xO;1Ww2v@%SrY1}^VqsJsSl%xPHKu$jyAZkg!`sNF zKQr6xnWVmNr}ouzoHM~;eOT~c-C+xyKoAALPz7rBI@-()->xe&6+XR`UrJg^%=Ubz zi$9oYfvSOF#U(4mc`eWPw@}2{2I`*lsnC5Nv2FYn=s-=^k_>J?q6lV-bV3{x`JH0p zTpz0w7axy*ooo1u{t7VX=0oB7eL(`|VfWFyd7NKl0`|Yo%kiuPy%O9=1F55zcaeF| ztL>6sR?-maaxKB?Q+F?qDSe#z>8H7ykF#VihSx)27D76aDevb@yIU6gw^JO_uzlus z(h4qWixk@JMsRN3%^ZAYby)9DwtpM;9m?9f@FE@a#)IcxB2}5jk&|6Bkl|x#qN44I zrlNwd?7OmU;-0g^#SiLWC86s_CZP4H*9q7`l61Sn%_$~(vSlMqA9NUDP7EnDD5!vE1pcWMIBt+Dy|6;M?}F>*mRImjU=(1 zi`Vnvn7V5rv4C*;m@g)gg9xeU5iHFs2|0j1uNrD=TvCDdV>)5XV5+a+pvo{8_7>;& z(fE28l0WFVZ{m4tp`+Ddk%{)=gYE|=E8RD5G1ICVLJ&m))Y3KAGX6y3rFknawW}Cl zE(?HJ@HNQy%~UfH897)O)Xa8ik?Y#Xy#eo)gQR&1x~pMnBH6y0y?9@$p3E3oyLON_ z?MUh8#OK28Tu|llEfJhGc_r#PL}cAo)WI3JhND_oE)d)9$qmkP`m=SPs{mkB}^CF7P`gsni-ibg_Wpa?y#H*nkTc{^j+0%>sky7+#hRADqUz|5s zVZw25>wy7{JeXXYQlq&?^g*f~iHB4{E6~3&(@n~;Jc{K-CncoEDbGEiLByTeo>5yh z=6=H2HjNv=3UL0SnlLXC;hhU+W4VJC8y&O=uc3noL}_*Pai0liXJ*gJwny}AKQ#`lv3v0s$b&3D18!Cs> zNI^an%B!#=@@oJoUG*gu1qiOVC_Xrczy{N=Nh3_9tjTVLcyI)we~~`fM^ZAUJbw@R zJ3O}+s1z&~NjcJ}dM_bAazedn4VREh3ltbit0O=EKR^H9T$#`DyAYlzpWo%_YlQkh zT%$Y>JFGJ`_4N!m$P_L07i9yboKp>bPs7M_H&Q5#}v0@;c4gxmrT6+-VMc!|Y7Od_|6y0udj7u0|IaqZ4iISH3mMWu+ zt)v7~%LCeuR)QCItdKoIZrOFp_vF=pBVpFK4Q0OXux=2O$&hEZ3XB%>wKNb3peITU z8Rk-EF*gk#AoG{Q=WBVJj@&O_OG+33zlbqX0^K6qdk~Fc5IA6H%LG%|ofNs5P+%gM zwFG;IBdr|GQ7j@9Ni%d;#TK=UUbFDkw=lqB^j_E9uSsYb0uvxK^s|H6w?L#&lrxkr zjd=6kPgAP-8H(We8-I&4r-a1J7ff;pwsVSZ$P@rxltN8BC*8^jYpUE&^A52Ww*~M< z^(SbH-u{~*@n%e_hINnhVk*cIGVr4*&#Ye-9r~g8W2$hPmYLgJkQs%5Y+uY5sCABH zaY*~p4DFjkg2enevp`}6Tx>`vo$5_%ajlEnuTZPO!lx69@ ze=GM^5iIT!o~MTwg0b8~OGR2vsXfJUeiw3G$_MA#~#V9jsKdFzjm^kUqcw1ge`|_ zXl-(1FGaLPD5sTHJ&BQL!LvP{H}0W!vTTrYf(tr|?K4|>Dl|ak7vvXqIfN{VwEoo(^)L2|nBY&qZ*d^mWTA0t;y4RLn8t~q z?l9X?3~DN8h?!l`eNkSw@u%mUp|gz@e4ix7&w!2VIM)W-#Bzi;#&*f z!kCu$hFt*LuD8i1#I2#F?%O$;4l*QI<~R+I5ws=k zvhh$%G+MU=+R7X)Y3@2}u>IW$+S}u+;?c!S{k$NJm&1F$%%7eemmba|bGQZkBkhM7 zo`+NfT!UBON5Xg*^3+rIrmlhSAD`zBP6GCcu2UQk#ly3lO5yA(@_aawZz>b0PHsLF zk7HDHZm*t_`D^e1WC#PEH!52*rLE`RQoT(Yx76<7lj@7o=&wd+6433QuS3h(*|F0X?YZ7zSE{YH&)3!FoLJQ6J{l|Ck(Gqw z-r4P5^z&Nb%U2$t4V0|PGiBrIN}we>4%)6TL|PdF-TRT%E}hWEA{XkV11&OZjd31s z0V6m(E7#Nga(sIN%rH>pxLo~GEO*cXgZ7}f+J7n^c4rFAG!6ta~&5S|7RI^ z-sb#v6E74}O0fUC$iWb^eO-|C#aZw!gp;d#yp&UmyD{Bv$zhELnI{{qY!u`N6MmZVw3sX4Eu6daANcHD>vG=V^hiO<|%ykDH zC?0W+Z^D{MB0iz+xCXr{H+ z%vD6%8RzZE|AB2|dSe_XkX8$9$R@OOFe}qulMb2DyuSdP*DOY%NmtYEwfCmeN9duZ zp4pljC)2{Q zOCoV1AlE}DYEPBFI9Hzlq?4rDgCJfP)@ltq*W zu92u$8yYD0PS|f0fMGl?S-rTPC3=Od^1WfRv$_)=YqrwyRRKMIUl-sMI1|vlAg@M< zu&l{@W;_8UpgTHSU6#uP8&gkwQ4= zwQ_7*eJK4_XR#Dc0*{?Fw_EVj$yJojVH|!$dQv#zIO;hO9pOa4j?TaZ@`@*m!sgg zZT&dSsc{h(MIiz`zB-Gm13L%0Bw1{_&7G<9^Tqccch0VAxwWwtqU_1nWAlB2lD5vv z!6z|8ZR|PU)Rt5$du>OIWQG~6>qGXUv4y~FnTc-zA4#mfTVSeio|r8-Fi{%))iTw; z{_QUdI|Zho^1zX$h5^EC_v(K3{Qdg=<$CpmZg-=4x=xI9aJ5~(CXQa^+#;B}1-v(D zh2&jBOPG5x>QusyeysmHFx!K)EYow8aNn|LRim21dfBL1971q_6{^4r3rX68hc;VF zB$j%UOTD>rpAl&=gpoyFk?`KQu)N4XsZcd+6A;8aq4esIJ=VEJ(jmPLl= zg8&nRiogL*vXSkdXD@Da=!)hemv*N~IM)u+d}vGi`Is3kfUgd#0&I||#A7rqk#%%( zwL3LEhv_D`?ia(mM#`aZ6F==r6q>L^SV`DZxRfqC9|NQ>1$ALNuRylK4bo+A=q#RE zA9q-IO;C>(n8SAxHb@dH6WdNx63uNJo|q2b-Hphm=J#f*r{&%AH52iq6M}#FU`}TZ zARy(_10^4Dnqb9?nRX~xz^i4J`tlqmgP6U(ce!B(`trf7)}dCflF*!#x9OIVVte!0 zb1vUu=x0-pze4A}Yc^KLUDyuzg;HKS+y3Hd=C_<~IQ(1Z6@Fa)76$$^J_#masTz{a zQ~CRUH9N z8KQ+SoJx8(EI9ZwakGJZg5m{msEdRj%Eu~=NR9BB$==Fgkr-;_V0v}m z?g%$>9t)^As~mW?$7TDP{u2HG@J|~6ej}0#;}%$u2O7aYG7W@$)xl|6o^OzfWn++v z_2y7e6davoB=aAB>+ej&@$v8V01LTb-A(CNw}Q+(@(X7C8YyP8cD{ofC0tcC9PO_ z1pr^NYFLl&sAR?bw&WXe7>T`6V2Wn5sN=@97=O$w-pZk&s~u)jOf^61xbwx!FAQ!T$hV1CEW4WrA9V~r9m=#Q(-be-(~WjUStBEuU7IdcMg{t4 z=!rbxZs%PKb=;Wg@JxsvCCcJd`PYm5J7?>r0Kft43t5noIk`H>WaIFXyJK17EIc{i z?~ojB9ItJkmH8;rl^}1crfpkBg%}s})lt5z5*uOv_;eaMEwwzroIklX!dk}?73akS zxjo`>L-#Quy-^YP_f{Mh@SrkufAe`Y%3Bm5`)kq?CkZyUXG1)NM zXma~To?tr6TLH~wbi6tOd=?m5_iV@UP5qY!=g<25>-Tvv{~Fxduxrq_m9|}7=Waga zWmbpBF?byqi%_Sx3p@v#Z0;qDTtw;y2!nQQbB~zMh0bx6-#^Q59(GmsOaXui?bU&ISJ0&MV9k>E%a4_SGf-zy7p;bh&Bt z_Vk(&@R1+Bo@#=hOC1?Ow2Zyr1!O2*Q{wrf&bu$W?-}$Nl0f);p*>0osL^b+o8f_u zAwCS%^QyYFE!q5_^X}uG`*J@GI1m;BDkrp|nU2?^F9_9vhawonUKhYdV99m%ehcO4 z@yN5UTfQ}Zg42zJobmiNIlp&3|6&(eQW@88_SvW1nbP3D{ zL`?@e7zdft!36G53W;C}F3=@SR^<>PlxwJTlQt1ZM}MC~8mG<1*Yf}9+cG_Kg7}f_ z!V;nx6wECu>m<6mX!%!~vqkNs`bAKBQ~}$vuTyhBi(pGG_nYR1=Al;bbv8iYx5 zx0nwgg5EH}MzsaLsrQMqf=d~=Vxn}oByv*kB5mUQFmMq05kY(A`y>V^VNgB>rxXEw zrZX(?*3Nr^3XTJ6SfMuNDPa5f6(pit){toIza526%|VGbnxc{x?9VQhwX%w`d+Loz zD__6*l2^R@E}7XsT|K+rmGR<2TO_|TcIWN)58Lkm04aH&4?O1efo)>WW-IT_#B--S zN<3~7j}98}MsA$2ei}2XhI_JaH3IH*-0|q1>k92Wr}PG=j=#`YY53R4`Ty|GpO*G= zlgohsDA}-LBo%u4A_>tw%Tu+mv0P^#c6d0f1*3J8`61K4+Q|YZH#Pe$Gema8pL4LG zVVPL$!OUE$f*Q!UCK}iyqQbPsqWm9`SeBymt7rvpTE_c^K{Q&N78SahwAKo=$jL_h zx*Ea77w5e(FJkA)@ZEQ5Dsx40T_MGJYR-Z@jRl`@B;2`@P6S_r`d1FipEZ*I(ii8_ zc5@SQEqn>v3Zx%A&&JoW*BaJUwxpfAxj#vLIkz!vZZBNOu5`uJ^)$Rr$y=xg+Cy&7 z6h*>M${JFp5}H%sHVvPa0ViqjVT`~qjw;`wU+L<(KB_PVm7EWCKk2X28-UQ@RZ80e z-kIs_#Kzf}&TZwyH)kte3aAaDD;a~W5eTU6{fb@~2{7T9q@}c(u5dO(YI5|N0D`%Smpi~(})6uW$ zMBdvzT~_$R;e~y9IJS)A-tBe-AYQhQy-d%xbGcE88_Wvlq4>b(M9T!~dgsUhBERbc zUhv}L0NnVY0Z#)@@cgYM=b$VC+B04175`BS{OQK2gfW|5ug}-7w>X0#IS5mCByZjK zAN4)r%wya|L*m8!kR~Yre zpkKVzXxtLU5P=NP(V$l->EK%uFd|FE!wo_z;J^vG5xs?U%aZsjTg_QB`R$^gGHRGu zPcAfQpQWSaXpCzqGQJ%BwPUjX&w8_B~^=Iv0vO($#n zg1z%5-QUL1{L*&PxgVJlNiWa9@t4#e$i`|(4Qq=emI@xD>?=I?5`X?xaQ;fYG+OGu zH9eD)Fu89r{q&PXGkYcV78-J2> zo6CesZS>*!KKeoAGz3L!yW!|EZXK#;YZ3X=@E_k7%Ah@ww47(PCXG2$BUsy-T+$&( z5u+G-H{4(lljb8UT?vB_M8+;7f?5E4(ED9maBFleV_;K=(V#tDvmJ}Ov;X?{fBmKL z`SU$La{l-EpMIJN#>I{HS#)^uJf%{QAOH2WoyaL=ZD(9DwS6+p)${y%7W%4+2a$PD z=-ZxJB+iJlcZI_X(-@ryjGZT_GL=X|F?Scv?fnIm24ih|=QoE%JkyJu0{)flgHSx6 z^k5S;bt>{SDymWIke&<957oCL63gRya~{eEzY8xymSgN7n1enD*!jjFXmRY9x&!LD z-+=eEaXDmi&83dlzazX|t1rpov~e}pLrR_l5T4sxfW)F|0C)ul!4tb%mSwCG znJs*P0#^>Xd$}d}D>RIK@wrqksGa~E@RUX>bp4S!z4ItQK*0s5Ab~I zSH<@~E?vD_I60f+n*mEyn7HPyVX(jGuQw)1t)m`qOjj|T!S2%-3d~R+usjT2bCqyf zTowmGr0ZLG{ogsx9_v%iN*6?d0Gzo+W=H^XwSueBJ0CMTjpkHJ%8O? zrhXH*Y|{X#=4AB#aC-TjKI?rDgL-=(iuNrAH}^hz*!o>_|3h>{{Uc)& zO#sJ&gTmorF(2+Q%y+4O=X@TTclBxi_-03z;<$lO@zd+I!Njj zX7aQH^tnqY1byrE#obFTH_JM4biSrKBKMjg`2dzU^o&!7WkXW{YqD#g9O5l?22#B) z-qiB1g!XLKOT&vjldFcs4~h8zTgdRl@~~un<#3juG7bU~W3I~^!{yrO%0Uv_C-`_) zW0|XY7lTBdH3E4T0b&MNC|5_)3!Ocww`d-!S0kv_b|%S2Sa%XKNbePi@)M{9tWww) z^R3~iRWE{*?0`@g_>L6UxghN8cHx3TC%TRL-DF5;TJbQ?b21+C? zxJvIGC*2O}Eu!h^m4O(^w-$=g{N^}-U45qoX8(roZU}xECIn|5{)vk&ud^#fS{X`xmbdw0^Jqo+($lgo*{4di6p=cdUffpG2+K@HI0I?@1kZ4CfWb)YffL@+T*!Y@ow3I`;pO<+9Sg32o#Uv%d}h=1PQ7{2_u!G-k6}28?&NDD)SXAdJ9wv2 znYJrVrJ86PfF?>Gv87?D(c6@3eZ#{Iy>Pb67Ql1hyhy`NZ_ zNCbw%YGWLM2bVywRVVUMWKAZ32NO?uWuEfNsDe?F;FUEn2Q*6O)_K+O>ZmnhUOCV+$AKh#HemihIbaUeo(Qi2WKQwY!LkWBMjJ@TbG1ye zl5Hz?J_o$eRvU4y=iYcw=RG#3xbI<;4s6eT=$z>uLR@*ji1UDrpgYN+&tU;Ka6(y5 zaBMydW)3pw^L~8uUdNp$konOUjs_#8IwoHU_!LQ^A1?xz!U$kK3?dI}+}bH4>ztPz zXs`~b`P2(cn;9n>siRV`N(09b`4O$FFpXge8)ZjkZR}TIb?WV|?GsfcpyCY&iLg6U zdgwu~9nCK86_LPoQ7z8p?XgmI)W7RTg>+>~p>5W1da*}|%7Q~Pt7Gbo`B}7GOcz;! zvD{H*PnY{7Diq@6T}UROecoiVb;W+}JXV4yoz1pVa6*6#nzBtcIGo_obZk`ci zPe;I%z)o%ttUy|Z1xeDB=ZbOd1CB$h`#DWsO)lJf_B;9NH~?hS^p=ezWB`BihxR+B z);{El$@?Wx;$%$nYM{s7678pG*_>*goardcwNKBq_DsKNPrhk*Q^)~2@i|*3-Z=XG z-SS~sijLi9amEbdjTnaq(zFigN)jxK<18D85n=D3l&e)`;9zG3ECjfH{N>bahlbSd z8TdCQr>xYM(LQ%B*4Yj>4?g+k^;h4%{Z5;yCEJRG7Rf&w=VWthx()M3ySa&^OTQo+ zvJ|Mb$VOA<(gl}0*(^sAB*i)c2ZU$2O>$+}FM>j2D z`#?ug0wSI_BK&Rc1c7hF`3UAJ$N%rYiUQFdj1#R}lXI!MLfZv#P2b}GKpr{&`;Wl? z#?-BPbdzT)*2%=WG~+mR^?rtz{zg5^sfhvLFbBV~Ihe%SR5N_n`>B)k<6OyTCt*`_ zyb@*`@Cj{{S*|Uk&xm%hj$aHudp-CR zEQ4%mav4QApb!3&sZN;cVic4(!#oqa()X=cN*3=#ufZA3i%j)FN(OGSqTK6$eEyGL zS)5(>-D?$uNt(_$buoaHGNtVn#-Fb1+=$~M4>!1UhEM0 z@esn_pv-xoo6j@#H;aBe()r5R-~RU5S)C$(89uxPwUcrmrCeZkB;Z()nrC_r*ouo4j~Nz1>hT4ID~%m-9X61#=- z!1AE9%AP_GL#eW@)aKGE(HP<3_}Ru1RH zNmiLyd6IceOe}~@H3fS#kwn0c_3010zd z&=4I#P9{$U#&W?4t`kwLl5PdT5AKne=l0OA89lf(%<=Ne=C8ka#mDA*%3qOxq&(KQ zIOtEz?9LfqqM*CedH4JFJNNWqJMTR0y!WK*uI5@t8t=EmhL!L`U97;;QJIQgmM%L5 zYPF8mC=Gx@{D(Cu0S1yyDEG_MH|N#rSw#fc zxg^8fq=0+WbMGIX{N}eWeg~&@vWWH%*{}6HP{`Znc89n0%<#C$Hw0xM0u{kalOKbk z+>E%=G>sJ6&NRsyRzt~c_D`a|!*e!-ePTt4&HH*_W^JGf1=>&2{w!`s<;$beNwmHn zUOn2lQhmUw>bQ}pOYadcP^%l$96q|tH>d4s7Ak= z9;Q#r1QEkpx8%ACtA;;z=xUv+LZ!%WtIF1FTOn86aFrZ_z;<^{dx-HJr$K(3fV1T6>IhT0fG81N6a{O?s}`8V$=gn@jVc73KIU*i z>WZ6Vs|VD_3;Ajam$<|5osnk)MT8<^Nzim}V=79QE16jv*g4(&`{#$IMgG!GQcAB# z5pO}n&Thxn*6B+9x)vUc$IDz0H|izGtxQ`5isAff_*r@q`p-na?6Q5&x4b zeJAh!rvN@b{#ucBgI{U4|&0nK>q;nIp9m20qyf9$b;( z2Lk} zUDz{zE(>+Rw1mloi7wby7+*=?!Xav^cC*##hBT(#CZ%C-)B-)IQ*eH9p6V7#A5q=& zDW)5QYAc`gzI0R)Fj+*Bj0OC@YKl4sKeJ^=Ob{rsh3C0C--|zk4I~ z1t@S{FbQ-1&{B@U_H#sV*XVC0@D24-Sim4{Prhr*aseTBFI}OmIwCE*;P?pseUvANDJ7nM70>MaQnCE2Xg#OalkVAkvHZ z4?FInfvHuI&7}I@y!i^5(_Nr3X@HGIUB%YnCl8vx{(*?Q_aQR@EqnZY*dvx2(t?T= zm9#+_v#dh?6#cGYjE5B)*m{@o-b`1Uy{J6fi;Q$3Lkv7 zwZp>S{`Qy8lyLs9FZ=g@`}_P>u;|sBLas%q5H@0C=X~8Tl*-A9nL+qYP)E}-GzWE{ zboX=yNzZUd%uIauu;p&1i05bm#25*SZww1$&LNMpna`<#kRNwA@8 z=gxemd{qdt@cdUEBJAs2+f5CbNz4NH@cXO(_2d8l_4UV3D_f@vr`P4n4=d)GnsLMW zmb{iN=8kUFVznb*1;0W9C$}wXQ_w=q1LCu(~6zx zB!_gTINc{%h<6BaSgObtCXBB}BL!!(?|l$`mS*6;3P z&5?M70jY**jK`(i7r;gM=FD4DqPW2@dr@?RB(Tsqr0A${u}dpwX17gXAIEM2@Cx-{ zA9H8$kcWz$5G2vYXGEky642&jA(Gih_;gTZltO2mC_zue2*?4Xl^}t1);v$V1SxyI zbksUWk%gK%E-~9ljMWtS=IS7>#nC;CtVzpHd;Sc5#7^TeN{upPe&F9v&(}%@5$BD?wRqIJbTGi(hf}*A1nSD zsn>KfEGY8lMP!d2R1>`Pk(Dv8kD0T7xgnHU2E^U{=AJL~d2%;;K(M^pn_{-qT|G+U z=Z1@tiSK^%=pVjo{?-bY&>Bi;jT!0ttpD-7)*s{>#DmI7pdVYO7@b_p8uW(9B>8t{ zv8q!7Ta7@iwqy%iLcV9!vo$DCdSePOX!tuD|2N%tCwSIvj)3pPy$Dn5Bf0~_i(T7i zm0uw!nN(Z9+LdfD`al?C0E@ZM!>*(UWv@mFy)VqQ?zNoNoR9_HR z_9kIRKpx#&O=Qbnz*h@cd*m%7Epnl7Bi~P!*2nxve#?$nQ=oP@Sy2cZ22o3x{wc6q zT9zO1P}o;dwoSD-<(c3S6u^93RS|W(+{1U`SGchM>7Up>TiiWgib*0zGp8c+k>t~- z;N6knu77k)&z8Lff10Q22mz1$Lqvlvk)e=}jQIzSFSVZB$9&^XZu~|AX_e z`q>XIP|leSrW1z|{uwUWn|zG|p4HPY_tQjZ-%o+*mU-Zepc*Qj>|CR!q$v1@d?_*L z#Bc|}PYa2(hfOCYrNv@ATY%HTD8qZ~17=OeK!H0tL9?n#Nq`G-YnvxFc^1ggyd? zlq7g3V${%FkbeklL4~whJ`rO&_ych$vmNYfVV`LmJ*jA;S@|sS5

>iv%OJZtC1P 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

OAM Uploader API + + Build Status +

+ + + +The Uploader API powers the [Uploader Interface](https://github.com/hotosm/oam-uploader) by issuing authentication tokens and receiving imagery uploads. Before proceeding, we suggest you read the ecosystem docs. ## Installation and Usage @@ -9,28 +24,23 @@ The steps below will walk you through setting up your own instance of the oam-up ### Install Project Dependencies - [MongoDB](https://www.mongodb.org/) -- [Node.js](https://nodejs.org/) -- [libvips](https://github.com/jcupitt/libvips) +- [Node.js](https://nodejs.org/) v0.12 +- [libvips](https://github.com/jcupitt/libvips) (Make sure to read the instructions specific to your OS) ### Install Application Dependencies If you use [`nvm`](https://github.com/creationix/nvm), activate the desired Node version: - $ nvm install +$ nvm install Install Node modules: - $ npm install +$ npm install ### Usage +You need to set environment variables before starting the API. We suggest you copy `local.sample.env` to `local.env` and modify it. Before starting the API you can run `source local.env` to export the environment variables to the shell. -#### Starting the API: - - $ npm start - -The API exposes endpoints used to access information form the system via a RESTful interface. - -### Environment Variables +#### Environment Variables - `PORT` - the port to listen on - `OIN_BUCKET` - The OIN bucket that will receive the uploads @@ -41,10 +51,16 @@ The API exposes endpoints used to access information form the system via a RESTf - `ADMIN_PASSWORD` - Token management Admin password - `DBURI` - MongoDB connection url - `DBURI_TEST` - MongoDB connection to the test database (not needed for - production) + production) - `SENDGRID_API_KEY` - sendgrid API key, for sending notification emails - `SENDGRID_FROM` - email address from which to send notification emails +#### Starting the API: + +$ npm start + +The API exposes endpoints used to access information form the system via a RESTful interface. + ### Install via Docker Alternatively, if you've got a mongo instance running elsewhere, install and @@ -52,7 +68,6 @@ run on a fresh instance using docker as follows: [Install Docker](https://docs.docker.com/installation/) - One-time setup: ```sh From 779318ebeffc655c8bb01bcd6f763d45020a15be Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Tue, 13 Dec 2016 18:29:57 -0500 Subject: [PATCH 111/144] Change link location for ecosystem --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 754360a..c9b1b69 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Ecosystem + Ecosystem | API Docs | From c3e0dc739558a4bdea366d25051903a2c7ff4f97 Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Tue, 13 Dec 2016 19:45:20 -0500 Subject: [PATCH 112/144] Add backticks for code blocks --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9b1b69..0e94cfe 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,15 @@ The steps below will walk you through setting up your own instance of the oam-up If you use [`nvm`](https://github.com/creationix/nvm), activate the desired Node version: -$ nvm install +``` +nvm install +``` Install Node modules: -$ npm install +``` +npm install +``` ### Usage You need to set environment variables before starting the API. We suggest you copy `local.sample.env` to `local.env` and modify it. Before starting the API you can run `source local.env` to export the environment variables to the shell. @@ -57,7 +61,9 @@ You need to set environment variables before starting the API. We suggest you co #### Starting the API: -$ npm start +``` +npm start +``` The API exposes endpoints used to access information form the system via a RESTful interface. From 397437a532ca728607fa40df3108e4e419d994f0 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 13 Dec 2016 19:49:56 -0500 Subject: [PATCH 113/144] New travis token [ci skip] --- .build_scripts/docs.sh | 23 +++++++++-------------- .travis.yml | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.build_scripts/docs.sh b/.build_scripts/docs.sh index 96a5ee6..c8c3a84 100644 --- a/.build_scripts/docs.sh +++ b/.build_scripts/docs.sh @@ -1,17 +1,12 @@ #!/usr/bin/env bash set -e # halt script on error -# Build docs and push to gh-pages -if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = ${PRODUCTION_BRANCH} ]; then - echo "Get ready, we're pushing to gh-pages!" - npm run docs - cd docs - git init - git config user.name "Travis-CI" - git config user.email "travis@somewhere.com" - git add . - git commit -m "CI deploy to gh-pages" - git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages -else - echo "Not a publishable branch so we're all done here" -fi +echo "Get ready, we're pushing to gh-pages!" +cd dist +git init +git config user.name "Travis-CI" +git config user.email "travis@somewhere.com" +git add . +git commit -m "CI deploy to gh-pages" +git push --force --quiet "https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git" master:gh-pages > /dev/null 2>&1 +echo "Good to go!" diff --git a/.travis.yml b/.travis.yml index 73505f0..db23415 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ env: global: - GH_REF=github.com/hotosm/oam-uploader-api.git - PRODUCTION_BRANCH=master - - secure: W0eGs5Lg+fZOIEYKLIFJZPsw7qFUNFTAUtPS/2UmlWwlQtzvYCYnNu4dd62kZKivzCuWZiwdwsm58l2r6tB6zRv7/3S4mhkiA1SrYFC3p34CXXV9XaJeEgOQ1SXoJuAW1ThSmq7CJu9dsk3Aix+KA4MggzBgTPG2RXyYhgr3Qzhcil1BX6KNqW4TP7cPaJTUCEhoDcyVLNvmjvUDQNwwJjCu1hifDptlyZt88jDshIVPi4wd2ITy10O22dkwvVaes79A7yYrpRyBJyAhb8w/fPZsCwT+9wQz28x1Q47at/eERrpeAVGOUbXE+5cla/dCCVG3rMlyJnaRP0HagaUQK71GJMYe/nnOEirmF3vtpYzRTmzbvaahEtWr6EbdVWqXB8WJCBShgEF2NICwQAYwgIsYyzIAI3wDvVyrdhbSqqARlUqRwEN4NUFAXfVYkXssYWiTz/s0Eaktg3I/Z0wL2lgBy0hz2JN9BcSCjSdzzj9FWaoWjFSDamb/M5U1R6h9rTGe2bgoDGsZbo4TV/QCXIsKvqjX5lC/W5GFyoUOyTrqtGyPIpOdkTg8Nf1V2ZPuAznln6YYchMdO64eABy7w5tKErM6JP8N4pHFgkgIPJI4gZdAlUELFe+mgqp/kl1GEuSd8TtlIdZ/ErvsmhH5ubYifIdBkpSZfUMRM/vueXI= + - secure: "F2qYonaBBpWfXPbOhCd61jn9cAztQIVwQqSmwFEZYGOQTLhg+xaWOGTbItsuDhXHt8dyHqbk8LC3wv/Y+sYyH46BVM6xb1BB+2kQXBenOYyBHhcWJXvDsQqeTeg6WeRQr/we6X1BsbA9zlDLxZthFFmqHzyaSt9l3+e2V8Xlz7l/bDbgm9dOKiqbeYVlMS+wc5C1ODYc8JdCGuX9VLHg5ITxcsJeaoNgh2s6BMOjHL29c+rtfiB1SWucOS3mCRpNXDUsBqiGeagDmAIbZ+Q8Jgr4Lq2QZDxBQYCdFUe4koelukzyIOgLYwj6Dw+MJv+iF3UmVQnvb4VJalOS0k8T8KAIjLApOEEr5Ea7A8C6SdNmjOa44ANSpBOIYCLxxLeioMFdNg5N2K28eqgizN54kicGLVDlH4SLSeSfDMoLsWMC1hkU36QAlH6MXygtCMMk+3zpKUUxindH/HTtAyh+Udwa/48VCBwZ44xbsmMpnlUwVTC+An5V13mmsxsSkIFvY7xNANOv7Juzqlu2JJJ7q0o8luO8N4Dp9zlrow8K759WkuyNKT/8KeRT/234Gy+3W81Yr3vQDBKKQbC4DQstY+J43Qqcp8OfZV8AXbAHRRu2sg9EX0ixh/eSlukDJEvvqTQxZKF5duLUis3mLX+R3L1I9lp3cIFOk7gdYByku/g=" before_install: - echo "Installing libvips..." - sudo add-apt-repository -y ppa:lovell/precise-backport-vips From d8e134fa07d1ebbe14a373a0671071658089e600 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 14 Dec 2016 13:04:14 +0700 Subject: [PATCH 114/144] bump up block size --- worker/process-image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index f457df1..8da3998 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -131,8 +131,8 @@ function translateImage (ext, path, tifPath, callback) { '-co', 'COMPRESS=DEFLATE', '-co', 'PREDICTOR=2', '-co', 'SPARSE_OK=yes', - '-co', 'BLOCKXSIZE=256', - '-co', 'BLOCKYSIZE=256', + '-co', 'BLOCKXSIZE=512', + '-co', 'BLOCKYSIZE=512', '-co', 'NUM_THREADS=ALL_CPUS' ]; From ddafb77d1c8654a2e0a5fb271f19c4c20878125e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 14 Dec 2016 16:11:14 +0700 Subject: [PATCH 115/144] bump up expected test size --- test/fixture/NE1_50M_SR.output.json | 2 +- test/fixture/upload-status.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixture/NE1_50M_SR.output.json b/test/fixture/NE1_50M_SR.output.json index 3728d9b..e743a68 100644 --- a/test/fixture/NE1_50M_SR.output.json +++ b/test/fixture/NE1_50M_SR.output.json @@ -1 +1 @@ -{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":400774,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}} +{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":402897,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}} diff --git a/test/fixture/upload-status.json b/test/fixture/upload-status.json index dc6634c..62b6f2b 100644 --- a/test/fixture/upload-status.json +++ b/test/fixture/upload-status.json @@ -1 +1 @@ -{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","tms":null,"images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":400774,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} +{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","tms":null,"images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":402897,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} From 0969116cbf6a211c75f7cc17bc862571542bd1f3 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 19 Dec 2016 10:01:38 +0000 Subject: [PATCH 116/144] Fix ecosystem url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1695e0..116d9d6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Ecosystem + Ecosystem | API Docs | From 228a99ecc3e2c35943df3f283e2c307c3e5b8d58 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Mon, 16 Jan 2017 18:36:45 -0800 Subject: [PATCH 117/144] Generate UUIDs for images --- package.json | 1 + worker/queue.js | 21 ++++++--------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 217fb73..da1cf65 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "sendgrid": "^1.9.2", "sharp": "^0.11.1", "tmp": "0.0.26", + "uuid": "^3.0.1", "wellknown": "^0.3.1", "xtend": "^4.0.0" }, diff --git a/worker/queue.js b/worker/queue.js index bf19f64..ef715a7 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -1,7 +1,7 @@ var MongoClient = require('mongodb').MongoClient; -var moment = require('moment'); var Promise = require('es6-promise').Promise; var promisify = require('es6-promisify'); +var uuidV4 = require('uuid/v4'); var processImage = require('./process-image'); var log = require('./log'); var config = require('../config'); @@ -98,7 +98,6 @@ JobQueue.prototype._mainloop = function mainloop () { } // we got a job! - var now = moment().format('YYYY-MM-DD'); var image = result.value; var s3 = this.s3; log(['info'], 'Processing job', image); @@ -108,19 +107,11 @@ JobQueue.prototype._mainloop = function mainloop () { .then(function (upload) { var found; upload.scenes.forEach(function (scene, i) { - scene.images.forEach(function (id, j) { - if (image._id.equals(id)) { - var filename = image.url.split('/').pop() || 'untitled'; - var qmIndex = filename.indexOf('?'); - if (qmIndex !== -1) { - filename = filename.substring(0, qmIndex); - } - filename = filename.replace(/[^a-zA-Z0-9 _\-\\.]/g, '').replace(/ /g, '-'); - filename = ['scene', i, 'image', j, filename].join('-'); - var key = ['uploads', now, upload._id, 'scene', i, filename].join('/'); - // now that we have the scene, we can process the image - found = processImage(s3, scene, image.url, key); - } + scene.images.forEach(function (id) { + var key = [upload._id, i, uuidV4()].join('/'); + + // now that we have the scene, we can process the image + found = processImage(aws, scene, image.url, key); }); }); From ef61451c9990e7ea748989f3f6c3e94b5d556c80 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Mon, 16 Jan 2017 18:48:41 -0800 Subject: [PATCH 118/144] Add 'uploaded_at' to metadata --- worker/process-image.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worker/process-image.js b/worker/process-image.js index 8da3998..c69cc39 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -164,7 +164,8 @@ function generateMetadata (scene, path, key, callback) { properties: { tms: scene.tms, sensor: scene.sensor - } + }, + uploaded_at: new Date() }; gdalinfo.local(path, function (err, gdaldata) { From aee70c9df9d567fbd698c0e8442af720cdac7498 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 17 Jan 2017 11:26:00 -0800 Subject: [PATCH 119/144] Use DEBIAN_FRONTEND --- .build_scripts/docker/Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.build_scripts/docker/Dockerfile b/.build_scripts/docker/Dockerfile index d0b2694..5bcf29c 100644 --- a/.build_scripts/docker/Dockerfile +++ b/.build_scripts/docker/Dockerfile @@ -1,12 +1,11 @@ # This base adds libvips, needed by the sharp library, to the ubuntu:14.04 image FROM marcbachmann/libvips:8.0.2 +ENV DEBIAN_FRONTEND noninteractive + # Replace shell with bash so we can source files RUN rm /bin/sh && ln -s /bin/bash /bin/sh -# Set debconf to run non-interactively -RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections - # Install base dependencies RUN apt-get update && apt-get install -y -q --no-install-recommends \ apt-transport-https \ From 0478d51d7abf74f19ade354b65eb558b73c8bcdd Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 17 Jan 2017 11:30:29 -0800 Subject: [PATCH 120/144] Upgrade to Node 4.x Further clean up apt + npm artifacts. --- .build_scripts/docker/Dockerfile | 46 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/.build_scripts/docker/Dockerfile b/.build_scripts/docker/Dockerfile index 5bcf29c..345cf9d 100644 --- a/.build_scripts/docker/Dockerfile +++ b/.build_scripts/docker/Dockerfile @@ -3,34 +3,30 @@ FROM marcbachmann/libvips:8.0.2 ENV DEBIAN_FRONTEND noninteractive -# Replace shell with bash so we can source files -RUN rm /bin/sh && ln -s /bin/bash /bin/sh - # Install base dependencies -RUN apt-get update && apt-get install -y -q --no-install-recommends \ - apt-transport-https \ - build-essential \ - ca-certificates \ - curl \ - git \ - libssl-dev \ - python \ - rsync \ - && rm -rf /var/lib/apt/lists/* - -# Install nvm with node and npm -# http://stackoverflow.com/questions/25899912/install-nvm-in-docker -ENV NVM_DIR /usr/local/nvm -ENV NODE_VERSION 0.12 -RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | bash \ - && source $NVM_DIR/nvm.sh \ - && nvm install $NODE_VERSION \ - && nvm alias default $NODE_VERSION \ - && nvm use default -ENV PATH $NVM_BIN:$PATH +RUN \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y -q --no-install-recommends \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + git \ + libssl-dev \ + python \ + software-properties-common \ + && curl -sf https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ + && add-apt-repository -s "deb https://deb.nodesource.com/node_4.x $(lsb_release -c -s) main" \ + && apt-get update \ + && apt-get install --no-install-recommends -y nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean # Go ahead and install nodemon for convenience while developing -RUN source $NVM_DIR/nvm.sh && npm install -g nodemon +RUN \ + npm install -g nodemon \ + && rm -rf /root/.npm # Set TMPDIR environment variable ENV TMPDIR /tmp From 1dac51adf93557c7d14851848ed822378a7a98de Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 17 Jan 2017 14:47:53 -0800 Subject: [PATCH 121/144] Move Dockerfile to root to incorporate app --- .dockerignore | 2 ++ .build_scripts/docker/Dockerfile => Dockerfile | 0 2 files changed, 2 insertions(+) create mode 100644 .dockerignore rename .build_scripts/docker/Dockerfile => Dockerfile (100%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d371036 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.env +.git diff --git a/.build_scripts/docker/Dockerfile b/Dockerfile similarity index 100% rename from .build_scripts/docker/Dockerfile rename to Dockerfile From 32f419a5e6d0f16ba7c168a547fff3ad0c1e91ac Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Wed, 18 Jan 2017 13:20:31 -0800 Subject: [PATCH 122/144] Use upgraded gdal for bundled binaries Also installs a matching gdal version (0.9.4 at this writing) to work around conflicts between npm@2 and node-pre-gyp ("cannot run in wd"). --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index da1cf65..1db1b6f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "es6-promise": "^2.3.0", "es6-promisify": "^3.0.0", "exit-hook": "^1.1.1", - "gdalinfo-json": "^0.5.1", + "gdal": "^0.9.4", + "gdalinfo-json": "mojodna/gdalinfo-json#upgrade-gdal", "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", From 0dbdb60ce96aac77e81315403477e3a2380d6a29 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 22 Jan 2017 17:34:05 +0000 Subject: [PATCH 123/144] add back doc build and deploy --- .build_scripts/docs.sh | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.build_scripts/docs.sh b/.build_scripts/docs.sh index c8c3a84..9c83531 100644 --- a/.build_scripts/docs.sh +++ b/.build_scripts/docs.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash set -e # halt script on error - -echo "Get ready, we're pushing to gh-pages!" -cd dist -git init -git config user.name "Travis-CI" -git config user.email "travis@somewhere.com" -git add . -git commit -m "CI deploy to gh-pages" -git push --force --quiet "https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git" master:gh-pages > /dev/null 2>&1 -echo "Good to go!" +if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = ${PRODUCTION_BRANCH} ]; then + echo "Get ready, we're pushing to gh-pages!" + npm run docs + cd docs + git init + git config user.name "Travis-CI" + git config user.email "travis@somewhere.com" + git add . + git commit -m "CI deploy to gh-pages" + git push --force --quiet "https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git" master:gh-pages > /dev/null 2>&1 + echo "Good to go!" +else + echo "Not a publishable branch so we're all done here" +if \ No newline at end of file From 0a20e81b4fe076032615e9254957840fc54bb5b6 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Sun, 22 Jan 2017 11:59:49 -0800 Subject: [PATCH 124/144] Process with oam-dynamic-tiler-tools This refactors the Dockerfile to use quay.io/hotosm/oam-dynamic-tiler-tools as the base image, which includes all of the necessary dependencies. It also uses the image processor from that image, matching the dynamic tiler's expectations. App code is now baked into the image (vs. needing to be mounted in); this will change the way things are deployed somewhat (potentially to be simpler). Since AWS environment vars, etc. were already using defaults, I dropped their explicit use when refactoring the image processing. --- .build_scripts/docker/install.sh | 9 -- .build_scripts/docker/run.sh | 2 +- .build_scripts/docker/start.sh | 4 - .build_scripts/docker/test.sh | 4 - .dockerignore | 2 + .gitignore | 1 - Brewfile | 2 - Dockerfile | 38 ++--- README.md | 9 +- config.js | 12 +- docker-compose.yml | 14 ++ package.json | 5 - services/s3.js | 88 ------------ test/test__worker.js | 46 ------ worker/index.js | 12 +- worker/process-image.js | 233 +++++++------------------------ worker/queue.js | 8 +- 17 files changed, 92 insertions(+), 397 deletions(-) delete mode 100755 .build_scripts/docker/install.sh create mode 100644 docker-compose.yml delete mode 100644 services/s3.js diff --git a/.build_scripts/docker/install.sh b/.build_scripts/docker/install.sh deleted file mode 100755 index 1280ef1..0000000 --- a/.build_scripts/docker/install.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Run the tests inside docker. -# Assumes that the base directory of this repo has been mounted as /local, e.g.: -# docker run -Pit -v $(pwd):/local oam-uploader-api /local/.build_scripts/docker/start.sh - -cd /local -source $NVM_DIR/nvm.sh -npm install diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index 7c0dd56..49d850f 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -49,6 +49,6 @@ exec docker run $MODE --rm \ -e AWS_SECRET_ACCESS_KEY \ -e AWS_REGION \ --net=\"host\" \ - -v $(pwd):/local \ + -v $(pwd):/app \ $ARGS \ oam-uploader-api $COMMAND diff --git a/.build_scripts/docker/start.sh b/.build_scripts/docker/start.sh index c25ed66..10776c6 100755 --- a/.build_scripts/docker/start.sh +++ b/.build_scripts/docker/start.sh @@ -1,9 +1,5 @@ #!/bin/bash # Run the server inside docker. -# Assumes that the base directory of this repo has been mounted as /local, e.g.: -# docker run -Pit -v $(pwd):/local oam-uploader-api /local/.build_scripts/docker/start.sh -cd /local -source $NVM_DIR/nvm.sh npm start diff --git a/.build_scripts/docker/test.sh b/.build_scripts/docker/test.sh index 7f9e625..b5d1789 100755 --- a/.build_scripts/docker/test.sh +++ b/.build_scripts/docker/test.sh @@ -1,9 +1,5 @@ #!/bin/bash # Run the tests inside docker. -# Assumes that the base directory of this repo has been mounted as /local, e.g.: -# docker run -Pit -v $(pwd):/local oam-uploader-api /local/.build_scripts/docker/start.sh -cd /local -source $NVM_DIR/nvm.sh npm test diff --git a/.dockerignore b/.dockerignore index d371036..b5ef9c9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ .env .git +local.env +node_modules diff --git a/.gitignore b/.gitignore index 75de4f2..f71d643 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -sharp.js local.js local.env node_modules diff --git a/Brewfile b/Brewfile index 77ea69b..a95026a 100644 --- a/Brewfile +++ b/Brewfile @@ -1,3 +1 @@ -tap 'homebrew/science' -brew 'vips' brew 'mongodb' diff --git a/Dockerfile b/Dockerfile index 345cf9d..fa1b86e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,7 @@ -# This base adds libvips, needed by the sharp library, to the ubuntu:14.04 image -FROM marcbachmann/libvips:8.0.2 +FROM quay.io/hotosm/oam-dynamic-tiler-tools ENV DEBIAN_FRONTEND noninteractive -# Install base dependencies -RUN \ - apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y -q --no-install-recommends \ - apt-transport-https \ - build-essential \ - ca-certificates \ - curl \ - git \ - libssl-dev \ - python \ - software-properties-common \ - && curl -sf https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && add-apt-repository -s "deb https://deb.nodesource.com/node_4.x $(lsb_release -c -s) main" \ - && apt-get update \ - && apt-get install --no-install-recommends -y nodejs \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get clean - # Go ahead and install nodemon for convenience while developing RUN \ npm install -g nodemon \ @@ -31,5 +10,16 @@ RUN \ # Set TMPDIR environment variable ENV TMPDIR /tmp -# copy install, test, run, etc. scripts for convenient access -COPY *.sh / +COPY package.json /app/package.json + +WORKDIR /app + +RUN \ + npm install \ + && rm -rf /root/.npm + +EXPOSE 4000 + +COPY . /app + +ENTRYPOINT ["npm", "start"] diff --git a/README.md b/README.md index 116d9d6..f8e3d1f 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,8 @@ The steps below will walk you through setting up your own instance of the oam-up - [MongoDB](https://www.mongodb.org/) - [Node.js](https://nodejs.org/) v0.12 -- [libvips](https://github.com/jcupitt/libvips) (Make sure to read the instructions specific to your OS) -- [gdal](http://www.sarasafavi.com/installing-gdalogr-on-ubuntu.html) -Using [homebrew](http://brew.sh/), you can install MongoDB and `libvips` using: +Using [homebrew](http://brew.sh/), you can install MongoDB and other dependencies: $ brew bundle @@ -54,7 +52,7 @@ You need to set environment variables before starting the API. We suggest you co - `PORT` - the port to listen on - `OIN_BUCKET` - The OIN bucket that will receive the uploads - `AWS_REGION` - AWS region of OIN_BUCKET -- `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN bucket +- `AWS_ACCESS_KEY_ID` - AWS access key id for reading OIN bucket - `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN bucket - `ADMIN_USERNAME` - Token management Admin username - `ADMIN_PASSWORD` - Token management Admin password @@ -63,6 +61,7 @@ You need to set environment variables before starting the API. We suggest you co production) - `SENDGRID_API_KEY` - sendgrid API key, for sending notification emails - `SENDGRID_FROM` - email address from which to send notification emails +- `TILER_BASE_URL` - Base URL for dynamic TMS/WMTS endpoints. Defaults to `http://tiles.openaerialmap.org`. #### Starting the API: @@ -87,7 +86,7 @@ git clone https://github.com/hotosm/oam-uploader-api # build the docker image cd oam-uploader-api -docker build -t oam-uploader-api .build_scripts/docker +docker build -t oam-uploader-api . # set up environment vars: cp local.sample.env local.env diff --git a/config.js b/config.js index 9cbf78d..e0765b0 100644 --- a/config.js +++ b/config.js @@ -9,13 +9,11 @@ var defaults = { adminPassword: null, // the administrator username adminUsername: null, // the administrator password oinBucket: 'oam-uploader', // name of the OpenImageryNetwork bucket to which imagery should be uploaded - awsRegion: 'us-west-2', // the AWS region of the oinBucket thumbnailSize: 300, // (very) approximate thumbnail size, in kilobytes maxWorkers: 1, // the maximum number of workers sendgridApiKey: null, // sendgrid API key, for sending notification emails sendgridFrom: 'info@hotosm.org', // the email address from which to send notification emails gdriveKey: null, - gdalTranslateBin: '/usr/bin/gdal_translate', emailNotification: { subject: '[ OAM Uploader ] Imagery upload submitted', text: 'Your upload has been successfully submitted and is now being ' + @@ -34,7 +32,8 @@ var defaults = { log: '*' } }] - } + }, + tilerBaseUrl: 'http://tiles.openaerialmap.org' }; // Environment variable overrides @@ -46,13 +45,10 @@ var environment = { maxWorkers: process.env.MAX_WORKERS, adminPassword: process.env.ADMIN_PASSWORD, adminUsername: process.env.ADMIN_USERNAME, - awsKeyId: process.env.AWS_SECRET_KEY_ID, - awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - awsRegion: process.env.AWS_REGION, sendgridApiKey: process.env.SENDGRID_API_KEY, sendgridFrom: process.env.SENDGRID_FROM, - gdalTranslateBin: process.env.GDAL_TRANSLATE_BIN, - gdriveKey: process.env.GDRIVE_KEY + gdriveKey: process.env.GDRIVE_KEY, + tilerBaseUrl: process.env.TILER_BASE_URL }; var config = xtend(defaults); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d987912 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '2' +services: + mongo: + image: mongo + web: + build: . + environment: + - DBURI=mongodb://mongo/oam-uploader + env_file: + - local.env + ports: + - "4000:4000" + depends_on: + - mongo diff --git a/package.json b/package.json index 1db1b6f..167c056 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,6 @@ "es6-promise": "^2.3.0", "es6-promisify": "^3.0.0", "exit-hook": "^1.1.1", - "gdal": "^0.9.4", - "gdalinfo-json": "mojodna/gdalinfo-json#upgrade-gdal", "good": "^6.3.0", "good-console": "^5.0.2", "hapi": "^8.4.0", @@ -42,12 +40,9 @@ "moment": "^2.10.6", "mongodb": "^2.0.40", "newrelic": "^1.20.0", - "oam-meta-generator": "openimagerynetwork/oin-meta-generator#c84bf8b035ad24ed464320b527b99b838d8ee601", "queue-async": "^1.0.7", "request": "^2.60.0", - "s3": "^4.4.0", "sendgrid": "^1.9.2", - "sharp": "^0.11.1", "tmp": "0.0.26", "uuid": "^3.0.1", "wellknown": "^0.3.1", diff --git a/services/s3.js b/services/s3.js deleted file mode 100644 index 4c2f961..0000000 --- a/services/s3.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -var s3 = require('s3'); -var meta = require('../controllers/meta.js'); - -/** -* S3 Constructor that handles intractions with S3 -* -* @constructor -* @param {String} secretId (optional) - AWS secret key id. Can be set by AWS_SECRET_KEY_ID env var. -* @param {String} secretKey (optional) - AWS secret access key. Can be set by AWS_SECRET_ACCESS_KEY env var. -* @param {String} bucket (optional) - S3 Bucket name. Can be set by S3_BUCKET_NAME env var. -*/ -var S3 = function (secretId, secretKey, bucket) { - this.client = s3.createClient({ - maxAsyncS3: 20, // this is the default - s3RetryCount: 3, // this is the default - s3RetryDelay: 1000, // this is the default - multipartUploadThreshold: 20971520, // this is the default (20 MB) - multipartUploadSize: 15728640, // this is the default (15 MB) - s3Options: { - accessKeyId: secretId || process.env.AWS_SECRET_KEY_ID, - secretAccessKey: secretKey || process.env.AWS_SECRET_ACCESS_KEY - } - }); - - this.params = { - s3Params: { - Bucket: bucket || process.env.S3_BUCKET_NAME /* required */ - } - }; -}; - -/** -* Read bucket method for S3. It reads the S3 bucket and adds all the `.json` metadata to Meta model -* -* @param {responseCallback} cb - The callback that handles the response -* @param {finishedCallback} finished - The callback that handles when reading is done -*/ -S3.prototype.readBucket = function (lastSystemUpdate, cb, done) { - var self = this; - var images = this.client.listObjects(this.params); - - images.on('error', function (err) { - cb(err); - }); - - images.on('data', function (data) { - for (var i = 0; i < data.Contents.length; i++) { - var format = data.Contents[i].Key.split('.'); - format = format[format.length - 1]; - - if (format === 'json') { - // Get the last time the metadata file was modified so we can determine - // if we need to update it. - var lastModified = data.Contents[i].LastModified; - var url = s3.getPublicUrlHttp(self.params.s3Params.Bucket, data.Contents[i].Key); - meta.addRemoteMeta(url, lastModified, lastSystemUpdate, function (err, msg) { - if (err) { - return cb(err); - } - cb(err, msg); - }); - } - } - }); - - images.on('end', function () { - done(null); - }); -}; - -module.exports = S3; - -/** - * The response callback returns the error and success message. - * - * @callback responseCallback - * @param {error} err - The error message - * @param {string} msg - The success message - */ - - /** - * The finished callback just calls back to the worker to let it know there is - * no more data coming. - * - * @callback finishedCallback - */ diff --git a/test/test__worker.js b/test/test__worker.js index 19f0f45..a561f96 100644 --- a/test/test__worker.js +++ b/test/test__worker.js @@ -4,10 +4,8 @@ var Lab = require('lab'); var chai = require('chai'); var http = require('http'); var ecstatic = require('ecstatic'); -var sharp = require('sharp'); var MongoClient = require('mongodb').MongoClient; var omit = require('omit-deep'); -var JobQueue = require('../worker/queue'); var config = require('../config'); var api = require('../'); @@ -26,7 +24,6 @@ suite('test worker', function () { before(function (done) { assert.match(config.dbUri, /test$/, 'use the test database'); assert.equal(config.oinBucket, 'oam-uploader'); - assert.equal(config.awsRegion, 'us-west-2'); // empty out database MongoClient.connect(config.dbUri, function (err, conn) { @@ -64,49 +61,6 @@ suite('test worker', function () { }); }); - test('process an upload', { timeout: 60000 }, function (done) { - var mockS3 = { - calls: [], - upload: function (options, cb) { - mockS3.calls.push(options); - cb(); - } - }; - var queue = new JobQueue(mockS3); - queue.run() - .then(function () { - assert(queue.workerId, 'has a worker id'); - assert.equal(mockS3.calls.length, 3, 'three calls to s3.upload'); - - var metadata = JSON.parse(mockS3.calls[2].Body); - var expected = require('./fixture/NE1_50M_SR.output.json'); - - db.collection('images') - .findOne({ url: 'http://localhost:8080/NE1_50M_SR.tif' }) - .then(function (image) { - assert(image.status === 'finished'); - assert.equal(JSON.stringify(metadata), - JSON.stringify(image.metadata), - 'the uploaded metadata is stored in the db entry for an image'); - assert.match(metadata.uuid, /http:\/\/oam-uploader.s3.amazonaws.com\/uploads\/.*\/.*\/scene\/0\/scene-0-image-0-NE1_50M_SR\.tif/); - assert.match(metadata.properties.thumbnail, /thumb\.(png|jpe?g)$/); - var omitted = ['uuid', 'thumbnail', 'tms']; - assert.deepEqual(omit(metadata, omitted), omit(expected, omitted), - 'generated metadata'); - - var thumb = mockS3.calls[1].Body; - thumb - .pipe(sharp().metadata(function (err, thumbdata) { - if (err) { return done(err); } - assert(thumbdata, 'thumbnail is an image'); - done(); - })); - }) - .catch(done); - }) - .catch(done); - }); - test('check the status of an upload', function (done) { api(function (hapi) { hapi.inject({ diff --git a/worker/index.js b/worker/index.js index 304f154..9f3ba2c 100644 --- a/worker/index.js +++ b/worker/index.js @@ -7,20 +7,10 @@ require('babel/register'); * A worker that queries the db for new uploads and process 'em. */ -var AWS = require('aws-sdk'); var JobQueue = require('./queue'); var onExit = require('exit-hook'); -var config = require('../config'); -AWS.config = { - accessKeyId: config.awsKeyId, - secretAccessKey: config.awsAccessKey, - region: config.awsRegion, - sslEnabled: true -}; - -var s3 = new AWS.S3(); -var queue = new JobQueue(s3); +var queue = new JobQueue(); onExit(function () { queue.cleanup() diff --git a/worker/process-image.js b/worker/process-image.js index c69cc39..2397c96 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -1,212 +1,77 @@ 'use strict'; -var fs = require('fs'); -var pathTools = require('path'); var cp = require('child_process'); -var tmp = require('tmp'); + +var AWS = require('aws-sdk'); var promisify = require('es6-promisify'); var request = require('request'); -var queue = require('queue-async'); -var gdalinfo = require('gdalinfo-json'); -var applyGdalinfo = require('oam-meta-generator/lib/apply-gdalinfo'); -var sharp = require('sharp'); + var log = require('./log'); var config = require('../config'); var s3bucket = config.oinBucket; -// desired size in kilobytes * 1000 bytes/kb / (~.75 byte/pixel) -var targetPixelArea = config.thumbnailSize * 1000 / 0.75; module.exports = promisify(_processImage); /** * Fully process one URL. * Callback called with (err, { metadata, messages }) */ -function _processImage (s3, scene, url, key, cb) { - var ext = pathTools.extname(url).toLowerCase(); - tmp.file({ postfix: ext }, function (err, path, fd, cleanupSource) { - var callback = function (err, data) { - cleanupSource(); - cb(err, data); - }; - - if (err) { return callback(err); } - - // Google drive url comes in the form of gdrive://FILE_ID - // We need this because large files can only be downloaded with an api key. - var pieces = url.match(/gdrive:\/\/(.+)/); - if (pieces) { - url = `https://www.googleapis.com/drive/v3/files/${pieces[1]}?alt=media&key=${config.gdriveKey}`; - } - - log(['debug'], 'Downloading ' + url + ' to ' + path); - - var downloadStatus; - request(url) - .on('response', function (resp) { downloadStatus = resp.statusCode; }) - .on('error', callback) - .pipe(fs.createWriteStream(path)) - .on('finish', function () { - if (downloadStatus < 200 || downloadStatus >= 400) { - return callback(new Error('Could not download ' + url + - '; server responded with status code ' + downloadStatus)); - } - - var messages = []; - - // we've successfully downloaded the file. now do stuff with it. - tmp.file({ postfix: '.tif' }, function (err, tifPath, fd, cleanupTif) { - if (err) { return callback(err); } - - translateImage(ext, path, tifPath, function (err, path) { - callback = function (err, data) { - cleanupSource(); - cleanupTif(); - cb(err, data); - }; - - if (err) { return callback(err); } - - generateMetadata(scene, path, key, function (err, metadata) { - if (err) { return callback(err); } - makeThumbnail(path, function (thumbErr, thumbPath) { - if (thumbErr) { - messages.push('Could not generate thumbnail: ' + thumbErr.message); - } - uploadToS3(s3, path, key, metadata, thumbPath, function (err) { - callback(err, { metadata: metadata, messages: messages }); - }); - }); - }); - }); - }); - }).on('error', callback); - }); -} - -function uploadToS3 (s3, path, key, metadata, thumbPath, callback) { - var q = queue(); - - // upload image - q.defer(s3.upload.bind(s3), { - Body: fs.createReadStream(path), - Bucket: s3bucket, - Key: key - }); - - // upload thumbnail, if we have one - if (thumbPath) { - q.defer(s3.upload.bind(s3), { - Body: fs.createReadStream(thumbPath), - Bucket: s3bucket, - Key: key + '.thumb.png' - }); - metadata.properties.thumbnail = publicUrl(s3bucket, key + '.thumb.png'); +function _processImage (scene, sourceUrl, targetPrefix, callback) { + // Google drive url comes in the form of gdrive://FILE_ID + // We need this because large files can only be downloaded with an api key. + var pieces = sourceUrl.match(/gdrive:\/\/(.+)/); + if (pieces) { + sourceUrl = `https://www.googleapis.com/drive/v3/files/${pieces[1]}?alt=media&key=${config.gdriveKey}`; } - // upload metadata - q.defer(s3.upload.bind(s3), { - Body: JSON.stringify(metadata), - Bucket: s3bucket, - Key: key + '_meta.json' - }); - - log(['debug'], 'Uploading to s3; bucket=' + s3bucket + ' key=' + key); - q.awaitAll(function (err) { - if (err) { return callback(err); } - log(['debug'], 'Finished uploading'); - callback(); - }); -} - -function translateImage (ext, path, tifPath, callback) { - if (!config.gdalTranslateBin) { - throw new Error('GDAL bin path missing.'); - } - log(['debug'], 'Converting image to OAM standard format.'); var args = [ - '-of', 'GTiff', - path, tifPath, - '-co', 'TILED=yes', - '-co', 'COMPRESS=DEFLATE', - '-co', 'PREDICTOR=2', - '-co', 'SPARSE_OK=yes', - '-co', 'BLOCKXSIZE=512', - '-co', 'BLOCKYSIZE=512', - '-co', 'NUM_THREADS=ALL_CPUS' + '-t', scene.title, + '-a', scene.acquisition_start.toISOString(), + '-A', scene.acquisition_end.toISOString(), + '-p', scene.provider, + '-P', scene.platform, + '-c', [scene.contact.name.replace(',', ';'), scene.contact.email].join(','), + '-U', new Date().toISOString() ]; - cp.execFile(config.gdalTranslateBin, args, function (err, stdout, stderr) { - if (err) { return callback(err); } - log(['debug'], 'Converted image to OAM standard format. Input: ', path, 'Output: ', tifPath); - return callback(null, tifPath); - }); -} + if (scene.tms) { + args.push('-m', `tms=${scene.tms}`); + } -function generateMetadata (scene, path, key, callback) { - log(['debug'], 'Generating metadata.'); - fs.stat(path, function (err, stat) { - if (err) { return callback(err); } + if (scene.sensor) { + args.push('-m', `sensor=${scene.sensor}`); + } - var metadata = { - uuid: null, - title: scene.title, - projection: null, - bbox: null, - footprint: null, - gsd: null, - file_size: stat.size, - acquisition_start: scene.acquisition_start, - acquisition_end: scene.acquisition_end, - platform: scene.platform, - provider: scene.provider, - contact: [scene.contact.name.replace(',', ';'), scene.contact.email].join(','), - properties: { - tms: scene.tms, - sensor: scene.sensor - }, - uploaded_at: new Date() - }; + var output = `s3://${s3bucket}/${targetPrefix}`; + args.push(sourceUrl, output); + + return cp.execFile('process.sh', args, { + AWS_ACCESS_KEY_ID: AWS.config.credentials.accessKeyId, + AWS_DEFAULT_REGION: AWS.config.region, + AWS_SECRET_ACCESS_KEY: AWS.config.credentials.secretAccessKey, + AWS_SESSION_TOKEN: AWS.config.credentials.sessionToken, + THUMBNAIL_SIZE: config.thumbnailSize, + TILER_BASE_URL: config.tilerBaseUrl + }, function (err, stdout, stderr) { + if (err) { + err.stdout = stdout; + err.stderr = stderr; + return callback(err); + } - gdalinfo.local(path, function (err, gdaldata) { - if (err) { return callback(err); } - applyGdalinfo(metadata, gdaldata); - // set uuid after doing applyGdalinfo because it actually sets it to - // gdaldata.url, which for us is blank since we used gdalinfo.local - metadata.uuid = publicUrl(s3bucket, key); - log(['debug'], 'Generated metadata: ', metadata); - callback(null, metadata); - }); - }); -} + log(['debug'], 'Converted image to OAM standard format. Input: ', sourceUrl, 'Output: ', output); -function makeThumbnail (imagePath, callback) { - tmp.file({ postfix: '.png' }, function (err, path, fd) { - if (err) { return callback(err); } - log(['debug'], 'Generating thumbnail', path); + return request.get({ + json: true, + uri: `http://${s3bucket}.s3.amazonaws.com/${targetPrefix}_meta.json` + }, function (err, rsp, metadata) { + if (err) { + return callback(err); + } - var original = sharp(imagePath) - // upstream: https://github.com/lovell/sharp/issues/250 - .limitInputPixels(2147483647) - .sequentialRead(); - original - .metadata() - .then(function (metadata) { - var pixelArea = metadata.width * metadata.height; - var ratio = Math.sqrt(targetPixelArea / pixelArea); - log(['debug'], 'Generating thumbnail, targetPixelArea=' + targetPixelArea); - original - .resize(Math.round(ratio * metadata.width)) - .toFile(path) - .then(function () { - log(['debug'], 'Finished generating thumbnail'); - callback(null, path); + return callback(null, { + metadata: metadata }); - }) - .catch(callback); + }); }); } - -function publicUrl (bucketName, key) { - return 'http://' + bucketName + '.s3.amazonaws.com/' + key; -} diff --git a/worker/queue.js b/worker/queue.js index ef715a7..aad1666 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -8,9 +8,8 @@ var config = require('../config'); module.exports = JobQueue; -function JobQueue (s3) { - if (!(this instanceof JobQueue)) { return new JobQueue(s3); } - this.s3 = s3; +function JobQueue () { + if (!(this instanceof JobQueue)) { return new JobQueue(); } } JobQueue.prototype.run = function () { @@ -99,7 +98,6 @@ JobQueue.prototype._mainloop = function mainloop () { // we got a job! var image = result.value; - var s3 = this.s3; log(['info'], 'Processing job', image); return this.db.collection('uploads') // find the upload / scene that contains this image @@ -111,7 +109,7 @@ JobQueue.prototype._mainloop = function mainloop () { var key = [upload._id, i, uuidV4()].join('/'); // now that we have the scene, we can process the image - found = processImage(aws, scene, image.url, key); + found = processImage(scene, image.url, key); }); }); From 7abb1dd9970dbdaedbdf169b8e4856a43495d35a Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 24 Jan 2017 17:40:07 +0000 Subject: [PATCH 125/144] Add information about the tokens --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 116d9d6..24e08e6 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ npm start The API exposes endpoints used to access information form the system via a RESTful interface. +### Tokens +The uploader endpoints are protected via a token authentication system. +Tokens are issued using the [oam-uploader-admin](https://github.com/hotosm/oam-uploader-admin), a friendly user interface for token management. Follow the [instructions](https://github.com/hotosm/oam-uploader-admin) to set it up. To understand better how all the components interact refer to the [Ecosystem Docs](https://docs.openaerialmap.org/ecosystem/getting-started/). + ### Install via Docker Alternatively, if you've got a mongo instance running elsewhere, install and @@ -136,3 +140,4 @@ deploy your local branch. ## License Oam Uploader Api is licensed under **BSD 3-Clause License**, see the [LICENSE](LICENSE) file for more details. + From a4faded830d23050c81cf318283f49915bd47f1b Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 24 Jan 2017 17:49:33 +0000 Subject: [PATCH 126/144] Add information about documentation --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 24e08e6..20522cd 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,15 @@ The API exposes endpoints used to access information form the system via a RESTf The uploader endpoints are protected via a token authentication system. Tokens are issued using the [oam-uploader-admin](https://github.com/hotosm/oam-uploader-admin), a friendly user interface for token management. Follow the [instructions](https://github.com/hotosm/oam-uploader-admin) to set it up. To understand better how all the components interact refer to the [Ecosystem Docs](https://docs.openaerialmap.org/ecosystem/getting-started/). +### Documentation +The documentation for the different endpoints can be generated by running: +``` +npm run docs +``` +This will compile the documentation and place it inside `docs`. The docs site can then be run by any web server. + +The documentation is also automatically built and deployed by [Travis CI](https://travis-ci.org/) whenever a Pull Request is merged to the production branch (in this case `master`). The deployment is done to `gh-pages`. + ### Install via Docker Alternatively, if you've got a mongo instance running elsewhere, install and From eaba8515ff1eb3bbb68550b668afda9c9d7d5cf3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 25 Jan 2017 10:14:32 +0000 Subject: [PATCH 127/144] add db info --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 20522cd..00f7384 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ You need to set environment variables before starting the API. We suggest you co - `SENDGRID_API_KEY` - sendgrid API key, for sending notification emails - `SENDGRID_FROM` - email address from which to send notification emails +#### Starting the database + +``` +mongod +``` + #### Starting the API: ``` @@ -76,6 +82,25 @@ The API exposes endpoints used to access information form the system via a RESTf The uploader endpoints are protected via a token authentication system. Tokens are issued using the [oam-uploader-admin](https://github.com/hotosm/oam-uploader-admin), a friendly user interface for token management. Follow the [instructions](https://github.com/hotosm/oam-uploader-admin) to set it up. To understand better how all the components interact refer to the [Ecosystem Docs](https://docs.openaerialmap.org/ecosystem/getting-started/). +#### Populate the database + +To get the API running quickly, you can skip running the admin interface and populate the database with a test token. + +``` +mongo +use oam-uploader +db.tokens.insert( + { + "name" : "admin", + "expiration" : false, + "status" : "active", + "token" : "MYTESTTOKEN", + "created" : ISODate("2016-12-15T17:28:19.823Z"), + "updated" : null + } +) +``` + ### Documentation The documentation for the different endpoints can be generated by running: ``` From fcc912347ebb00a7c131f6d31902fb511d968267 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 24 Jan 2017 17:13:20 -0500 Subject: [PATCH 128/144] Add license and tag to scene/ image properties --- routes/uploads.js | 2 ++ worker/process-image.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/routes/uploads.js b/routes/uploads.js index f6c3fcc..aac9d29 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -33,6 +33,8 @@ function includeImages (db, scene, callback) { }) .toArray(function (err, images) { scene.images = images; + delete scene.tags; + delete scene.license; callback(err, scene); }); } diff --git a/worker/process-image.js b/worker/process-image.js index 2397c96..352d0e8 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -42,6 +42,14 @@ function _processImage (scene, sourceUrl, targetPrefix, callback) { args.push('-m', `sensor=${scene.sensor}`); } + if (scene.license) { + args.push('-m', `license=${scene.license}`); + } + + if (scene.tags) { + args.push('-m', `tags=${scene.tags}`); + } + var output = `s3://${s3bucket}/${targetPrefix}`; args.push(sourceUrl, output); From a1933927bfdf6fa4ef74408f94cdccf9c2a8713b Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Wed, 25 Jan 2017 14:54:22 -0800 Subject: [PATCH 129/144] Use long opts for confusing args --- worker/process-image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index 352d0e8..c49ac29 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -28,8 +28,8 @@ function _processImage (scene, sourceUrl, targetPrefix, callback) { '-t', scene.title, '-a', scene.acquisition_start.toISOString(), '-A', scene.acquisition_end.toISOString(), - '-p', scene.provider, - '-P', scene.platform, + '--provider', scene.provider, + '--platform', scene.platform, '-c', [scene.contact.name.replace(',', ';'), scene.contact.email].join(','), '-U', new Date().toISOString() ]; From 118aac521fcadf50018440fd62e3a9a883bc4cc2 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 26 Jan 2017 10:19:35 +0000 Subject: [PATCH 130/144] Fix typo in license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a2d4010..8836006 100644 --- a/LICENSE +++ b/LICENSE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of oam-design-system nor the names of its +* Neither the name of oam-uploader-api nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. From 73d243c14ed2d15e845ba1f6fb3b3d3d4b77c7fa Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 26 Jan 2017 10:57:32 +0000 Subject: [PATCH 131/144] Improve descriptions of env variables. Add info about newrelic --- README.md | 30 ++++++++++++++++++------------ local.sample.env | 29 ++++++++++++++++++++--------- newrelic.js | 1 + worker/newrelic.js | 1 + 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 20522cd..0be1a87 100644 --- a/README.md +++ b/README.md @@ -51,18 +51,24 @@ You need to set environment variables before starting the API. We suggest you co #### Environment Variables -- `PORT` - the port to listen on -- `OIN_BUCKET` - The OIN bucket that will receive the uploads -- `AWS_REGION` - AWS region of OIN_BUCKET -- `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN bucket -- `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading OIN bucket -- `ADMIN_USERNAME` - Token management Admin username -- `ADMIN_PASSWORD` - Token management Admin password -- `DBURI` - MongoDB connection url -- `DBURI_TEST` - MongoDB connection to the test database (not needed for - production) -- `SENDGRID_API_KEY` - sendgrid API key, for sending notification emails -- `SENDGRID_FROM` - email address from which to send notification emails +- `PORT` - The port to listen on (Default to 4000). +- `HOST` - The hostname or ip address (Default to 0.0.0.0) +- `AWS_SECRET_KEY_ID` - AWS secret key id for reading `OIN_BUCKET`. +- `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading `OIN_BUCKET`. +- `AWS_REGION` - AWS region of `OIN_BUCKET` (Default to us-west-2). +- `SENDGRID_API_KEY` - Sendgrid API key, for sending notification emails. +- `SENDGRID_FROM` - Email address from which to send notification emails (Default to info@hotosm.org). +- `OIN_BUCKET` - The OIN bucket that will receive the uploads (Default to oam-uploader). +- `DBURI` - MongoDB connection url (Default to mongodb://localhost/oam-uploader) +- `DBURI_TEST` - MongoDB connection url for tests. Will be loaded when `NODE_ENV` is `test`. It's not needed for production. (Default to mongodb://localhost/oam-uploader-test) +- `ADMIN_USERNAME` - Username to access the [Token Management](https://github.com/hotosm/oam-uploader-admin) panel. +- `ADMIN_PASSWORD` - Password to access the [Token Management](https://github.com/hotosm/oam-uploader-admin) panel. +- `GDRIVE_KEY` - Google Api key. Needed to use the upload from google drive functionality. +- `GDAL_TRANSLATE_BIN` - Full path to the gdal bin (Default to /usr/bin/gdal_translate) +- `MAX_WORKERS` - Max number of workers used to process the uploads (Default to 1) +- `NEW_RELIC_LICENSE_KEY` - New relic license key. + +For a quick local setup for development the following variables can be omitted: `SENDGRID_API_KEY`, `SENDGRID_FROM`, `GDRIVE_KEY`, `NEW_RELIC_LICENSE_KEY`. Be aware that although the system will work some functionalities will not be available and errors may be triggered. #### Starting the API: diff --git a/local.sample.env b/local.sample.env index 77847ad..ada29d8 100644 --- a/local.sample.env +++ b/local.sample.env @@ -1,13 +1,24 @@ -export AWS_SECRET_KEY_ID=your-id -export AWS_SECRET_ACCESS_KEY=your-key -export SENDGRID_API_KEY=your-sendgrid-key -export SENDGRID_FROM=emailaccount@that.sends.notifications.com -export AWS_REGION=us-east-1 -export OIN_BUCKET=bucket-name +export PORT=4000 +export HOST=0.0.0.0 +export AWS_SECRET_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export AWS_REGION=us-west-2 + +export SENDGRID_API_KEY= +export SENDGRID_FROM= + +export OIN_BUCKET=oam-uploader + export DBURI=mongodb://localhost/oam-uploader export DBURI_TEST=mongodb://localhost/oam-uploader-test -export DBURI_TEST=mongodb://localhost/oam-uploader-test + export ADMIN_USERNAME=admin export ADMIN_PASSWORD=admin -export GDRIVE_KEY=your-google-key -export GDAL_TRANSLATE_BIN= \ No newline at end of file + +export GDRIVE_KEY= + +export GDAL_TRANSLATE_BIN=/usr/bin/gdal_translate + +export MAX_WORKERS=1 + +export NEW_RELIC_LICENSE_KEY= \ No newline at end of file diff --git a/newrelic.js b/newrelic.js index f4c4268..f45e90a 100644 --- a/newrelic.js +++ b/newrelic.js @@ -11,6 +11,7 @@ exports.config = { app_name: ['oam-uploader', 'server'], /** * Your New Relic license key. + * The license key is set through the env variable NEW_RELIC_LICENSE_KEY */ license_key: 'license key here', logging: { diff --git a/worker/newrelic.js b/worker/newrelic.js index eb08403..aa24a1a 100644 --- a/worker/newrelic.js +++ b/worker/newrelic.js @@ -11,6 +11,7 @@ exports.config = { app_name: ['oam-uploader', 'worker'], /** * Your New Relic license key. + * The license key is set through the env variable NEW_RELIC_LICENSE_KEY */ license_key: 'license key here', logging: { From 8153db777d9088aaaf0fc2ef678207260324433f Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 26 Jan 2017 11:15:27 +0000 Subject: [PATCH 132/144] Move cookie password to config file --- README.md | 4 +++- config.js | 4 +++- index.js | 4 ++-- local.sample.env | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e63df1a..d804db8 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,10 @@ You need to set environment variables before starting the API. We suggest you co #### Environment Variables +- `NODE_ENV` - Node environment. When in production should be set to `production`, otherwise can be ignored. - `PORT` - The port to listen on (Default to 4000). -- `HOST` - The hostname or ip address (Default to 0.0.0.0) +- `HOST` - The hostname or ip address (Default to 0.0.0.0). +- `COOKIE_PASSWORD` - Password used for cookie encoding. Should be at least 32 characters long. IMPORTANT to change the default one for production. - `AWS_SECRET_KEY_ID` - AWS secret key id for reading `OIN_BUCKET`. - `AWS_SECRET_ACCESS_KEY` - AWS secret access key for reading `OIN_BUCKET`. - `AWS_REGION` - AWS region of `OIN_BUCKET` (Default to us-west-2). diff --git a/config.js b/config.js index 9cbf78d..dd264bb 100644 --- a/config.js +++ b/config.js @@ -22,6 +22,7 @@ var defaults = { 'processed. You can check on the status of the upload at ' + 'http://upload.openaerialmap.org/#/status/{UPLOAD_ID}.' }, + cookiePassword: '3b296ce42ec560abeabaef', logOptions: { opsInterval: 3000, reporters: [{ @@ -52,7 +53,8 @@ var environment = { sendgridApiKey: process.env.SENDGRID_API_KEY, sendgridFrom: process.env.SENDGRID_FROM, gdalTranslateBin: process.env.GDAL_TRANSLATE_BIN, - gdriveKey: process.env.GDRIVE_KEY + gdriveKey: process.env.GDRIVE_KEY, + cookiePassword: process.env.COOKIE_PASSWORD }; var config = xtend(defaults); diff --git a/index.js b/index.js index b603b66..3c11262 100644 --- a/index.js +++ b/index.js @@ -47,11 +47,11 @@ var OAMUploader = function (readyCb) { // Setup cookie auth. // Hapi cookie plugin configuration. hapi.auth.strategy('session', 'cookie', { - password: '3b296ce42ec560abeabaef57379aee68249a6d7912ac19cf70f10a35021fc9df7453225c4dcd9f6defaf242e701f50bbd3f2b63616029bfcd8ddf53f406079d6', + password: config.cookiePassword, cookie: 'oam-uploader-api', redirectTo: false, // Change for production. - isSecure: false, + isSecure: process.env.NODE_ENV === 'production', validateFunc: validateUserCookie(hapi.plugins.db.connection) }); diff --git a/local.sample.env b/local.sample.env index ada29d8..83fa3e8 100644 --- a/local.sample.env +++ b/local.sample.env @@ -1,5 +1,6 @@ export PORT=4000 export HOST=0.0.0.0 +export COOKIE_PASSWORD=3b296ce42ec560abeabaef export AWS_SECRET_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_REGION=us-west-2 From ebe51568d0747c8470123fe0f1d4cd06a9321249 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Thu, 16 Feb 2017 15:12:29 -0500 Subject: [PATCH 133/144] Reenable tags and license metadata attributes --- local.sample.env | 13 ------------- models/upload.js | 2 ++ package.json | 1 + routes/uploads.js | 2 ++ 4 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 local.sample.env diff --git a/local.sample.env b/local.sample.env deleted file mode 100644 index 77847ad..0000000 --- a/local.sample.env +++ /dev/null @@ -1,13 +0,0 @@ -export AWS_SECRET_KEY_ID=your-id -export AWS_SECRET_ACCESS_KEY=your-key -export SENDGRID_API_KEY=your-sendgrid-key -export SENDGRID_FROM=emailaccount@that.sends.notifications.com -export AWS_REGION=us-east-1 -export OIN_BUCKET=bucket-name -export DBURI=mongodb://localhost/oam-uploader -export DBURI_TEST=mongodb://localhost/oam-uploader-test -export DBURI_TEST=mongodb://localhost/oam-uploader-test -export ADMIN_USERNAME=admin -export ADMIN_PASSWORD=admin -export GDRIVE_KEY=your-google-key -export GDAL_TRANSLATE_BIN= \ No newline at end of file diff --git a/models/upload.js b/models/upload.js index 6b1b16d..3b4efa7 100644 --- a/models/upload.js +++ b/models/upload.js @@ -14,6 +14,8 @@ var sceneSchema = Joi.object().keys({ acquisition_start: Joi.date().required(), acquisition_end: Joi.date().required(), tms: Joi.string().allow(null), + license: Joi.string().required(), + tags: Joi.string().allow(null), urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https', 'gdrive']})) .min(1).required() }); diff --git a/package.json b/package.json index 167c056..447982b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/hotosm/oam-uploader-api", "dependencies": { + "amazon-s3-url-signer": "^0.1.0", "aws-sdk": "^2.1.44", "babel": "^5.8.21", "boom": "^2.8.0", diff --git a/routes/uploads.js b/routes/uploads.js index f6c3fcc..aac9d29 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -33,6 +33,8 @@ function includeImages (db, scene, callback) { }) .toArray(function (err, images) { scene.images = images; + delete scene.tags; + delete scene.license; callback(err, scene); }); } From 0530f8d8084a4417bd45f8ca2bf2dfd7942a5c9b Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Fri, 17 Feb 2017 13:47:38 -0500 Subject: [PATCH 134/144] Begin reimplementing direct upload support --- config.js | 4 +++- index.js | 17 +++++++++++++++++ routes/uploads.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/config.js b/config.js index e0765b0..a2a79d0 100644 --- a/config.js +++ b/config.js @@ -48,7 +48,9 @@ var environment = { sendgridApiKey: process.env.SENDGRID_API_KEY, sendgridFrom: process.env.SENDGRID_FROM, gdriveKey: process.env.GDRIVE_KEY, - tilerBaseUrl: process.env.TILER_BASE_URL + tilerBaseUrl: process.env.TILER_BASE_URL, + awsSecretKeyId: process.env.AWS_SECRET_KEY_ID, + awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY }; var config = xtend(defaults); diff --git a/index.js b/index.js index b603b66..ac589c3 100644 --- a/index.js +++ b/index.js @@ -72,4 +72,21 @@ var OAMUploader = function (readyCb) { }); }; +// https://medium.com/the-spumko-suite/testing-hapi-services-with-lab-96ac463c490a +// The if (!module.parent) {…} conditional makes sure that if the script is +// being required as a module by another script, we don’t start the server. +// This is done to prevent the server from starting when we’re testing it. +// With Hapi, we don’t need to have the server listening to test it. +if (!module.parent) { + OAMUploader(function (hapi) { + // Start the server. + hapi.start(function () { + hapi.log(['info'], 'Server running at:' + hapi.info.uri); + // spawn a worker to handle any unprocessed uploads that may be sitting + // around in the database + hapi.plugins.workers.spawn(); + }); + }); +} + module.exports = OAMUploader; diff --git a/routes/uploads.js b/routes/uploads.js index aac9d29..09b8cc3 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -4,11 +4,22 @@ var ObjectID = require('mongodb').ObjectID; var queue = require('queue-async'); var Boom = require('boom'); var Joi = require('joi'); +var AWS = require('aws-sdk'); + var uploadSchema = require('../models/upload'); var config = require('../config'); var sendgrid = require('sendgrid')(config.sendgridApiKey); +var awsConfig = new AWS.Config(); +awsConfig.update({ + region: 'us-east-1', + credentials: { + 'accessKeyId': config.awsSecretKeyId, + 'secretAccessKey': config.awsSecretAccessKey + } +}); + function insertImages (db, scene, callback) { var imageIds = []; db.collection('images').insertMany(scene.urls.map(function (url) { @@ -72,6 +83,39 @@ module.exports = [ }); }); } + }, + /** + * @api {get} /uploads/url Get presigned URL for upload to S3 + * @apiPermission Token + * @apiParam {Object} payload Parameters sent as object resolvable from request.payload + * @apiParam {string} payload.name The name of the file to be uploaded + * @apiParam {string} payload.type The content type of the file to be uploaded + * @apiUse uploadUrlStatusSuccess + */ + { + method: 'POST', + path: '/uploads/url', + config: { + auth: 'api-token' + }, + handler: function (request, reply) { + var payload = JSON.parse(request.payload); + var s3 = new AWS.S3(); + var params = { + Bucket: config.oinBucket, + Key: payload.name, + ContentType: payload.type, + Expires: 60 + }; + s3.getSignedUrl('putObject', params, function (err, url) { + if (err) { + console.log(err); + return reply({code: 500, url: null}); + } else { + return reply({code: 200, url: url}); + } + }); + } }, /** * @api {get} /uploads/:id Get the status of a given upload From dac1e17c494647bbc0ebe5281f69baf57d0a8039 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 14 Mar 2017 13:53:10 -0400 Subject: [PATCH 135/144] Add direct uploads --- routes/uploads.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/routes/uploads.js b/routes/uploads.js index 09b8cc3..3735627 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -11,14 +11,11 @@ var config = require('../config'); var sendgrid = require('sendgrid')(config.sendgridApiKey); -var awsConfig = new AWS.Config(); -awsConfig.update({ +AWS.config = { region: 'us-east-1', - credentials: { - 'accessKeyId': config.awsSecretKeyId, - 'secretAccessKey': config.awsSecretAccessKey - } -}); + accessKeyId: config.awsSecretKeyId, + secretAccessKey: config.awsSecretAccessKey +}; function insertImages (db, scene, callback) { var imageIds = []; From 82ac619c46f43594a39cd40bfaed4f891d2570e9 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 14 Mar 2017 14:15:55 -0400 Subject: [PATCH 136/144] Sync with current develop --- config.js | 4 ++-- index.js | 21 ++------------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/config.js b/config.js index a2a79d0..2ba842f 100644 --- a/config.js +++ b/config.js @@ -20,6 +20,7 @@ var defaults = { 'processed. You can check on the status of the upload at ' + 'http://upload.openaerialmap.org/#/status/{UPLOAD_ID}.' }, + cookiePassword: '3b296ce42ec560abeabaef', logOptions: { opsInterval: 3000, reporters: [{ @@ -49,8 +50,7 @@ var environment = { sendgridFrom: process.env.SENDGRID_FROM, gdriveKey: process.env.GDRIVE_KEY, tilerBaseUrl: process.env.TILER_BASE_URL, - awsSecretKeyId: process.env.AWS_SECRET_KEY_ID, - awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + cookiePassword: process.env.COOKIE_PASSWORD }; var config = xtend(defaults); diff --git a/index.js b/index.js index ac589c3..3c11262 100644 --- a/index.js +++ b/index.js @@ -47,11 +47,11 @@ var OAMUploader = function (readyCb) { // Setup cookie auth. // Hapi cookie plugin configuration. hapi.auth.strategy('session', 'cookie', { - password: '3b296ce42ec560abeabaef57379aee68249a6d7912ac19cf70f10a35021fc9df7453225c4dcd9f6defaf242e701f50bbd3f2b63616029bfcd8ddf53f406079d6', + password: config.cookiePassword, cookie: 'oam-uploader-api', redirectTo: false, // Change for production. - isSecure: false, + isSecure: process.env.NODE_ENV === 'production', validateFunc: validateUserCookie(hapi.plugins.db.connection) }); @@ -72,21 +72,4 @@ var OAMUploader = function (readyCb) { }); }; -// https://medium.com/the-spumko-suite/testing-hapi-services-with-lab-96ac463c490a -// The if (!module.parent) {…} conditional makes sure that if the script is -// being required as a module by another script, we don’t start the server. -// This is done to prevent the server from starting when we’re testing it. -// With Hapi, we don’t need to have the server listening to test it. -if (!module.parent) { - OAMUploader(function (hapi) { - // Start the server. - hapi.start(function () { - hapi.log(['info'], 'Server running at:' + hapi.info.uri); - // spawn a worker to handle any unprocessed uploads that may be sitting - // around in the database - hapi.plugins.workers.spawn(); - }); - }); -} - module.exports = OAMUploader; From 70de22c4c62da272254454ce68687c224a25fd55 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 21 Mar 2017 17:19:04 -0700 Subject: [PATCH 137/144] Filter for selected image Prevents duplicate copies of images from being created. image._id is a Mongo type, so === is insufficient. Fixes #72 --- worker/queue.js | 112 +++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/worker/queue.js b/worker/queue.js index aad1666..aa26614 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -76,66 +76,70 @@ JobQueue.prototype._setupQueries = function _setupQueries () { // main loop JobQueue.prototype._mainloop = function mainloop () { return this.images - // look for an unprocessed image that hasn't been claimed, and (atomically) - // mark it as claimed by this worker - .findOneAndUpdate({ - status: 'initial' - }, this.update.jobClaimed, { returnOriginal: false }) - .then((result) => { - if (!result.value) { - // no jobs left; try to shut down. - // avoid race condition by making sure our state wasn't changed from - // 'working' to something else (by the server) before we actually quit. - return this.workers.updateOne(this.query.myself, this.update.stopping) - .then((result) => { - // failed to set our state, so continue processing - if (result.modifiedCount === 0) { return this._mainloop(); } - // we're in the clear - clean up and exit - return this.cleanup(); - }) - .catch(this.cleanup.bind(this)); - } + // look for an unprocessed image that hasn't been claimed, and (atomically) + // mark it as claimed by this worker + .findOneAndUpdate({ + status: 'initial' + }, this.update.jobClaimed, { returnOriginal: false }) + .then((result) => { + if (!result.value) { + // no jobs left; try to shut down. + // avoid race condition by making sure our state wasn't changed from + // 'working' to something else (by the server) before we actually quit. + return this.workers.updateOne(this.query.myself, this.update.stopping) + .then((result) => { + // failed to set our state, so continue processing + if (result.modifiedCount === 0) { return this._mainloop(); } + // we're in the clear - clean up and exit + return this.cleanup(); + }) + .catch(this.cleanup.bind(this)); + } - // we got a job! - var image = result.value; - log(['info'], 'Processing job', image); - return this.db.collection('uploads') - // find the upload / scene that contains this image - .findOne({ 'scenes.images': image._id }) - .then(function (upload) { - var found; - upload.scenes.forEach(function (scene, i) { - scene.images.forEach(function (id) { - var key = [upload._id, i, uuidV4()].join('/'); + // we got a job! + var image = result.value; + log(['info'], 'Processing job', image); - // now that we have the scene, we can process the image - found = processImage(scene, image.url, key); - }); - }); + return this.db.collection('uploads') + // find the upload / scene that contains this image + .findOne({ 'scenes.images': image._id }) + .then(function (upload) { + var found; - if (found) { return found; } - // this should never happen - throw new Error('Could not find the scene for image ' + image._id); - }) - .then((processed) => { - // mark the job as finished - return this.images.findOneAndUpdate(result.value, this.update.jobFinished(processed)); - }) - .then(() => { - // update this worker's timestamp - return this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); - }) - // keep going - .then(this._mainloop.bind(this)) - .catch((error) => { - log(['error'], error); - return this.images.findOneAndUpdate(result.value, this.update.jobErrored(error)) + upload.scenes.forEach(function (scene, i) { + scene.images + .filter(id => image._id.equals(id)) + .forEach(function (id) { + var key = [upload._id, i, uuidV4()].join('/'); + + // now that we have the scene, we can process the image + found = processImage(scene, image.url, key); + }); + }); + + if (found) { return found; } + // this should never happen + throw new Error('Could not find the scene for image ' + image._id); + }) + .then((processed) => { + // mark the job as finished + return this.images.findOneAndUpdate(result.value, this.update.jobFinished(processed)); + }) .then(() => { - this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); + // update this worker's timestamp + return this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); }) - .then(this._mainloop.bind(this)); + // keep going + .then(this._mainloop.bind(this)) + .catch((error) => { + log(['error'], error); + return this.images.findOneAndUpdate(result.value, this.update.jobErrored(error)) + .then(() => { + this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); + }) + .then(this._mainloop.bind(this)); + }); }); - }); }; JobQueue.prototype.cleanup = function cleanup (err) { From 814227ba1a72782fb00ca65debd564e2eb72e421 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 21 Mar 2017 17:22:34 -0700 Subject: [PATCH 138/144] Use spawn instead of execFile to avoid maxBuffer execFile buffers stdout and stderr internally. spawn does no such thing, so instead we pipe stdout/stderr from the child to the worker's stdio and only buffer stderr (allowing us to return it in an error callback). Fixes #73 --- worker/process-image.js | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index c49ac29..7e17e99 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -53,18 +53,35 @@ function _processImage (scene, sourceUrl, targetPrefix, callback) { var output = `s3://${s3bucket}/${targetPrefix}`; args.push(sourceUrl, output); - return cp.execFile('process.sh', args, { + var child = cp.spawn('process.sh', args, { AWS_ACCESS_KEY_ID: AWS.config.credentials.accessKeyId, AWS_DEFAULT_REGION: AWS.config.region, AWS_SECRET_ACCESS_KEY: AWS.config.credentials.secretAccessKey, AWS_SESSION_TOKEN: AWS.config.credentials.sessionToken, THUMBNAIL_SIZE: config.thumbnailSize, TILER_BASE_URL: config.tilerBaseUrl - }, function (err, stdout, stderr) { - if (err) { - err.stdout = stdout; - err.stderr = stderr; - return callback(err); + }); + + var stderr = []; + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stderr); + child.stderr.on('data', chunk => stderr.push(chunk)); + + child.on('error', err => { + // prevent callback from being called twice + var _callback = callback; + callback = function () {}; + + return _callback(err); + }); + + child.on('exit', code => { + // prevent callback from being called twice + var _callback = callback; + callback = function () {}; + + if (code !== 0) { + return _callback(new Error('Exited with ' + code + ': ' + Buffer.concat(stderr).toString())); } log(['debug'], 'Converted image to OAM standard format. Input: ', sourceUrl, 'Output: ', output); @@ -74,10 +91,10 @@ function _processImage (scene, sourceUrl, targetPrefix, callback) { uri: `http://${s3bucket}.s3.amazonaws.com/${targetPrefix}_meta.json` }, function (err, rsp, metadata) { if (err) { - return callback(err); + return _callback(err); } - return callback(null, { + return _callback(null, { metadata: metadata }); }); From f4f9f87a5fa8cbb8b143a8eeb2009e13a0c8124c Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 21 Mar 2017 17:26:14 -0700 Subject: [PATCH 139/144] Indentation grumble grumble --- worker/queue.js | 110 ++++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/worker/queue.js b/worker/queue.js index aa26614..cadddfc 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -76,70 +76,70 @@ JobQueue.prototype._setupQueries = function _setupQueries () { // main loop JobQueue.prototype._mainloop = function mainloop () { return this.images - // look for an unprocessed image that hasn't been claimed, and (atomically) - // mark it as claimed by this worker - .findOneAndUpdate({ - status: 'initial' - }, this.update.jobClaimed, { returnOriginal: false }) - .then((result) => { - if (!result.value) { - // no jobs left; try to shut down. - // avoid race condition by making sure our state wasn't changed from - // 'working' to something else (by the server) before we actually quit. - return this.workers.updateOne(this.query.myself, this.update.stopping) - .then((result) => { - // failed to set our state, so continue processing - if (result.modifiedCount === 0) { return this._mainloop(); } - // we're in the clear - clean up and exit - return this.cleanup(); - }) - .catch(this.cleanup.bind(this)); - } + // look for an unprocessed image that hasn't been claimed, and (atomically) + // mark it as claimed by this worker + .findOneAndUpdate({ + status: 'initial' + }, this.update.jobClaimed, { returnOriginal: false }) + .then((result) => { + if (!result.value) { + // no jobs left; try to shut down. + // avoid race condition by making sure our state wasn't changed from + // 'working' to something else (by the server) before we actually quit. + return this.workers.updateOne(this.query.myself, this.update.stopping) + .then((result) => { + // failed to set our state, so continue processing + if (result.modifiedCount === 0) { return this._mainloop(); } + // we're in the clear - clean up and exit + return this.cleanup(); + }) + .catch(this.cleanup.bind(this)); + } - // we got a job! - var image = result.value; - log(['info'], 'Processing job', image); + // we got a job! + var image = result.value; + log(['info'], 'Processing job', image); - return this.db.collection('uploads') - // find the upload / scene that contains this image - .findOne({ 'scenes.images': image._id }) - .then(function (upload) { - var found; + return this.db.collection('uploads') + // find the upload / scene that contains this image + .findOne({ 'scenes.images': image._id }) + .then(function (upload) { + var found; - upload.scenes.forEach(function (scene, i) { - scene.images - .filter(id => image._id.equals(id)) - .forEach(function (id) { - var key = [upload._id, i, uuidV4()].join('/'); + upload.scenes.forEach(function (scene, i) { + scene.images + .filter(id => image._id.equals(id)) + .forEach(function (id) { + var key = [upload._id, i, uuidV4()].join('/'); - // now that we have the scene, we can process the image - found = processImage(scene, image.url, key); - }); + // now that we have the scene, we can process the image + found = processImage(scene, image.url, key); }); + }); - if (found) { return found; } - // this should never happen - throw new Error('Could not find the scene for image ' + image._id); - }) - .then((processed) => { - // mark the job as finished - return this.images.findOneAndUpdate(result.value, this.update.jobFinished(processed)); - }) + if (found) { return found; } + // this should never happen + throw new Error('Could not find the scene for image ' + image._id); + }) + .then((processed) => { + // mark the job as finished + return this.images.findOneAndUpdate(result.value, this.update.jobFinished(processed)); + }) + .then(() => { + // update this worker's timestamp + return this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); + }) + // keep going + .then(this._mainloop.bind(this)) + .catch((error) => { + log(['error'], error); + return this.images.findOneAndUpdate(result.value, this.update.jobErrored(error)) .then(() => { - // update this worker's timestamp - return this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); + this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); }) - // keep going - .then(this._mainloop.bind(this)) - .catch((error) => { - log(['error'], error); - return this.images.findOneAndUpdate(result.value, this.update.jobErrored(error)) - .then(() => { - this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); - }) - .then(this._mainloop.bind(this)); - }); + .then(this._mainloop.bind(this)); }); + }); }; JobQueue.prototype.cleanup = function cleanup (err) { From 822a33198d31017d4bf3a2c8089831dad8d8eff7 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 21 Mar 2017 17:27:01 -0700 Subject: [PATCH 140/144] Indentation grumble grumble --- worker/queue.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/worker/queue.js b/worker/queue.js index cadddfc..682af52 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -87,13 +87,13 @@ JobQueue.prototype._mainloop = function mainloop () { // avoid race condition by making sure our state wasn't changed from // 'working' to something else (by the server) before we actually quit. return this.workers.updateOne(this.query.myself, this.update.stopping) - .then((result) => { - // failed to set our state, so continue processing - if (result.modifiedCount === 0) { return this._mainloop(); } - // we're in the clear - clean up and exit - return this.cleanup(); - }) - .catch(this.cleanup.bind(this)); + .then((result) => { + // failed to set our state, so continue processing + if (result.modifiedCount === 0) { return this._mainloop(); } + // we're in the clear - clean up and exit + return this.cleanup(); + }) + .catch(this.cleanup.bind(this)); } // we got a job! @@ -108,13 +108,13 @@ JobQueue.prototype._mainloop = function mainloop () { upload.scenes.forEach(function (scene, i) { scene.images - .filter(id => image._id.equals(id)) - .forEach(function (id) { - var key = [upload._id, i, uuidV4()].join('/'); + .filter(id => image._id.equals(id)) + .forEach(function (id) { + var key = [upload._id, i, uuidV4()].join('/'); - // now that we have the scene, we can process the image - found = processImage(scene, image.url, key); - }); + // now that we have the scene, we can process the image + found = processImage(scene, image.url, key); + }); }); if (found) { return found; } From 4b3aa1e17a371d47a0e57e663239ef6015093df8 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 22 Mar 2017 17:01:06 +0000 Subject: [PATCH 141/144] adjust for temp bucket --- config.js | 2 ++ local.sample.env | 1 + routes/uploads.js | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config.js b/config.js index 2ba842f..89ba976 100644 --- a/config.js +++ b/config.js @@ -9,6 +9,7 @@ var defaults = { adminPassword: null, // the administrator username adminUsername: null, // the administrator password oinBucket: 'oam-uploader', // name of the OpenImageryNetwork bucket to which imagery should be uploaded + uploadBucket: 'oam-uploader-temp', // name of the bucket for temporary storage for direct uploads thumbnailSize: 300, // (very) approximate thumbnail size, in kilobytes maxWorkers: 1, // the maximum number of workers sendgridApiKey: null, // sendgrid API key, for sending notification emails @@ -42,6 +43,7 @@ var environment = { port: process.env.PORT, host: process.env.HOST, oinBucket: process.env.OIN_BUCKET, + uploadBucket: process.env.UPLOAD_BUCKET, dbUri: process.env.NODE_ENV === 'test' ? process.env.DBURI_TEST : process.env.DBURI, maxWorkers: process.env.MAX_WORKERS, adminPassword: process.env.ADMIN_PASSWORD, diff --git a/local.sample.env b/local.sample.env index 83fa3e8..84fc157 100644 --- a/local.sample.env +++ b/local.sample.env @@ -9,6 +9,7 @@ export SENDGRID_API_KEY= export SENDGRID_FROM= export OIN_BUCKET=oam-uploader +export UPLOAD_BUCKET=oam-uploader-temp export DBURI=mongodb://localhost/oam-uploader export DBURI_TEST=mongodb://localhost/oam-uploader-test diff --git a/routes/uploads.js b/routes/uploads.js index 3735627..1d21698 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -99,7 +99,7 @@ module.exports = [ var payload = JSON.parse(request.payload); var s3 = new AWS.S3(); var params = { - Bucket: config.oinBucket, + Bucket: config.uploadBucket, Key: payload.name, ContentType: payload.type, Expires: 60 From c07fb6e842e275679ef26608802c636c5a5301b3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 23 Mar 2017 10:10:32 +0000 Subject: [PATCH 142/144] fix validation check for empty tags --- models/upload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/upload.js b/models/upload.js index 3b4efa7..c127362 100644 --- a/models/upload.js +++ b/models/upload.js @@ -15,7 +15,7 @@ var sceneSchema = Joi.object().keys({ acquisition_end: Joi.date().required(), tms: Joi.string().allow(null), license: Joi.string().required(), - tags: Joi.string().allow(null), + tags: Joi.string().allow(''), urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https', 'gdrive']})) .min(1).required() }); From 326191c01c29facbd2a0939fced229a4aebf835b Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Thu, 13 Apr 2017 10:54:49 -0700 Subject: [PATCH 143/144] Mount /tmp from the host This increases the amount of available temp space from 10GB (default root volume size) to whatever the partition that /tmp is on has free. --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index d987912..bf78f92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,3 +12,5 @@ services: - "4000:4000" depends_on: - mongo + volumes: + - /tmp/uploader-api:/tmp From 366533c3fe822e88b27010edee0ffa1033129420 Mon Sep 17 00:00:00 2001 From: Thomas Buckley-Houston Date: Wed, 31 May 2017 18:22:53 +0700 Subject: [PATCH 144/144] Fix broken tests They'd been broken for a while! Worth noting that the refactoring of the image processing into the dynamic tiler means that it's harder to test actual image processing here. So now we're just testing that an image upload got submitted to the queue. --- config.js | 4 ++-- test/fixture/NE1_50M_SR.input.json | 1 + test/fixture/upload-status.json | 34 +++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index 89ba976..84fa016 100644 --- a/config.js +++ b/config.js @@ -46,8 +46,8 @@ var environment = { uploadBucket: process.env.UPLOAD_BUCKET, dbUri: process.env.NODE_ENV === 'test' ? process.env.DBURI_TEST : process.env.DBURI, maxWorkers: process.env.MAX_WORKERS, - adminPassword: process.env.ADMIN_PASSWORD, - adminUsername: process.env.ADMIN_USERNAME, + adminPassword: process.env.ADMIN_PASSWORD || 'admin', + adminUsername: process.env.ADMIN_USERNAME || 'admin', sendgridApiKey: process.env.SENDGRID_API_KEY, sendgridFrom: process.env.SENDGRID_FROM, gdriveKey: process.env.GDRIVE_KEY, diff --git a/test/fixture/NE1_50M_SR.input.json b/test/fixture/NE1_50M_SR.input.json index a2f1aef..2ba69ad 100644 --- a/test/fixture/NE1_50M_SR.input.json +++ b/test/fixture/NE1_50M_SR.input.json @@ -11,6 +11,7 @@ }, "title": "Natural Earth Image", "provider": "Natural Earth", + "license": "CC-BY", "sensor": "Some Algorithm", "platform": "satellite", "acquisition_start": "2015-04-01T00:00:00.000", diff --git a/test/fixture/upload-status.json b/test/fixture/upload-status.json index 62b6f2b..ff3c6a0 100644 --- a/test/fixture/upload-status.json +++ b/test/fixture/upload-status.json @@ -1 +1,33 @@ -{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","tms":null,"images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":402897,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} +{ + "_id": "55e0c86b24c379c000544d24", + "uploader": { + "name": "Lady Stardust", + "email": "lady@stardust.xyz" + }, + "scenes": [ + { + "contact": { + "name": "Ziggy", + "email": "ziggy@bowie.net" + }, + "title": "Natural Earth Image", + "provider": "Natural Earth", + "sensor": "Some Algorithm", + "platform": "satellite", + "acquisition_start": "2015-04-01T00:00:00.000Z", + "acquisition_end": "2015-04-30T00:00:00.000Z", + "tms": null, + "images": [ + { + "_id": "55e0c86a24c379c000544d23", + "url": "http://localhost:8080/NE1_50M_SR.tif", + "status": "initial", + "messages": [], + "startedAt": "2015-08-28T20:45:31.062Z", + "stoppedAt": "2015-08-28T20:45:31.247Z" + } + ] + } + ], + "createdAt": "2015-08-28T20:45:30.820Z" +}

)H4nR8gv@YAQ($8bgtc`IaVpy>R2tTyZ5Se=t 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 ZrVQC0vEuo3&Y0|pARq_`0{= 400) { return callback(new Error('Could not download ' + url + '; server responded with status code ' + downloadStatus)); } + + var messages = []; // we've successfully downloaded the file. now do stuff with it. generateMetadata(scene, path, key, function (err, metadata) { if (err) { return callback(err); } makeThumbnail(path, function (thumbErr, thumbPath) { - var messages = []; - var q = queue(); - - // upload image - q.defer(s3.upload.bind(s3), { - Body: fs.createReadStream(path), - Bucket: s3bucket, - Key: key - }); - - // upload thumbnail, if it worked - if (!thumbErr) { - q.defer(s3.upload.bind(s3), { - Body: fs.createReadStream(thumbPath), - Bucket: s3bucket, - Key: key + '.thumb.png' - }); - metadata.properties.thumbnail = publicUrl(s3bucket, key + '.thumb.png'); - } else { + if (thumbErr) { messages.push('Could not generate thumbnail: ' + thumbErr.message); + thumbPath = null; } - - // upload metadata - q.defer(s3.upload.bind(s3), { - Body: JSON.stringify(metadata), - Bucket: s3bucket, - Key: key + '_meta.json' - }); - - log(['debug'], 'Uploading to s3; bucket=' + s3bucket + ' key=' + key); - q.awaitAll(function (err, data) { + uploadToS3(s3, path, key, metadata, thumbPath, function (err) { cleanup(); // delete tempfile - log(['debug'], 'Uploaded', data); callback(err, { metadata: metadata, messages: messages }); }); }); @@ -116,6 +56,41 @@ function processUrl (upload, scene, url, key, callback) { }); } +function uploadToS3 (s3, path, key, metadata, thumbPath, callback) { + var q = queue(); + + // upload image + q.defer(s3.upload.bind(s3), { + Body: fs.createReadStream(path), + Bucket: s3bucket, + Key: key + }); + + // upload thumbnail, if we have one + if (thumbPath) { + q.defer(s3.upload.bind(s3), { + Body: fs.createReadStream(thumbPath), + Bucket: s3bucket, + Key: key + '.thumb.png' + }); + metadata.properties.thumbnail = publicUrl(s3bucket, key + '.thumb.png'); + } + + // upload metadata + q.defer(s3.upload.bind(s3), { + Body: JSON.stringify(metadata), + Bucket: s3bucket, + Key: key + '_meta.json' + }); + + log(['debug'], 'Uploading to s3; bucket=' + s3bucket + ' key=' + key); + q.awaitAll(function (err) { + if (err) { return callback(err); } + log(['debug'], 'Finished uploading'); + callback(); + }); +} + function generateMetadata (scene, path, key, callback) { log(['debug'], 'Generating metadata.'); fs.stat(path, function (err, stat) { diff --git a/worker/queue.js b/worker/queue.js new file mode 100644 index 0000000..b9f7569 --- /dev/null +++ b/worker/queue.js @@ -0,0 +1,163 @@ +var MongoClient = require('mongodb').MongoClient; +var moment = require('moment'); +var Promise = require('es6-promise'); +var promisify = require('es6-promisify'); +var processImage = require('./process-image'); +var log = require('./log'); +var config = require('../config'); + +module.exports = JobQueue; + +function JobQueue (s3) { + if (!(this instanceof JobQueue)) { return new JobQueue(s3); } + this.s3 = s3; +} + +JobQueue.prototype.run = function () { + return this._initialize() + .then(this._mainloop.bind(this)); +}; + +JobQueue.prototype._initialize = function init () { + if (this._initialized) { return Promise.resolve(true); } + this._initialized = true; + return promisify(MongoClient.connect.bind(MongoClient))(config.dbUri) + .then(function (connection) { + this.db = connection; + this.workers = this.db.collection('workers'); + this.images = this.db.collection('images'); + + return this.workers.insertOne({ state: 'working' }) + .then(function (result) { + this.workerId = result.ops[0]._id; + log.workerId = this.workerId; + this._setupQueries(); + log('Initialized'); + }.bind(this)) + .catch(this.cleanup.bind(this)); + }.bind(this)); +}; + +JobQueue.prototype._setupQueries = function _setupQueries () { + // mongodb queries and updates + this.query = {}; + this.query.myself = { _id: this.workerId, state: 'working' }; + + this.update = {}; + this.update.lastJobTimestamp = { $currentDate: { lastJobTimestamp: true } }; + this.update.stopping = { $set: { state: 'stopping' } }; + this.update.jobClaimed = { + $set: { status: 'processing', _workerId: this.workerId }, + $currentDate: { startedAt: true } + }; + this.update.jobFinished = { + $set: { status: 'finished' }, + $unset: { _workerId: '' }, + $currentDate: { stoppedAt: true } + }; + this.update.jobErrored = function jobErrored (error) { + error = { + message: error.message, + data: JSON.stringify(error) + }; + return { + $set: { status: 'errored', error: error }, + $unset: { _workerId: '' }, + $currentDate: { stoppedAt: true } + }; + }; +}; + +// main loop +JobQueue.prototype._mainloop = function mainloop () { + return this.images + // look for an unprocessed image that hasn't been claimed, and (atomically) + // mark it as claimed by this worker + .findOneAndUpdate({ + status: 'initial' + }, this.update.jobClaimed, { returnOriginal: false }) + .then(function (result) { + if (!result.value) { + // no jobs left; try to shut down. + // avoid race condition by making sure our state wasn't changed from + // 'working' to something else (by the server) before we actually quit. + return this.workers.updateOne(this.query.myself, this.update.stopping) + .then(function (result) { + // failed to set our state, so continue processing + if (result.modifiedCount === 0) { return this._mainloop(); } + // we're in the clear - clean up and exit + return this.cleanup(); + }.bind(this)) + .catch(this.cleanup.bind(this)); + } + + // we got a job! + log(['info'], 'Processing job', image); + var now = moment().format('YYYY-MM-DD'); + var image = result.value; + var s3 = this.s3; + return this.db.collection('uploads') + // find the upload / scene that contains this image + .findOne({ 'scenes.images': image._id }) + .then(function (upload) { + var found; + upload.scenes.forEach(function (scene, i) { + scene.images.forEach(function (id, j) { + if (image._id.equals(id)) { + var filename = image.url.split('/').pop() || 'untitled'; + filename = ['scene', i, 'image', j, filename].join('-'); + var key = ['uploads', now, upload._id, 'scene', i, filename].join('/'); + // now that we have the scene, we can process the image + found = processImage(s3, scene, image.url, key); + } + }); + }); + + if (found) { return found; } + // this should never happen + throw new Error('Could not find the scene for image ' + image._id); + }) + .then(function () { + // mark the job as finished + return this.images.findOneAndUpdate(result.value, this.update.jobFinished); + }.bind(this)) + .then(function () { + // update this worker's timestamp + return this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); + }.bind(this)) + // keep going + .then(this._mainloop.bind(this)) + .catch(function (error) { + log(['error'], error); + return this.images.findOneAndUpdate(result.value, this.update.jobErrored(error)) + .then(function () { + this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); + }.bind(this)) + .then(this._mainloop.bind(this)); + }.bind(this)); + }.bind(this)); +}; + +JobQueue.prototype.cleanup = function cleanup (err) { + log('Cleaning up.'); + + if (err) { log(['error'], err, err.stack); } + if (!this.db) { return; } + if (!this.workerId) { return this.db.close(); } + + return this.workers.deleteOne({ _id: this.workerId }) + .then(function () { + return this.images.updateMany({ _workerId: this.workerId, status: 'processing' }, { + $set: { status: 'initial' }, + $unset: { _workerId: '', startedAt: '' } + }); + }.bind(this)) + .catch(function (error) { + log(['error'], 'Error cleaning up. Bad news.'); + log(['error'], error); + this.db.close(); + throw error; + }.bind(this)) + .then(function () { this.db.close(); }.bind(this)) + .then(function () { if (err) { throw err; } }); +}; From 78374029bfaaa36e014d479ae40e92d0673390f7 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 20 Aug 2015 10:21:38 -0400 Subject: [PATCH 052/144] Use latest mongo on travis --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9591c7e..2ff4524 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,13 +11,18 @@ env: - GH_REF=github.com/hotosm/oam-uploader-api.git - PRODUCTION_BRANCH=master - secure: W0eGs5Lg+fZOIEYKLIFJZPsw7qFUNFTAUtPS/2UmlWwlQtzvYCYnNu4dd62kZKivzCuWZiwdwsm58l2r6tB6zRv7/3S4mhkiA1SrYFC3p34CXXV9XaJeEgOQ1SXoJuAW1ThSmq7CJu9dsk3Aix+KA4MggzBgTPG2RXyYhgr3Qzhcil1BX6KNqW4TP7cPaJTUCEhoDcyVLNvmjvUDQNwwJjCu1hifDptlyZt88jDshIVPi4wd2ITy10O22dkwvVaes79A7yYrpRyBJyAhb8w/fPZsCwT+9wQz28x1Q47at/eERrpeAVGOUbXE+5cla/dCCVG3rMlyJnaRP0HagaUQK71GJMYe/nnOEirmF3vtpYzRTmzbvaahEtWr6EbdVWqXB8WJCBShgEF2NICwQAYwgIsYyzIAI3wDvVyrdhbSqqARlUqRwEN4NUFAXfVYkXssYWiTz/s0Eaktg3I/Z0wL2lgBy0hz2JN9BcSCjSdzzj9FWaoWjFSDamb/M5U1R6h9rTGe2bgoDGsZbo4TV/QCXIsKvqjX5lC/W5GFyoUOyTrqtGyPIpOdkTg8Nf1V2ZPuAznln6YYchMdO64eABy7w5tKErM6JP8N4pHFgkgIPJI4gZdAlUELFe+mgqp/kl1GEuSd8TtlIdZ/ErvsmhH5ubYifIdBkpSZfUMRM/vueXI= -services: mongodb before_install: - echo "Installing libvips..." - sudo add-apt-repository -y ppa:lovell/precise-backport-vips - sudo apt-get update - sudo apt-get install -y libvips-dev - echo "Completed libvips installation." +- echo "Installing latest mongodb" +- sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 +- echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list +- sudo apt-get update && sudo apt-get install -y mongodb-org +- sudo service mongod start +- echo "Completed installing mongodb" - chmod +x ./.build_scripts/docs.sh after_success: - "./.build_scripts/docs.sh" From 99322ed3a9b325486aa125f088694976da6dd412 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 20 Aug 2015 10:27:43 -0400 Subject: [PATCH 053/144] Fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2ff4524..73505f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 - echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list - sudo apt-get update && sudo apt-get install -y mongodb-org -- sudo service mongod start +- sudo service mongod restart - echo "Completed installing mongodb" - chmod +x ./.build_scripts/docs.sh after_success: From b150692028a58cd08191e7653fdf8fb91ec16fcd Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 20 Aug 2015 10:59:31 -0400 Subject: [PATCH 054/144] Use arrow functions for readability --- package.json | 5 +++-- test/babel.js | 20 ++++++++++++++++++++ worker/index.js | 1 + worker/queue.js | 42 +++++++++++++++++++++--------------------- 4 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 test/babel.js diff --git a/package.json b/package.json index 78e6c7e..36f52ad 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "docker-test": ".build_scripts/docker/run.sh /test.sh", "docker-start": ".build_scripts/docker/run.sh /start.sh", "build-docker": "docker build -t oam-uploader-api .build_scripts/docker", - "test": "semistandard && lab test/test__*.js", - "lab": "lab test/test__*.js", + "test": "semistandard && npm run lab", + "lab": "lab -T test/babel.js test/test__*.js", "docs": "apidoc -i routes/ -o docs/" }, "repository": { @@ -26,6 +26,7 @@ "dependencies": { "async": "^1.3.0", "aws-sdk": "^2.1.44", + "babel": "^5.8.21", "boom": "^2.8.0", "envloader": "0.0.2", "es6-promise": "^2.3.0", diff --git a/test/babel.js b/test/babel.js new file mode 100644 index 0000000..adb7716 --- /dev/null +++ b/test/babel.js @@ -0,0 +1,20 @@ +var Babel = require('babel'); + +module.exports = [ + { + ext: '.js', + transform: function (content, filename) { + + if (filename.indexOf('node_modules') === -1) { + var result = Babel.transform(content, { + sourceMap: 'inline', + filename: filename, + sourceFileName: filename + }); + return result.code; + } + + return content; + } + } +]; diff --git a/worker/index.js b/worker/index.js index 3117db6..ccf8967 100644 --- a/worker/index.js +++ b/worker/index.js @@ -1,6 +1,7 @@ 'use strict'; require('newrelic'); +require('babel/register'); /* * A worker that queries the db for new uploads and process 'em. diff --git a/worker/queue.js b/worker/queue.js index b9f7569..a0c2cbe 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -22,20 +22,20 @@ JobQueue.prototype._initialize = function init () { if (this._initialized) { return Promise.resolve(true); } this._initialized = true; return promisify(MongoClient.connect.bind(MongoClient))(config.dbUri) - .then(function (connection) { + .then((connection) => { this.db = connection; this.workers = this.db.collection('workers'); this.images = this.db.collection('images'); return this.workers.insertOne({ state: 'working' }) - .then(function (result) { + .then((result) => { this.workerId = result.ops[0]._id; log.workerId = this.workerId; this._setupQueries(); log('Initialized'); - }.bind(this)) + }) .catch(this.cleanup.bind(this)); - }.bind(this)); + }); }; JobQueue.prototype._setupQueries = function _setupQueries () { @@ -76,18 +76,18 @@ JobQueue.prototype._mainloop = function mainloop () { .findOneAndUpdate({ status: 'initial' }, this.update.jobClaimed, { returnOriginal: false }) - .then(function (result) { + .then((result) => { if (!result.value) { // no jobs left; try to shut down. // avoid race condition by making sure our state wasn't changed from // 'working' to something else (by the server) before we actually quit. return this.workers.updateOne(this.query.myself, this.update.stopping) - .then(function (result) { + .then((result) => { // failed to set our state, so continue processing if (result.modifiedCount === 0) { return this._mainloop(); } // we're in the clear - clean up and exit return this.cleanup(); - }.bind(this)) + }) .catch(this.cleanup.bind(this)); } @@ -117,25 +117,25 @@ JobQueue.prototype._mainloop = function mainloop () { // this should never happen throw new Error('Could not find the scene for image ' + image._id); }) - .then(function () { + .then(() => { // mark the job as finished return this.images.findOneAndUpdate(result.value, this.update.jobFinished); - }.bind(this)) - .then(function () { + }) + .then(() => { // update this worker's timestamp return this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); - }.bind(this)) + }) // keep going .then(this._mainloop.bind(this)) - .catch(function (error) { + .catch((error) => { log(['error'], error); return this.images.findOneAndUpdate(result.value, this.update.jobErrored(error)) - .then(function () { + .then(() => { this.workers.updateOne(this.query.myself, this.update.lastJobTimestamp); - }.bind(this)) + }) .then(this._mainloop.bind(this)); - }.bind(this)); - }.bind(this)); + }); + }); }; JobQueue.prototype.cleanup = function cleanup (err) { @@ -146,18 +146,18 @@ JobQueue.prototype.cleanup = function cleanup (err) { if (!this.workerId) { return this.db.close(); } return this.workers.deleteOne({ _id: this.workerId }) - .then(function () { + .then(() => { return this.images.updateMany({ _workerId: this.workerId, status: 'processing' }, { $set: { status: 'initial' }, $unset: { _workerId: '', startedAt: '' } }); - }.bind(this)) - .catch(function (error) { + }) + .catch((error) => { log(['error'], 'Error cleaning up. Bad news.'); log(['error'], error); this.db.close(); throw error; - }.bind(this)) - .then(function () { this.db.close(); }.bind(this)) + }) + .then(() => { this.db.close(); }) .then(function () { if (err) { throw err; } }); }; From d691cb183044c7dbc40f99d267fc5213bae3de8f Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 21 Aug 2015 15:27:04 -0400 Subject: [PATCH 055/144] Add deploy and upstart config --- .build_scripts/deploy.conf | 7 +++++++ .build_scripts/upstart.conf | 23 +++++++++++++++++++++++ package.json | 5 ++--- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .build_scripts/deploy.conf create mode 100644 .build_scripts/upstart.conf diff --git a/.build_scripts/deploy.conf b/.build_scripts/deploy.conf new file mode 100644 index 0000000..708bea6 --- /dev/null +++ b/.build_scripts/deploy.conf @@ -0,0 +1,7 @@ +[stage] +host 52.11.15.48 +user ubuntu +repo https://github.com/hotosm/oam-uploader-api.git +path /home/ubuntu/oam-uploader-api +ref origin/develop +post-deploy .build_scripts/docker/run.sh /install.sh && .build_scripts/docker/run.sh /start.sh diff --git a/.build_scripts/upstart.conf b/.build_scripts/upstart.conf new file mode 100644 index 0000000..e03b6ad --- /dev/null +++ b/.build_scripts/upstart.conf @@ -0,0 +1,23 @@ +#!upstart +description "oam-uploader-api server" +author "anand" + +start on startup +stop on shutdown + +script + export HOME="/home/ubuntu" + + echo $$ > /var/run/oam-uploader-api.pid + exec sudo -u ubuntu $HOME/oam-uploader-api/current/.build_scripts/docker/run.sh /start.sh >> /var/log/oam-uploader-api.sys.log 2>&1 +end script + +pre-start script + # Date format same as (new Date()).toISOString() for consistency + echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Starting" >> /var/log/oam-uploader-api.sys.log +end script + +pre-stop script + rm /var/run/oam-uploader-api.pid + echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> /var/log/oam-uploader-api.sys.log +end script diff --git a/package.json b/package.json index 36f52ad..e443723 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "description": "An uploader API for Open Aerial Map Imagery", "main": "index.js", "scripts": { - "start": "node index.js", "docker-install": ".build_scripts/docker/run.sh /install.sh", "docker-test": ".build_scripts/docker/run.sh /test.sh", "docker-start": ".build_scripts/docker/run.sh /start.sh", "build-docker": "docker build -t oam-uploader-api .build_scripts/docker", + "start": "nodemon index.js", "test": "semistandard && npm run lab", "lab": "lab -T test/babel.js test/test__*.js", "docs": "apidoc -i routes/ -o docs/" @@ -24,11 +24,9 @@ }, "homepage": "https://github.com/hotosm/oam-uploader-api", "dependencies": { - "async": "^1.3.0", "aws-sdk": "^2.1.44", "babel": "^5.8.21", "boom": "^2.8.0", - "envloader": "0.0.2", "es6-promise": "^2.3.0", "es6-promisify": "^3.0.0", "exit-hook": "^1.1.1", @@ -58,6 +56,7 @@ "chai": "^2.3.0", "ecstatic": "^0.8.0", "lab": "^5.15.0", + "nodemon": "^1.4.1", "semistandard": "^7.0.2", "tape": "^4.0.3" }, From a484ff64c31d029ade82721495aefc5ddadf6eae Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 24 Aug 2015 15:04:07 -0400 Subject: [PATCH 056/144] Deployment. UGH. --- .build_scripts/deploy.conf | 2 +- .build_scripts/docker/run.sh | 8 +++++- .build_scripts/upstart.conf | 18 +++++++++----- README.md | 48 +++++++++++++++++++++++++++++------- package.json | 2 +- 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/.build_scripts/deploy.conf b/.build_scripts/deploy.conf index 708bea6..b897181 100644 --- a/.build_scripts/deploy.conf +++ b/.build_scripts/deploy.conf @@ -4,4 +4,4 @@ user ubuntu repo https://github.com/hotosm/oam-uploader-api.git path /home/ubuntu/oam-uploader-api ref origin/develop -post-deploy .build_scripts/docker/run.sh /install.sh && .build_scripts/docker/run.sh /start.sh +post-deploy "source $NVM_DIR/nvm.sh && npm run docker-install && sudo stop oam-uploader-api && sudo start oam-uploader-api" diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index 6ead9da..7950883 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -20,8 +20,14 @@ else ENVFILE="" fi +if [[ -t 0 || -p /dev/stdin ]] ; then + MODE=-it +else + MODE= +fi + PORT=${PORT:-3000} -docker run -it \ +exec docker run $MODE \ -p $PORT \ $ENVFILE \ -e OAM_TEST \ diff --git a/.build_scripts/upstart.conf b/.build_scripts/upstart.conf index e03b6ad..881e065 100644 --- a/.build_scripts/upstart.conf +++ b/.build_scripts/upstart.conf @@ -2,18 +2,19 @@ description "oam-uploader-api server" author "anand" -start on startup -stop on shutdown +start on filesystem and started docker +stop on runlevel [!2345] -script - export HOME="/home/ubuntu" +env USER=ubuntu +env OAM_UPLOADER_DIR=/home/ubuntu/oam-uploader-api/current +script echo $$ > /var/run/oam-uploader-api.pid - exec sudo -u ubuntu $HOME/oam-uploader-api/current/.build_scripts/docker/run.sh /start.sh >> /var/log/oam-uploader-api.sys.log 2>&1 + cd $OAM_UPLOADER_DIR + exec sudo -u $USER $OAM_UPLOADER_DIR/.build_scripts/docker/run.sh /start.sh >> /var/log/oam-uploader-api.sys.log 2>&1 end script pre-start script - # Date format same as (new Date()).toISOString() for consistency echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Starting" >> /var/log/oam-uploader-api.sys.log end script @@ -21,3 +22,8 @@ pre-stop script rm /var/run/oam-uploader-api.pid echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> /var/log/oam-uploader-api.sys.log end script + +post-stop script + rm /var/run/oam-uploader-api.pid + echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopped" >> /var/log/oam-uploader-api.sys.log +end script diff --git a/README.md b/README.md index d902304..94dce64 100644 --- a/README.md +++ b/README.md @@ -43,28 +43,58 @@ run on a fresh instance using docker as follows: [Install Docker](https://docs.docker.com/installation/) -```sh -# install nvm and node -curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.26.0/install.sh | bash && source ~/.nvm/nvm.sh -nvm install 0.12 +One-time setup: + +```sh # clone the repo git clone https://github.com/hotosm/oam-uploader-api # build the docker image cd oam-uploader-api -npm run build-docker +docker build -t oam-uploader-api .build_scripts/docker # set up environment vars: cp local.sample.env local.env nano local.env ``` -Now, for each deployment: +To start up the API server after pulling from the repo: + +```sh +# install node dependencies +.build_scripts/docker/run.sh /install.sh +# start server +.build_scripts/docker/run.sh /start.sh +``` + +# Deployment + +To automate some of the above on a remote, you can use +[visionmedia/deploy](https://github.com/visionmedia/deploy) and upstart for +deployment and process management. + +First, add a new section to +https://github.com/hotosm/oam-uploader-api/blob/develop/.build_scripts/deploy.conf. +(If you don't want to commit it to the repo, you can just make your own copy of the +config file wherever you want.) Make sure you have ssh creds to the server from +wherever you're running `deploy`. Then do the following, with `ENV` replaced with +whatever you called the section you added to `deploy.conf`. ```sh -npm run docker-install && npm run docker-start +deploy -c .build_scripts/deploy.conf ENV setup ``` -### Docs Deployment -Changes to `master` branch are automatically deployed via Travis to https://oam-uploader-api.herokuapp.com. +Now ssh into the server with `deploy -c .build_scripts/deploy.conf ENV console`, +and set up the upstart script and start up the server + +```sh +sudo cp .build_scripts/upstart.conf /etc/init/oam-uploader-api.conf +sudo nano /etc/init/oam-uplodaer-api.conf +sudo start oam-uploader-api +exit +``` + +Now you can use `deploy -c .build_scripts/deploy.conf ENV` at any time to +deploy your local branch. + diff --git a/package.json b/package.json index e443723..0f0592e 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "An uploader API for Open Aerial Map Imagery", "main": "index.js", "scripts": { + "docker-build": "docker build -t oam-uploader-api .build_scripts/docker", "docker-install": ".build_scripts/docker/run.sh /install.sh", "docker-test": ".build_scripts/docker/run.sh /test.sh", "docker-start": ".build_scripts/docker/run.sh /start.sh", - "build-docker": "docker build -t oam-uploader-api .build_scripts/docker", "start": "nodemon index.js", "test": "semistandard && npm run lab", "lab": "lab -T test/babel.js test/test__*.js", From c092e672bbc2589ba3a75eb79c15fba739d3ca84 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 24 Aug 2015 15:09:06 -0400 Subject: [PATCH 057/144] Fix deploy --- .build_scripts/deploy.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.build_scripts/deploy.conf b/.build_scripts/deploy.conf index b897181..2665860 100644 --- a/.build_scripts/deploy.conf +++ b/.build_scripts/deploy.conf @@ -4,4 +4,4 @@ user ubuntu repo https://github.com/hotosm/oam-uploader-api.git path /home/ubuntu/oam-uploader-api ref origin/develop -post-deploy "source $NVM_DIR/nvm.sh && npm run docker-install && sudo stop oam-uploader-api && sudo start oam-uploader-api" +post-deploy ".build_scripts/docker/run.sh /install.sh && sudo stop oam-uploader-api && sudo start oam-uploader-api" From 330ea1f9cfe24a7f840751ddcaa88204a03a8def Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 24 Aug 2015 15:11:21 -0400 Subject: [PATCH 058/144] Fix deploy again --- .build_scripts/deploy.conf | 2 +- .build_scripts/post-deploy.sh | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100755 .build_scripts/post-deploy.sh diff --git a/.build_scripts/deploy.conf b/.build_scripts/deploy.conf index 2665860..322947e 100644 --- a/.build_scripts/deploy.conf +++ b/.build_scripts/deploy.conf @@ -4,4 +4,4 @@ user ubuntu repo https://github.com/hotosm/oam-uploader-api.git path /home/ubuntu/oam-uploader-api ref origin/develop -post-deploy ".build_scripts/docker/run.sh /install.sh && sudo stop oam-uploader-api && sudo start oam-uploader-api" +post-deploy .build_scripts/docker/post-deploy.sh diff --git a/.build_scripts/post-deploy.sh b/.build_scripts/post-deploy.sh new file mode 100755 index 0000000..0a6ee17 --- /dev/null +++ b/.build_scripts/post-deploy.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +.build_scripts/docker/run.sh /install.sh +sudo stop oam-uploader-api +sudo start oam-uploader-api From ed21bddc629b7200d4235ad0aac4bad0b601d4af Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 24 Aug 2015 15:11:57 -0400 Subject: [PATCH 059/144] fix deploy YET again --- .build_scripts/deploy.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.build_scripts/deploy.conf b/.build_scripts/deploy.conf index 322947e..6aa85c7 100644 --- a/.build_scripts/deploy.conf +++ b/.build_scripts/deploy.conf @@ -4,4 +4,4 @@ user ubuntu repo https://github.com/hotosm/oam-uploader-api.git path /home/ubuntu/oam-uploader-api ref origin/develop -post-deploy .build_scripts/docker/post-deploy.sh +post-deploy .build_scripts/post-deploy.sh From 48458457f192545b8b164cb49f11dae3efc44af7 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 24 Aug 2015 16:10:39 -0400 Subject: [PATCH 060/144] Try to fix deploy --- .build_scripts/deploy.conf | 1 + .build_scripts/docker/run.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/.build_scripts/deploy.conf b/.build_scripts/deploy.conf index 6aa85c7..ebb7549 100644 --- a/.build_scripts/deploy.conf +++ b/.build_scripts/deploy.conf @@ -4,4 +4,5 @@ user ubuntu repo https://github.com/hotosm/oam-uploader-api.git path /home/ubuntu/oam-uploader-api ref origin/develop +needs_tty yes post-deploy .build_scripts/post-deploy.sh diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index 7950883..bc92525 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -21,6 +21,7 @@ else fi if [[ -t 0 || -p /dev/stdin ]] ; then + echo "Running in tty mode." MODE=-it else MODE= From ebfc28bb50c03fc89d5fe4872ad01e488195a8f0 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 24 Aug 2015 17:40:59 -0400 Subject: [PATCH 061/144] Add single upload status endpoint --- .build_scripts/reset-db.js | 21 ++++++++++++++++++--- routes/uploads.js | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.build_scripts/reset-db.js b/.build_scripts/reset-db.js index 4faffcc..aadfa3e 100755 --- a/.build_scripts/reset-db.js +++ b/.build_scripts/reset-db.js @@ -9,18 +9,33 @@ var rl = readline.createInterface({ output: process.stdout }); -rl.question("Type 'yes' if you really want to clear the database.", function (answer) { +rl.question("Type 'yes' if you really want to clear the database: ", function (answer) { if (answer === 'yes') { console.log('Okay, doing it!'); MongoClient.connect(dbUri, function (err, connection) { if (err) throw err; - connection.collection('workers').deleteMany({}) + console.log('Connected to db.'); + console.log('Dropping workers'); + connection.dropCollection('workers') .then(function () { - return connection.collection('uploads').deleteMany({}); + console.log('Dropping uploads'); + return connection.dropCollection('uploads'); + }) + .then(function () { + console.log('Dropping tokens'); + return connection.dropCollection('tokens'); + }) + .then(function () { + console.log('Dropping images'); + return connection.dropCollection('images'); }) .then(function () { console.log('Done.'); connection.close(); + }) + .catch(function (err) { + console.error(err); + connection.close(); }); }); } else { diff --git a/routes/uploads.js b/routes/uploads.js index 981e38d..3583e20 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -66,6 +66,23 @@ module.exports = [ }); } }, + { + method: 'GET', + path: '/uploads/{id}', + config: { + auth: 'api-token' + }, + handler: function (request, reply) { + var user = request.auth.credentials.user.id; + var db = request.server.plugins.db.connection; + db.collection('uploads').findOne({ + _id: new ObjectID(request.params.id), + user: user + }) + .then(reply) + .catch(function (err) { reply(Boom.wrap(err)); }); + } + }, /** * @api {post} /uploads Add an upload to the queue * @apiGroup uploads @@ -119,7 +136,6 @@ module.exports = [ var db = request.server.plugins.db.connection; data.user = request.auth.credentials.user.id; - data.status = 'initial'; data.createdAt = new Date(); // pull out the actual images into their own collection, so it can be From 3c903c7e67746c8b556f875fc7a6a3d56c90e172 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 25 Aug 2015 15:18:14 -0400 Subject: [PATCH 062/144] Don't require token for upload status --- routes/uploads.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/routes/uploads.js b/routes/uploads.js index 3583e20..9eba1b1 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -47,7 +47,7 @@ module.exports = [ auth: 'api-token' }, handler: function (request, reply) { - var user = request.auth.credentials.user.id; + var user = request.auth.credentials.id; var db = request.server.plugins.db.connection; db.collection('uploads').find({ user: user }) .toArray(function (err, uploads) { @@ -69,15 +69,13 @@ module.exports = [ { method: 'GET', path: '/uploads/{id}', - config: { - auth: 'api-token' - }, handler: function (request, reply) { - var user = request.auth.credentials.user.id; + if (!ObjectID.isValid(request.params.id)) { + return reply(Boom.badRequest('Invalid id: ' + request.params.id)); + } var db = request.server.plugins.db.connection; db.collection('uploads').findOne({ - _id: new ObjectID(request.params.id), - user: user + _id: new ObjectID(request.params.id) }) .then(reply) .catch(function (err) { reply(Boom.wrap(err)); }); @@ -135,7 +133,7 @@ module.exports = [ var db = request.server.plugins.db.connection; - data.user = request.auth.credentials.user.id; + data.user = request.auth.credentials.id; data.createdAt = new Date(); // pull out the actual images into their own collection, so it can be From 1bb26c61212005cad1a4604edee3e2772ddd8cac Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 25 Aug 2015 15:18:16 -0400 Subject: [PATCH 063/144] Actually check against the tokens, with tests --- services/validate-token.js | 33 ++++---- test/test__token_management.js | 14 ++++ test/test__token_validation.js | 135 +++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 test/test__token_validation.js diff --git a/services/validate-token.js b/services/validate-token.js index cde425e..4248931 100644 --- a/services/validate-token.js +++ b/services/validate-token.js @@ -5,19 +5,24 @@ */ module.exports = function (db) { return function (token, callback) { - // TODO: replace the following with real auth (hit the db, etc.) - if (token === 'usertoken') { - // successful authentication - callback(null, true, { - user: { - id: 1, // <- can be anything as long as it's unique; used for associations w uploads - name: 'Some Body' - }, - token: token - }); - } else { - // bad token - callback(null, false, { token: token }); - } + db.collection('tokens').findOne({ + token: token, + status: 'active' + }) + .then(function (activeToken) { + if (!activeToken) { return callback(null, false, {token: token}); } + + var expired = false; + if (activeToken.expiration) { + var exp = new Date(activeToken.expiration); + var now = new Date(); + expired = exp < now; + } + + // routes expect an 'id' property on the credentials object + activeToken.id = activeToken._id; + callback(null, !expired, activeToken); + }) + .catch(callback); }; }; diff --git a/test/test__token_management.js b/test/test__token_management.js index 622b7e2..576a70a 100644 --- a/test/test__token_management.js +++ b/test/test__token_management.js @@ -2,6 +2,8 @@ var Lab = require('lab'); var server = require('../'); +var config = require('../config'); +var createValidateToken = require('../services/validate-token'); var chai = require('chai'); var ObjectId = require('mongodb').ObjectID; @@ -12,10 +14,13 @@ var before = lab.before; var assert = chai.assert; var cookie = null; +var activeToken = null; +var validateToken = null; suite('test tokens', function () { before(function (done) { + assert.match(config.dbUri, /test$/, 'use the test database'); // Get a reference to the server. // Wait for everything to load. // Change to test db @@ -23,6 +28,7 @@ suite('test tokens', function () { server = hapi; // Prepare db. var db = hapi.plugins.db.connection; + validateToken = createValidateToken(db); db.collection('tokens').deleteMany({}, function (err) { if (err) { throw err; } @@ -251,10 +257,18 @@ suite('test tokens', function () { assert.equal(response.statusCode, 201); assert.isDefined(result.data._id); + activeToken = result.data.token; done(); }); }); + test('should validate active token', function (done) { + validateToken(activeToken, function (error, valid, creds) { + assert(valid); + done(error); + }); + }); + test('should fail token update with invalid boolean expiration', function (done) { var options = { method: 'PUT', diff --git a/test/test__token_validation.js b/test/test__token_validation.js new file mode 100644 index 0000000..19dc895 --- /dev/null +++ b/test/test__token_validation.js @@ -0,0 +1,135 @@ + +var Lab = require('lab'); +var MongoClient = require('mongodb').MongoClient; +var dbUri = require('../config').dbUri; +var createValidateToken = require('../services/validate-token'); +var chai = require('chai'); + +var lab = exports.lab = Lab.script(); +var suite = lab.experiment; +var test = lab.test; +var before = lab.before; +var assert = chai.assert; + +var validateToken = null; + +var now = new Date(); +var nextyear = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate()); +var tokens = [ + { + name: 'Active Never Expires', + expiration: false, + status: 'active', + token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', + created: now, + updated: null + }, + { + name: 'Active Not Expired', + expiration: nextyear, + status: 'active', + token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', + created: now, + updated: null + }, + { + name: 'Active Expired', + expiration: now, + status: 'active', + token: 'a3b1e1d9971a427c67d0f4e277d808c334e3981e43e31643e0021e5af28121eafe46443529829c8fe9a5ce6836f1e19adc429b009923464384361a52b0da15d0', + created: now, + updated: null + }, + { + name: 'Blocked Never Expired', + expiration: false, + status: 'blocked', + token: '195364531c28d71fe85f194410b1aa1f4d96f31c001c383eaf8cd703245fd01001d600ce6314214bffb07b43a195a2183c5301c2ce7ae4cb259ac42e647e40dd', + created: now, + updated: null + }, + { + name: 'Blocked Not Expired', + expiration: nextyear, + status: 'blocked', + token: '21a06762eab1678ab8049709d6a3865547bade218cd8d28bdd9efc08f851403681d71012be5f20ad86c7418764efa64b00c7d267d0eadb9eccc07694ab3e2064', + created: now, + updated: null + }, + { + name: 'Blocked Expired', + expiration: now, + status: 'blocked', + token: '1e57246b246f7f79ff109ec9672769c4f105428048b012419ea8fb3967441543fe981670ca6b0cf9eadf4fbeeb2e3c8f0ad5e00a1645c2bab63bdef4348658c4', + created: now, + updated: null + } +]; + +suite('test token validation', function () { + + before(function (done) { + assert.match(dbUri, /test$/, 'use the test database'); + MongoClient.connect(dbUri, function (err, db) { + if (err) { return done(err); } + validateToken = createValidateToken(db); + db.collection('tokens').deleteMany({}, function (err) { + if (err) { return done(err); } + db.collection('tokens').insert(tokens, function (err, res) { + done(err); + }); + }); + }); + }); + + test('should validate active token', function (done) { + validateToken(tokens[0].token, function (error, valid, creds) { + assert(valid); + done(error); + }); + }); + + test('should validate active, unexpired token', function (done) { + validateToken(tokens[1].token, function (error, valid, creds) { + assert(valid); + done(error); + }); + }); + + test('should not validate active, expired token', function (done) { + validateToken(tokens[2].token, function (error, valid, creds) { + assert.notOk(valid); + done(error); + }); + }); + + test('should not validate blocked token', function (done) { + validateToken(tokens[3].token, function (error, valid, creds) { + assert.notOk(valid); + done(error); + }); + }); + + test('should not validate blocked, unexpired token', function (done) { + validateToken(tokens[4].token, function (error, valid, creds) { + assert.notOk(valid); + done(error); + }); + }); + + test('should not validate blocked, expired token', function (done) { + validateToken(tokens[5].token, function (error, valid, creds) { + assert.notOk(valid); + done(error); + }); + }); + + test('should not validate missing token', function (done) { + validateToken('no token!', function (error, valid, creds) { + assert.notOk(valid); + done(error); + }); + }); + +}); + From fb4ce3b4a362690bb95e38151a24dd9906f4d267 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 25 Aug 2015 15:30:44 -0400 Subject: [PATCH 064/144] Add --rm to docker run script --- .build_scripts/docker/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index bc92525..bd47c71 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -28,7 +28,7 @@ else fi PORT=${PORT:-3000} -exec docker run $MODE \ +exec docker run $MODE --rm \ -p $PORT \ $ENVFILE \ -e OAM_TEST \ From 7e3729fafbd391752d13ec29e34300787c403692 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 25 Aug 2015 16:37:26 -0400 Subject: [PATCH 065/144] Stop the docker container on stop --- .build_scripts/docker/run.sh | 8 +++++++- .build_scripts/upstart.conf | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.build_scripts/docker/run.sh b/.build_scripts/docker/run.sh index bd47c71..7c0dd56 100755 --- a/.build_scripts/docker/run.sh +++ b/.build_scripts/docker/run.sh @@ -28,6 +28,11 @@ else fi PORT=${PORT:-3000} + +ARGS_LENGTH=$(($#-1)) +ARGS=${@:1:$ARGS_LENGTH} +COMMAND=${@:$((ARGS_LENGTH+1)):1} + exec docker run $MODE --rm \ -p $PORT \ $ENVFILE \ @@ -45,4 +50,5 @@ exec docker run $MODE --rm \ -e AWS_REGION \ --net=\"host\" \ -v $(pwd):/local \ - oam-uploader-api $1 + $ARGS \ + oam-uploader-api $COMMAND diff --git a/.build_scripts/upstart.conf b/.build_scripts/upstart.conf index 881e065..1b54294 100644 --- a/.build_scripts/upstart.conf +++ b/.build_scripts/upstart.conf @@ -11,7 +11,7 @@ env OAM_UPLOADER_DIR=/home/ubuntu/oam-uploader-api/current script echo $$ > /var/run/oam-uploader-api.pid cd $OAM_UPLOADER_DIR - exec sudo -u $USER $OAM_UPLOADER_DIR/.build_scripts/docker/run.sh /start.sh >> /var/log/oam-uploader-api.sys.log 2>&1 + exec sudo -u $USER $OAM_UPLOADER_DIR/.build_scripts/docker/run.sh --name=oam-uploader-api /start.sh >> /var/log/oam-uploader-api.sys.log 2>&1 end script pre-start script @@ -21,6 +21,7 @@ end script pre-stop script rm /var/run/oam-uploader-api.pid echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> /var/log/oam-uploader-api.sys.log + docker stop oam-uploader-api end script post-stop script From 053bb81ef02d0d7290db06ea323254dcf7e8b91b Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 28 Aug 2015 12:00:19 -0400 Subject: [PATCH 066/144] Dereference images in /upload/{id} response --- routes/uploads.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/routes/uploads.js b/routes/uploads.js index 9eba1b1..0ade20f 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -30,7 +30,7 @@ function includeImages (db, scene, callback) { }) .toArray(function (err, images) { scene.images = images; - callback(err, images); + callback(err, scene); }); } @@ -77,7 +77,17 @@ module.exports = [ db.collection('uploads').findOne({ _id: new ObjectID(request.params.id) }) - .then(reply) + .then(function (upload) { + var q = queue(); + upload.scenes.forEach(function (scene) { + q.defer(includeImages, db, scene); + }); + + q.awaitAll(function (err) { + if (err) { return reply(Boom.wrap(err)); } + reply(upload); + }); + }) .catch(function (err) { reply(Boom.wrap(err)); }); } }, From 74f14fc139e0a664d6587a6803415c56ab845fff Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 28 Aug 2015 14:49:17 -0400 Subject: [PATCH 067/144] Return upload id from POST /uploads --- plugins/workers.js | 2 ++ routes/uploads.js | 8 ++++++-- worker/queue.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/workers.js b/plugins/workers.js index 68293a8..4429234 100644 --- a/plugins/workers.js +++ b/plugins/workers.js @@ -3,6 +3,7 @@ var fork = require('child_process').fork; var path = require('path'); var ObjectID = require('mongodb').ObjectID; +var Promise = require('es6-promise').Promise; var config = require('../config'); module.exports = function register (server, options, next) { @@ -20,6 +21,7 @@ module.exports = function register (server, options, next) { ' max: ' + config.maxWorkers); if (available.length < config.maxWorkers) { spawnWorker(); + return Promise.resolve(); } else if (config.maxWorkers > 0) { var id = available[0]; server.log(['debug'], 'Attempting to pause ' + id); diff --git a/routes/uploads.js b/routes/uploads.js index 0ade20f..7289c91 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -156,8 +156,12 @@ module.exports = [ q.awaitAll(function (err) { if (err) { return reply(Boom.wrap(err)); } db.collection('uploads').insertOne(data) - .then(request.server.plugins.workers.spawn) - .then(function () { reply('Success'); }) + .then(function (result) { + return request.server.plugins.workers.spawn() + .then(function () { + reply({ upload: data._id }); + }); + }) .catch(function (err) { reply(Boom.wrap(err)); }); }); }); diff --git a/worker/queue.js b/worker/queue.js index a0c2cbe..50158cd 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -1,6 +1,6 @@ var MongoClient = require('mongodb').MongoClient; var moment = require('moment'); -var Promise = require('es6-promise'); +var Promise = require('es6-promise').Promise; var promisify = require('es6-promisify'); var processImage = require('./process-image'); var log = require('./log'); From e1a78e867c51c86798c88a456e21915df69614c5 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 28 Aug 2015 17:04:05 -0400 Subject: [PATCH 068/144] Include metadata with processed images status --- package.json | 1 + test/fixture/upload-status.json | 1 + test/test__worker.js | 67 ++++++++++++++++++++++++--------- worker/queue.js | 18 ++++++--- 4 files changed, 64 insertions(+), 23 deletions(-) create mode 100644 test/fixture/upload-status.json diff --git a/package.json b/package.json index 0f0592e..330e1b8 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "ecstatic": "^0.8.0", "lab": "^5.15.0", "nodemon": "^1.4.1", + "omit-deep": "^0.1.2", "semistandard": "^7.0.2", "tape": "^4.0.3" }, diff --git a/test/fixture/upload-status.json b/test/fixture/upload-status.json new file mode 100644 index 0000000..cc09128 --- /dev/null +++ b/test/fixture/upload-status.json @@ -0,0 +1 @@ +{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} diff --git a/test/test__worker.js b/test/test__worker.js index 3504969..30c06b4 100644 --- a/test/test__worker.js +++ b/test/test__worker.js @@ -6,6 +6,7 @@ var http = require('http'); var ecstatic = require('ecstatic'); var sharp = require('sharp'); var MongoClient = require('mongodb').MongoClient; +var omit = require('omit-deep'); var JobQueue = require('../worker/queue'); var config = require('../config'); var api = require('../'); @@ -19,6 +20,8 @@ var assert = chai.assert; suite('test worker', function () { var server; + var db; + var uploadId; before(function (done) { assert.match(config.dbUri, /test$/, 'use the test database'); @@ -26,8 +29,9 @@ suite('test worker', function () { assert.equal(config.awsRegion, 'us-west-2'); // empty out database - MongoClient.connect(config.dbUri, function (err, db) { + MongoClient.connect(config.dbUri, function (err, conn) { if (err) { return done(err); } + db = conn; db.dropDatabase(function (err) { if (err) { return done(err); } // serve up our test fixtures to be downloaded @@ -35,12 +39,12 @@ suite('test worker', function () { .on('listening', function () { done(); }) .on('error', done); server.listen(8080); - db.close(); }); }); }); after(function (done) { + db.close(); server.close(); done(); }); @@ -54,7 +58,7 @@ suite('test worker', function () { credentials: { user: { id: -1 } } }, function (response) { assert.equal(response.statusCode, 200, 'add upload job 200 status'); - assert.equal(response.payload, 'Success', 'add upload job response'); + uploadId = JSON.parse(response.payload).upload; done(); }); }); @@ -76,22 +80,51 @@ suite('test worker', function () { var metadata = JSON.parse(mockS3.calls[2].Body); var expected = require('./fixture/NE1_50M_SR.output.json'); - assert.match(metadata.uuid, /http:\/\/oam-uploader.s3.amazonaws.com\/uploads\/.*\/.*\/scene\/0\/scene-0-image-0-NE1_50M_SR\.tif/); - assert.match(metadata.properties.thumbnail, /thumb\.(png|jpe?g)$/); - delete metadata.uuid; - delete expected.uuid; - delete metadata.properties.thumbnail; - delete expected.properties.thumbnail; - assert.deepEqual(metadata, expected, 'generated metadata'); - var thumb = mockS3.calls[1].Body; - thumb - .pipe(sharp().metadata(function (err, thumbdata) { - if (err) { return done(err); } - assert(thumbdata, 'thumbnail is an image'); - done(); - })); + db.collection('images') + .findOne({ url: 'http://localhost:8080/NE1_50M_SR.tif' }) + .then(function (image) { + assert(image.status === 'finished'); + assert.equal(JSON.stringify(metadata), JSON.stringify(image.metadata), + 'the uploaded metadata is stored in the db entry for an image'); + assert.match(metadata.uuid, /http:\/\/oam-uploader.s3.amazonaws.com\/uploads\/.*\/.*\/scene\/0\/scene-0-image-0-NE1_50M_SR\.tif/); + assert.match(metadata.properties.thumbnail, /thumb\.(png|jpe?g)$/); + var omitted = ['uuid', 'thumbnail']; + assert.deepEqual(omit(metadata, omitted), omit(expected, omitted), + 'generated metadata'); + + var thumb = mockS3.calls[1].Body; + thumb + .pipe(sharp().metadata(function (err, thumbdata) { + if (err) { return done(err); } + assert(thumbdata, 'thumbnail is an image'); + done(); + })); + }) + .catch(done); }) .catch(done); }); + + test('check the status of an upload', function (done) { + api(function (hapi) { + hapi.inject({ + method: 'GET', + url: '/uploads/' + uploadId, + credentials: { user: { id: -1 } } + }, function (response) { + var omitted = [ '_id', 'startedAt', 'createdAt', 'stoppedAt', 'uuid', 'thumbnail' ]; + var status = JSON.parse(response.payload); + var expected = require('./fixture/upload-status.json'); + status = omit(status, omitted); + expected = omit(expected, omitted); + status.scenes[0].images[0] = omit(status.scenes[0].images[0], omitted); + expected.scenes[0].images[0] = omit(expected.scenes[0].images[0], omitted); + + assert.deepEqual(status, expected); + done(); + }); + }); + }); + }); diff --git a/worker/queue.js b/worker/queue.js index 50158cd..643c8da 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -50,10 +50,16 @@ JobQueue.prototype._setupQueries = function _setupQueries () { $set: { status: 'processing', _workerId: this.workerId }, $currentDate: { startedAt: true } }; - this.update.jobFinished = { - $set: { status: 'finished' }, - $unset: { _workerId: '' }, - $currentDate: { stoppedAt: true } + this.update.jobFinished = function jobFinished (processed) { + return { + $set: { + status: 'finished', + messages: processed.messages, + metadata: processed.metadata + }, + $unset: { _workerId: '' }, + $currentDate: { stoppedAt: true } + }; }; this.update.jobErrored = function jobErrored (error) { error = { @@ -117,9 +123,9 @@ JobQueue.prototype._mainloop = function mainloop () { // this should never happen throw new Error('Could not find the scene for image ' + image._id); }) - .then(() => { + .then((processed) => { // mark the job as finished - return this.images.findOneAndUpdate(result.value, this.update.jobFinished); + return this.images.findOneAndUpdate(result.value, this.update.jobFinished(processed)); }) .then(() => { // update this worker's timestamp From 8f4a5159b3e5878a9e25ae436a7c102427877d55 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 28 Aug 2015 17:33:43 -0400 Subject: [PATCH 069/144] Email notifications! --- config.js | 16 +++++++++++++--- local.sample.env | 3 +++ package.json | 1 + routes/uploads.js | 12 ++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index a4c5334..2ebada5 100644 --- a/config.js +++ b/config.js @@ -6,11 +6,19 @@ var defaults = { oinBucket: 'oam-uploader', dbUri: process.env.NODE_ENV === 'test' ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', - maxWorkers: 1, adminPassword: null, adminUsername: null, awsRegion: 'us-west-2', - thumbnailSize: 300, // thumbnail size, in kilobytes + sendgridApiKey: null, + sendgridFrom: 'info@hotosm.org', + thumbnailSize: 300, // (very) approximate thumbnail size, in kilobytes + maxWorkers: 1, + emailNotification: { + subject: '[ OAM Uploader ] Imagery upload submitted', + text: 'Your upload has been successfully submitted and is now being ' + + 'processed. You can check on the status of the upload at ' + + 'http://upload.openaerialmap.org/#/status/{UPLOAD_ID}.' + }, logOptions: { opsInterval: 3000, reporters: [{ @@ -37,7 +45,9 @@ var environment = { adminUsername: process.env.ADMIN_USERNAME, awsKeyId: process.env.AWS_SECRET_KEY_ID, awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - awsRegion: process.env.AWS_REGION + awsRegion: process.env.AWS_REGION, + sendgridApiKey: process.env.SENDGRID_API_KEY, + sendgridFrom: process.env.SENDGRID_FROM }; var config = xtend(defaults); diff --git a/local.sample.env b/local.sample.env index f2343d2..bfa4b2f 100644 --- a/local.sample.env +++ b/local.sample.env @@ -1,8 +1,11 @@ AWS_SECRET_KEY_ID=your-id AWS_SECRET_ACCESS_KEY=your-key +SENDGRID_API_KEY=your-sendgrid-key +SENDGRID_FROM=emailaccount@that.sends.notifications.com AWS_REGION=us-east-1 OIN_BUCKET=bucket-name DBURI=mongodb://localhost/oam-uploader DBURI_TEST=mongodb://localhost/oam-uploader-test +DBURI_TEST=mongodb://localhost/oam-uploader-test # ADMIN_USERNAME=admin # ADMIN_PASSWORD= diff --git a/package.json b/package.json index 330e1b8..6a8a704 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "queue-async": "^1.0.7", "request": "^2.60.0", "s3": "^4.4.0", + "sendgrid": "^1.9.2", "sharp": "^0.11.1", "tmp": "0.0.26", "wellknown": "^0.3.1", diff --git a/routes/uploads.js b/routes/uploads.js index 7289c91..25d3f80 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -5,6 +5,9 @@ var queue = require('queue-async'); var Boom = require('boom'); var Joi = require('joi'); var uploadSchema = require('../models/upload'); +var config = require('../config'); + +var sendgrid = require('sendgrid')(config.sendridApiKey); function insertImages (db, scene, callback) { var imageIds = []; @@ -157,6 +160,15 @@ module.exports = [ if (err) { return reply(Boom.wrap(err)); } db.collection('uploads').insertOne(data) .then(function (result) { + sendgrid.send({ + to: data.uploader.email, + from: config.sendgridFrom, + subject: config.emailNotification.subject, + text: config.emailNotification.text.replace('{UPLOAD_ID}', data._id) + }, function (err, json) { + if (err) { request.log(['error', 'email'], err.message); } + if (json) { request.log(['debug', 'email'], json); } + }); return request.server.plugins.workers.spawn() .then(function () { reply({ upload: data._id }); From 67fe4e840c400f46518f5272df6748daaea09a1f Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 31 Aug 2015 08:23:52 -0400 Subject: [PATCH 070/144] Misc worker improvements - Spawn a worker on server start - Simplify worker logs - Handle download errors appropriately --- index.js | 3 +++ plugins/workers.js | 19 ++++++++++--------- worker/log.js | 2 +- worker/process-image.js | 1 + worker/queue.js | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index c007fc2..39ac984 100644 --- a/index.js +++ b/index.js @@ -82,6 +82,9 @@ if (!module.parent) { // Start the server. hapi.start(function () { hapi.log(['info'], 'Server running at:' + hapi.info.uri); + // spawn a worker to handle any unprocessed uploads that may be sitting + // around in the database + hapi.plugins.workers.spawn(); }); }); } diff --git a/plugins/workers.js b/plugins/workers.js index 4429234..5c8997c 100644 --- a/plugins/workers.js +++ b/plugins/workers.js @@ -17,14 +17,14 @@ module.exports = function register (server, options, next) { function spawn () { var available = Object.keys(myWorkers); var workers = server.plugins.db.connection.collection('workers'); - server.log(['debug'], 'maybe spawn... available: ' + available + - ' max: ' + config.maxWorkers); + server.log(['worker', 'debug'], 'Maybe spawn... available: ' + + available.length + ' max: ' + config.maxWorkers); if (available.length < config.maxWorkers) { spawnWorker(); return Promise.resolve(); } else if (config.maxWorkers > 0) { var id = available[0]; - server.log(['debug'], 'Attempting to pause ' + id); + server.log(['worker', 'debug'], 'Attempting to pause ' + id); id = new ObjectID(id); // we think we have a worker available already, but there's a potential // race condition between the worker checking for new jobs and deciding @@ -34,18 +34,17 @@ module.exports = function register (server, options, next) { $set: { state: 'paused' } }) .then(function (result) { - server.log(['debug'], result); + server.log(['worker', 'debug'], result); if (result.value) { // we successfully switched the state to 'paused', so we can safely // unpause it and know that it will try to look for another job. - server.log(['debug'], 'Successfully paused; resuming.'); + server.log(['worker', 'debug'], 'Successfully paused; resuming.'); return workers.updateOne({ _id: id, state: 'paused' }, { $set: { state: 'working' } }); } else { // our worker must have shut itself down before we were able to // pause it, so let's spawn a new one - server.log(['debug'], 'Could not pause; spawning.'); return spawnWorker(); } }); @@ -53,15 +52,17 @@ module.exports = function register (server, options, next) { } function spawnWorker () { - server.log(['debug'], 'spawnWorker'); + server.log(['worker', 'info'], 'Spawning worker.'); var cp = fork(path.join(__dirname, '../worker')) .on('message', function (msg) { myWorkers[msg.workerId] = cp.pid; myProcesses[cp.pid] = msg.workerId; - server.log(['worker'].concat(msg.tags), msg.message); + var id = msg.workerId.slice(0, 6); + var message = msg.message.length === 1 ? msg.message[0] : msg.message; + server.log(['worker', 'worker ' + id].concat(msg.tags), message); }) .on('exit', function (info) { - server.log(['debug'], 'Worker exited: ' + JSON.stringify(info)); + server.log(['worker', 'debug'], 'Worker exited: ' + JSON.stringify(info)); delete myWorkers[myProcesses[cp.pid]]; delete myProcesses[cp.pid]; }); diff --git a/worker/log.js b/worker/log.js index 8569d8d..0884600 100644 --- a/worker/log.js +++ b/worker/log.js @@ -4,7 +4,6 @@ module.exports.workerId = -1; function log () { var args = Array.prototype.slice.call(arguments); var tags = args.length > 1 ? [args.shift()] : []; - tags.unshift('Worker ' + module.exports.workerId); if (typeof process.send === 'function') { // hack to allow error info to get passed back to main thread args = args.map(function (a) { @@ -20,6 +19,7 @@ function log () { workerId: module.exports.workerId }); } else { + tags.unshift('Worker ' + module.exports.workerId); console.log.apply(console, [tags].concat(args)); } } diff --git a/worker/process-image.js b/worker/process-image.js index e2788a9..ea5d9d0 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -29,6 +29,7 @@ function _processImage (s3, scene, url, key, callback) { var downloadStatus; request(url) .on('response', function (resp) { downloadStatus = resp.statusCode; }) + .on('error', callback) .pipe(fs.createWriteStream(path)) .on('finish', function () { if (downloadStatus < 200 || downloadStatus >= 400) { diff --git a/worker/queue.js b/worker/queue.js index 643c8da..c640d53 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -98,10 +98,10 @@ JobQueue.prototype._mainloop = function mainloop () { } // we got a job! - log(['info'], 'Processing job', image); var now = moment().format('YYYY-MM-DD'); var image = result.value; var s3 = this.s3; + log(['info'], 'Processing job', image); return this.db.collection('uploads') // find the upload / scene that contains this image .findOne({ 'scenes.images': image._id }) From 3855732fef08a63bf0b14585bac31ac2125f5db0 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 31 Aug 2015 08:25:53 -0400 Subject: [PATCH 071/144] Fix typo --- routes/uploads.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/uploads.js b/routes/uploads.js index 25d3f80..6ea1f42 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -7,7 +7,7 @@ var Joi = require('joi'); var uploadSchema = require('../models/upload'); var config = require('../config'); -var sendgrid = require('sendgrid')(config.sendridApiKey); +var sendgrid = require('sendgrid')(config.sendgridApiKey); function insertImages (db, scene, callback) { var imageIds = []; From 00e66cd7d1e8b7e7aa8f2c42814e89de6161ba39 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 31 Aug 2015 14:46:48 -0400 Subject: [PATCH 072/144] Update API docs --- README.md | 3 +++ config.js | 25 +++++++++++++---------- routes/_apidoc.js | 30 ++++++++++++++++++++++++++++ routes/uploads.js | 51 +++++++++++++++++++++++++++++++++++++---------- 4 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 routes/_apidoc.js diff --git a/README.md b/README.md index 94dce64..b29e5c2 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The API exposes endpoints used to access information form the system via a RESTf ### Environment Variables +- `PORT` - the port to listen on - `OIN_BUCKET` - The OIN bucket that will receive the uploads - `AWS_REGION` - AWS region of OIN_BUCKET - `AWS_SECRET_KEY_ID` - AWS secret key id for reading OIN bucket @@ -35,6 +36,8 @@ The API exposes endpoints used to access information form the system via a RESTf - `DBURI` - MongoDB connection url - `DBURI_TEST` - MongoDB connection to the test database (not needed for production) +- `SENDGRID_API_KEY` - sendgrid API key, for sending notification emails +- `SENDGRID_FROM` - email address from which to send notification emails ### Install via Docker diff --git a/config.js b/config.js index 2ebada5..b6713de 100644 --- a/config.js +++ b/config.js @@ -1,18 +1,20 @@ var xtend = require('xtend'); +// Default configuration options used by the app. Most of these can be +// overriden by environment variables (see below). var defaults = { - host: '0.0.0.0', - port: 3000, - oinBucket: 'oam-uploader', + host: '0.0.0.0', // cosmetic + port: 3000, // port to listen on dbUri: process.env.NODE_ENV === 'test' ? - 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', - adminPassword: null, - adminUsername: null, - awsRegion: 'us-west-2', - sendgridApiKey: null, - sendgridFrom: 'info@hotosm.org', + 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', // the mongodb database uri (mongodb://user:pass@host:port/db) + adminPassword: null, // the administrator username + adminUsername: null, // the administrator password + oinBucket: 'oam-uploader', // name of the OpenImageryNetwork bucket to which imagery should be uploaded + awsRegion: 'us-west-2', // the AWS region of the oinBucket thumbnailSize: 300, // (very) approximate thumbnail size, in kilobytes - maxWorkers: 1, + maxWorkers: 1, // the maximum number of workers + sendgridApiKey: null, // sendgrid API key, for sending notification emails + sendgridFrom: 'info@hotosm.org', // the email address from which to send notification emails emailNotification: { subject: '[ OAM Uploader ] Imagery upload submitted', text: 'Your upload has been successfully submitted and is now being ' + @@ -34,6 +36,7 @@ var defaults = { } }; +// Environment variable overrides var environment = { port: process.env.PORT, host: process.env.HOST, @@ -57,7 +60,7 @@ for (var k in environment) { } } -// override json.stringify behavior so we don't accidentally log keys +// override json.stringify behavior so we don't accidentally log secret keys config.toJSON = function () { return '[ hidden ]'; }; diff --git a/routes/_apidoc.js b/routes/_apidoc.js new file mode 100644 index 0000000..b9cdbeb --- /dev/null +++ b/routes/_apidoc.js @@ -0,0 +1,30 @@ +/** + * @apiDefine Token API Token Authentication + * API token must be included either as an HTTP `Bearer: your-token` header or as a query + * parameter `?access_token=your-token` + * [Request a token](https://github.com/hotosm/oam-uploader). + */ + +/** + * @apiDefine uploadStatusSuccess + * @apiSuccess {Object} results.uploader Uploader contact info + * @apiSuccess {String} results.uploader.name + * @apiSuccess {String} results.uploader.email + * @apiSuccess {Object[]} results.scenes + * @apiSuccess {Object} results.scenes.contact Contact person for this scene + * @apiSuccess {String} results.scenes.contact.name + * @apiSuccess {String} results.scenes.contact.email + * @apiSuccess {String} results.scenes.title Scene title + * @apiSuccess {String="satellite","aircraft","UAV","balloon","kite"} results.scenes.platform + * @apiSuccess {String} results.scenes.provider Imagery provider + * @apiSuccess {String} results.scenes.sensor Sensor/device + * @apiSuccess {String} results.scenes.acquisition_start Date and time of imagery acquisition + * @apiSuccess {String} results.scenes.acquisition_end Date and time of imagery acquisition + * @apiSuccess {Object[]} results.scenes.images Array of images in this scene + * @apiSuccess {String} results.scenes.images.url + * @apiSuccess {String="initial","processing","finished","errored"} results.scenes.images.status + * @apiSuccess {String} results.scenes.images.error + * @apiSuccess {String[]} results.scenes.images.messages + * @apiSuccess {String} results.scenes.images.startedAt Date and time the processing started + * @apiSuccess {String} results.scenes.images.stoppedAt Date and time the processing stopped + */ diff --git a/routes/uploads.js b/routes/uploads.js index 6ea1f42..5ddddd3 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -42,6 +42,8 @@ module.exports = [ * @api {get} /uploads List uploads of currently authenticated user. * @apiGroup uploads * @apiSuccess {Object[]} results + * @apiUse uploadStatusSuccess + * @apiPermission Token */ { method: 'GET', @@ -69,6 +71,12 @@ module.exports = [ }); } }, + /** + * @api {get} /uploads/:id Get the status of a given upload + * @apiGroup uploads + * @apiParam {String} id The id of the upload + * @apiUse uploadStatusSuccess + */ { method: 'GET', path: '/uploads/{id}', @@ -97,6 +105,7 @@ module.exports = [ /** * @api {post} /uploads Add an upload to the queue * @apiGroup uploads + * @apiPermission Token * * @apiParam {Object} uploaderInfo * @pariParam {string} uploaderInfo.name @@ -110,20 +119,42 @@ module.exports = [ * * @apiExample {js} Example post * { - * "uploaderInfo": { - * "name": "Anand", - * "email": "me@foo.com" - * }, - * "contactInfo": { - * "name": "Anand", - * "email": "me@foo.com" + * "uploader": { + * "name": "Lady Stardust", + * "email": "lady@stardust.xyz" * }, * "scenes": [ * { - * "metadata": {}, + * "contact": { + * "name": "Sat E Lyte", + * "email": "foo@bar.com" + * }, + * "title": "A scene title", + * "platform": "UAV", + * "provider": "Drones R Us", + * "sensor": "DroneModel01", + * "acquisition_start": "2015-04-01T00:00:00.000", + * "acquisition_end": "2015-04-30T00:00:00.000", + * "urls": [ + * "http://dron.es/image1.tif", + * "http://dron.es/image2.tif", + * "http://dron.es/image3.tif", + * ] + * }, + * { + * "contact": { + * "name": "Someone Else", + * "email": "birds@eye.view.com" + * }, + * "title": "Another title", + * "platform": "satellite", + * "provider": "Satellites R Us", + * "sensor": "SATELLITE_I", + * "acquisition_start": "2015-04-01T00:00:00.000", + * "acquisition_end": "2015-04-30T00:00:00.000", * "urls": [ - * "http://myimagery.com/image01.tif", - * "http://myimagery.com/image02.tif" + * "http://satellit.es/image1.tif", + * "http://satellit.es/image2.tif", * ] * } * ] From b7d7a7333a4544e729912ec773a3a6da4c4cd458 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Mon, 31 Aug 2015 17:04:46 -0400 Subject: [PATCH 073/144] Change contact metadata to string instead of object --- index.js | 3 ++- test/fixture/NE1_50M_SR.output.json | 2 +- test/fixture/upload-status.json | 2 +- worker/process-image.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 39ac984..d7eb752 100644 --- a/index.js +++ b/index.js @@ -60,7 +60,8 @@ var OAMUploader = function (readyCb) { { register: require('hapi-router'), options: { - routes: './routes/*.js' + routes: './routes/*.js', + ignore: './routes/_apidoc.js' } } ], function (err) { diff --git a/test/fixture/NE1_50M_SR.output.json b/test/fixture/NE1_50M_SR.output.json index a1ee0f9..08468f6 100644 --- a/test/fixture/NE1_50M_SR.output.json +++ b/test/fixture/NE1_50M_SR.output.json @@ -1 +1 @@ -{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}} \ No newline at end of file +{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}} diff --git a/test/fixture/upload-status.json b/test/fixture/upload-status.json index cc09128..2c58763 100644 --- a/test/fixture/upload-status.json +++ b/test/fixture/upload-status.json @@ -1 +1 @@ -{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} +{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name": "Ziggy", "email": "ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} diff --git a/worker/process-image.js b/worker/process-image.js index ea5d9d0..3d11f5e 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -109,7 +109,7 @@ function generateMetadata (scene, path, key, callback) { acquisition_end: scene.acquisition_end, platform: scene.platform, provider: scene.provider, - contact: scene.contact, + contact: [scene.contact.name.replace(',', ';'), scene.contact.email].join(','), properties: { tms: scene.tms, sensor: scene.sensor From 65cdccf8f4865f0de23f50021c82e8edfe546ad4 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Wed, 2 Sep 2015 15:24:50 -0400 Subject: [PATCH 074/144] Better tempfile cleanup --- package.json | 4 ++-- worker/index.js | 10 +++------- worker/process-image.js | 10 +++++++--- worker/queue.js | 5 +++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 6a8a704..a372b5a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "docker-install": ".build_scripts/docker/run.sh /install.sh", "docker-test": ".build_scripts/docker/run.sh /test.sh", "docker-start": ".build_scripts/docker/run.sh /start.sh", - "start": "nodemon index.js", + "docker-console": ".build_scripts/docker/run.sh bash", + "start": "TMPDIR=/tmp node index.js", "test": "semistandard && npm run lab", "lab": "lab -T test/babel.js test/test__*.js", "docs": "apidoc -i routes/ -o docs/" @@ -57,7 +58,6 @@ "chai": "^2.3.0", "ecstatic": "^0.8.0", "lab": "^5.15.0", - "nodemon": "^1.4.1", "omit-deep": "^0.1.2", "semistandard": "^7.0.2", "tape": "^4.0.3" diff --git a/worker/index.js b/worker/index.js index ccf8967..304f154 100644 --- a/worker/index.js +++ b/worker/index.js @@ -23,18 +23,14 @@ var s3 = new AWS.S3(); var queue = new JobQueue(s3); onExit(function () { - process.removeAllListeners(); queue.cleanup() .then(process.exit.bind(process, 0)) .catch(process.exit.bind(process, 1)); }); queue.run() -.then(function () { - process.removeAllListeners(); - process.exit(); -}) -.catch(function () { - process.removeAllListeners(); +.then(function () { process.exit(); }) +.catch(function (err) { + console.error(err); process.exit(1); }); diff --git a/worker/process-image.js b/worker/process-image.js index 3d11f5e..d76545b 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -20,8 +20,13 @@ module.exports = promisify(_processImage); * Fully process one URL. * Callback called with (err, { metadata, messages }) */ -function _processImage (s3, scene, url, key, callback) { +function _processImage (s3, scene, url, key, cb) { tmp.file({ postfix: '.tif' }, function (err, path, fd, cleanup) { + function callback (err, data) { + cleanup(); + cb(err, data); + } + if (err) { return callback(err); } log(['debug'], 'Downloading ' + url + ' to ' + path); @@ -47,7 +52,6 @@ function _processImage (s3, scene, url, key, callback) { thumbPath = null; } uploadToS3(s3, path, key, metadata, thumbPath, function (err) { - cleanup(); // delete tempfile callback(err, { metadata: metadata, messages: messages }); }); }); @@ -129,7 +133,7 @@ function generateMetadata (scene, path, key, callback) { } function makeThumbnail (imagePath, callback) { - tmp.file({ postfix: '.png' }, function (err, path, fd, cleanup) { + tmp.file({ postfix: '.png' }, function (err, path, fd) { if (err) { return callback(err); } log(['debug'], 'Generating thumbnail', path); diff --git a/worker/queue.js b/worker/queue.js index c640d53..ff88319 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -145,10 +145,11 @@ JobQueue.prototype._mainloop = function mainloop () { }; JobQueue.prototype.cleanup = function cleanup (err) { + if (this._cleanupCalled) { return Promise.resolve(true); } + this._cleanupCalled = true; log('Cleaning up.'); - if (err) { log(['error'], err, err.stack); } - if (!this.db) { return; } + if (!this.db) { return Promise.resolve(true); } if (!this.workerId) { return this.db.close(); } return this.workers.deleteOne({ _id: this.workerId }) From 5e667674978220cdac40ad8c2225db621b0e5554 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Fri, 4 Sep 2015 10:19:15 -0400 Subject: [PATCH 075/144] Shorten token to 64 chars Fix tests --- config.js | 6 ++---- index.js | 1 - routes/tokens.js | 4 ++-- test/babel.js | 1 - test/test__token_management.js | 8 ++------ test/test__token_validation.js | 15 ++++++--------- test/test__user_authentication.js | 1 - test/test__worker.js | 1 - 8 files changed, 12 insertions(+), 25 deletions(-) diff --git a/config.js b/config.js index b6713de..fb78cfb 100644 --- a/config.js +++ b/config.js @@ -5,8 +5,7 @@ var xtend = require('xtend'); var defaults = { host: '0.0.0.0', // cosmetic port: 3000, // port to listen on - dbUri: process.env.NODE_ENV === 'test' ? - 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', // the mongodb database uri (mongodb://user:pass@host:port/db) + dbUri: process.env.NODE_ENV === 'test' ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', // the mongodb database uri (mongodb://user:pass@host:port/db) adminPassword: null, // the administrator username adminUsername: null, // the administrator password oinBucket: 'oam-uploader', // name of the OpenImageryNetwork bucket to which imagery should be uploaded @@ -41,8 +40,7 @@ var environment = { port: process.env.PORT, host: process.env.HOST, oinBucket: process.env.OIN_BUCKET, - dbUri: process.env.NODE_ENV === 'test' ? - process.env.DBURI_TEST : process.env.DBURI, + dbUri: process.env.NODE_ENV === 'test' ? process.env.DBURI_TEST : process.env.DBURI, maxWorkers: process.env.MAX_WORKERS, adminPassword: process.env.ADMIN_PASSWORD, adminUsername: process.env.ADMIN_USERNAME, diff --git a/index.js b/index.js index d7eb752..ac589c3 100644 --- a/index.js +++ b/index.js @@ -70,7 +70,6 @@ var OAMUploader = function (readyCb) { readyCb(hapi); }); }); - }; // https://medium.com/the-spumko-suite/testing-hapi-services-with-lab-96ac463c490a diff --git a/routes/tokens.js b/routes/tokens.js index 46c6bd4..28b1544 100644 --- a/routes/tokens.js +++ b/routes/tokens.js @@ -26,9 +26,9 @@ module.exports = [ message: null, data: tokens }); - }); } + }, { @@ -54,7 +54,7 @@ module.exports = [ hmac.end(); var data = request.payload; - data.token = hmac.read(); + data.token = hmac.read().substring(64); data.created = new Date(); data.updated = null; diff --git a/test/babel.js b/test/babel.js index adb7716..9d91f05 100644 --- a/test/babel.js +++ b/test/babel.js @@ -4,7 +4,6 @@ module.exports = [ { ext: '.js', transform: function (content, filename) { - if (filename.indexOf('node_modules') === -1) { var result = Babel.transform(content, { sourceMap: 'inline', diff --git a/test/test__token_management.js b/test/test__token_management.js index 576a70a..6edc6d4 100644 --- a/test/test__token_management.js +++ b/test/test__token_management.js @@ -18,7 +18,6 @@ var activeToken = null; var validateToken = null; suite('test tokens', function () { - before(function (done) { assert.match(config.dbUri, /test$/, 'use the test database'); // Get a reference to the server. @@ -39,7 +38,7 @@ suite('test tokens', function () { name: 'Primary token', expiration: false, status: 'active', - token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', + token: '87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', created: new Date('2015-08-10T13:38:48.684Z'), updated: null }, @@ -48,7 +47,7 @@ suite('test tokens', function () { name: 'Secondary token', expiration: new Date('2020-08-10T13:38:48.684Z'), status: 'active', - token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', + token: 'b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', created: new Date('2015-08-10T13:38:48.684Z'), updated: null } @@ -429,9 +428,6 @@ suite('test tokens', function () { assert.lengthOf(result.data, 2); done(); }); - }); }); - }); - diff --git a/test/test__token_validation.js b/test/test__token_validation.js index 19dc895..945215d 100644 --- a/test/test__token_validation.js +++ b/test/test__token_validation.js @@ -20,7 +20,7 @@ var tokens = [ name: 'Active Never Expires', expiration: false, status: 'active', - token: '067e6c88d03021957a13b6406ca753b805a03331c914f36a33539de38622a90f87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', + token: '87f10851e0e70eeb76f4e652ab282f0502ce3f420522d7c7d68f0fada19adb14', created: now, updated: null }, @@ -28,7 +28,7 @@ var tokens = [ name: 'Active Not Expired', expiration: nextyear, status: 'active', - token: '112fec071ca3414f8e1a7538eab67bfaf3d6fc3376147434b6f0ac9923c4a145b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', + token: 'b35d33e0970adf0a71110473dec4299cd2c6abcdf7d06dab6f344e5d14951a0d', created: now, updated: null }, @@ -36,7 +36,7 @@ var tokens = [ name: 'Active Expired', expiration: now, status: 'active', - token: 'a3b1e1d9971a427c67d0f4e277d808c334e3981e43e31643e0021e5af28121eafe46443529829c8fe9a5ce6836f1e19adc429b009923464384361a52b0da15d0', + token: 'fe46443529829c8fe9a5ce6836f1e19adc429b009923464384361a52b0da15d0', created: now, updated: null }, @@ -44,7 +44,7 @@ var tokens = [ name: 'Blocked Never Expired', expiration: false, status: 'blocked', - token: '195364531c28d71fe85f194410b1aa1f4d96f31c001c383eaf8cd703245fd01001d600ce6314214bffb07b43a195a2183c5301c2ce7ae4cb259ac42e647e40dd', + token: '01d600ce6314214bffb07b43a195a2183c5301c2ce7ae4cb259ac42e647e40dd', created: now, updated: null }, @@ -52,7 +52,7 @@ var tokens = [ name: 'Blocked Not Expired', expiration: nextyear, status: 'blocked', - token: '21a06762eab1678ab8049709d6a3865547bade218cd8d28bdd9efc08f851403681d71012be5f20ad86c7418764efa64b00c7d267d0eadb9eccc07694ab3e2064', + token: '81d71012be5f20ad86c7418764efa64b00c7d267d0eadb9eccc07694ab3e2064', created: now, updated: null }, @@ -60,14 +60,13 @@ var tokens = [ name: 'Blocked Expired', expiration: now, status: 'blocked', - token: '1e57246b246f7f79ff109ec9672769c4f105428048b012419ea8fb3967441543fe981670ca6b0cf9eadf4fbeeb2e3c8f0ad5e00a1645c2bab63bdef4348658c4', + token: 'fe981670ca6b0cf9eadf4fbeeb2e3c8f0ad5e00a1645c2bab63bdef4348658c4', created: now, updated: null } ]; suite('test token validation', function () { - before(function (done) { assert.match(dbUri, /test$/, 'use the test database'); MongoClient.connect(dbUri, function (err, db) { @@ -130,6 +129,4 @@ suite('test token validation', function () { done(error); }); }); - }); - diff --git a/test/test__user_authentication.js b/test/test__user_authentication.js index 571dac6..6d8d213 100644 --- a/test/test__user_authentication.js +++ b/test/test__user_authentication.js @@ -14,7 +14,6 @@ var assert = chai.assert; var cookie = null; suite('test authentication', function () { - before(function (done) { // Get a reference to the server. // Wait for everything to load. diff --git a/test/test__worker.js b/test/test__worker.js index 30c06b4..cb98db0 100644 --- a/test/test__worker.js +++ b/test/test__worker.js @@ -126,5 +126,4 @@ suite('test worker', function () { }); }); }); - }); From df5cdeb682309c75d5f893250fcd9167ce87b4f7 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 17 Sep 2015 15:39:18 -0400 Subject: [PATCH 076/144] Cover MAX_WORKERS=0 edge case --- plugins/workers.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/workers.js b/plugins/workers.js index 5c8997c..f10c160 100644 --- a/plugins/workers.js +++ b/plugins/workers.js @@ -49,6 +49,8 @@ module.exports = function register (server, options, next) { } }); } + + return Promise.resolve(); } function spawnWorker () { From 7664cb90bbc781aa45ed80b61768bc7ba047d315 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 17 Sep 2015 15:47:42 -0400 Subject: [PATCH 077/144] Correct upload request validation - Close #25 - Add better server logs when request is rejected --- models/upload.js | 2 +- routes/uploads.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/models/upload.js b/models/upload.js index f0fcee3..f17689a 100644 --- a/models/upload.js +++ b/models/upload.js @@ -8,7 +8,7 @@ var infoSchema = Joi.object().keys({ var sceneSchema = Joi.object().keys({ contact: infoSchema.required(), title: Joi.string().min(1).required(), - provider: Joi.string(), + provider: Joi.string().min(1).required(), platform: Joi.any().allow('satellite', 'aircraft', 'UAV', 'balloon', 'kite').required(), sensor: Joi.string(), acquisition_start: Joi.date().required(), diff --git a/routes/uploads.js b/routes/uploads.js index 5ddddd3..f6c3fcc 100644 --- a/routes/uploads.js +++ b/routes/uploads.js @@ -173,7 +173,10 @@ module.exports = [ }, handler: function (request, reply) { Joi.validate(request.payload, uploadSchema, function (err, data) { - if (err) { return reply(Boom.badRequest(err)); } + if (err) { + request.log(['info'], err); + return reply(Boom.badRequest(err)); + } var db = request.server.plugins.db.connection; From 5c91537d99b8b35c63dbc16b936631e63a3d37f5 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 8 Apr 2016 12:57:15 +0100 Subject: [PATCH 078/144] Solves tms validation issue --- models/upload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/upload.js b/models/upload.js index f17689a..0d5faf0 100644 --- a/models/upload.js +++ b/models/upload.js @@ -13,7 +13,7 @@ var sceneSchema = Joi.object().keys({ sensor: Joi.string(), acquisition_start: Joi.date().required(), acquisition_end: Joi.date().required(), - tms: Joi.string().uri(), + tms: Joi.string(), urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https']})) .min(1).required() }); From 15a6e2533f2e179c698897a1f1e1985d5731946e Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 8 Apr 2016 14:27:27 +0100 Subject: [PATCH 079/144] Indentation --- test/test__token_management.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/test__token_management.js b/test/test__token_management.js index 6edc6d4..b72258e 100644 --- a/test/test__token_management.js +++ b/test/test__token_management.js @@ -52,23 +52,23 @@ suite('test tokens', function () { updated: null } ], function (err, res) { - if (err) throw err; - // Cookie - var options = { - method: 'POST', - url: '/login', - payload: { - username: 'admin', - password: 'admin' - } - }; - - server.inject(options, function (response) { - // Store the cookie to use on next requests - cookie = response.headers['set-cookie'][0].split(' ')[0]; - done(); - }); + if (err) throw err; + // Cookie + var options = { + method: 'POST', + url: '/login', + payload: { + username: 'admin', + password: 'admin' + } + }; + + server.inject(options, function (response) { + // Store the cookie to use on next requests + cookie = response.headers['set-cookie'][0].split(' ')[0]; + done(); }); + }); }); }); }); From 1bf94f4de74df23a7a90f3fcdc6b74f85edfbf67 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 12 Apr 2016 12:31:10 -0400 Subject: [PATCH 080/144] Attempt to fix test --- test/test__worker.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/test__worker.js b/test/test__worker.js index cb98db0..589a29c 100644 --- a/test/test__worker.js +++ b/test/test__worker.js @@ -113,7 +113,16 @@ suite('test worker', function () { url: '/uploads/' + uploadId, credentials: { user: { id: -1 } } }, function (response) { - var omitted = [ '_id', 'startedAt', 'createdAt', 'stoppedAt', 'uuid', 'thumbnail' ]; + var omitted = [ + '_id', + 'startedAt', + 'createdAt', + 'stoppedAt', + 'uuid', + 'thumbnail', + 'user', + 'tms' + ]; var status = JSON.parse(response.payload); var expected = require('./fixture/upload-status.json'); status = omit(status, omitted); From 7ac40a19e2578a1cee50e656c46524c098e7a41c Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 12 Apr 2016 12:39:44 -0400 Subject: [PATCH 081/144] Update fixture --- test/fixture/NE1_50M_SR.output.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixture/NE1_50M_SR.output.json b/test/fixture/NE1_50M_SR.output.json index 08468f6..bc815c1 100644 --- a/test/fixture/NE1_50M_SR.output.json +++ b/test/fixture/NE1_50M_SR.output.json @@ -1 +1 @@ -{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}} +{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png", "tms": null}} From 2c4c31bbff6977dfdbd12babd8f5a65ceb042d62 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 12 Apr 2016 13:16:33 -0400 Subject: [PATCH 082/144] More test fixtures schema fixes --- models/upload.js | 2 +- test/fixture/NE1_50M_SR.input.json | 1 + test/fixture/upload-status.json | 2 +- test/test__worker.js | 5 +++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/models/upload.js b/models/upload.js index 0d5faf0..4bce1f1 100644 --- a/models/upload.js +++ b/models/upload.js @@ -13,7 +13,7 @@ var sceneSchema = Joi.object().keys({ sensor: Joi.string(), acquisition_start: Joi.date().required(), acquisition_end: Joi.date().required(), - tms: Joi.string(), + tms: Joi.string().allow(null), urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https']})) .min(1).required() }); diff --git a/test/fixture/NE1_50M_SR.input.json b/test/fixture/NE1_50M_SR.input.json index 51808ab..a2f1aef 100644 --- a/test/fixture/NE1_50M_SR.input.json +++ b/test/fixture/NE1_50M_SR.input.json @@ -15,6 +15,7 @@ "platform": "satellite", "acquisition_start": "2015-04-01T00:00:00.000", "acquisition_end": "2015-04-30T00:00:00.000", + "tms": null, "urls": [ "http://localhost:8080/NE1_50M_SR.tif" ] diff --git a/test/fixture/upload-status.json b/test/fixture/upload-status.json index 2c58763..ef0599c 100644 --- a/test/fixture/upload-status.json +++ b/test/fixture/upload-status.json @@ -1 +1 @@ -{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name": "Ziggy", "email": "ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png"}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} +{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name": "Ziggy", "email": "ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","tms": null, "images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png", "tms": null}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} diff --git a/test/test__worker.js b/test/test__worker.js index 589a29c..19f0f45 100644 --- a/test/test__worker.js +++ b/test/test__worker.js @@ -85,11 +85,12 @@ suite('test worker', function () { .findOne({ url: 'http://localhost:8080/NE1_50M_SR.tif' }) .then(function (image) { assert(image.status === 'finished'); - assert.equal(JSON.stringify(metadata), JSON.stringify(image.metadata), + assert.equal(JSON.stringify(metadata), + JSON.stringify(image.metadata), 'the uploaded metadata is stored in the db entry for an image'); assert.match(metadata.uuid, /http:\/\/oam-uploader.s3.amazonaws.com\/uploads\/.*\/.*\/scene\/0\/scene-0-image-0-NE1_50M_SR\.tif/); assert.match(metadata.properties.thumbnail, /thumb\.(png|jpe?g)$/); - var omitted = ['uuid', 'thumbnail']; + var omitted = ['uuid', 'thumbnail', 'tms']; assert.deepEqual(omit(metadata, omitted), omit(expected, omitted), 'generated metadata'); From f000e2bdc2d6ce4cda7fbbf52e22a071704a5c05 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Wed, 25 May 2016 12:32:51 +0100 Subject: [PATCH 083/144] Add proper licensing --- LICENSE | 29 +++++++++++++++++++++++++++++ README.md | 3 +++ package.json | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2d4010 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2016, Development Seed +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of oam-design-system nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index b29e5c2..b6ba142 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,6 @@ exit Now you can use `deploy -c .build_scripts/deploy.conf ENV` at any time to deploy your local branch. +## License +Oam Uploader Api is licensed under **BSD 3-Clause License**, see the [LICENSE](LICENSE) file for more details. + diff --git a/package.json b/package.json index a372b5a..d4a4e49 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/hotosm/oam-uploader-api.git" }, "author": "Development Seed", - "license": "CC01", + "license": "BSD 3-Clause", "bugs": { "url": "https://github.com/hotosm/oam-uploader-api/issues" }, From d735e658b8569295340fd1f06373029096e3723c Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 13 Sep 2016 14:31:37 -0400 Subject: [PATCH 084/144] Sanitize pathname. Fix #33 --- worker/queue.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worker/queue.js b/worker/queue.js index ff88319..bf19f64 100644 --- a/worker/queue.js +++ b/worker/queue.js @@ -111,6 +111,11 @@ JobQueue.prototype._mainloop = function mainloop () { scene.images.forEach(function (id, j) { if (image._id.equals(id)) { var filename = image.url.split('/').pop() || 'untitled'; + var qmIndex = filename.indexOf('?'); + if (qmIndex !== -1) { + filename = filename.substring(0, qmIndex); + } + filename = filename.replace(/[^a-zA-Z0-9 _\-\\.]/g, '').replace(/ /g, '-'); filename = ['scene', i, 'image', j, filename].join('-'); var key = ['uploads', now, upload._id, 'scene', i, filename].join('/'); // now that we have the scene, we can process the image From 5a121e3cbfc0ebd8971441ccf532ccdf257074d3 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 16 Sep 2016 15:24:24 -0400 Subject: [PATCH 085/144] Add config for gdrive key --- config.js | 4 +++- local.sample.env | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/config.js b/config.js index fb78cfb..609b41f 100644 --- a/config.js +++ b/config.js @@ -14,6 +14,7 @@ var defaults = { maxWorkers: 1, // the maximum number of workers sendgridApiKey: null, // sendgrid API key, for sending notification emails sendgridFrom: 'info@hotosm.org', // the email address from which to send notification emails + gdriveKey: null, emailNotification: { subject: '[ OAM Uploader ] Imagery upload submitted', text: 'Your upload has been successfully submitted and is now being ' + @@ -48,7 +49,8 @@ var environment = { awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, sendgridApiKey: process.env.SENDGRID_API_KEY, - sendgridFrom: process.env.SENDGRID_FROM + sendgridFrom: process.env.SENDGRID_FROM, + gdriveKey: process.env.GDRIVE_KEY }; var config = xtend(defaults); diff --git a/local.sample.env b/local.sample.env index bfa4b2f..29b79aa 100644 --- a/local.sample.env +++ b/local.sample.env @@ -1,11 +1,12 @@ -AWS_SECRET_KEY_ID=your-id -AWS_SECRET_ACCESS_KEY=your-key -SENDGRID_API_KEY=your-sendgrid-key -SENDGRID_FROM=emailaccount@that.sends.notifications.com -AWS_REGION=us-east-1 -OIN_BUCKET=bucket-name -DBURI=mongodb://localhost/oam-uploader -DBURI_TEST=mongodb://localhost/oam-uploader-test -DBURI_TEST=mongodb://localhost/oam-uploader-test -# ADMIN_USERNAME=admin -# ADMIN_PASSWORD= +export AWS_SECRET_KEY_ID=your-id +export AWS_SECRET_ACCESS_KEY=your-key +export SENDGRID_API_KEY=your-sendgrid-key +export SENDGRID_FROM=emailaccount@that.sends.notifications.com +export AWS_REGION=us-east-1 +export OIN_BUCKET=bucket-name +export DBURI=mongodb://localhost/oam-uploader +export DBURI_TEST=mongodb://localhost/oam-uploader-test +export DBURI_TEST=mongodb://localhost/oam-uploader-test +export GDRIVE_KEY=your-google-key +export ADMIN_USERNAME=admin +export ADMIN_PASSWORD=admin From c0416563cb50f186e459f25731c05222b079d517 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 16 Sep 2016 15:24:45 -0400 Subject: [PATCH 086/144] Change server port --- config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js b/config.js index 609b41f..f26c79f 100644 --- a/config.js +++ b/config.js @@ -4,7 +4,7 @@ var xtend = require('xtend'); // overriden by environment variables (see below). var defaults = { host: '0.0.0.0', // cosmetic - port: 3000, // port to listen on + port: 4000, // port to listen on dbUri: process.env.NODE_ENV === 'test' ? 'mongodb://localhost/oam-uploader-test' : 'mongodb://localhost/oam-uploader', // the mongodb database uri (mongodb://user:pass@host:port/db) adminPassword: null, // the administrator username adminUsername: null, // the administrator password From 80268bd2caedf9b6855c5d4cb8b3a6b9113452e4 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 16 Sep 2016 15:25:19 -0400 Subject: [PATCH 087/144] Include special check for gdrive files --- models/upload.js | 2 +- worker/process-image.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/models/upload.js b/models/upload.js index 4bce1f1..6b1b16d 100644 --- a/models/upload.js +++ b/models/upload.js @@ -14,7 +14,7 @@ var sceneSchema = Joi.object().keys({ acquisition_start: Joi.date().required(), acquisition_end: Joi.date().required(), tms: Joi.string().allow(null), - urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https']})) + urls: Joi.array().items(Joi.string().uri({scheme: ['http', 'https', 'gdrive']})) .min(1).required() }); diff --git a/worker/process-image.js b/worker/process-image.js index d76545b..e67bdbc 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -29,6 +29,13 @@ function _processImage (s3, scene, url, key, cb) { if (err) { return callback(err); } + // Google drive url comes in the form of gdrive://FILE_ID + // We need this because large files can only be downloaded with an api key. + var pieces = url.match(/gdrive:\/\/(.+)/); + if (pieces) { + url = `https://www.googleapis.com/drive/v3/files/${pieces[1]}?alt=media&key=${config.gdriveKey}`; + } + log(['debug'], 'Downloading ' + url + ' to ' + path); var downloadStatus; From ed67f1e75ec5d43098eb3188c8d828cf9be2b115 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 27 Sep 2016 11:01:28 -0400 Subject: [PATCH 088/144] Add JPEG2000 conversion to process-image.js worker --- worker/process-image.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index d76545b..40bc242 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -1,6 +1,8 @@ 'use strict'; var fs = require('fs'); +var pathTools = require('path'); +var cp = require('child_process'); var tmp = require('tmp'); var promisify = require('es6-promisify'); var request = require('request'); @@ -10,6 +12,8 @@ var applyGdalinfo = require('oam-meta-generator/lib/apply-gdalinfo'); var sharp = require('sharp'); var log = require('./log'); var config = require('../config'); +// Must be modified to match the local path to the gdal_translate executable +var translateExe = '/Library/Frameworks/GDAL.framework/Versions/2.1/Programs/gdal_translate'; var s3bucket = config.oinBucket; // desired size in kilobytes * 1000 bytes/kb / (~.75 byte/pixel) @@ -21,7 +25,8 @@ module.exports = promisify(_processImage); * Callback called with (err, { metadata, messages }) */ function _processImage (s3, scene, url, key, cb) { - tmp.file({ postfix: '.tif' }, function (err, path, fd, cleanup) { + var ext = pathTools.extname(url).toLowerCase(); + tmp.file({ postfix: ext }, function (err, path, fd, cleanup) { function callback (err, data) { cleanup(); cb(err, data); @@ -43,6 +48,16 @@ function _processImage (s3, scene, url, key, cb) { } var messages = []; + + // Convert JPEG2000 to TIFF, if applicable + if (ext === '.jp2') { + var parsedPath = pathTools.parse(path); + var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; + cp.execSync(`${translateExe} -of GTiff ${path} ${outPath}`); + fs.unlinkSync(path); + path = outPath; + } + // we've successfully downloaded the file. now do stuff with it. generateMetadata(scene, path, key, function (err, metadata) { if (err) { return callback(err); } @@ -162,4 +177,3 @@ function makeThumbnail (imagePath, callback) { function publicUrl (bucketName, key) { return 'http://' + bucketName + '.s3.amazonaws.com/' + key; } - From 5c76c16ad6b72a35479746bd32f75e74ae87e352 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 27 Sep 2016 11:25:07 -0400 Subject: [PATCH 089/144] Added translate parameters favored by mojodna (https://goo.gl/1ce4EQ) --- worker/process-image.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index 40bc242..aabc86a 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -53,8 +53,16 @@ function _processImage (s3, scene, url, key, cb) { if (ext === '.jp2') { var parsedPath = pathTools.parse(path); var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; - cp.execSync(`${translateExe} -of GTiff ${path} ${outPath}`); - fs.unlinkSync(path); + var cmd = `${translateExe} -of GTiff ${path} ${outPath} ` + + '-co TILED=yes ' + + '-co COMPRESS=DEFLATE ' + + '-co PREDICTOR=2 ' + + '-co SPARSE_OK=yes ' + + '-co BLOCKXSIZE=256 ' + + '-co BLOCKYSIZE=256 ' + + '-co INTERLEAVE=band ' + + '-co NUM_THREADS=ALL_CPUS'; + cp.execSync(cmd); path = outPath; } From 3454ab1a9af37473fa538c7401e208204c221df2 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 27 Sep 2016 12:40:20 -0400 Subject: [PATCH 090/144] Convert inline conversion to asynchronous function --- worker/process-image.js | 67 ++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index aabc86a..48acf63 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -49,33 +49,19 @@ function _processImage (s3, scene, url, key, cb) { var messages = []; - // Convert JPEG2000 to TIFF, if applicable - if (ext === '.jp2') { - var parsedPath = pathTools.parse(path); - var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; - var cmd = `${translateExe} -of GTiff ${path} ${outPath} ` + - '-co TILED=yes ' + - '-co COMPRESS=DEFLATE ' + - '-co PREDICTOR=2 ' + - '-co SPARSE_OK=yes ' + - '-co BLOCKXSIZE=256 ' + - '-co BLOCKYSIZE=256 ' + - '-co INTERLEAVE=band ' + - '-co NUM_THREADS=ALL_CPUS'; - cp.execSync(cmd); - path = outPath; - } - // we've successfully downloaded the file. now do stuff with it. - generateMetadata(scene, path, key, function (err, metadata) { + translateJPEG2000(ext, path, key, function (err, path, key) { if (err) { return callback(err); } - makeThumbnail(path, function (thumbErr, thumbPath) { - if (thumbErr) { - messages.push('Could not generate thumbnail: ' + thumbErr.message); - thumbPath = null; - } - uploadToS3(s3, path, key, metadata, thumbPath, function (err) { - callback(err, { metadata: metadata, messages: messages }); + generateMetadata(scene, path, key, function (err, metadata) { + if (err) { return callback(err); } + makeThumbnail(path, function (thumbErr, thumbPath) { + if (thumbErr) { + messages.push('Could not generate thumbnail: ' + thumbErr.message); + thumbPath = null; + } + uploadToS3(s3, path, key, metadata, thumbPath, function (err) { + callback(err, { metadata: metadata, messages: messages }); + }); }); }); }); @@ -119,6 +105,37 @@ function uploadToS3 (s3, path, key, metadata, thumbPath, callback) { }); } +function translateJPEG2000 (ext, path, key, callback) { + if (ext === '.jp2') { + log(['debug'], 'Converting JPEG2000 to TIFF.'); + var parsedPath = pathTools.parse(path); + var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; + + var cmd = `${translateExe} -of GTiff ${path} ${outPath} ` + + '-co TILED=yes ' + + '-co COMPRESS=DEFLATE ' + + '-co PREDICTOR=2 ' + + '-co SPARSE_OK=yes ' + + '-co BLOCKXSIZE=256 ' + + '-co BLOCKYSIZE=256 ' + + '-co INTERLEAVE=band ' + + '-co NUM_THREADS=ALL_CPUS'; + + cp.execSync(cmd); + + // cp.exec(cmd, (err, stdout, stderr) => { + // if (err) { return callback(err); } + // console.log(`\n\n\nstdout: ${stdout}`); + // console.log(`\n\n\nstderr: ${stderr}`); + // }); + const parsedKey = pathTools.parse(key); + key = pathTools.join(parsedKey.dir, parsedKey.name) + '.tif'; + path = outPath; + log(['debug'], 'Converted JPEG2000 to TIFF: ', path); + } + callback(null, path, key); +} + function generateMetadata (scene, path, key, callback) { log(['debug'], 'Generating metadata.'); fs.stat(path, function (err, stat) { From 80ea53722788f8a5da0a56a55f9cf8cd4496f035 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 27 Sep 2016 16:04:30 -0400 Subject: [PATCH 091/144] removed commented-out code --- worker/process-image.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index 48acf63..350de45 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -120,14 +120,8 @@ function translateJPEG2000 (ext, path, key, callback) { '-co BLOCKYSIZE=256 ' + '-co INTERLEAVE=band ' + '-co NUM_THREADS=ALL_CPUS'; - cp.execSync(cmd); - // cp.exec(cmd, (err, stdout, stderr) => { - // if (err) { return callback(err); } - // console.log(`\n\n\nstdout: ${stdout}`); - // console.log(`\n\n\nstderr: ${stderr}`); - // }); const parsedKey = pathTools.parse(key); key = pathTools.join(parsedKey.dir, parsedKey.name) + '.tif'; path = outPath; From a99f805d1b55ed45c7aa5db1deea417113a5c72f Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 29 Sep 2016 09:47:50 +0100 Subject: [PATCH 092/144] Add gdal dependency reference to the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b6ba142..6c070c1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The steps below will walk you through setting up your own instance of the oam-up - [MongoDB](https://www.mongodb.org/) - [Node.js](https://nodejs.org/) - [libvips](https://github.com/jcupitt/libvips) +- [gdal](http://www.sarasafavi.com/installing-gdalogr-on-ubuntu.html) ### Install Application Dependencies From f86e1ef0e08bfda5814a7d5416843962be0987ec Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 29 Sep 2016 09:48:20 +0100 Subject: [PATCH 093/144] Use config var for gdal path. Handle gdal_translate error --- config.js | 3 ++- local.sample.env | 23 ++++++++++----------- worker/process-image.js | 44 +++++++++++++++++++++++------------------ 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/config.js b/config.js index fb78cfb..43a71ac 100644 --- a/config.js +++ b/config.js @@ -48,7 +48,8 @@ var environment = { awsAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, sendgridApiKey: process.env.SENDGRID_API_KEY, - sendgridFrom: process.env.SENDGRID_FROM + sendgridFrom: process.env.SENDGRID_FROM, + gdalTranslateBin: process.env.GDAL_TRANSLATE_BIN }; var config = xtend(defaults); diff --git a/local.sample.env b/local.sample.env index bfa4b2f..9c6fec1 100644 --- a/local.sample.env +++ b/local.sample.env @@ -1,11 +1,12 @@ -AWS_SECRET_KEY_ID=your-id -AWS_SECRET_ACCESS_KEY=your-key -SENDGRID_API_KEY=your-sendgrid-key -SENDGRID_FROM=emailaccount@that.sends.notifications.com -AWS_REGION=us-east-1 -OIN_BUCKET=bucket-name -DBURI=mongodb://localhost/oam-uploader -DBURI_TEST=mongodb://localhost/oam-uploader-test -DBURI_TEST=mongodb://localhost/oam-uploader-test -# ADMIN_USERNAME=admin -# ADMIN_PASSWORD= +export AWS_SECRET_KEY_ID=your-id +export AWS_SECRET_ACCESS_KEY=your-key +export SENDGRID_API_KEY=your-sendgrid-key +export SENDGRID_FROM=emailaccount@that.sends.notifications.com +export AWS_REGION=us-east-1 +export OIN_BUCKET=bucket-name +export DBURI=mongodb://localhost/oam-uploader +export DBURI_TEST=mongodb://localhost/oam-uploader-test +export DBURI_TEST=mongodb://localhost/oam-uploader-test +# export ADMIN_USERNAME=admin +# export ADMIN_PASSWORD= +export GDAL_TRANSLATE_BIN= \ No newline at end of file diff --git a/worker/process-image.js b/worker/process-image.js index 350de45..83eca2e 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -12,8 +12,6 @@ var applyGdalinfo = require('oam-meta-generator/lib/apply-gdalinfo'); var sharp = require('sharp'); var log = require('./log'); var config = require('../config'); -// Must be modified to match the local path to the gdal_translate executable -var translateExe = '/Library/Frameworks/GDAL.framework/Versions/2.1/Programs/gdal_translate'; var s3bucket = config.oinBucket; // desired size in kilobytes * 1000 bytes/kb / (~.75 byte/pixel) @@ -106,28 +104,36 @@ function uploadToS3 (s3, path, key, metadata, thumbPath, callback) { } function translateJPEG2000 (ext, path, key, callback) { - if (ext === '.jp2') { - log(['debug'], 'Converting JPEG2000 to TIFF.'); - var parsedPath = pathTools.parse(path); - var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; - - var cmd = `${translateExe} -of GTiff ${path} ${outPath} ` + - '-co TILED=yes ' + - '-co COMPRESS=DEFLATE ' + - '-co PREDICTOR=2 ' + - '-co SPARSE_OK=yes ' + - '-co BLOCKXSIZE=256 ' + - '-co BLOCKYSIZE=256 ' + - '-co INTERLEAVE=band ' + - '-co NUM_THREADS=ALL_CPUS'; - cp.execSync(cmd); + if (ext !== '.jp2') { + return callback(null, path, key); + } + + if (!config.gdalTranslateBin) { + throw new Error('GDAL bin path missing.'); + } + log(['debug'], 'Converting JPEG2000 to TIFF.'); + var parsedPath = pathTools.parse(path); + var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; + + var cmd = `${config.gdalTranslateBin} -of GTiff ${path} ${outPath} ` + + '-co TILED=yes ' + + '-co COMPRESS=DEFLATE ' + + '-co PREDICTOR=2 ' + + '-co SPARSE_OK=yes ' + + '-co BLOCKXSIZE=256 ' + + '-co BLOCKYSIZE=256 ' + + '-co INTERLEAVE=band ' + + '-co NUM_THREADS=ALL_CPUS'; + + cp.exec(cmd, function (err, stdout, stderr) { + if (err) { return callback(err); } const parsedKey = pathTools.parse(key); key = pathTools.join(parsedKey.dir, parsedKey.name) + '.tif'; path = outPath; log(['debug'], 'Converted JPEG2000 to TIFF: ', path); - } - callback(null, path, key); + return callback(null, path, key); + }); } function generateMetadata (scene, path, key, callback) { From ae01ed0031732a38fc82f897fc29ae662b6e6229 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 29 Sep 2016 09:56:57 +0100 Subject: [PATCH 094/144] Add missing comma --- config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js b/config.js index d92ebe6..09e91ec 100644 --- a/config.js +++ b/config.js @@ -51,7 +51,7 @@ var environment = { awsRegion: process.env.AWS_REGION, sendgridApiKey: process.env.SENDGRID_API_KEY, sendgridFrom: process.env.SENDGRID_FROM, - gdalTranslateBin: process.env.GDAL_TRANSLATE_BIN + gdalTranslateBin: process.env.GDAL_TRANSLATE_BIN, gdriveKey: process.env.GDRIVE_KEY }; From e9f51c147b107f1d9f0a8dd14860aef84e939c29 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 29 Sep 2016 12:49:00 +0100 Subject: [PATCH 095/144] Use execFile instead of exec. Remove old code --- worker/process-image.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/worker/process-image.js b/worker/process-image.js index 770d0c7..3b40ed1 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -62,7 +62,6 @@ function _processImage (s3, scene, url, key, cb) { makeThumbnail(path, function (thumbErr, thumbPath) { if (thumbErr) { messages.push('Could not generate thumbnail: ' + thumbErr.message); - thumbPath = null; } uploadToS3(s3, path, key, metadata, thumbPath, function (err) { callback(err, { metadata: metadata, messages: messages }); @@ -123,17 +122,20 @@ function translateJPEG2000 (ext, path, key, callback) { var parsedPath = pathTools.parse(path); var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; - var cmd = `${config.gdalTranslateBin} -of GTiff ${path} ${outPath} ` + - '-co TILED=yes ' + - '-co COMPRESS=DEFLATE ' + - '-co PREDICTOR=2 ' + - '-co SPARSE_OK=yes ' + - '-co BLOCKXSIZE=256 ' + - '-co BLOCKYSIZE=256 ' + - '-co INTERLEAVE=band ' + - '-co NUM_THREADS=ALL_CPUS'; - - cp.exec(cmd, function (err, stdout, stderr) { + var args = [ + '-of', 'GTiff', + path, outPath, + '-co', 'TILED=yes', + '-co', 'COMPRESS=DEFLATE', + '-co', 'PREDICTOR=2', + '-co', 'SPARSE_OK=yes', + '-co', 'BLOCKXSIZE=256', + '-co', 'BLOCKYSIZE=256', + '-co', 'INTERLEAVE=band', + '-co', 'NUM_THREADS=ALL_CPUS' + ]; + + cp.execFile(config.gdalTranslateBin, args, function (err, stdout, stderr) { if (err) { return callback(err); } const parsedKey = pathTools.parse(key); key = pathTools.join(parsedKey.dir, parsedKey.name) + '.tif'; From 444264c02d84bc9c52fb7c80c619a77e9e09b4c3 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Thu, 29 Sep 2016 13:18:45 -0400 Subject: [PATCH 096/144] Removes band interleave flag from gdal_translate call --- worker/process-image.js | 1 - 1 file changed, 1 deletion(-) diff --git a/worker/process-image.js b/worker/process-image.js index 3b40ed1..a458f7f 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -131,7 +131,6 @@ function translateJPEG2000 (ext, path, key, callback) { '-co', 'SPARSE_OK=yes', '-co', 'BLOCKXSIZE=256', '-co', 'BLOCKYSIZE=256', - '-co', 'INTERLEAVE=band', '-co', 'NUM_THREADS=ALL_CPUS' ]; From 90a809601f45095a387831019cdcd499866f14af Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 22 Nov 2016 15:56:39 -0800 Subject: [PATCH 097/144] ESLint dependencies Adds configurations referenced in .eslintrc --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package.json b/package.json index d4a4e49..21730b4 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,13 @@ "apidoc": "^0.13.1", "chai": "^2.3.0", "ecstatic": "^0.8.0", + "eslint": "^3.10.2", + "eslint-config-semistandard": "^7.0.0", + "eslint-config-standard": "^6.2.1", + "eslint-config-standard-react": "^4.2.0", + "eslint-plugin-promise": "^3.4.0", + "eslint-plugin-react": "^6.7.1", + "eslint-plugin-standard": "^2.0.1", "lab": "^5.15.0", "omit-deep": "^0.1.2", "semistandard": "^7.0.2", From a381f7e7e8cddc6d3ed1ba0a97897457231c788e Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 22 Nov 2016 15:58:13 -0800 Subject: [PATCH 098/144] Match node version to package.json engines --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..c43e105 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +0.12 From 0440800bfd0ccd176eb98fd6dfe027b5ba20d8d0 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 22 Nov 2016 16:02:05 -0800 Subject: [PATCH 099/144] nvm-related doc updates --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b6ba142..0facb57 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ The steps below will walk you through setting up your own instance of the oam-up ### Install Application Dependencies +If you use [`nvm`](https://github.com/creationix/nvm), activate the desired Node version: + + $ nvm install + +Install Node modules: + $ npm install ### Usage From 10093b5bc677e332bb3dc544b497e2e0a350ec33 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 22 Nov 2016 16:03:44 -0800 Subject: [PATCH 100/144] Add a Brewfile for easier dependency installation --- Brewfile | 3 +++ README.md | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 Brewfile diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..77ea69b --- /dev/null +++ b/Brewfile @@ -0,0 +1,3 @@ +tap 'homebrew/science' +brew 'vips' +brew 'mongodb' diff --git a/README.md b/README.md index b6ba142..4e6c249 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ The steps below will walk you through setting up your own instance of the oam-up - [Node.js](https://nodejs.org/) - [libvips](https://github.com/jcupitt/libvips) +Using [homebrew](http://brew.sh/), you can install MongoDB and `libvips` using: + + $ brew bundle + ### Install Application Dependencies $ npm install From a74b977b1e42bae98e5098172fb0386ea393faf7 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 22 Nov 2016 16:11:03 -0800 Subject: [PATCH 101/144] Fix warning about SPDX-formatted licenses --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4a4e49..c65edd3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/hotosm/oam-uploader-api.git" }, "author": "Development Seed", - "license": "BSD 3-Clause", + "license": "BSD-3-Clause", "bugs": { "url": "https://github.com/hotosm/oam-uploader-api/issues" }, From f52f9a923e545f8f2bf2745461647d653da6dbb3 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 22 Nov 2016 16:19:37 -0800 Subject: [PATCH 102/144] Extract server This allows the module to follow default Node conventions where `index.js` contains default exports and `npm start` runs `node server.js`. --- Procfile | 2 +- README.md | 2 +- index.js | 17 ----------------- package.json | 2 -- server.js | 11 +++++++++++ 5 files changed, 13 insertions(+), 21 deletions(-) create mode 100644 server.js diff --git a/Procfile b/Procfile index 1da0cd6..063b78f 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node index.js +web: npm start diff --git a/README.md b/README.md index b6ba142..e26854a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The steps below will walk you through setting up your own instance of the oam-up #### Starting the API: - $ node index.js + $ npm start The API exposes endpoints used to access information form the system via a RESTful interface. diff --git a/index.js b/index.js index ac589c3..b603b66 100644 --- a/index.js +++ b/index.js @@ -72,21 +72,4 @@ var OAMUploader = function (readyCb) { }); }; -// https://medium.com/the-spumko-suite/testing-hapi-services-with-lab-96ac463c490a -// The if (!module.parent) {…} conditional makes sure that if the script is -// being required as a module by another script, we don’t start the server. -// This is done to prevent the server from starting when we’re testing it. -// With Hapi, we don’t need to have the server listening to test it. -if (!module.parent) { - OAMUploader(function (hapi) { - // Start the server. - hapi.start(function () { - hapi.log(['info'], 'Server running at:' + hapi.info.uri); - // spawn a worker to handle any unprocessed uploads that may be sitting - // around in the database - hapi.plugins.workers.spawn(); - }); - }); -} - module.exports = OAMUploader; diff --git a/package.json b/package.json index d4a4e49..19cbab0 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,12 @@ "name": "oam-uploader-api", "version": "1.0.0", "description": "An uploader API for Open Aerial Map Imagery", - "main": "index.js", "scripts": { "docker-build": "docker build -t oam-uploader-api .build_scripts/docker", "docker-install": ".build_scripts/docker/run.sh /install.sh", "docker-test": ".build_scripts/docker/run.sh /test.sh", "docker-start": ".build_scripts/docker/run.sh /start.sh", "docker-console": ".build_scripts/docker/run.sh bash", - "start": "TMPDIR=/tmp node index.js", "test": "semistandard && npm run lab", "lab": "lab -T test/babel.js test/test__*.js", "docs": "apidoc -i routes/ -o docs/" diff --git a/server.js b/server.js new file mode 100644 index 0000000..0735ba9 --- /dev/null +++ b/server.js @@ -0,0 +1,11 @@ +var OAMUploader = require('./'); + +OAMUploader(function (hapi) { + // Start the server. + hapi.start(function () { + hapi.log(['info'], 'Server running at:' + hapi.info.uri); + // spawn a worker to handle any unprocessed uploads that may be sitting + // around in the database + hapi.plugins.workers.spawn(); + }); +}); From 9340e7d5e192b359696c836f693aa69c679cbe56 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 22 Nov 2016 20:40:02 -0500 Subject: [PATCH 103/144] Adds conversion to OAM's tif specs for all gdal_translate-supported image types, rather than only jp2 --- log.txt | 1 + worker/process-image.js | 59 ++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 log.txt diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..d99e0fc --- /dev/null +++ b/log.txt @@ -0,0 +1 @@ +Some log diff --git a/worker/process-image.js b/worker/process-image.js index a458f7f..f457df1 100644 --- a/worker/process-image.js +++ b/worker/process-image.js @@ -24,11 +24,11 @@ module.exports = promisify(_processImage); */ function _processImage (s3, scene, url, key, cb) { var ext = pathTools.extname(url).toLowerCase(); - tmp.file({ postfix: ext }, function (err, path, fd, cleanup) { - function callback (err, data) { - cleanup(); + tmp.file({ postfix: ext }, function (err, path, fd, cleanupSource) { + var callback = function (err, data) { + cleanupSource(); cb(err, data); - } + }; if (err) { return callback(err); } @@ -55,22 +55,32 @@ function _processImage (s3, scene, url, key, cb) { var messages = []; // we've successfully downloaded the file. now do stuff with it. - translateJPEG2000(ext, path, key, function (err, path, key) { + tmp.file({ postfix: '.tif' }, function (err, tifPath, fd, cleanupTif) { if (err) { return callback(err); } - generateMetadata(scene, path, key, function (err, metadata) { + + translateImage(ext, path, tifPath, function (err, path) { + callback = function (err, data) { + cleanupSource(); + cleanupTif(); + cb(err, data); + }; + if (err) { return callback(err); } - makeThumbnail(path, function (thumbErr, thumbPath) { - if (thumbErr) { - messages.push('Could not generate thumbnail: ' + thumbErr.message); - } - uploadToS3(s3, path, key, metadata, thumbPath, function (err) { - callback(err, { metadata: metadata, messages: messages }); + + generateMetadata(scene, path, key, function (err, metadata) { + if (err) { return callback(err); } + makeThumbnail(path, function (thumbErr, thumbPath) { + if (thumbErr) { + messages.push('Could not generate thumbnail: ' + thumbErr.message); + } + uploadToS3(s3, path, key, metadata, thumbPath, function (err) { + callback(err, { metadata: metadata, messages: messages }); + }); }); }); }); }); - }) - .on('error', callback); + }).on('error', callback); }); } @@ -109,22 +119,14 @@ function uploadToS3 (s3, path, key, metadata, thumbPath, callback) { }); } -function translateJPEG2000 (ext, path, key, callback) { - if (ext !== '.jp2') { - return callback(null, path, key); - } - +function translateImage (ext, path, tifPath, callback) { if (!config.gdalTranslateBin) { throw new Error('GDAL bin path missing.'); } - - log(['debug'], 'Converting JPEG2000 to TIFF.'); - var parsedPath = pathTools.parse(path); - var outPath = pathTools.join(parsedPath.dir, parsedPath.name) + '.tif'; - + log(['debug'], 'Converting image to OAM standard format.'); var args = [ '-of', 'GTiff', - path, outPath, + path, tifPath, '-co', 'TILED=yes', '-co', 'COMPRESS=DEFLATE', '-co', 'PREDICTOR=2', @@ -136,11 +138,8 @@ function translateJPEG2000 (ext, path, key, callback) { cp.execFile(config.gdalTranslateBin, args, function (err, stdout, stderr) { if (err) { return callback(err); } - const parsedKey = pathTools.parse(key); - key = pathTools.join(parsedKey.dir, parsedKey.name) + '.tif'; - path = outPath; - log(['debug'], 'Converted JPEG2000 to TIFF: ', path); - return callback(null, path, key); + log(['debug'], 'Converted image to OAM standard format. Input: ', path, 'Output: ', tifPath); + return callback(null, tifPath); }); } From 569f684906b955fd26bb2ab4ea6441f12354f350 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Tue, 22 Nov 2016 20:40:43 -0500 Subject: [PATCH 104/144] Removes log --- log.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 log.txt diff --git a/log.txt b/log.txt deleted file mode 100644 index d99e0fc..0000000 --- a/log.txt +++ /dev/null @@ -1 +0,0 @@ -Some log From 943bb167dc19809a0096485a21682f7a00a4a882 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 5 Dec 2016 16:26:53 +1300 Subject: [PATCH 105/144] fix api link in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4a4e49..aee8296 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "title": "OAM API", "name": "OpenAerialMap Uploader API", "description": "", - "url": "https://oam-uploader-api.herokuapp.com", + "url": "https://upload-api.openaerialmap.org/", "template": { "withCompare": false }, From df87f047cce92255f95e865110792fd3c6c9fcf7 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Wed, 7 Dec 2016 12:38:42 -0500 Subject: [PATCH 106/144] Add gdal installation to deploy script, add gdal_translate path to default configuration --- .travis.yml | 5 +++++ config.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 73505f0..60d4951 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: - GH_REF=github.com/hotosm/oam-uploader-api.git - PRODUCTION_BRANCH=master - secure: W0eGs5Lg+fZOIEYKLIFJZPsw7qFUNFTAUtPS/2UmlWwlQtzvYCYnNu4dd62kZKivzCuWZiwdwsm58l2r6tB6zRv7/3S4mhkiA1SrYFC3p34CXXV9XaJeEgOQ1SXoJuAW1ThSmq7CJu9dsk3Aix+KA4MggzBgTPG2RXyYhgr3Qzhcil1BX6KNqW4TP7cPaJTUCEhoDcyVLNvmjvUDQNwwJjCu1hifDptlyZt88jDshIVPi4wd2ITy10O22dkwvVaes79A7yYrpRyBJyAhb8w/fPZsCwT+9wQz28x1Q47at/eERrpeAVGOUbXE+5cla/dCCVG3rMlyJnaRP0HagaUQK71GJMYe/nnOEirmF3vtpYzRTmzbvaahEtWr6EbdVWqXB8WJCBShgEF2NICwQAYwgIsYyzIAI3wDvVyrdhbSqqARlUqRwEN4NUFAXfVYkXssYWiTz/s0Eaktg3I/Z0wL2lgBy0hz2JN9BcSCjSdzzj9FWaoWjFSDamb/M5U1R6h9rTGe2bgoDGsZbo4TV/QCXIsKvqjX5lC/W5GFyoUOyTrqtGyPIpOdkTg8Nf1V2ZPuAznln6YYchMdO64eABy7w5tKErM6JP8N4pHFgkgIPJI4gZdAlUELFe+mgqp/kl1GEuSd8TtlIdZ/ErvsmhH5ubYifIdBkpSZfUMRM/vueXI= + before_install: - echo "Installing libvips..." - sudo add-apt-repository -y ppa:lovell/precise-backport-vips @@ -23,6 +24,10 @@ before_install: - sudo apt-get update && sudo apt-get install -y mongodb-org - sudo service mongod restart - echo "Completed installing mongodb" +- echo "Installing gdal-bin" +- sudo add-apt-repository ppa:ubuntugis/ppa && sudo apt-get update +- sudo apt-get install gdal-bin +- echo "Completed installing gdal-bin" - chmod +x ./.build_scripts/docs.sh after_success: - "./.build_scripts/docs.sh" diff --git a/config.js b/config.js index 09e91ec..9cbf78d 100644 --- a/config.js +++ b/config.js @@ -15,7 +15,7 @@ var defaults = { sendgridApiKey: null, // sendgrid API key, for sending notification emails sendgridFrom: 'info@hotosm.org', // the email address from which to send notification emails gdriveKey: null, - gdalTranslateBin: null, + gdalTranslateBin: '/usr/bin/gdal_translate', emailNotification: { subject: '[ OAM Uploader ] Imagery upload submitted', text: 'Your upload has been successfully submitted and is now being ' + From a75e96b1b6107b8f9d5f44f009dce856ab3d3028 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Wed, 7 Dec 2016 13:34:17 -0500 Subject: [PATCH 107/144] Remove addition of Ubuntu PPA from install script It required interactive input that caused the build to hang. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 60d4951..8d62f7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,6 @@ before_install: - sudo service mongod restart - echo "Completed installing mongodb" - echo "Installing gdal-bin" -- sudo add-apt-repository ppa:ubuntugis/ppa && sudo apt-get update - sudo apt-get install gdal-bin - echo "Completed installing gdal-bin" - chmod +x ./.build_scripts/docs.sh From ad9898d8719b5eaf1c2e71cc08322fe37fee209a Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Wed, 7 Dec 2016 13:36:47 -0500 Subject: [PATCH 108/144] Update file size in upload-status test fixture. The addition of tiff conversion with deflate compression results in much smaller file sizes than previously expected. --- test/fixture/upload-status.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixture/upload-status.json b/test/fixture/upload-status.json index ef0599c..dc6634c 100644 --- a/test/fixture/upload-status.json +++ b/test/fixture/upload-status.json @@ -1 +1 @@ -{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name": "Ziggy", "email": "ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","tms": null, "images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png", "tms": null}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} +{"_id":"55e0c86b24c379c000544d24","uploader":{"name":"Lady Stardust","email":"lady@stardust.xyz"},"scenes":[{"contact":{"name":"Ziggy","email":"ziggy@bowie.net"},"title":"Natural Earth Image","provider":"Natural Earth","sensor":"Some Algorithm","platform":"satellite","acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","tms":null,"images":[{"_id":"55e0c86a24c379c000544d23","url":"http://localhost:8080/NE1_50M_SR.tif","status":"finished","messages":[],"startedAt":"2015-08-28T20:45:31.062Z","metadata":{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":400774,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-28/55e0c86b24c379c000544d24/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}},"stoppedAt":"2015-08-28T20:45:31.247Z"}]}],"createdAt":"2015-08-28T20:45:30.820Z"} From 2901be56c425bf475bc4ab06c3a87016ea790741 Mon Sep 17 00:00:00 2001 From: Nick Bumbarger Date: Wed, 7 Dec 2016 13:45:02 -0500 Subject: [PATCH 109/144] Update file size in NE1_50M_SR.output test fixture --- test/fixture/NE1_50M_SR.output.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixture/NE1_50M_SR.output.json b/test/fixture/NE1_50M_SR.output.json index bc815c1..3728d9b 100644 --- a/test/fixture/NE1_50M_SR.output.json +++ b/test/fixture/NE1_50M_SR.output.json @@ -1 +1 @@ -{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":1149210,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png", "tms": null}} +{"uuid":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif","title":"Natural Earth Image","projection":"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"]]","bbox":[128.99999999999997,29.000000000000004,146,54],"footprint":"POLYGON((128.99999999999997 54,146 54,146 29.000000000000004,128.99999999999997 29.000000000000004,128.99999999999997 54))","gsd":0.03333333333333333,"file_size":400774,"acquisition_start":"2015-04-01T00:00:00.000Z","acquisition_end":"2015-04-30T00:00:00.000Z","platform":"satellite","provider":"Natural Earth","contact":"Ziggy,ziggy@bowie.net","properties":{"sensor":"Some Algorithm","thumbnail":"http://oam-uploader.s3.amazonaws.com/uploads/2015-08-18/55d3b052f885a1bb0221434b/scene/0/scene-0-image-0-NE1_50M_SR.tif.thumb.png","tms":null}} From b6c95352607fe61aea8f831c30c1b95504ebee99 Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Tue, 13 Dec 2016 14:16:12 -0500 Subject: [PATCH 110/144] Fix README * Fix language for sourcing environment variables * Add ecosystem docs links and links to other components within the uploader component group --- README.md | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 124bfe9..754360a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,21 @@ -# OAM Uploader API [![Build Status](https://travis-ci.org/hotosm/oam-uploader-api.svg)](https://travis-ci.org/hotosm/oam-uploader-api) - -## [API Docs](http://hotosm.github.io/oam-uploader-api/) +