blob: 79d530388a702cb144774e84cb458dfe22b0d62f [file] [log] [blame]
#!/usr/bin/env python2.7
# Copyright 2015 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.
"""Simple module to implement a build-local cache."""
import logging
import os
BUILD_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
os.pardir, os.pardir))
class ConfigCacheError(RuntimeError):
"""Parent error type for configcache-related failures."""
class FetcherError(ConfigCacheError):
"""Exception raised if a write operation is performed on a read-only cache."""
class ReadOnlyError(ConfigCacheError):
"""Exception raised if a write operation is performed on a read-only cache."""
class CacheManager(object):
"""Class that manages the pinned configuration cache."""
# The default cache path.
CACHE_PATH = os.path.join(BUILD_PATH, '.config_cache')
def __init__(self, cache_name, cache_dir=None, fetcher=None,
base_logger=None):
"""
The CacheManager uses a configured Fetcher callable to actually perform
cache fetches. The Fetcher is a callable with the following form:
Fetcher Args:
name: (str) The name of the item to fetch.
version: (str) The version of the item to fetch. If None, select a
default version.
Fetcher Returns: (data, version)
data: (str) The data content of the fetched item.
version: (str) The version that was fetched.
Fetcher Raises:
FetcherError: If there was an error fetching the requested artifact.
If no Fetcher is specified, the cache is read-only. It can still retrieve
existing cached values, but will refuse to cache new values.
Args:
cache_name: (str) The cache sub-name.
fetcher: (callable) A Fetcher callable, or None to initialize the cache
as read-only.
base_logger: (logging.Logger) If not None, the logger to use as the
parent for this cache's internal logger.
"""
assert fetcher is None or callable(fetcher)
self.logger = (base_logger or logging.getLogger()).getChild(
"Cache[%s]" % (cache_name,))
self.cache_dir = cache_dir or self.CACHE_PATH
self.fetcher = fetcher
self.cache_name = cache_name
def _ArtifactPath(self, name):
return os.path.join(self.cache_dir, self.cache_name, name)
def _ArtifactVersionPath(self, name):
return '%s.version' % (self._ArtifactPath(name),)
def _ArtifactDataPath(self, name):
return '%s.data' % (self._ArtifactPath(name),)
@property
def read_only(self):
"""Returns: (bool) whether or not the cache is read-only.
A cache is read-only if it has no configured fetcher.
"""
return not callable(self.fetcher)
@staticmethod
def _WriteFile(path, data):
with open(path, 'w') as fd:
fd.write(data)
@staticmethod
def _ReadFile(path):
with open(path) as fd:
return fd.read()
def FetchAndCache(self, name, version=None):
"""Forces a cache artifact to be fetched and cached.
This will operate even if the artifact is currently present in the cache.
Returns: (data, version)
data: (str) The fetched cache artifact.
version: (str) The fetched artifact version.
Raises:
ReadOnlyError: If this cache is read-only.
ValueError: If the fetcher doesn't return the requested version.
"""
if self.read_only:
raise ReadOnlyError("Cache is read-only.")
# Get artifact paths. Create directory if needed.
artifact_dir = os.path.dirname(self._ArtifactPath(name))
if not os.path.isdir(artifact_dir):
os.makedirs(artifact_dir)
# Get the artifact.
data, fetched_version = self.fetcher(name, version)
if version and version != fetched_version:
raise ValueError("Fetched artifact version (%s) differs from "
"requested (%s)" % (fetched_version, version))
version = fetched_version
logging.debug("Fetched artifact '%s' version '%s' (%d bytes)",
name, version, len(data))
# Create the cached artifact.
data_path = self._ArtifactDataPath(name)
version_path = self._ArtifactVersionPath(name)
self._WriteFile(data_path, data)
self._WriteFile(version_path, version)
return data, version
def GetArtifactVersion(self, name):
"""Returns: (str) The artifact version, or None if it doesn't exist.
"""
try:
return self._ReadFile(self._ArtifactVersionPath(name))
except IOError:
# Artifact version information doesn't exist.
return None
def Get(self, name, version=None):
"""Retrieves a currently-cached data result.
Args:
name: (str) The artifact name.
version: (str) The requested artifact version, or None to use the current
version, if it exists.
Returns: (str) The artifact data, or None if it does not exist.
"""
if version:
current_version = self.GetArtifactVersion(name)
if current_version != version:
# No current artifact with the requested version.
return None
try:
return self._ReadFile(self._ArtifactDataPath(name))
except IOError:
# Path does not exist / can't be opened.
pass
return None