94f2e1d6841db5d0147ba072d1d416fc38b21603
[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 * Maximum allowed file path length in the current environment.
51 * Will be set in initializeObject()
52 *
53 * @var integer
54 */
55 protected $maximumPathLength = null;
56
57 /**
58 * Constructs this backend
59 *
60 * @param mixed Configuration options - depends on the actual backend
61 */
62 public function __construct(array $options = array()) {
63 parent::__construct($options);
64 if (TYPO3_OS === 'WIN') {
65 $this->root = '';
66 }
67
68 if (empty($this->cacheDirectory)) {
69 $cacheDirectory = 'typo3temp/cache/';
70 try {
71 $this->setCacheDirectory($cacheDirectory);
72 } catch(t3lib_cache_Exception $exception) {
73
74 }
75 }
76
77 if (is_null($this->maximumPathLength)) {
78 $this->maximumPathLength = t3lib_div::getMaximumPathLength();
79 }
80 }
81
82 /**
83 * Sets the directory where the cache files are stored. By default it is
84 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
85 * absolute path can be selected, too.
86 *
87 * @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.
88 * @return void
89 * @throws t3lib_cache_Exception if the directory does not exist, is not writable or could not be created.
90 * @author Robert Lemke <robert@typo3.org>
91 * @author Ingo Renner <ingo@typo3.org>
92 */
93 public function setCacheDirectory($cacheDirectory) {
94 $documentRoot = PATH_site;
95
96 // resetting if an absolute path is given
97 if ($cacheDirectory{0} == '/') {
98 $documentRoot = '/';
99 }
100
101 if ($cacheDirectory{strlen($cacheDirectory) - 1} !== '/') {
102 $cacheDirectory .= '/';
103 }
104
105 if (!is_writable($documentRoot . $cacheDirectory)) {
106 t3lib_div::mkdir_deep(
107 $documentRoot,
108 $cacheDirectory
109 );
110 }
111
112 if (!is_dir($documentRoot . $cacheDirectory)) {
113 throw new t3lib_cache_Exception(
114 'The directory "' . $cacheDirectory . '" does not exist.',
115 1203965199
116 );
117 }
118
119 if (!is_writable($documentRoot . $cacheDirectory)) {
120 throw new t3lib_cache_Exception(
121 'The directory "' . $cacheDirectory . '" is not writable.',
122 1203965200
123 );
124 }
125
126 $tagsDirectory = $cacheDirectory . 'tags/';
127
128 if (!is_writable($tagsDirectory)) {
129 t3lib_div::mkdir_deep(
130 $documentRoot,
131 $tagsDirectory
132 );
133 }
134
135 $this->cacheDirectory = $documentRoot . $cacheDirectory;
136 }
137
138 /**
139 * Returns the directory where the cache files are stored
140 *
141 * @return string Full path of the cache directory
142 * @author Robert Lemke <robert@typo3.org>
143 */
144 public function getCacheDirectory() {
145 return $this->cacheDirectory;
146 }
147
148 /**
149 * Saves data in a cache file.
150 *
151 * @param string An identifier for this specific cache entry
152 * @param string The data to be stored
153 * @param array Tags to associate with this cache entry
154 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
155 * @return void
156 * @throws t3lib_cache_Exception if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set.
157 * @throws t3lib_cache_exception_InvalidData if the data to bes stored is not a string.
158 * @author Robert Lemke <robert@typo3.org>
159 */
160 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
161 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
162 throw new t3lib_cache_Exception(
163 'No cache frontend has been set yet via setCache().',
164 1204111375
165 );
166 }
167
168 if (!is_string($data)) {
169 throw new t3lib_cache_Exception_InvalidData(
170 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
171 1204481674
172 );
173 }
174
175 $expirytime = $this->calculateExpiryTime($lifetime);
176 $cacheEntryPath = $this->renderCacheEntryPath($entryIdentifier);
177
178 if (!is_writable($cacheEntryPath)) {
179 try {
180 t3lib_div::mkdir_deep(
181 $this->root,
182 $cacheEntryPath
183 );
184 } catch(Exception $exception) {
185
186 }
187 if (!is_writable($cacheEntryPath)) {
188 throw new t3lib_cache_Exception(
189 'The cache directory "' . $cacheEntryPath . '" could not be created.',
190 1204026250
191 );
192 }
193 }
194
195 $this->remove($entryIdentifier);
196
197 $data = $expirytime->format(self::EXPIRYTIME_FORMAT) . $data;
198 $cacheEntryPathAndFilename = $cacheEntryPath . uniqid() . '.temp';
199 if (strlen($cacheEntryPathAndFilename) > $this->maximumPathLength) {
200 throw new t3lib_cache_Exception(
201 'The length of the temporary cache file path "' . $cacheEntryPathAndFilename . '" is ' . strlen($cacheEntryPathAndFilename) . ' characters long and exceeds the maximum path length of ' . $this->maximumPathLength . '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
202 1248710426
203 );
204 }
205
206 $result = file_put_contents($cacheEntryPathAndFilename, $data);
207 if ($result === FALSE) {
208 throw new t3lib_cache_Exception(
209 'The temporary cache file "' . $cacheEntryPathAndFilename . '" could not be written.',
210 1204026251
211 );
212 }
213
214 for ($i = 0; $i < 5; $i++) {
215 $result = rename($cacheEntryPathAndFilename, $cacheEntryPath . $entryIdentifier);
216 if ($result === TRUE) {
217 break;
218 }
219 }
220
221 if ($result === FALSE) {
222 throw new t3lib_cache_Exception(
223 'The cache file "' . $entryIdentifier . '" could not be written.',
224 1222361632
225 );
226 }
227
228 foreach ($tags as $tag) {
229 $this->setTag($entryIdentifier, $tag);
230 }
231 }
232
233 /**
234 * Creates a tag that is associated with the given cache identifier
235 *
236 * @param string $entryIdentifier An identifier for this specific cache entry
237 * @param string Tag to associate with this cache entry
238 * @return void
239 * @throws t3lib_cache_Exception if the tag path is not writable or exceeds the maximum allowed path length
240 * @author Bastian Waidelich <bastian@typo3.org>
241 * @author Ingo Renner <ingo@typo3.org>
242 */
243 protected function setTag($entryIdentifier, $tag) {
244 $tagPath = $this->cacheDirectory . 'tags/' . $tag . '/';
245
246 if (!is_writable($tagPath)) {
247 t3lib_div::mkdir_deep($this->root, $tagPath);
248 if (!is_writable($tagPath)) {
249 throw new t3lib_cache_Exception(
250 'The tag directory "' . $tagPath . '" could not be created.',
251 1238242144
252 );
253 }
254 }
255
256 $tagPathAndFilename = $tagPath . $this->cache->getIdentifier()
257 . self::SEPARATOR . $entryIdentifier;
258 if (strlen($tagPathAndFilename) > $this->maximumPathLength) {
259 throw new t3lib_cache_Exception(
260 'The length of the tag path "' . $tagPathAndFilename . '" is ' . strlen($tagPathAndFilename) . ' characters long and exceeds the maximum path length of ' . $maximumPathLength . '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
261 1248710426
262 );
263 }
264 touch($tagPathAndFilename);
265 }
266
267 /**
268 * Loads data from a cache file.
269 *
270 * @param string An identifier which describes the cache entry to load
271 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
272 * @author Robert Lemke <robert@typo3.org>
273 * @author Karsten Dambekalns <karsten@typo3.org>
274 */
275 public function get($entryIdentifier) {
276 $pathAndFilename = $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
277 return ($this->isCacheFileExpired($pathAndFilename)) ? FALSE : file_get_contents($pathAndFilename, NULL, NULL, self::EXPIRYTIME_LENGTH);
278 }
279
280 /**
281 * Checks if a cache entry with the specified identifier exists.
282 *
283 * @param string $entryIdentifier
284 * @return boolean TRUE if such an entry exists, FALSE if not
285 * @author Robert Lemke <robert@typo3.org>
286 */
287 public function has($entryIdentifier) {
288 return !$this->isCacheFileExpired($this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier);
289 }
290
291 /**
292 * Removes all cache entries matching the specified identifier.
293 * Usually this only affects one entry.
294 *
295 * @param string Specifies the cache entry to remove
296 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
297 * @author Robert Lemke <robert@typo3.org>
298 */
299 public function remove($entryIdentifier) {
300 $pathAndFilename = $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
301
302 if (!file_exists($pathAndFilename)) {
303 return FALSE;
304 }
305
306 if (unlink ($pathAndFilename) === FALSE) {
307 return FALSE;
308 }
309
310 foreach($this->findTagFilesByEntry($entryIdentifier) as $pathAndFilename) {
311 if (!file_exists($pathAndFilename)) {
312 return FALSE;
313 }
314
315 if (unlink ($pathAndFilename) === FALSE) {
316 return FALSE;
317 }
318 }
319
320 return TRUE;
321 }
322
323 /**
324 * Finds and returns all cache entry identifiers which are tagged by the
325 * specified tag.
326 *
327 * @param string The tag to search for
328 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
329 * @author Robert Lemke <robert@typo3.org>
330 * @author Karsten Dambekalns <karsten@typo3.org>
331 */
332 public function findIdentifiersByTag($tag) {
333 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
334 throw new t3lib_cache_Exception(
335 'Yet no cache frontend has been set via setCache().',
336 1204111376
337 );
338 }
339
340 $path = $this->cacheDirectory . 'tags/';
341 $pattern = $path . $tag . '/'
342 . $this->cache->getIdentifier() . self::SEPARATOR . '*';
343 $filesFound = glob($pattern);
344
345 if ($filesFound === FALSE || count($filesFound) === 0) {
346 return array();
347 }
348
349 $cacheEntries = array();
350 foreach ($filesFound as $filename) {
351 list(,$entryIdentifier) = explode(self::SEPARATOR, basename($filename));
352 if ($this->has($entryIdentifier)) {
353 $cacheEntries[$entryIdentifier] = $entryIdentifier;
354 }
355 }
356
357 return array_values($cacheEntries);
358 }
359
360 /**
361 * Finds and returns all cache entry identifiers which are tagged by the
362 * specified tags.
363 *
364 * @param array Array of tags to search for
365 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
366 * @author Ingo Renner <ingo@typo3.org>
367 */
368 public function findIdentifiersByTags(array $tags) {
369 $taggedEntries = array();
370 $foundEntries = array();
371
372 foreach ($tags as $tag) {
373 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
374 }
375
376 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
377
378 foreach ($intersectedTaggedEntries as $entryIdentifier) {
379 if ($this->has($entryIdentifier)) {
380 $foundEntries[$entryIdentifier] = $entryIdentifier;
381 }
382 }
383
384 return $foundEntries;
385 }
386
387 /**
388 * Removes all cache entries of this cache.
389 *
390 * @return void
391 * @author Robert Lemke <robert@typo3.org>
392 */
393 public function flush() {
394 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
395 throw new t3lib_cache_Exception(
396 'Yet no cache frontend has been set via setCache().',
397 1204111376
398 );
399 }
400
401 $dataPath = $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/';
402 $tagsPath = $this->cacheDirectory . 'tags/';
403
404 t3lib_div::rmdir($dataPath, true);
405 t3lib_div::rmdir($tagsPath, true);
406 }
407
408 /**
409 * Removes all cache entries of this cache which are tagged by the specified tag.
410 *
411 * @param string The tag the entries must have
412 * @return void
413 * @author Ingo Renner <ingo@typo3.org>
414 */
415 public function flushByTag($tag) {
416 $identifiers = $this->findIdentifiersByTag($tag);
417 if (count($identifiers) === 0) {
418 return;
419 }
420
421 foreach ($identifiers as $entryIdentifier) {
422 $this->remove($entryIdentifier);
423 }
424 }
425
426 /**
427 * Removes all cache entries of this cache which are tagged by the specified tag.
428 *
429 * @param array The tags the entries must have
430 * @return void
431 * @author Ingo Renner <ingo@typo3.org>
432 */
433 public function flushByTags(array $tags) {
434 foreach ($tags as $tag) {
435 $this->flushByTag($tag);
436 }
437 }
438
439 /**
440 * Checks if the given cache entry files are still valid or if their
441 * lifetime has exceeded.
442 *
443 * @param string $cacheFilename
444 * @return boolean
445 * @author Robert Lemke <robert@typo3.org>
446 */
447 protected function isCacheFileExpired($cacheFilename) {
448 $timestamp = (file_exists($cacheFilename)) ? file_get_contents($cacheFilename, NULL, NULL, 0, self::EXPIRYTIME_LENGTH) : 1;
449 return $timestamp < gmdate(self::EXPIRYTIME_FORMAT);
450 }
451
452 /**
453 * Does garbage collection
454 *
455 * @return void
456 * @author Karsten Dambekalns <karsten@typo3.org>
457 */
458 public function collectGarbage() {
459 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
460 throw new t3lib_cache_Exception(
461 'Yet no cache frontend has been set via setCache().',
462 1222686150
463 );
464 }
465
466 $pattern = $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/*/*/*';
467 $filesFound = glob($pattern);
468
469 foreach ($filesFound as $cacheFilename) {
470 if ($this->isCacheFileExpired($cacheFilename)) {
471 $this->remove(basename($cacheFilename));
472 }
473 }
474 }
475
476 /**
477 * Renders the full path (excluding file name) leading to the given cache entry.
478 * Doesn't check if such a cache entry really exists.
479 *
480 * @param string $identifier Identifier for the cache entry
481 * @return string Absolute path leading to the directory containing the cache entry
482 * @author Robert Lemke <robert@typo3.org>
483 * @internal
484 */
485 protected function renderCacheEntryPath($identifier) {
486 $identifierHash = sha1($identifier);
487 return $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/' . $identifierHash[0] . '/' . $identifierHash[1] . '/';
488 }
489
490 /**
491 * Tries to find the cache entry for the specified identifier.
492 * Usually only one cache entry should be found - if more than one exist, this
493 * is due to some error or crash.
494 *
495 * @param string The cache entry identifier
496 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
497 * @author Robert Lemke <robert@typo3.org>
498 * @throws t3lib_cache_Exception if no frontend has been set
499 * @internal
500 */
501 protected function findCacheFilesByIdentifier($entryIdentifier) {
502 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
503 throw new t3lib_cache_Exception(
504 'Yet no cache frontend has been set via setCache().',
505 1204111376
506 );
507 }
508
509 $pattern = $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
510 $filesFound = glob($pattern);
511 if ($filesFound === FALSE || count($filesFound) === 0) {
512 return FALSE;
513 }
514
515 return $filesFound;
516 }
517
518
519 /**
520 * Tries to find the tag entries for the specified cache entry.
521 *
522 * @param string The cache entry identifier to find tag files for
523 * @return array The file names (including path)
524 * @author Robert Lemke <robert@typo3.org>
525 * @throws t3lib_cache_Exception if no frontend has been set
526 * @internal
527 */
528 protected function findTagFilesByEntry($entryIdentifier) {
529 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
530 throw new t3lib_cache_Exception(
531 'Yet no cache frontend has been set via setCache().',
532 1204111376
533 );
534 }
535
536 $path = $this->cacheDirectory . 'tags/';
537 $pattern = $path . '*/' . $this->cache->getIdentifier() . self::SEPARATOR . $entryIdentifier;
538 return glob($pattern);
539 }
540 }
541
542
543 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']) {
544 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']);
545 }
546
547 ?>