devserver: support for querying statically staged file size and hashes

This adds a new 'api/fileinfo' URL, which returns a JSON encoded
dictionary containing the size, SHA1 and SHA256 hash values of a given
file under the devserver's static directory.

* Migrated size/hash methods from autoupdate.Autoupdate to common_util.
  This makes more sense as they are applicable to any file and are not
  necessarily AU related. Also renamed and reorganized their code.

* Added a new CherryPy-exposed method implementing the new
  functionality.

BUG=chromium-os:33762
TEST=New method returns desired values; unit tests pass.

Change-Id: Iff7e0af2c8962de4976da7c6deaa9892d5b106a5
Reviewed-on: https://gerrit.chromium.org/gerrit/34426
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
diff --git a/autoupdate.py b/autoupdate.py
index 591fe80..548f794 100644
--- a/autoupdate.py
+++ b/autoupdate.py
@@ -14,6 +14,7 @@
 import cherrypy
 
 from build_util import BuildObject
+import common_util
 import log_util
 
 
@@ -240,16 +241,6 @@
       image_name = 'chromiumos_image.bin'
     return image_name
 
-  def _GetSize(self, update_path):
-    """Returns the size of the file given."""
-    return os.path.getsize(update_path)
-
-  def _GetHash(self, update_path):
-    """Returns the sha1 of the file given."""
-    cmd = ('cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';'
-           % update_path)
-    return os.popen(cmd).read().rstrip()
-
   def _IsDeltaFormatFile(self, filename):
     try:
       file_handle = open(filename, 'r')
@@ -259,29 +250,6 @@
     except Exception:
       return False
 
-  # TODO(petkov): Consider optimizing getting both SHA-1 and SHA-256 so that
-  # it takes advantage of reduced I/O and multiple processors. Something like:
-  # % tee < FILE > /dev/null \
-  #     >( openssl dgst -sha256 -binary | openssl base64 ) \
-  #     >( openssl sha1 -binary | openssl base64 )
-  def _GetSHA256(self, update_path):
-    """Returns the sha256 of the file given."""
-    cmd = ('cat %s | openssl dgst -sha256 -binary | openssl base64' %
-           update_path)
-    return os.popen(cmd).read().rstrip()
-
-  def _GetMd5(self, update_path):
-    """Returns the md5 checksum of the file given."""
-    cmd = ("md5sum %s | awk '{print $1}'" % update_path)
-    return os.popen(cmd).read().rstrip()
-
-  def _Copy(self, source, dest):
-    """Copies a file from dest to source (if different)"""
-    _Log('Copy File %s -> %s' % (source, dest))
-    if os.path.lexists(dest):
-      os.remove(dest)
-    shutil.copy(source, dest)
-
   def GetUpdatePayload(self, hash, sha256, size, url, is_delta_format):
     """Returns a payload to the client corresponding to a new update.
 
@@ -392,29 +360,30 @@
   def FindCachedUpdateImageSubDir(self, src_image, dest_image):
     """Find directory to store a cached update.
 
-    Given one, or two images for an update, this finds which
-    cache directory should hold the update files, even if they don't exist
-    yet. The directory will be inside static_image_dir, and of the form:
+    Given one, or two images for an update, this finds which cache directory
+    should hold the update files, even if they don't exist yet.
 
-    Non-delta updates:
-      CACHE_DIR/12345678
-    Delta updates:
-      CACHE_DIR/12345678_12345678
-
-    If self.private_key -- Signed updates:
-      CACHE_DIR/from_above+12345678
+    Returns:
+      A directory path for storing a cached update, of the following form:
+        Non-delta updates:
+          CACHE_DIR/<dest_hash>
+        Delta updates:
+          CACHE_DIR/<src_hash>_<dest_hash>
+        Signed updates (self.private_key):
+          CACHE_DIR/<src_hash>_<dest_hash>+<private_key_hash>
     """
-    sub_dir = self._GetMd5(dest_image)
+    update_dir = ''
     if src_image:
-      sub_dir = '%s_%s' % (self._GetMd5(src_image), sub_dir)
+      update_dir += common_util.GetFileMd5(src_image) + '_'
 
+    update_dir += common_util.GetFileMd5(dest_image)
     if self.private_key:
-      sub_dir = '%s+%s' % (sub_dir, self._GetMd5(self.private_key))
+      update_dir += '+' + common_util.GetFileMd5(self.private_key)
 
     if not self.vm:
-      sub_dir = '%s+patched_kernel' % sub_dir
+      update_dir += '+patched_kernel'
 
-    return os.path.join(CACHE_DIR, sub_dir)
+    return os.path.join(CACHE_DIR, update_dir)
 
   def GenerateUpdateImage(self, image_path, output_dir):
     """Force generates an update payload based on the given image_path.
@@ -498,8 +467,8 @@
                                     UPDATE_FILE)
       stateful_payload = os.path.join(static_image_dir,
                                       STATEFUL_FILE)
-      self._Copy(cache_update_payload, update_payload)
-      self._Copy(cache_stateful_payload, stateful_payload)
+      common_util.CopyFile(cache_update_payload, update_payload)
+      common_util.CopyFile(cache_stateful_payload, stateful_payload)
       return UPDATE_FILE
     else:
       return self.pregenerated_path
@@ -585,11 +554,11 @@
         suffix = '_image'
         if key.endswith(suffix):
           kind = key[:-len(suffix)]
-          stanza[kind + '_size'] = self._GetSize(os.path.join(
+          stanza[kind + '_size'] = common_util.GetFileSize(os.path.join(
               self.static_dir, stanza[kind + '_image']))
           if validate_checksums:
-            factory_checksum = self._GetHash(os.path.join(self.static_dir,
-                                             stanza[kind + '_image']))
+            factory_checksum = common_util.GetFileSha1(
+                os.path.join(self.static_dir, stanza[kind + '_image']))
             if factory_checksum != stanza[kind + '_checksum']:
               print ('Error: checksum mismatch for %s. Expected "%s" but file '
                      'has checksum "%s".' % (stanza[kind + '_image'],
@@ -646,11 +615,11 @@
 
       # Only copy the files if the source directory is different from dest.
       if os.path.dirname(src_path) != os.path.abspath(static_image_dir):
-        self._Copy(src_path, dest_path)
+        common_util.CopyFile(src_path, dest_path)
 
         # The stateful payload is optional.
         if os.path.exists(src_stateful):
-          self._Copy(src_stateful, dest_stateful)
+          common_util.CopyFile(src_stateful, dest_stateful)
         else:
           _Log('WARN: %s not found. Expected for dev and test builds.' %
                STATEFUL_FILE)
@@ -806,9 +775,9 @@
                                                              static_image_dir)
       if payload_path:
         filename = os.path.join(static_image_dir, payload_path)
-        hash = self._GetHash(filename)
-        sha256 = self._GetSHA256(filename)
-        size = self._GetSize(filename)
+        hash = common_util.GetFileSha1(filename)
+        sha256 = common_util.GetFileSha256(filename)
+        size = common_util.GetFileSize(filename)
         is_delta_format = self._IsDeltaFormatFile(filename)
         if label:
           url = '%s/%s/%s' % (static_urlbase, label, payload_path)