1# Copyright 2014 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""File based cache for the discovery document.
16
17The cache is stored in a single file so that multiple processes can
18share the same cache. It locks the file whenever accesing to the
19file. When the cache content is corrupted, it will be initialized with
20an empty cache.
21"""
22
23from __future__ import division
24
25import datetime
26import json
27import logging
28import os
29import tempfile
30import threading
31
32try:
33    from oauth2client.contrib.locked_file import LockedFile
34except ImportError:
35    # oauth2client < 2.0.0
36    try:
37        from oauth2client.locked_file import LockedFile
38    except ImportError:
39        # oauth2client > 4.0.0 or google-auth
40        raise ImportError(
41            "file_cache is unavailable when using oauth2client >= 4.0.0 or google-auth"
42        )
43
44from . import base
45from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
46
47LOGGER = logging.getLogger(__name__)
48
49FILENAME = "google-api-python-client-discovery-doc.cache"
50EPOCH = datetime.datetime.utcfromtimestamp(0)
51
52
53def _to_timestamp(date):
54    try:
55        return (date - EPOCH).total_seconds()
56    except AttributeError:
57        # The following is the equivalent of total_seconds() in Python2.6.
58        # See also: https://docs.python.org/2/library/datetime.html
59        delta = date - EPOCH
60        return (
61            delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6
62        ) / 10 ** 6
63
64
65def _read_or_initialize_cache(f):
66    f.file_handle().seek(0)
67    try:
68        cache = json.load(f.file_handle())
69    except Exception:
70        # This means it opens the file for the first time, or the cache is
71        # corrupted, so initializing the file with an empty dict.
72        cache = {}
73        f.file_handle().truncate(0)
74        f.file_handle().seek(0)
75        json.dump(cache, f.file_handle())
76    return cache
77
78
79class Cache(base.Cache):
80    """A file based cache for the discovery documents."""
81
82    def __init__(self, max_age):
83        """Constructor.
84
85      Args:
86        max_age: Cache expiration in seconds.
87      """
88        self._max_age = max_age
89        self._file = os.path.join(tempfile.gettempdir(), FILENAME)
90        f = LockedFile(self._file, "a+", "r")
91        try:
92            f.open_and_lock()
93            if f.is_locked():
94                _read_or_initialize_cache(f)
95            # If we can not obtain the lock, other process or thread must
96            # have initialized the file.
97        except Exception as e:
98            LOGGER.warning(e, exc_info=True)
99        finally:
100            f.unlock_and_close()
101
102    def get(self, url):
103        f = LockedFile(self._file, "r+", "r")
104        try:
105            f.open_and_lock()
106            if f.is_locked():
107                cache = _read_or_initialize_cache(f)
108                if url in cache:
109                    content, t = cache.get(url, (None, 0))
110                    if _to_timestamp(datetime.datetime.now()) < t + self._max_age:
111                        return content
112                return None
113            else:
114                LOGGER.debug("Could not obtain a lock for the cache file.")
115                return None
116        except Exception as e:
117            LOGGER.warning(e, exc_info=True)
118        finally:
119            f.unlock_and_close()
120
121    def set(self, url, content):
122        f = LockedFile(self._file, "r+", "r")
123        try:
124            f.open_and_lock()
125            if f.is_locked():
126                cache = _read_or_initialize_cache(f)
127                cache[url] = (content, _to_timestamp(datetime.datetime.now()))
128                # Remove stale cache.
129                for k, (_, timestamp) in list(cache.items()):
130                    if (
131                        _to_timestamp(datetime.datetime.now())
132                        >= timestamp + self._max_age
133                    ):
134                        del cache[k]
135                f.file_handle().truncate(0)
136                f.file_handle().seek(0)
137                json.dump(cache, f.file_handle())
138            else:
139                LOGGER.debug("Could not obtain a lock for the cache file.")
140        except Exception as e:
141            LOGGER.warning(e, exc_info=True)
142        finally:
143            f.unlock_and_close()
144
145
146cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
147