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/common_util.py b/common_util.py
index e740d63..2f3e10c 100644
--- a/common_util.py
+++ b/common_util.py
@@ -4,8 +4,11 @@
 
 """Helper class for interacting with the Dev Server."""
 
+import base64
+import binascii
 import distutils.version
 import errno
+import hashlib
 import os
 import random
 import re
@@ -31,6 +34,8 @@
 UPLOADED_LIST = 'UPLOADED'
 DEVSERVER_LOCK_FILE = 'devserver'
 
+_HASH_BLOCK_SIZE = 8192
+
 
 def CommaSeparatedList(value_list, is_quoted=False):
   """Concatenates a list of strings.
@@ -403,7 +408,7 @@
   """
   build_dir = os.path.join(static_dir, tag)
   if not SafeSandboxAccess(static_dir, build_dir):
-    raise CommonUtilError('Invaid tag "%s".' % tag)
+    raise CommonUtilError('Invalid tag "%s".' % tag)
 
   lock = lockfile.FileLock(os.path.join(build_dir, DEVSERVER_LOCK_FILE))
   try:
@@ -542,7 +547,7 @@
   control_path = os.path.join(static_dir, build, 'autotest',
                               control_path)
   if not SafeSandboxAccess(static_dir, control_path):
-    raise CommonUtilError('Invaid control file "%s".' % control_path)
+    raise CommonUtilError('Invalid control file "%s".' % control_path)
 
   if not os.path.exists(control_path):
     # TODO(scottz): Come up with some sort of error mechanism.
@@ -598,3 +603,69 @@
     List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
   """
   return os.listdir(os.path.join(static_dir, board, build, AU_BASE))
+
+
+def GetFileSize(file_path):
+  """Returns the size in bytes of the file given."""
+  return os.path.getsize(file_path)
+
+
+def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
+  """Computes and returns a list of requested hashes.
+
+  Args:
+    file_path: path to file to be hashed
+    do_sha1:   whether or not to compute a SHA1 hash
+    do_sha256: whether or not to compute a SHA256 hash
+    do_md5:    whether or not to compute a MD5 hash
+  Returns:
+    A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
+    'md5', respectively.
+  """
+  hashes = {}
+  if (do_sha1 or do_sha256 or do_md5):
+    # Initialize hashers.
+    hasher_sha1 = hashlib.sha1() if do_sha1 else None
+    hasher_sha256 = hashlib.sha256() if do_sha256 else None
+    hasher_md5 = hashlib.md5() if do_md5 else None
+
+    # Read blocks from file, update hashes.
+    with open(file_path, 'rb') as fd:
+      while True:
+        block = fd.read(_HASH_BLOCK_SIZE)
+        if not block:
+          break
+        hasher_sha1 and hasher_sha1.update(block)
+        hasher_sha256 and hasher_sha256.update(block)
+        hasher_md5 and hasher_md5.update(block)
+
+    # Update return values.
+    if hasher_sha1:
+      hashes['sha1'] = hasher_sha1.digest()
+    if hasher_sha256:
+      hashes['sha256'] = hasher_sha256.digest()
+    if hasher_md5:
+      hashes['md5'] = hasher_md5.digest()
+
+  return hashes
+
+
+def GetFileSha1(file_path):
+  """Returns the SHA1 checksum of the file given (base64 encoded)."""
+  return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
+
+
+def GetFileSha256(file_path):
+  """Returns the SHA256 checksum of the file given (base64 encoded)."""
+  return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
+
+
+def GetFileMd5(file_path):
+  """Returns the MD5 checksum of the file given (hex encoded)."""
+  return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
+
+
+def CopyFile(source, dest):
+  """Copies a file from |source| to |dest|."""
+  _Log('Copy File %s -> %s' % (source, dest))
+  shutil.copy(source, dest)