1<?php
2/*
3 * Copyright 2008 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18if (!class_exists('Google_Client')) {
19  require_once dirname(__FILE__) . '/../autoload.php';
20}
21
22/*
23 * This class implements a basic on disk storage. While that does
24 * work quite well it's not the most elegant and scalable solution.
25 * It will also get you into a heap of trouble when you try to run
26 * this in a clustered environment.
27 *
28 * @author Chris Chabot <chabotc@google.com>
29 */
30class Google_Cache_File extends Google_Cache_Abstract
31{
32  const MAX_LOCK_RETRIES = 10;
33  private $path;
34  private $fh;
35
36  /**
37   * @var Google_Client the current client
38   */
39  private $client;
40
41  public function __construct(Google_Client $client)
42  {
43    $this->client = $client;
44    $this->path = $this->client->getClassConfig($this, 'directory');
45  }
46
47  public function get($key, $expiration = false)
48  {
49    $storageFile = $this->getCacheFile($key);
50    $data = false;
51
52    if (!file_exists($storageFile)) {
53      $this->client->getLogger()->debug(
54          'File cache miss',
55          array('key' => $key, 'file' => $storageFile)
56      );
57      return false;
58    }
59
60    if ($expiration) {
61      $mtime = filemtime($storageFile);
62      if ((time() - $mtime) >= $expiration) {
63        $this->client->getLogger()->debug(
64            'File cache miss (expired)',
65            array('key' => $key, 'file' => $storageFile)
66        );
67        $this->delete($key);
68        return false;
69      }
70    }
71
72    if ($this->acquireReadLock($storageFile)) {
73      if (filesize($storageFile) > 0) {
74        $data = fread($this->fh, filesize($storageFile));
75        $data =  unserialize($data);
76      } else {
77        $this->client->getLogger()->debug(
78            'Cache file was empty',
79            array('file' => $storageFile)
80        );
81      }
82      $this->unlock($storageFile);
83    }
84
85    $this->client->getLogger()->debug(
86        'File cache hit',
87        array('key' => $key, 'file' => $storageFile, 'var' => $data)
88    );
89
90    return $data;
91  }
92
93  public function set($key, $value)
94  {
95    $storageFile = $this->getWriteableCacheFile($key);
96    if ($this->acquireWriteLock($storageFile)) {
97      // We serialize the whole request object, since we don't only want the
98      // responseContent but also the postBody used, headers, size, etc.
99      $data = serialize($value);
100      $result = fwrite($this->fh, $data);
101      $this->unlock($storageFile);
102
103      $this->client->getLogger()->debug(
104          'File cache set',
105          array('key' => $key, 'file' => $storageFile, 'var' => $value)
106      );
107    } else {
108      $this->client->getLogger()->notice(
109          'File cache set failed',
110          array('key' => $key, 'file' => $storageFile)
111      );
112    }
113  }
114
115  public function delete($key)
116  {
117    $file = $this->getCacheFile($key);
118    if (file_exists($file) && !unlink($file)) {
119      $this->client->getLogger()->error(
120          'File cache delete failed',
121          array('key' => $key, 'file' => $file)
122      );
123      throw new Google_Cache_Exception("Cache file could not be deleted");
124    }
125
126    $this->client->getLogger()->debug(
127        'File cache delete',
128        array('key' => $key, 'file' => $file)
129    );
130  }
131
132  private function getWriteableCacheFile($file)
133  {
134    return $this->getCacheFile($file, true);
135  }
136
137  private function getCacheFile($file, $forWrite = false)
138  {
139    return $this->getCacheDir($file, $forWrite) . '/' . md5($file);
140  }
141
142  private function getCacheDir($file, $forWrite)
143  {
144    // use the first 2 characters of the hash as a directory prefix
145    // this should prevent slowdowns due to huge directory listings
146    // and thus give some basic amount of scalability
147    $storageDir = $this->path . '/' . substr(md5($file), 0, 2);
148    if ($forWrite && ! is_dir($storageDir)) {
149      if (! mkdir($storageDir, 0700, true)) {
150        $this->client->getLogger()->error(
151            'File cache creation failed',
152            array('dir' => $storageDir)
153        );
154        throw new Google_Cache_Exception("Could not create storage directory: $storageDir");
155      }
156    }
157    return $storageDir;
158  }
159
160  private function acquireReadLock($storageFile)
161  {
162    return $this->acquireLock(LOCK_SH, $storageFile);
163  }
164
165  private function acquireWriteLock($storageFile)
166  {
167    $rc = $this->acquireLock(LOCK_EX, $storageFile);
168    if (!$rc) {
169      $this->client->getLogger()->notice(
170          'File cache write lock failed',
171          array('file' => $storageFile)
172      );
173      $this->delete($storageFile);
174    }
175    return $rc;
176  }
177
178  private function acquireLock($type, $storageFile)
179  {
180    $mode = $type == LOCK_EX ? "w" : "r";
181    $this->fh = fopen($storageFile, $mode);
182    if (!$this->fh) {
183      $this->client->getLogger()->error(
184          'Failed to open file during lock acquisition',
185          array('file' => $storageFile)
186      );
187      return false;
188    }
189    if ($type == LOCK_EX) {
190      chmod($storageFile, 0600);
191    }
192    $count = 0;
193    while (!flock($this->fh, $type | LOCK_NB)) {
194      // Sleep for 10ms.
195      usleep(10000);
196      if (++$count < self::MAX_LOCK_RETRIES) {
197        return false;
198      }
199    }
200    return true;
201  }
202
203  public function unlock($storageFile)
204  {
205    if ($this->fh) {
206      flock($this->fh, LOCK_UN);
207    }
208  }
209}
210