# Copyright 2010 The Go Authors. All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. # This is the server part of the package dashboard. # It must be run by App Engine. from google.appengine.api import memcache from google.appengine.runtime import DeadlineExceededError from google.appengine.ext import db from google.appengine.ext import webapp from google.appengine.ext.webapp import template from google.appengine.ext.webapp.util import run_wsgi_app import binascii import datetime import hashlib import hmac import logging import os import re import struct import time import urllib2 # Storage model for package info recorded on server. # Just path, count, and time of last install. class Package(db.Model): path = db.StringProperty() web_url = db.StringProperty() # derived from path count = db.IntegerProperty() last_install = db.DateTimeProperty() re_bitbucket = re.compile(r'^bitbucket\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+$') re_googlecode = re.compile(r'^[a-z0-9\-]+\.googlecode\.com/(svn|hg)$') re_github = re.compile(r'^github\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+$') MaxPathLength = 100 class PackagePage(webapp.RequestHandler): def get(self): if self.request.get('fmt') == 'json': return self.json() q = Package.all() q.order('-last_install') by_time = q.fetch(100) q = Package.all() q.order('-count') by_count = q.fetch(100) self.response.headers['Content-Type'] = 'text/html; charset=utf-8' path = os.path.join(os.path.dirname(__file__), 'package.html') self.response.out.write(template.render(path, {"by_time": by_time, "by_count": by_count})) def json(self): self.response.set_status(200) self.response.headers['Content-Type'] = 'text/plain; charset=utf-8' q = Package.all() s = '{"packages": [' sep = '' for r in q.fetch(1000): s += '%s\n\t{"path": "%s", "last_install": "%s", "count": "%s"}' % (sep, r.path, r.last_install, r.count) sep = ',' s += '\n]}\n' self.response.out.write(s) def can_get_url(self, url): try: req = urllib2.Request(url) response = urllib2.urlopen(req) return True except: return False def is_valid_package_path(self, path): return (re_bitbucket.match(path) or re_googlecode.match(path) or re_github.match(path)) def record_pkg(self, path): # sanity check string if not path or len(path) > MaxPathLength or not self.is_valid_package_path(path): return False # look in datastore key = 'pkg-' + path p = Package.get_by_key_name(key) if p is None: # not in datastore - verify URL before creating if re_bitbucket.match(path): check_url = 'http://' + path + '/?cmd=heads' web = 'http://' + path + '/' elif re_github.match(path): # github doesn't let you fetch the .git directory anymore. # fetch .git/info/refs instead, like git clone would. check_url = 'http://'+path+'.git/info/refs' web = 'http://' + path elif re_googlecode.match(path): check_url = 'http://'+path web = 'http://code.google.com/p/' + path[:path.index('.')] else: logging.error('unrecognized path: %s', path) return False if not self.can_get_url(check_url): logging.error('cannot get %s', check_url) return False p = Package(key_name = key, path = path, count = 0, web_url = web) # update package object p.count += 1 p.last_install = datetime.datetime.utcnow() p.put() return True def post(self): path = self.request.get('path') ok = self.record_pkg(path) if ok: self.response.set_status(200) self.response.out.write('ok') else: logging.error('invalid path in post: %s', path) self.response.set_status(500) self.response.out.write('not ok') def main(): app = webapp.WSGIApplication([('/package', PackagePage)], debug=True) run_wsgi_app(app) if __name__ == '__main__': main()