Updated the caching framework to the according FLOW3 revision 2616
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_filebackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Ingo Renner <ingo@typo3.org>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25
26 /**
27 * A caching backend which stores cache entries in files
28 *
29 * This file is a backport from FLOW3
30 *
31 * @package TYPO3
32 * @subpackage t3lib_cache
33 * @version $Id$
34 */
35 class t3lib_cache_backend_FileBackend extends t3lib_cache_backend_AbstractBackend {
36
37 const SEPARATOR = '^';
38
39 const EXPIRYTIME_FORMAT = 'YmdHis';
40 const EXPIRYTIME_LENGTH = 14;
41
42 /**
43 * @var string Directory where the files are stored
44 */
45 protected $cacheDirectory = '';
46
47 protected $root = '/';
48
49 /**
50 * Constructs this backend
51 *
52 * @param mixed Configuration options - depends on the actual backend
53 */
54 public function __construct(array $options = array()) {
55 parent::__construct($options);
56 if (TYPO3_OS === 'WIN') {
57 $this->root = '';
58 }
59 if (empty($this->cacheDirectory)) {
60 $cacheDirectory = 'typo3temp/cache/';
61 try {
62 $this->setCacheDirectory($cacheDirectory);
63 } catch(t3lib_cache_Exception $exception) {
64
65 }
66 }
67 }
68
69 /**
70 * Sets the directory where the cache files are stored. By default it is
71 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
72 * absolute path can be selected, too.
73 *
74 * @param string The directory. If a relative path is given, it's assumed it's in TYPO3_DOCUMENT_ROOT. If an absolute path is given it is taken as is.
75 * @return void
76 * @throws t3lib_cache_Exception if the directory does not exist, is not writable or could not be created.
77 * @author Robert Lemke <robert@typo3.org>
78 * @author Ingo Renner <ingo@typo3.org>
79 */
80 public function setCacheDirectory($cacheDirectory) {
81 $documentRoot = PATH_site;
82
83 // resetting if an absolute path is given
84 if ($cacheDirectory{0} == '/') {
85 $documentRoot = '/';
86 }
87
88 if ($cacheDirectory{strlen($cacheDirectory) - 1} !== '/') {
89 $cacheDirectory .= '/';
90 }
91
92 if (!is_writable($documentRoot . $cacheDirectory)) {
93 t3lib_div::mkdir_deep(
94 $documentRoot,
95 $cacheDirectory
96 );
97 }
98
99 if (!is_dir($documentRoot . $cacheDirectory)) {
100 throw new t3lib_cache_Exception(
101 'The directory "' . $cacheDirectory . '" does not exist.',
102 1203965199
103 );
104 }
105
106 if (!is_writable($documentRoot . $cacheDirectory)) {
107 throw new t3lib_cache_Exception(
108 'The directory "' . $cacheDirectory . '" is not writable.',
109 1203965200
110 );
111 }
112
113 $tagsDirectory = $cacheDirectory . 'tags/';
114
115 if (!is_writable($tagsDirectory)) {
116 t3lib_div::mkdir_deep(
117 $documentRoot,
118 $tagsDirectory
119 );
120 }
121
122 $this->cacheDirectory = $documentRoot . $cacheDirectory;
123 }
124
125 /**
126 * Returns the directory where the cache files are stored
127 *
128 * @return string Full path of the cache directory
129 * @author Robert Lemke <robert@typo3.org>
130 */
131 public function getCacheDirectory() {
132 return $this->cacheDirectory;
133 }
134
135 /**
136 * Saves data in a cache file.
137 *
138 * @param string An identifier for this specific cache entry
139 * @param string The data to be stored
140 * @param array Tags to associate with this cache entry
141 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
142 * @return void
143 * @throws t3lib_cache_Exception if the directory does not exist or is not writable, or if no cache frontend has been set.
144 * @throws t3lib_cache_exception_InvalidData if the data to bes stored is not a string.
145 * @author Robert Lemke <robert@typo3.org>
146 */
147 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
148 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
149 throw new t3lib_cache_Exception(
150 'No cache frontend has been set yet via setCache().',
151 1204111375
152 );
153 }
154
155 if (!is_string($data)) {
156 throw new t3lib_cache_Exception_InvalidData(
157 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
158 1204481674
159 );
160 }
161
162 $expirytime = $this->calculateExpiryTime($lifetime);
163 $cacheEntryPath = $this->renderCacheEntryPath($entryIdentifier);
164
165 if (!is_writable($cacheEntryPath)) {
166 try {
167 t3lib_div::mkdir_deep(
168 $this->root,
169 $cacheEntryPath
170 );
171 } catch(Exception $exception) {
172
173 }
174 if (!is_writable($cacheEntryPath)) {
175 throw new t3lib_cache_Exception(
176 'The cache directory "' . $cacheEntryPath . '" could not be created.',
177 1204026250
178 );
179 }
180 }
181
182 $this->remove($entryIdentifier);
183
184 $data = $expirytime->format(self::EXPIRYTIME_FORMAT) . $data;
185 $temporaryFilename = uniqid() . '.temp';
186 $result = file_put_contents($cacheEntryPath . $temporaryFilename, $data);
187 if ($result === FALSE) {
188 throw new t3lib_cache_Exception(
189 'The temporary cache file "' . $temporaryFilename . '" could not be written.',
190 1204026251
191 );
192 }
193
194 for ($i = 0; $i < 5; $i++) {
195 $result = rename($cacheEntryPath . $temporaryFilename, $cacheEntryPath . $entryIdentifier);
196 if ($result === TRUE) {
197 break;
198 }
199 }
200
201 if ($result === FALSE) {
202 throw new t3lib_cache_Exception(
203 'The cache file "' . $entryIdentifier . '" could not be written.',
204 1222361632
205 );
206 }
207
208 foreach ($tags as $tag) {
209 $tagPath = $this->cacheDirectory . 'tags/' . $tag . '/';
210
211 if (!is_writable($tagPath)) {
212 t3lib_div::mkdir_deep(
213 $this->root,
214 $tagPath
215 );
216 if (!is_writable($tagPath)) {
217 throw new t3lib_cache_Exception(
218 'The tag directory "' . $tagPath . '" could not be created.',
219 1238242144
220 );
221 }
222 }
223
224 touch($tagPath
225 . $this->cache->getIdentifier()
226 . self::SEPARATOR
227 . $entryIdentifier
228 );
229 }
230 }
231
232 /**
233 * Loads data from a cache file.
234 *
235 * @param string An identifier which describes the cache entry to load
236 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
237 * @author Robert Lemke <robert@typo3.org>
238 * @author Karsten Dambekalns <karsten@typo3.org>
239 */
240 public function get($entryIdentifier) {
241 $pathAndFilename = $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
242 return ($this->isCacheFileExpired($pathAndFilename)) ? FALSE : file_get_contents($pathAndFilename, NULL, NULL, self::EXPIRYTIME_LENGTH);
243 }
244
245 /**
246 * Checks if a cache entry with the specified identifier exists.
247 *
248 * @param string $entryIdentifier
249 * @return boolean TRUE if such an entry exists, FALSE if not
250 * @author Robert Lemke <robert@typo3.org>
251 */
252 public function has($entryIdentifier) {
253 return !$this->isCacheFileExpired($this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier);
254 }
255
256 /**
257 * Removes all cache entries matching the specified identifier.
258 * Usually this only affects one entry.
259 *
260 * @param string Specifies the cache entry to remove
261 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
262 * @author Robert Lemke <robert@typo3.org>
263 */
264 public function remove($entryIdentifier) {
265 $pathAndFilename = $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
266
267 if (!file_exists($pathAndFilename)) {
268 return FALSE;
269 }
270
271 if (unlink ($pathAndFilename) === FALSE) {
272 return FALSE;
273 }
274
275 foreach($this->findTagFilesByEntry($entryIdentifier) as $pathAndFilename) {
276 if (!file_exists($pathAndFilename)) {
277 return FALSE;
278 }
279
280 if (unlink ($pathAndFilename) === FALSE) {
281 return FALSE;
282 }
283 }
284
285 return TRUE;
286 }
287
288 /**
289 * Finds and returns all cache entry identifiers which are tagged by the
290 * specified tag.
291 *
292 * @param string The tag to search for
293 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
294 * @author Robert Lemke <robert@typo3.org>
295 * @author Karsten Dambekalns <karsten@typo3.org>
296 */
297 public function findIdentifiersByTag($tag) {
298 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
299 throw new t3lib_cache_Exception(
300 'Yet no cache frontend has been set via setCache().',
301 1204111376
302 );
303 }
304
305 $path = $this->cacheDirectory . 'tags/';
306 $pattern = $path . $tag . '/'
307 . $this->cache->getIdentifier() . self::SEPARATOR . '*';
308 $filesFound = glob($pattern);
309
310 if ($filesFound === FALSE || count($filesFound) === 0) {
311 return array();
312 }
313
314 $cacheEntries = array();
315 foreach ($filesFound as $filename) {
316 list(,$entryIdentifier) = explode(self::SEPARATOR, basename($filename));
317 if ($this->has($entryIdentifier)) {
318 $cacheEntries[$entryIdentifier] = $entryIdentifier;
319 }
320 }
321
322 return array_values($cacheEntries);
323 }
324
325 /**
326 * Finds and returns all cache entry identifiers which are tagged by the
327 * specified tags.
328 *
329 * @param array Array of tags to search for
330 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
331 * @author Ingo Renner <ingo@typo3.org>
332 */
333 public function findIdentifiersByTags(array $tags) {
334 $taggedEntries = array();
335 $foundEntries = array();
336
337 foreach ($tags as $tag) {
338 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
339 }
340
341 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
342
343 foreach ($intersectedTaggedEntries as $entryIdentifier) {
344 if ($this->has($entryIdentifier)) {
345 $foundEntries[$entryIdentifier] = $entryIdentifier;
346 }
347 }
348
349 return $foundEntries;
350 }
351
352 /**
353 * Removes all cache entries of this cache.
354 *
355 * @return void
356 * @author Robert Lemke <robert@typo3.org>
357 */
358 public function flush() {
359 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
360 throw new t3lib_cache_Exception(
361 'Yet no cache frontend has been set via setCache().',
362 1204111376
363 );
364 }
365
366 $dataPath = $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/';
367 $tagsPath = $this->cacheDirectory . 'tags/';
368
369 t3lib_div::rmdir($dataPath, true);
370 t3lib_div::rmdir($tagsPath, true);
371 }
372
373 /**
374 * Removes all cache entries of this cache which are tagged by the specified tag.
375 *
376 * @param string The tag the entries must have
377 * @return void
378 * @author Ingo Renner <ingo@typo3.org>
379 */
380 public function flushByTag($tag) {
381 $identifiers = $this->findIdentifiersByTag($tag);
382 if (count($identifiers) === 0) {
383 return;
384 }
385
386 foreach ($identifiers as $entryIdentifier) {
387 $this->remove($entryIdentifier);
388 }
389 }
390
391 /**
392 * Removes all cache entries of this cache which are tagged by the specified tag.
393 *
394 * @param array The tags the entries must have
395 * @return void
396 * @author Ingo Renner <ingo@typo3.org>
397 */
398 public function flushByTags(array $tags) {
399 foreach ($tags as $tag) {
400 $this->flushByTag($tag);
401 }
402 }
403
404 /**
405 * Checks if the given cache entry files are still valid or if their
406 * lifetime has exceeded.
407 *
408 * @param string $cacheFilename
409 * @return boolean
410 * @author Robert Lemke <robert@typo3.org>
411 */
412 protected function isCacheFileExpired($cacheFilename) {
413 $timestamp = (file_exists($cacheFilename)) ? file_get_contents($cacheFilename, NULL, NULL, 0, self::EXPIRYTIME_LENGTH) : 1;
414 return $timestamp < gmdate(self::EXPIRYTIME_FORMAT);
415 }
416
417 /**
418 * Does garbage collection
419 *
420 * @return void
421 * @author Karsten Dambekalns <karsten@typo3.org>
422 */
423 public function collectGarbage() {
424 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
425 throw new t3lib_cache_Exception(
426 'Yet no cache frontend has been set via setCache().',
427 1222686150
428 );
429 }
430
431 $pattern = $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/*/*/*';
432 $filesFound = glob($pattern);
433
434 foreach ($filesFound as $cacheFilename) {
435 if ($this->isCacheFileExpired($cacheFilename)) {
436 $this->remove(basename($cacheFilename));
437 }
438 }
439 }
440
441 /**
442 * Renders the full path (excluding file name) leading to the given cache entry.
443 * Doesn't check if such a cache entry really exists.
444 *
445 * @param string $identifier Identifier for the cache entry
446 * @return string Absolute path leading to the directory containing the cache entry
447 * @author Robert Lemke <robert@typo3.org>
448 * @internal
449 */
450 protected function renderCacheEntryPath($identifier) {
451 $identifierHash = sha1($identifier);
452 return $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/' . $identifierHash[0] . '/' . $identifierHash[1] . '/';
453 }
454
455 /**
456 * Tries to find the cache entry for the specified identifier.
457 * Usually only one cache entry should be found - if more than one exist, this
458 * is due to some error or crash.
459 *
460 * @param string The cache entry identifier
461 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
462 * @author Robert Lemke <robert@typo3.org>
463 * @throws t3lib_cache_Exception if no frontend has been set
464 * @internal
465 */
466 protected function findCacheFilesByIdentifier($entryIdentifier) {
467 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
468 throw new t3lib_cache_Exception(
469 'Yet no cache frontend has been set via setCache().',
470 1204111376
471 );
472 }
473
474 $pattern = $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
475 $filesFound = glob($pattern);
476 if ($filesFound === FALSE || count($filesFound) === 0) {
477 return FALSE;
478 }
479
480 return $filesFound;
481 }
482
483
484 /**
485 * Tries to find the tag entries for the specified cache entry.
486 *
487 * @param string The cache entry identifier to find tag files for
488 * @return array The file names (including path)
489 * @author Robert Lemke <robert@typo3.org>
490 * @throws t3lib_cache_Exception if no frontend has been set
491 * @internal
492 */
493 protected function findTagFilesByEntry($entryIdentifier) {
494 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
495 throw new t3lib_cache_Exception(
496 'Yet no cache frontend has been set via setCache().',
497 1204111376
498 );
499 }
500
501 $path = $this->cacheDirectory . 'tags/';
502 $pattern = $path . '*/' . $this->cache->getIdentifier() . self::SEPARATOR . $entryIdentifier;
503 return glob($pattern);
504 }
505 }
506
507
508 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']) {
509 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']);
510 }
511
512 ?>