diff options
Diffstat (limited to 'cwp/bartlett')
-rw-r--r-- | cwp/bartlett/app.yaml | 22 | ||||
-rwxr-xr-x | cwp/bartlett/server.py | 152 | ||||
-rw-r--r-- | cwp/bartlett/static/favicon.ico | bin | 0 -> 198 bytes | |||
-rw-r--r-- | cwp/bartlett/test/server_tester.py | 102 | ||||
-rwxr-xr-x | cwp/bartlett/update_appengine_server | 1 |
5 files changed, 277 insertions, 0 deletions
diff --git a/cwp/bartlett/app.yaml b/cwp/bartlett/app.yaml new file mode 100644 index 00000000..60010f70 --- /dev/null +++ b/cwp/bartlett/app.yaml @@ -0,0 +1,22 @@ +application: chromeoswideprofiling +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /favicon.ico + static_files: static/favicon.ico + upload: static/favicon.ico +- url: /remote_api + script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py + login: admin +- url: / + script: server.py +- url: /upload + script: server.py +- url: /serve + script: server.py +- url: /serve/.* + script: server.py +- url: /del/.* + script: server.py diff --git a/cwp/bartlett/server.py b/cwp/bartlett/server.py new file mode 100755 index 00000000..3e4f1be7 --- /dev/null +++ b/cwp/bartlett/server.py @@ -0,0 +1,152 @@ +#!/usr/bin/python2.6 +# Copyright 2012 Google Inc. All Rights Reserved. +# Author: mrdmnd@ (Matt Redmond) +# Based off of code in //depot/google3/experimental/mobile_gwp +"""Code to transport profile data between a user's machine and the CWP servers. + Pages: + "/": the main page for the app, left blank so that users cannot access + the file upload but left in the code for debugging purposes + "/upload": Updates the datastore with a new file. the upload depends on + the format which is templated on the main page ("/") + input includes: + profile_data: the zipped file containing profile data + board: the architecture we ran on + chromeos_version: the chromeos_version + "/serve": Lists all of the files in the datastore. Each line is a new entry + in the datastore. The format is key~date, where key is the entry's + key in the datastore and date is the file upload time and date. + (Authentication Required) + "/serve/([^/]+)?": For downloading a file of profile data, ([^/]+)? means + any character sequence so to download the file go to + '/serve/$key' where $key is the datastore key of the file + you want to download. + (Authentication Required) + "/del/([^/]+)?": For deleting an entry in the datastore. To use go to + '/del/$key' where $key is the datastore key of the entry + you want to be deleted form the datastore. + (Authentication Required) + TODO: Add more extensive logging""" + +import cgi +import logging +import md5 +import urllib + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp.util import run_wsgi_app + +logging.getLogger().setLevel(logging.DEBUG) + + +class FileEntry(db.Model): + profile_data = db.BlobProperty() # The profile data + date = db.DateTimeProperty(auto_now_add=True) # Date it was uploaded + data_md5 = db.ByteStringProperty() # md5 of the profile data + board = db.StringProperty() # board arch + chromeos_version = db.StringProperty() # ChromeOS version + + +class MainPage(webapp.RequestHandler): + """Main page only used as the form template, not actually displayed.""" + + def get(self, response=""): # pylint: disable-msg=C6409 + if response: + self.response.out.write("<html><body>") + self.response.out.write("""<br> + <form action="/upload" enctype="multipart/form-data" method="post"> + <div><label>Profile Data:</label></div> + <div><input type="file" name="profile_data"/></div> + <div><label>Board</label></div> + <div><input type="text" name="board"/></div> + <div><label>ChromeOS Version</label></div> + <div><input type="text" name="chromeos_version"></div> + <div><input type="submit" value="send" name="submit"></div> + </form> + </body> + </html>""") + + +class Upload(webapp.RequestHandler): + """Handler for uploading data to the datastore, accessible by anyone.""" + + def post(self): # pylint: disable-msg=C6409 + """Takes input based on the main page's form.""" + getfile = FileEntry() + f1 = self.request.get("profile_data") + getfile.profile_data = db.Blob(f1) + getfile.data_md5 = md5.new(f1).hexdigest() + getfile.board = self.request.get("board") + getfile.chromeos_version = self.request.get("chromeos_version") + getfile.put() + self.response.out.write(getfile.key()) + #self.redirect('/') + + +class ServeHandler(webapp.RequestHandler): + """Given the entry's key in the database, output the profile data file. Only + accessible from @google.com accounts.""" + + def get(self, resource): # pylint: disable-msg=C6409 + if Authenticate(self): + file_key = str(urllib.unquote(resource)) + request = db.get(file_key) + self.response.out.write(request.profile_data) + + +class ListAll(webapp.RequestHandler): + """Displays all files uploaded. Only accessible by @google.com accounts.""" + + def get(self): # pylint: disable-msg=C6409 + """Displays all information in FileEntry, ~ delimited.""" + if Authenticate(self): + query_str = "SELECT * FROM FileEntry ORDER BY date ASC" + query = db.GqlQuery(query_str) + delimiter = "~" + + for item in query: + display_list = [item.key(), item.date, item.data_md5, + item.board, item.chromeos_version] + str_list = [cgi.escape(str(i)) for i in display_list] + self.response.out.write(delimiter.join(str_list)+"</br>") + + +class DelEntries(webapp.RequestHandler): + """Deletes entries. Only accessible from @google.com accounts.""" + + def get(self, resource): # pylint: disable-msg=C6409 + """A specific entry is deleted, when the key is given.""" + if Authenticate(self): + fkey = str(urllib.unquote(resource)) + request = db.get(fkey) + if request: + db.delete(fkey) + + +def Authenticate(webpage): + """Some urls are only accessible if logged in with a @google.com account.""" + user = users.get_current_user() + if user is None: + webpage.redirect(users.create_login_url(webpage.request.uri)) + elif user.email().endswith("@google.com"): + return True + else: + webpage.response.out.write("Not Authenticated") + return False + + +def main(): + application = webapp.WSGIApplication([ + ("/", MainPage), + ("/upload", Upload), + ("/serve/([^/]+)?", ServeHandler), + ("/serve", ListAll), + ("/del/([^/]+)?", DelEntries), + ], debug=False) + run_wsgi_app(application) + + +if __name__ == "__main__": + main() + diff --git a/cwp/bartlett/static/favicon.ico b/cwp/bartlett/static/favicon.ico Binary files differnew file mode 100644 index 00000000..19b58c2e --- /dev/null +++ b/cwp/bartlett/static/favicon.ico diff --git a/cwp/bartlett/test/server_tester.py b/cwp/bartlett/test/server_tester.py new file mode 100644 index 00000000..e5a2341d --- /dev/null +++ b/cwp/bartlett/test/server_tester.py @@ -0,0 +1,102 @@ +#!/usr/bin/python2.6 +# Copyright 2012 Google Inc. All Rights Reserved. +# Author: mrdmnd@ (Matt Redmond) +"""A unit test for sending data to Bartlett. Requires poster module.""" + +import cookielib +import os +import signal +import subprocess +import tempfile +import time +import unittest +import urllib2 + +from poster.encode import multipart_encode +from poster.streaminghttp import register_openers + + +SERVER_DIR = "../." +SERVER_URL = "http://localhost:8080/" +GET = "_ah/login?email=googler@google.com&action=Login&continue=%s" +AUTH_URL = SERVER_URL + GET + + +class ServerTest(unittest.TestCase): + """A unit test for the bartlett server. Tests upload, serve, and delete.""" + + def setUp(self): + """Instantiate the files and server needed to test upload functionality.""" + self._server_proc = LaunchLocalServer() + self._jar = cookielib.LWPCookieJar() + self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._jar)) + + # We need these files to not delete when closed, because we have to reopen + # them in read mode after we write and close them. + self.profile_data = tempfile.NamedTemporaryFile(delete=False) + + size = 16 * 1024 + self.profile_data.write(os.urandom(size)) + + def tearDown(self): + self.profile_data.close() + os.remove(self.profile_data.name) + os.kill(self._server_proc.pid, signal.SIGINT) + + def testIntegration(self): # pylint: disable-msg=C6409 + key = self._testUpload() + self._testListAll() + self._testServeKey(key) + self._testDelKey(key) + + def _testUpload(self): # pylint: disable-msg=C6409 + register_openers() + data = {"profile_data": self.profile_data, + "board": "x86-zgb", + "chromeos_version": "2409.0.2012_06_08_1114"} + datagen, headers = multipart_encode(data) + request = urllib2.Request(SERVER_URL + "upload", datagen, headers) + response = urllib2.urlopen(request).read() + self.assertTrue(response) + return response + + def _testListAll(self): # pylint: disable-msg=C6409 + request = urllib2.Request(AUTH_URL % (SERVER_URL + "serve")) + response = self.opener.open(request).read() + self.assertTrue(response) + + def _testServeKey(self, key): # pylint: disable-msg=C6409 + request = urllib2.Request(AUTH_URL % (SERVER_URL + "serve/" + key)) + response = self.opener.open(request).read() + self.assertTrue(response) + + def _testDelKey(self, key): # pylint: disable-msg=C6409 + # There is no response to a delete request. + # We will check the listAll page to ensure there is no data. + request = urllib2.Request(AUTH_URL % (SERVER_URL + "del/" + key)) + response = self.opener.open(request).read() + request = urllib2.Request(AUTH_URL % (SERVER_URL + "serve")) + response = self.opener.open(request).read() + self.assertFalse(response) + + +def LaunchLocalServer(): + """Launch and store an authentication cookie with a local server.""" + proc = subprocess.Popen(["dev_appserver.py", "--clear_datastore", SERVER_DIR], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Wait for server to come up + while True: + time.sleep(1) + try: + request = urllib2.Request(SERVER_URL + "serve") + response = urllib2.urlopen(request).read() + if response: + break + except urllib2.URLError: + continue + return proc + + +if __name__ == "__main__": + unittest.main() + diff --git a/cwp/bartlett/update_appengine_server b/cwp/bartlett/update_appengine_server new file mode 100755 index 00000000..f3812057 --- /dev/null +++ b/cwp/bartlett/update_appengine_server @@ -0,0 +1 @@ +appcfg.py --oauth2 update . |