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