Skip to content

Instantly share code, notes, and snippets.

@czechdude
Created June 30, 2012 16:11
Show Gist options
  • Save czechdude/3024415 to your computer and use it in GitHub Desktop.
Save czechdude/3024415 to your computer and use it in GitHub Desktop.
FileStorage + SQLITE3 : Fix pro Nette 0.9.7 (asi celá větev 0.9.x) pokud používáte cache a máte PHP > 5.3.x a nemůžete tam rozběhnout SQLite2
<?php
/**
* This file is part of the Nette Framework.
*
* Copyright (c) 2004, 2010 David Grudl (http://davidgrudl.com)
*
* This source file is subject to the "Nette license", and/or
* GPL license. For more information please see http://nette.org
* @package Nette\Caching
*/
/**
* Cache file storage.
*
* @author David Grudl
*/
class FileStorage extends Object implements ICacheStorage
{
/**
* Atomic thread safe logic:
*
* 1) reading: open(r+b), lock(SH), read
* - delete?: delete*, close
* 2) deleting: delete*
* 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
*
* delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
*/
/**#@+ @internal cache file structure */
const META_HEADER_LEN = 28; // 22b signature + 6b meta-struct size + serialized meta-struct + data
// meta structure: array of
const META_TIME = 'time'; // timestamp
const META_SERIALIZED = 'serialized'; // is content serialized?
const META_EXPIRE = 'expire'; // expiration timestamp
const META_DELTA = 'delta'; // relative (sliding) expiration
const META_ITEMS = 'di'; // array of dependent items (file => timestamp)
const META_CALLBACKS = 'callbacks'; // array of callbacks (function, args)
/**#@-*/
/**#@+ additional cache structure */
const FILE = 'file';
const HANDLE = 'handle';
/**#@-*/
/** @var float probability that the clean() routine is started */
public static $gcProbability = 0.001;
/** @var bool */
public static $useDirectories;
/** @var string */
private $dir;
/** @var bool */
private $useDirs;
/** @var resource */
private $db;
public function __construct($dir)
{
if (self::$useDirectories === NULL) {
// checks whether directory is writable
$uniq = uniqid('_', TRUE);
umask(0000);
if (!@mkdir("$dir/$uniq", 0777)) { // @ - is escalated to exception
throw new InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
}
// tests subdirectory mode
self::$useDirectories = !ini_get('safe_mode');
if (!self::$useDirectories && @file_put_contents("$dir/$uniq/_", '') !== FALSE) { // @ - error is expected
self::$useDirectories = TRUE;
unlink("$dir/$uniq/_");
}
@rmdir("$dir/$uniq"); // @ - directory may not already exist
}
$this->dir = $dir;
$this->useDirs = (bool) self::$useDirectories;
if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
$this->clean(array());
}
}
/**
* Read from cache.
* @param string key
* @return mixed|NULL
*/
public function read($key)
{
$meta = $this->readMeta($this->getCacheFile($key), LOCK_SH);
if ($meta && $this->verify($meta)) {
return $this->readData($meta); // calls fclose()
} else {
return NULL;
}
}
/**
* Verifies dependencies.
* @param array
* @return bool
*/
private function verify($meta)
{
do {
if (!empty($meta[self::META_DELTA])) {
// meta[file] was added by readMeta()
if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) break;
touch($meta[self::FILE]);
} elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
break;
}
if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
break;
}
if (!empty($meta[self::META_ITEMS])) {
foreach ($meta[self::META_ITEMS] as $depFile => $time) {
$m = $this->readMeta($depFile, LOCK_SH);
if ($m[self::META_TIME] !== $time) break 2;
if ($m && !$this->verify($m)) break 2;
}
}
return TRUE;
} while (FALSE);
$this->delete($meta[self::FILE], $meta[self::HANDLE]); // meta[handle] & meta[file] was added by readMeta()
return FALSE;
}
/**
* Writes item into the cache.
* @param string key
* @param mixed data
* @param array dependencies
* @return void
*/
public function write($key, $data, array $dp)
{
$meta = array(
self::META_TIME => microtime(),
);
if (isset($dp[Cache::EXPIRATION])) {
if (empty($dp[Cache::SLIDING])) {
$meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time(); // absolute time
} else {
$meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; // sliding time
}
}
if (isset($dp[Cache::ITEMS])) {
foreach ((array) $dp[Cache::ITEMS] as $item) {
$depFile = $this->getCacheFile($item);
$m = $this->readMeta($depFile, LOCK_SH);
$meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
unset($m);
}
}
if (isset($dp[Cache::CALLBACKS])) {
$meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
}
$cacheFile = $this->getCacheFile($key);
if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
umask(0000);
if (!mkdir($dir, 0777)) {
return;
}
}
$handle = @fopen($cacheFile, 'r+b'); // @ - file may not exist
if (!$handle) {
$handle = fopen($cacheFile, 'wb');
if (!$handle) {
return;
}
}
if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
$db = $this->getDb();
$dbFile = $db->escapeString($cacheFile);
$query = '';
if (!empty($dp[Cache::TAGS])) {
foreach ((array) $dp[Cache::TAGS] as $tag) {
$query .= "INSERT INTO cache (file, tag) VALUES ('$dbFile', '" . $db->escapeString($tag) . "');";
}
}
if (isset($dp[Cache::PRIORITY])) {
$query .= "INSERT INTO cache (file, priority) VALUES ('$dbFile', '" . (int) $dp[Cache::PRIORITY] . "');";
}
if (!$db->exec("BEGIN; DELETE FROM cache WHERE file = '$dbFile'; $query COMMIT;")) {
$db->exec("ROLLBACK");
return;
}
}
flock($handle, LOCK_EX);
ftruncate($handle, 0);
if (!is_string($data)) {
$data = serialize($data);
$meta[self::META_SERIALIZED] = TRUE;
}
$head = serialize($meta) . '?>';
$head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
$headLen = strlen($head);
$dataLen = strlen($data);
do {
if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
break;
}
if (fwrite($handle, $data, $dataLen) !== $dataLen) {
break;
}
fseek($handle, 0);
if (fwrite($handle, $head, $headLen) !== $headLen) {
break;
}
fclose($handle);
return TRUE;
} while (FALSE);
$this->delete($cacheFile, $handle);
}
/**
* Removes item from the cache.
* @param string key
* @return void
*/
public function remove($key)
{
$this->delete($this->getCacheFile($key));
}
/**
* Removes items from the cache by conditions & garbage collector.
* @param array conditions
* @return void
*/
public function clean(array $conds)
{
$db = $this->getDb();
$all = !empty($conds[Cache::ALL]);
$collector = empty($conds);
// cleaning using file iterator
if ($all || $collector) {
$now = time();
$base = $this->dir . DIRECTORY_SEPARATOR . 'c';
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $entry) {
$path = (string) $entry;
if (strncmp($path, $base, strlen($base))) { // skip files out of cache
continue;
}
if ($entry->isDir()) { // collector: remove empty dirs
@rmdir($path); // @ - removing dirs is not necessary
continue;
}
if ($all) {
$this->delete($path);
} else { // collector
$meta = $this->readMeta($path, LOCK_SH);
if (!$meta) continue;
if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
$this->delete($path, $meta[self::HANDLE]);
continue;
}
fclose($meta[self::HANDLE]);
}
}
if ($all) { //&& extension_loaded('sqlite')
$db->exec("DELETE FROM cache");
}
return;
}
// cleaning using journal
if (!empty($conds[Cache::TAGS])) {
foreach ((array) $conds[Cache::TAGS] as $tag) {
$tmp[] = "'" . $db->escapeString($tag) . "'";
}
$query[] = "tag IN (" . implode(',', $tmp) . ")";
}
if (isset($conds[Cache::PRIORITY])) {
$query[] = "priority <= " . (int) $conds[Cache::PRIORITY];
}
if (isset($query)) {
$db = $this->getDb();
$query = implode(' OR ', $query);
$files = $db->query("SELECT file FROM cache WHERE $query");
while ($row = $files->fetchArray()) {
$this->delete($row['file']);
}
$ret = $db->exec("DELETE FROM cache WHERE $query");
}
}
/**
* Reads cache data from disk.
* @param string file path
* @param int lock mode
* @return array|NULL
*/
protected function readMeta($file, $lock)
{
$handle = @fopen($file, 'r+b'); // @ - file may not exist
if (!$handle) return NULL;
flock($handle, $lock);
$head = stream_get_contents($handle, self::META_HEADER_LEN);
if ($head && strlen($head) === self::META_HEADER_LEN) {
$size = (int) substr($head, -6);
$meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
$meta = @unserialize($meta); // intentionally @
if (is_array($meta)) {
fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
$meta[self::FILE] = $file;
$meta[self::HANDLE] = $handle;
return $meta;
}
}
fclose($handle);
return NULL;
}
/**
* Reads cache data from disk and closes cache file handle.
* @param array
* @return mixed
*/
protected function readData($meta)
{
$data = stream_get_contents($meta[self::HANDLE]);
fclose($meta[self::HANDLE]);
if (empty($meta[self::META_SERIALIZED])) {
return $data;
} else {
return @unserialize($data); // intentionally @
}
}
/**
* Returns file name.
* @param string
* @return string
*/
protected function getCacheFile($key)
{
if ($this->useDirs) {
$key = explode(Cache::NAMESPACE_SEPARATOR, $key, 2);
return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
} else {
return $this->dir . '/c_' . urlencode($key);
}
}
/**
* Deletes and closes file.
* @param string
* @param resource
* @return void
*/
private static function delete($file, $handle = NULL)
{
if (@unlink($file)) { // @ - file may not already exist
if ($handle) fclose($handle);
return;
}
if (!$handle) {
$handle = @fopen($file, 'r+'); // @ - file may not exist
}
if ($handle) {
flock($handle, LOCK_EX);
ftruncate($handle, 0);
fclose($handle);
@unlink($file); // @ - file may not already exist
}
}
/**
* Returns SQLite resource.
* @return SQLite3
*/
protected function getDb()
{
if ($this->db === NULL) {
/*if (!extension_loaded('sqlite')) {
throw new InvalidStateException("SQLite extension is required for storing tags and priorities.");
}*/
$this->db = new SQLite3($this->dir . '/cachejournal.sdb');
@$this->db->exec('CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
CREATE INDEX IDX_FILE ON cache (file); CREATE INDEX IDX_PRI ON cache (priority); CREATE INDEX IDX_TAG ON cache (tag);'); // @ - table may already exist
}
return $this->db;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment