blob: d3efe11efcf426e6a096b8a6e1a67dcb8b17c296 [file]
#!/usr/bin/env python
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility for creating a write-through and a read-through checksum cache."""
__author__ = 'Ravi Mistry'
import hashlib
import json
import os
import posixpath
# Add required paths to PYTHONPATH if running as a script.
if '__main__' == __name__:
import sys
sys.path.append(
os.path.join(os.pardir, os.pardir, os.pardir, 'third_party',
'chromium_buildbot', 'scripts'))
sys.path.append(
os.path.join(os.pardir, os.pardir, os.pardir, 'third_party',
'chromium_buildbot', 'site_config'))
import gs_utils
HASHING_ALGORITHM = 'bytewise-md5'
def GetFileMD5(f, block_size=1**20):
"""Creates MD5 hash of the specified file in a memory efficient manner.
Breaks the file into 1MB chunks (or a different specified size) because
hashlib.md5 tries to fit the whole file in memory.
"""
md5 = hashlib.md5()
while True:
data = f.read(block_size)
if not data:
break
md5.update(data)
return md5.hexdigest()
def _GetFilenamesToHashes(local_root):
"""Populates and returns a map of file names to their hash values.
Each entry in the map will look like this:
"amazon.skp" : {
"bytewise-md5" : "a01234",
},
Params:
local_root: str, the directory that contains the files we have to hash.
"""
local_files_to_hash = {}
for local_file in os.listdir(local_root):
if not os.path.isfile(os.path.join(local_root, local_file)):
# Ignore directories.
continue
local_file_path = os.path.join(local_root, local_file)
file_checksum = GetFileMD5(open(local_file_path))
local_files_to_hash[local_file] = {HASHING_ALGORITHM: file_checksum}
return local_files_to_hash
def _GetRemoteFilesLookup(remote_root):
"""Populates and returns a map of remote files for fast lookup.
Each entry in the map will look like this:
"amazon.skp_a01234.skp" : 1,
The value for every dictionary entry will be 1. This value is meaningless
and is not actually used. The keys are important here and are used to test for
file membership in the remote directory.
Params:
remote_root: str, the Google Storage root directory. The root directory
contains a HASHING_ALGORITHM directory that contains the remote files.
"""
# Get list of all remote files in Google Storage.
try:
remote_files = gs_utils.list_storage_directory(
dest_gsbase=remote_root, subdir=HASHING_ALGORITHM)
except Exception:
# The directory does not exist yet.
remote_files = []
# Create a map from the remote files list for quicker lookup.
remote_files_lookup = {}
for remote_file in remote_files:
# 'gsutil ls' returns the complete file path (not just the file names like
# os.listdir. Put only the basenames in the map.
remote_files_lookup[os.path.basename(remote_file)] = 1
return remote_files_lookup
def WriteThroughCache(local_root, remote_root, output_json_path,
gs_acl='private'):
"""Writes local files to Google Storage if they do not exist there.
The algorithm used by this function is:
* Populate a dictionary of local files and their hash values. Each entry in
the dictionary will look like this:
"amazon.skp" : {
"bytewise-md5" : "a01234",
},
* Get the list of files from the HASHING_ALGORITHM directory in remote_root.
* If a local file and its hash value does not exist in Google Storage's
remote_root/HASHING_ALGORITHM directory then copy the file over. The
remote filename will be: filename.extention_hash.extension
* Output the dictionary of local files and their hash values to the specified
output_json_path using json.dump.
Params:
local_root: str, directory containing local files.
remote_root: str, the Google Storage root directory. The root directory
contains a HASHING_ALGORITHM directory that contains the remote files.
output_json_path: str, complete file path of the JSON file this function
will create with mappings of file names to their hash values.
gs_acl: str, canned ACL to use when uploading to Google Storage. Default
value is 'private'.
"""
if not os.path.isdir(local_root):
raise ValueError('%s does not exist!' % local_root)
# Create a map of local files to their hashes.
local_files_to_hash = _GetFilenamesToHashes(local_root)
if not local_files_to_hash:
print '%s has no files!' % local_root
return
# Create a map of remote files for quicker lookup.
remote_files_lookup = _GetRemoteFilesLookup(remote_root)
# Loop through all local files and check against the remote files lookup map
# to see if they should be uploaded.
for filename, hash_dict in local_files_to_hash.iteritems():
# Construct the remote file name we will check the existence of.
file_extension = filename.split('.')[-1]
remote_filename = '%s_%s.%s' % (filename, hash_dict[HASHING_ALGORITHM],
file_extension)
if not remote_files_lookup.get(remote_filename):
gs_utils.copy_storage_directory(
src_dir=os.path.join(local_root, filename),
dest_dir=posixpath.join(remote_root, HASHING_ALGORITHM,
remote_filename),
gs_acl=gs_acl)
# Output a mapping of all local files to their hashes into a JSON file.
json.dump(local_files_to_hash, open(output_json_path, 'w'), indent=4,
sort_keys=True)
def ReadThroughCache(local_root, remote_root, input_json_path):
"""Downloads files from Google Storage if they do not exist locally.
The algorithm used by this function is:
* Create the local_root directory if it does not already exist.
* Get a dictionary listing all the desired files to download, and the hash
digest of each file indicating its latest contents, from input_json_path.
Each entry in the dictionary will look like this:
"amazon.skp" : {
"bytewise-md5" : "a01234",
},
* Get the dictionary of file names to their hash values from the specified
JSON doc. The dictionary will look like the one above.
* Delete all local files which are not in the JSON dictionary.
* Download all files which do not exist locally or which have different
hashes.
Params:
local_root: str, directory containing local files.
remote_root: str, the Google Storage root directory. The root directory
contains a HASHING_ALGORITHM directory that contains the remote files.
input_json_path: str, complete file path of the JSON file this function
will read with mappings of file names to their hash values.
"""
if not os.path.isdir(local_root):
# Create the local_root directory.
os.makedirs(local_root)
# Create a map of local files to their hashes.
local_files_to_hash = _GetFilenamesToHashes(local_root)
# Get the dict of filename to hashes from the JSON file.
json_dict = json.load(open(input_json_path))
# Delete any files in local_root that are not in the json_dict.
extra_files = set(local_files_to_hash) - set(json_dict)
if extra_files:
print 'Deleting all these extra files: %s' % extra_files
for extra_file in extra_files:
os.remove(os.path.join(local_root, extra_file))
# Loop through files and hashes in the specified JSON file.
for filename, hash_dict in json_dict.iteritems():
file_exists_locally = local_files_to_hash.get(filename) != None
files_are_equal = hash_dict == local_files_to_hash.get(filename)
if file_exists_locally and files_are_equal:
# The local and remote files are the same, no need to download.
pass
else:
# Download the missing file from Google Storage.
file_extension = filename.split('.')[-1]
remote_filename = '%s_%s.%s' % (filename, hash_dict[HASHING_ALGORITHM],
file_extension)
gs_utils.copy_storage_directory(
src_dir=posixpath.join(remote_root, HASHING_ALGORITHM,
remote_filename),
dest_dir=os.path.join(local_root, filename))
if '__main__' == __name__:
WriteThroughCache(
local_root='/tmp/local_root',
remote_root='gs://chromium-skia-gm/test/checksum-test',
output_json_path='/tmp/json_root/test.json')
ReadThroughCache(
local_root='/tmp/local_root2',
remote_root='gs://chromium-skia-gm/test/checksum-test',
input_json_path='/tmp/json_root/test.json')