Added feature 9097: Introduce a new caching framework (backported from FLOW3)
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_file.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2008 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_File extends t3lib_cache_AbstractBackend {
36
37 /**
38 * @var string Directory where the files are stored
39 */
40 protected $cacheDirectory = '';
41
42 /**
43 * Constructs this backend
44 *
45 * @param mixed Configuration options - depends on the actual backend
46 */
47 public function __construct(array $options = array()) {
48 parent::__construct($options);
49
50 if (empty($this->cacheDirectory)) {
51 $cacheDirectory = 'typo3temp/cache/';
52 try {
53 $this->setCacheDirectory($cacheDirectory);
54 } catch(t3lib_cache_Exception $exception) {
55
56 }
57 }
58 }
59
60 /**
61 * Sets the directory where the cache files are stored.
62 *
63 * @param string The directory
64 * @return void
65 * @throws t3lib_cache_Exception if the directory does not exist, is not writable or could not be created.
66 * @author Robert Lemke <robert@typo3.org>
67 */
68 public function setCacheDirectory($cacheDirectory) {
69
70 if ($cacheDirectory{strlen($cacheDirectory) - 1} !== '/') {
71 $cacheDirectory .= '/';
72 }
73
74 if (!is_writable($cacheDirectory)) {
75 t3lib_div::mkdir_deep(
76 t3lib_div::getIndpEnv('TYPO3_DOCUMENT_ROOT') . '/',
77 $cacheDirectory
78 );
79 }
80
81 if (!is_dir($cacheDirectory)) {
82 throw new t3lib_cache_Exception(
83 'The directory "' . $cacheDirectory . '" does not exist.',
84 1203965199
85 );
86 }
87
88 if (!is_writable($cacheDirectory)) {
89 throw new t3lib_cache_Exception(
90 'The directory "' . $cacheDirectory . '" is not writable.',
91 1203965200
92 );
93 }
94
95 $tagsDirectory = $cacheDirectory . 'tags/';
96
97 if (!is_writable($tagsDirectory)) {
98 t3lib_div::mkdir_deep(
99 t3lib_div::getIndpEnv('TYPO3_DOCUMENT_ROOT') . '/',
100 $tagsDirectory
101 );
102 }
103
104 $this->cacheDirectory = $cacheDirectory;
105 }
106
107 /**
108 * Returns the directory where the cache files are stored
109 *
110 * @return string Full path of the cache directory
111 * @author Robert Lemke <robert@typo3.org>
112 */
113 public function getCacheDirectory() {
114 return $this->cacheDirectory;
115 }
116
117 /**
118 * Saves data in a cache file.
119 *
120 * @param string An identifier for this specific cache entry
121 * @param string The data to be stored
122 * @param array Tags to associate with this cache entry
123 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
124 * @return void
125 * @throws t3lib_cache_Exception if the directory does not exist or is not writable, or if no cache frontend has been set.
126 * @author Robert Lemke <robert@typo3.org>
127 */
128 public function save($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
129 if (!self::isValidEntryIdentifier($entryIdentifier)) {
130 throw new InvalidArgumentException(
131 '"' . $entryIdentifier . '" is not a valid cache entry identifier.',
132 1207139693
133 );
134 }
135
136 if (!is_object($this->cache)) {
137 throw new t3lib_cache_Exception(
138 'No cache frontend has been set yet via setCache().',
139 1204111375
140 );
141 }
142
143 if (!is_string($data)) {
144 throw new t3lib_cache_Exception_InvalidData(
145 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
146 1204481674
147 );
148 }
149
150 foreach ($tags as $tag) {
151 if (!self::isValidTag($tag)) {
152 throw new InvalidArgumentException(
153 '"' . $tag . '" is not a valid tag for a cache entry.',
154 1213105438
155 );
156 }
157 }
158
159 if (is_null($lifetime)) {
160 $lifetime = $this->defaultLifetime;
161 }
162
163 $expiryTime = new DateTime(
164 'now +' . $lifetime . ' seconds',
165 new DateTimeZone('UTC')
166 );
167 $entryIdentifierHash = sha1($entryIdentifier);
168 $cacheEntryPath = $this->cacheDirectory
169 . 'data/' . $this->cache->getIdentifier()
170 . '/' . $entryIdentifierHash{0} . '/' . $entryIdentifierHash {1} . '/';
171 $filename = $this->renderCacheFilename($entryIdentifier, $expiryTime);
172
173 if (!is_writable($cacheEntryPath)) {
174 try {
175 t3lib_div::mkdir_deep(
176 t3lib_div::getIndpEnv('TYPO3_DOCUMENT_ROOT') . '/',
177 $cacheEntryPath
178 );
179 } catch(Exception $exception) {
180
181 }
182 if (!is_writable($cacheEntryPath)) {
183 throw new t3lib_cache_Exception(
184 'The cache directory "' . $cacheEntryPath . '" could not be created.',
185 1204026250
186 );
187 }
188 }
189
190 $this->remove($entryIdentifier);
191
192 $temporaryFilename = $filename . '.' . uniqid() . '.temp';
193 $result = file_put_contents($cacheEntryPath . $temporaryFilename, $data);
194 if ($result === FALSE) {
195 throw new t3lib_cache_Exception(
196 'The temporary cache file "' . $temporaryFilename . '" could not be written.',
197 1204026251
198 );
199 }
200
201 for ($i = 0; $i < 5; $i++) {
202 $result = rename(
203 $cacheEntryPath . $temporaryFilename,
204 $cacheEntryPath . $filename
205 );
206
207 if ($result === TRUE) {
208 break;
209 }
210 }
211
212 foreach ($tags as $tag) {
213 $tagPath = $this->cacheDirectory . 'tags/' . $tag . '/';
214
215 if (!is_writable($tagPath)) {
216 mkdir($tagPath);
217 }
218
219 touch($tagPath . $this->cache->getIdentifier() . '_' . $entryIdentifier);
220 }
221 }
222
223 /**
224 * Loads data from a cache file.
225 *
226 * @param string An identifier which describes the cache entry to load
227 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
228 * @author Robert Lemke <robert@typo3.org>
229 */
230 public function load($entryIdentifier) {
231 $pathsAndFilenames = $this->findCacheFilesByEntry($entryIdentifier);
232 $cacheEntry = FALSE;
233
234 if ($pathsAndFilenames !== FALSE) {
235 $cacheEntry = file_get_contents(array_pop($pathsAndFilenames));
236 }
237
238 return $cacheEntry;
239 }
240
241 /**
242 * Checks if a cache entry with the specified identifier exists.
243 *
244 * @param unknown_type
245 * @return boolean TRUE if such an entry exists, FALSE if not
246 * @author Robert Lemke <robert@typo3.org>
247 */
248 public function has($entryIdentifier) {
249 return $this->findCacheFilesByEntry($entryIdentifier) !== FALSE;
250 }
251
252 /**
253 * Removes all cache entries matching the specified identifier.
254 * Usually this only affects one entry.
255 *
256 * @param string Specifies the cache entry to remove
257 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
258 * @author Robert Lemke <robert@typo3.org>
259 */
260 public function remove($entryIdentifier) {
261 $pathsAndFilenames = $this->findCacheFilesByEntry($entryIdentifier);
262
263 if ($pathsAndFilenames === FALSE) {
264 return FALSE;
265 }
266
267 foreach ($pathsAndFilenames as $pathAndFilename) {
268 $result = unlink($pathAndFilename);
269 if ($result === FALSE) {
270 return FALSE;
271 }
272 }
273
274 $pathsAndFilenames = $this->findTagFilesByEntry($entryIdentifier);
275 if ($pathsAndFilenames === FALSE) {
276 return FALSE;
277 }
278
279 foreach ($pathsAndFilenames as $pathAndFilename) {
280 $result = unlink($pathAndFilename);
281 if ($result === FALSE) {
282 return FALSE;
283 }
284 }
285
286 return TRUE;
287 }
288
289 /**
290 * Finds and returns all cache entries which are tagged by the specified tag.
291 * The asterisk ("*") is allowed as a wildcard at the beginning and the end of
292 * the tag.
293 *
294 * @param string The tag to search for, the "*" wildcard is supported
295 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
296 * @author Robert Lemke <robert@typo3.org>
297 */
298 public function findEntriesByTag($tag) {
299 if (!is_object($this->cache)) {
300 throw new t3lib_cache_Exception(
301 'Yet no cache frontend has been set via setCache().',
302 1204111376
303 );
304 }
305
306 $path = $this->cacheDirectory . 'tags/';
307 $pattern = $path . $tag . '/*';
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('_', basename($filename));
317 $cacheEntries[$entryIdentifier] = $entryIdentifier;
318 }
319
320 return array_values($cacheEntries);
321 }
322
323 /**
324 * Finds and returns all cache entry identifiers which are tagged by the specified tags.
325 * The asterisk ("*") is allowed as a wildcard at the beginning and the end of
326 * a tag.
327 *
328 * @param array Array of tags to search for, the "*" wildcard is supported
329 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
330 * @author Ingo Renner <ingo@typo3.org>
331 */
332 public function findEntriesByTags(array $tags) {
333 $taggedEntries = array();
334 $foundEntries = array();
335
336 foreach ($tags as $tag) {
337 $taggedEntries[$tag] = $this->findEntriesByTag($tag);
338 }
339
340 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
341
342 foreach ($intersectedTaggedEntries as $entryIdentifier) {
343 $foundEntries[$entryIdentifier] = $entryIdentifier;
344 }
345
346 return $foundEntries;
347 }
348
349 /**
350 * Removes all cache entries of this cache.
351 *
352 * @return void
353 * @author Robert Lemke <robert@typo3.org>
354 */
355 public function flush() {
356 if (!is_object($this->cache)) {
357 throw new t3lib_cache_Exception(
358 'Yet no cache frontend has been set via setCache().',
359 1204111376
360 );
361 }
362
363 $path = $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/';
364 $pattern = $path . '*/*/*';
365 $filesFound = glob($pattern);
366
367 if ($filesFound === FALSE || count($filesFound) == 0) {
368 return;
369 }
370
371 foreach($filesFound as $filename) {
372 list(,$entryIdentifier) = explode('_', basename($filename));
373 $this->remove($entryIdentifier);
374 }
375 }
376
377 /**
378 * Removes all cache entries of this cache which are tagged by the specified tag.
379 *
380 * @param string The tag the entries must have
381 * @return void
382 * @author Ingo Renner <ingo@typo3.org>
383 */
384 public function flushByTag($tag) {
385 $path = $this->cacheDirectory . 'tags/' . $tag . '/';
386 $pattern = $path . '*';
387 $filesFound = glob($pattern);
388
389 foreach ($filesFound as $file) {
390 unlink($file);
391 }
392 rmdir($path);
393 }
394
395 /**
396 * Renders a file name for the specified cache entry
397 *
398 * @param string Identifier for the cache entry
399 * @param DateTime Date and time specifying the expiration of the entry. Must be a UTC time.
400 * @return string Filename of the cache data file
401 * @author Robert Lemke <robert@typo3.org>
402 */
403 protected function renderCacheFilename($identifier, DateTime $expiryTime) {
404 $filename = $expiryTime->format('Y-m-d\TH\;i\;sO') . '_' . $identifier;
405
406 return $filename;
407 }
408
409 /**
410 * Tries to find the cache entry for the specified identifier.
411 * Usually only one cache entry should be found - if more than one exist, this
412 * is due to some error or crash.
413 *
414 * @param string The cache entry identifier
415 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
416 * @author Robert Lemke <robert@typo3.org>
417 * @throws t3lib_cache_Exception if no frontend has been set
418 */
419 protected function findCacheFilesByEntry($entryIdentifier) {
420 if (!is_object($this->cache)) {
421 throw new t3lib_cache_Exception(
422 'Yet no cache frontend has been set via setCache().',
423 1204111376
424 );
425 }
426
427 $path = $this->cacheDirectory . 'data/' . $this->cache->getIdentifier() . '/';
428 $pattern = $path . '*/*/????-??-?????;??;???????_' . $entryIdentifier;
429 $filesFound = glob($pattern);
430 $validFilesFound = array();
431
432 if ($filesFound === FALSE || count($filesFound) == 0) {
433 return FALSE;
434 }
435
436 foreach ($filesFound as $pathAndFilename) {
437 $expiryTimeAndIdentifier = explode('/', $pathAndFilename);
438 $expiryTime = substr(array_pop($expiryTimeAndIdentifier), 0, -(strlen($entryIdentifier) + 1));
439
440 $expiryTimeParsed = strtotime(str_replace(';', ':', $expiryTime));
441
442 $now = new DateTime(
443 'now',
444 new DateTimeZone('UTC')
445 );
446 $now = (int) $now->format('U');
447
448 if ($expiryTimeParsed > $now) {
449 $validFilesFound[] = $pathAndFilename;
450 } else {
451 unlink($pathAndFilename);
452 }
453 }
454
455 if (count($validFilesFound) == 0) {
456 return FALSE;
457 }
458
459 return $validFilesFound;
460 }
461
462
463 /**
464 * Tries to find the tag entries for the specified cache entry.
465 *
466 * @param string The cache entry identifier to find tag files for
467 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
468 * @author Robert Lemke <robert@typo3.org>
469 * @throws t3lib_cache_Exception if no frontend has been set
470 */
471 protected function findTagFilesByEntry($entryIdentifier) {
472 if (!is_object($this->cache)) {
473 throw new t3lib_cache_Exception(
474 'Yet no cache frontend has been set via setCache().',
475 1204111376
476 );
477 }
478
479 $path = $this->cacheDirectory . 'tags/';
480 $pattern = $path . '*/' . $this->cache->getIdentifier() . '_' . $entryIdentifier;
481 $filesFound = glob($pattern);
482
483 if ($filesFound === FALSE || count($filesFound) == 0) {
484 return FALSE;
485 }
486
487 return $filesFound;
488 }
489 }
490
491
492 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_file.php']) {
493 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_file.php']);
494 }
495
496 ?>