#!/usr/bin/python
# Copyright (c) 2012 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Cache accesses to GSDStorage locally.

Operations are provided to read/write whole files and to
read/write strings.
Read from GSDStorage if nothing exists locally.
"""

import os
import re

import file_tools


KEY_PATTERN = re.compile('^[A-Za-z0-9_/.]+$')


def ValidateKey(key):
  if KEY_PATTERN.match(key) is None:
    raise KeyError('Invalid storage key "%s"' % key)

def LocalFileURL(local_file):
  abs_path = os.path.abspath(local_file)
  if not abs_path.startswith('/'):
    # Windows paths needs an extra slash for the file protocol.
    return 'file:///' + abs_path
  else:
    return 'file://' + abs_path

class LocalStorageCache(object):
  """A caching wrapper for reading a GSDStorage object or storing locally.

  Allow reading/writing to key, value pairs in local files.
  Reads fall back to remote storage.
  Restricts keys to a limited regex.
  Is not atomic in the face of concurrent writers / readers on Windows.
  """
  def __init__(self, cache_path, storage):
    """Init for this class.

    Args:
      cache_path: Path to a database to store a local cache in.
      storage: A GSDStorage style object to fallback to for reads.
    """
    self._cache_path = os.path.abspath(cache_path)
    file_tools.MakeDirectoryIfAbsent(self._cache_path)
    self._storage = storage

  def PutFile(self, path, key):
    """Write a file to storage.

    Args:
      path: Path of the file to write.
      key: Key to store file under.
    Returns:
      URL written to.
    """
    return self.PutData(file_tools.ReadFile(path), key)

  def PutData(self, data, key):
    """Write data to storage.

    Args:
      data: Data to store.
      key: Key to store file under.
    Returns:
      URL written to.
    """
    ValidateKey(key)
    cache_file = os.path.join(self._cache_path, key)
    cache_dir = os.path.dirname(cache_file)
    if not os.path.exists(cache_dir):
      os.makedirs(cache_dir)
    file_tools.AtomicWriteFile(data, cache_file)
    return LocalFileURL(cache_file)

  def GetFile(self, key, path):
    """Read a file from storage.

    Args:
      key: Key to store file under.
      path: Destination filename.
    Returns:
      URL used on success or None for failure.
    """
    ValidateKey(key)
    cache_file = os.path.join(self._cache_path, key)
    if os.path.exists(cache_file):
      data = file_tools.ReadFile(cache_file)
      file_tools.WriteFile(data, path)
      return LocalFileURL(cache_file)
    else:
      return self._storage.GetFile(key, path)

  def GetData(self, key):
    """Read data from global storage.

    Args:
      key: Key to store file under.
    Returns:
      Data from storage, or None for failure.
    """
    ValidateKey(key)
    cache_file = os.path.join(self._cache_path, key)
    if os.path.exists(cache_file):
      return file_tools.ReadFile(cache_file)
    else:
      return self._storage.GetData(key)
