diff options
Diffstat (limited to 'morph-cache-server')
-rwxr-xr-x | morph-cache-server | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/morph-cache-server b/morph-cache-server new file mode 100755 index 00000000..a3c3c978 --- /dev/null +++ b/morph-cache-server @@ -0,0 +1,349 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013, 2014 Codethink Limited +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +import base64 +import cliapp +import json +import logging +import os +import urllib +import urllib2 +import shutil + +from bottle import Bottle, request, response, run, static_file +from flup.server.fcgi import WSGIServer +from morphcacheserver.repocache import RepoCache + + +defaults = { + 'repo-dir': '/var/cache/morph-cache-server/gits', + 'bundle-dir': '/var/cache/morph-cache-server/bundles', + 'artifact-dir': '/var/cache/morph-cache-server/artifacts', + 'port': 8080, +} + + +class MorphCacheServer(cliapp.Application): + + def add_settings(self): + self.settings.integer(['port'], + 'port to listen on', + metavar='PORTNUM', + default=defaults['port']) + self.settings.string(['repo-dir'], + 'path to the repository cache directory', + metavar='PATH', + default=defaults['repo-dir']) + self.settings.string(['bundle-dir'], + 'path to the bundle cache directory', + metavar='PATH', + default=defaults['bundle-dir']) + self.settings.string(['artifact-dir'], + 'path to the artifact cache directory', + metavar='PATH', + default=defaults['artifact-dir']) + self.settings.boolean(['direct-mode'], + 'cache directories are directly managed') + self.settings.boolean(['enable-writes'], + 'enable the write methods (fetch and delete)') + self.settings.boolean(['fcgi-server'], + 'runs a fcgi-server', + default=True) + + + def _fetch_artifact(self, url, filename): + in_fh = None + try: + in_fh = urllib2.urlopen(url) + with open(filename, "w") as localtmp: + shutil.copyfileobj(in_fh, localtmp) + in_fh.close() + except Exception, e: + if in_fh is not None: + in_fh.close() + raise + else: + if in_fh is not None: + in_fh.close() + return os.stat(filename) + + def _fetch_artifacts(self, server, cacheid, artifacts): + ret = {} + try: + for artifact in artifacts: + artifact_name = "%s.%s" % (cacheid, artifact) + tmpname = os.path.join(self.settings['artifact-dir'], + ".dl.%s" % artifact_name) + url = "http://%s/1.0/artifacts?filename=%s" % ( + server, urllib.quote(artifact_name)) + stinfo = self._fetch_artifact(url, tmpname) + ret[artifact_name] = { + "size": stinfo.st_size, + "used": stinfo.st_blocks * 512, + } + except Exception, e: + for artifact in ret.iterkeys(): + os.unlink(os.path.join(self.settings['artifact-dir'], + ".dl.%s" % artifact)) + raise + + for artifact in ret.iterkeys(): + tmpname = os.path.join(self.settings['artifact-dir'], + ".dl.%s" % artifact) + artifilename = os.path.join(self.settings['artifact-dir'], + artifact) + os.rename(tmpname, artifilename) + + return ret + + + def process_args(self, args): + app = Bottle() + + repo_cache = RepoCache(self, + self.settings['repo-dir'], + self.settings['bundle-dir'], + self.settings['direct-mode']) + + def writable(prefix): + """Selectively enable bottle prefixes. + + prefix -- The path prefix we are enabling + + If the runtime configuration setting --enable-writes is provided + then we return the app.get() decorator for the given path prefix + otherwise we return a lambda which passes the function through + undecorated. + + This has the effect of being a runtime-enablable @app.get(...) + + """ + if self.settings['enable-writes']: + return app.get(prefix) + return lambda fn: fn + + @writable('/list') + def list(): + response.set_header('Cache-Control', 'no-cache') + results = {} + files = {} + results["files"] = files + for artifactdir, __, filenames in \ + os.walk(self.settings['artifact-dir']): + fsstinfo = os.statvfs(artifactdir) + results["freespace"] = fsstinfo.f_bsize * fsstinfo.f_bavail + for fname in filenames: + if not fname.startswith(".dl."): + try: + stinfo = os.stat("%s/%s" % (artifactdir, fname)) + files[fname] = { + "atime": stinfo.st_atime, + "size": stinfo.st_size, + "used": stinfo.st_blocks * 512, + } + except Exception, e: + print(e) + return results + + @writable('/fetch') + def fetch(): + host = self._unescape_parameter(request.query.host) + cacheid = self._unescape_parameter(request.query.cacheid) + artifacts = self._unescape_parameter(request.query.artifacts) + try: + response.set_header('Cache-Control', 'no-cache') + artifacts = artifacts.split(",") + return self._fetch_artifacts(host, cacheid, artifacts) + + except Exception, e: + response.status = 500 + logging.debug('%s' % e) + + @writable('/delete') + def delete(): + artifact = self._unescape_parameter(request.query.artifact) + try: + os.unlink('%s/%s' % (self.settings['artifact-dir'], + artifact)) + return { "status": 0, "reason": "success" } + except OSError, ose: + return { "status": ose.errno, "reason": ose.strerror } + except Exception, e: + response.status = 500 + logging.debug('%s' % e) + + @app.get('/sha1s') + def sha1(): + repo = self._unescape_parameter(request.query.repo) + ref = self._unescape_parameter(request.query.ref) + try: + response.set_header('Cache-Control', 'no-cache') + sha1, tree = repo_cache.resolve_ref(repo, ref) + return { + 'repo': '%s' % repo, + 'ref': '%s' % ref, + 'sha1': '%s' % sha1, + 'tree': '%s' % tree + } + except Exception, e: + response.status = 404 + logging.debug('%s' % e) + + @app.post('/sha1s') + def sha1s(): + result = [] + for pair in request.json: + repo = pair['repo'] + ref = pair['ref'] + try: + sha1, tree = repo_cache.resolve_ref(repo, ref) + result.append({ + 'repo': '%s' % repo, + 'ref': '%s' % ref, + 'sha1': '%s' % sha1, + 'tree': '%s' % tree + }) + except Exception, e: + logging.debug('%s' % e) + result.append({ + 'repo': '%s' % repo, + 'ref': '%s' % ref, + 'error': '%s' % e + }) + response.set_header('Cache-Control', 'no-cache') + response.set_header('Content-Type', 'application/json') + return json.dumps(result) + + @app.get('/files') + def file(): + repo = self._unescape_parameter(request.query.repo) + ref = self._unescape_parameter(request.query.ref) + filename = self._unescape_parameter(request.query.filename) + try: + content = repo_cache.cat_file(repo, ref, filename) + response.set_header('Content-Type', 'application/octet-stream') + return content + except Exception, e: + response.status = 404 + logging.debug('%s' % e) + + @app.post('/files') + def files(): + result = [] + for pair in request.json: + repo = pair['repo'] + ref = pair['ref'] + filename = pair['filename'] + try: + content = repo_cache.cat_file(repo, ref, filename) + result.append({ + 'repo': '%s' % repo, + 'ref': '%s' % ref, + 'filename': '%s' % filename, + 'data': '%s' % base64.b64encode(content), + }) + except Exception, e: + logging.debug('%s' % e) + result.append({ + 'repo': '%s' % repo, + 'ref': '%s' % ref, + 'filename': '%s' % filename, + 'error': '%s' % e + }) + response.set_header('Content-Type', 'application/json') + return json.dumps(result) + + @app.get('/trees') + def tree(): + repo = self._unescape_parameter(request.query.repo) + ref = self._unescape_parameter(request.query.ref) + path = self._unescape_parameter(request.query.path) + try: + tree = repo_cache.ls_tree(repo, ref, path) + return { + 'repo': '%s' % repo, + 'ref': '%s' % ref, + 'tree': tree, + } + except Exception, e: + response.status = 404 + logging.debug('%s' % e) + + @app.get('/bundles') + def bundle(): + repo = self._unescape_parameter(request.query.repo) + filename = repo_cache.get_bundle_filename(repo) + dirname = os.path.dirname(filename) + basename = os.path.basename(filename) + return static_file(basename, root=dirname, download=True) + + @app.get('/artifacts') + def artifact(): + basename = self._unescape_parameter(request.query.filename) + filename = os.path.join(self.settings['artifact-dir'], basename) + if os.path.exists(filename): + return static_file(basename, + root=self.settings['artifact-dir'], + download=True) + else: + response.status = 404 + logging.debug('artifact %s does not exist' % basename) + + @app.post('/artifacts') + def post_artifacts(): + if request.content_type != 'application/json': + logging.warning('Content-type is not json: ' + 'expecting a json post request') + + artifacts = json.load(request.body) + results = {} + + logging.debug('Received a POST request for /artifacts') + + for artifact in artifacts: + if artifact.startswith('/'): + response.status = 500 + logging.error("%s: artifact name cannot start with a '/'" + % artifact) + return + + filename = os.path.join(self.settings['artifact-dir'], artifact) + results[artifact] = os.path.exists(filename) + + if results[artifact]: + logging.debug('%s is in the cache', artifact) + else: + logging.debug('%s is NOT in the cache', artifact) + + return results + + root = Bottle() + root.mount(app, '/1.0') + + + if self.settings['fcgi-server']: + WSGIServer(root).run() + else: + run(root, host='0.0.0.0', port=self.settings['port'], reloader=True) + + def _unescape_parameter(self, param): + return urllib.unquote(param) + + +if __name__ == '__main__': + MorphCacheServer().run() |