tao-test/app/generis/common/persistence/class.PhpFileDriver.php

480 lines
13 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2013 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
* @author Lionel Lecaque <lionel@taotesting.com>
* @license GPLv2
* @package
*
*/
class common_persistence_PhpFileDriver implements common_persistence_KvDriver, common_persistence_Purgable
{
/**
* The TTL mode offset in the connection parameters.
*/
const OPTION_TTL = 'ttlMode';
/**
* The value offset in the record.
*/
const ENTRY_VALUE = 'value';
/**
* The expiration timestamp of the record.
*/
const ENTRY_EXPIRATION = 'expiresAt';
/**
* List of characters permited in filename
* @var array
*/
private static $ALLOWED_CHARACTERS = ['A' => '','B' => '','C' => '','D' => '','E' => '','F' => '','G' => '','H' => '','I' => '','J' => '','K' => '','L' => '','M' => '','N' => '','O' => '','P' => '','Q' => '','R' => '','S' => '','T' => '','U' => '','V' => '','W' => '','X' => '','Y' => '','Z' => '','a' => '','b' => '','c' => '','d' => '','e' => '','f' => '','g' => '','h' => '','i' => '','j' => '','k' => '','l' => '','m' => '','n' => '','o' => '','p' => '','q' => '','r' => '','s' => '','t' => '','u' => '','v' => '','w' => '','x' => '','y' => '','z' => '',0 => '',1 => '',2 => '',3 => '',4 => '',5 => '',6 => '',7 => '',8 => '',9 => '','_' => '','-' => ''];
/**
* absolute path of the directory to use
* ending on a directory seperator
*
* @var string
*/
private $directory;
/**
* Nr of subfolder levels in order to prevent filesystem bottlenecks
* Only used in non human readable mode
*
* @var int
*/
private $levels;
/**
* Whenever or not the filenames should be human readable
* FALSE by default for performance issues with many keys
*
* @var boolean
*/
private $humanReadable;
/**
* @var bool
*/
private $ttlMode;
/**
* Using 3 default levels, so the files get split up into
* 16^3 = 4096 induvidual directories
*
* @var int
*/
const DEFAULT_LEVELS = 3;
const DEFAULT_MASK = 0700;
/**
* (non-PHPdoc)
* @see common_persistence_Driver::connect()
*/
public function connect($id, array $params)
{
$this->directory = isset($params['dir'])
? $params['dir'] . ($params['dir'][strlen($params['dir']) - 1] === DIRECTORY_SEPARATOR ? '' : DIRECTORY_SEPARATOR)
: FILES_PATH . 'generis' . DIRECTORY_SEPARATOR . $id . DIRECTORY_SEPARATOR;
$this->levels = isset($params['levels']) ? $params['levels'] : self::DEFAULT_LEVELS;
$this->humanReadable = isset($params['humanReadable']) ? $params['humanReadable'] : false;
// Sets ttl mode TRUE when the passed ttl mode is true.
$this->setTtlMode(
(isset($params[static::OPTION_TTL]) && $params[static::OPTION_TTL] == true)
);
return new common_persistence_KeyValuePersistence($params, $this);
}
/**
* (non-PHPdoc)
* @see common_persistence_KvDriver::set()
*
* @throws common_exception_NotImplemented
* @throws \common_exception_Error
*/
public function set($id, $value, $ttl = null, $nx = false)
{
if ($this->isTtlMode()) {
$value = [
static::ENTRY_VALUE => $value,
static::ENTRY_EXPIRATION => $this->calculateExpiresAt($ttl),
];
} elseif (null !== $ttl) {
throw new common_exception_NotImplemented('TTL not implemented in ' . __CLASS__);
}
if ($nx) {
throw new common_exception_NotImplemented('NX not implemented in ' . __CLASS__);
}
return $this->writeFile($id, $value);
}
/**
* Calculates and returns the expires at timestamp or null on empty ttl.
*
* @param $ttl
*
* @return int|null
*/
protected function calculateExpiresAt($ttl)
{
return $ttl === null
? null
: $this->getTime() + $ttl
;
}
/**
* Writes the file.
*
* @param $id
* @param $value
* @param callable $preWriteValueProcessor The value preprocessor method.
*
* @return bool
*
* @throws \common_exception_Error
*/
private function writeFile($id, $value, $preWriteValueProcessor = null)
{
$filePath = $this->getPath($id);
$this->makeDirectory(dirname($filePath), self::DEFAULT_MASK);
// we first open with 'c' in case the flock fails
// 'w' would empty the file that someone else might be working on
if (false !== ($fp = @fopen($filePath, 'c')) && true === flock($fp, LOCK_EX)) {
// Runs the pre write callable.
if (is_callable($preWriteValueProcessor)) {
$value = call_user_func($preWriteValueProcessor, $id);
}
// We first need to truncate.
ftruncate($fp, 0);
$string = $this->getContent($id, $value);
$success = fwrite($fp, $string);
@flock($fp, LOCK_UN);
@fclose($fp);
if ($success) {
// OPcache workaround
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filePath, true);
}
} else {
common_Logger::w('Could not write ' . $filePath);
}
return $success !== false;
} else {
common_Logger::w('Could not obtain lock on ' . $filePath);
return false;
}
}
/**
* Create directory and suppress warning message
* @param $path
* @param int $mode
* @return bool
*/
private function makeDirectory(string $path, int $mode)
{
if (is_dir($path) || @mkdir($path, $mode, true)) {
return true;
}
if (is_dir($path)) {
\common_Logger::w(sprintf('Directory already exists. Path: \'%s\'', $path));
} elseif (is_file($path)) {
\common_Logger::w(sprintf('Directory was not created. File with the same name already exists. Path: \'%s\'', $path));
} else {
\common_Logger::w(sprintf('Directory was not created. Path: \'%s\'', $path));
}
return false;
}
/**
* (non-PHPdoc)
* @see common_persistence_KvDriver::get()
*/
public function get($id)
{
$entry = $this->readFile($id);
if ($entry != false && $this->isTtlMode()) {
$entry = (is_null($entry[static::ENTRY_EXPIRATION]) || $entry[static::ENTRY_EXPIRATION] > $this->getTime())
? $entry[static::ENTRY_VALUE]
: false
;
}
return $entry;
}
/**
* Returns the processed entry.
*
* @param $id
*
* @return mixed
*/
private function readFile($id)
{
return @include $this->getPath($id);
}
/**
* Returns the current timestamp.
*
* @return int
*/
public function getTime()
{
return time();
}
/**
* (non-PHPdoc)
* @see common_persistence_KvDriver::exists()
*/
public function exists($id)
{
if (!$this->isTtlMode()) {
return file_exists($this->getPath($id));
} else {
return $this->get($id) !== false;
}
}
/**
* (non-PHPdoc)
* @see common_persistence_KvDriver::del()
*/
public function del($id)
{
$filePath = $this->getPath($id);
// invalidate opcache first, fails on already deleted file
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filePath, true);
}
$success = @unlink($filePath);
return $success;
}
/**
* Increment existing value
*
* @param string $id
*
* @return mixed
*
* @throws \common_exception_Error
*/
public function incr($id)
{
return $this->writeFile($id, '', [$this, 'getIncreasedValueEntry']);
}
/**
* Returns the increased value entry.
*
* @param $id
*
* @return mixed
*/
private function getIncreasedValueEntry($id)
{
$value = intval($this->get($id));
$value++;
if ($this->isTtlMode()) {
$value = [
static::ENTRY_VALUE => $value,
static::ENTRY_EXPIRATION => null,
];
}
return $value;
}
/**
* Decrement existing value
*
* @param $id
*
* @return mixed
*
* @throws \common_exception_Error
*/
public function decr($id)
{
return $this->writeFile($id, '', [$this, 'getDecreasedValueEntry']);
}
/**
* Returns the decreased value entry.
*
* @param $id
*
* @return mixed
*/
private function getDecreasedValueEntry($id)
{
$value = intval($this->get($id));
$value--;
if ($this->isTtlMode()) {
$value = [
static::ENTRY_VALUE => $value,
static::ENTRY_EXPIRATION => null,
];
}
return $value;
}
/**
* purge the persistence directory
*
* @return boolean
*/
public function purge()
{
if (file_exists($this->directory)) {
$files = $this->getCachedFiles();
$successDeleted = true;
foreach ($files as $file) {
$successDeleted &= $this->removeCacheFile($file);
}
return (bool)$successDeleted;
}
return false;
}
/**
* Map the provided key to a relativ path
*
* @param string $key
* @return string
*/
protected function getPath($key)
{
if ($this->humanReadable) {
$path = $this->sanitizeReadableFileName($key);
} else {
$encoded = hash('md5', $key);
$path = implode(DIRECTORY_SEPARATOR, str_split(substr($encoded, 0, $this->levels))) . DIRECTORY_SEPARATOR . $encoded;
}
return $this->directory . $path . '.php';
}
/**
* Cannot use helpers_File::sanitizeInjectively() because
* of backwards compatibility
*
* @param string $key
* @return string
*/
protected function sanitizeReadableFileName($key)
{
$path = '';
foreach (str_split($key) as $char) {
$path .= isset(self::$ALLOWED_CHARACTERS[$char]) ? $char : base64_encode($char);
}
return $path;
}
/**
* Generate the php code that returns the provided value
*
* @param string $key
* @param mixed $value
*
* @return string
*
* @throws \common_exception_Error
*/
protected function getContent($key, $value)
{
return $this->humanReadable
? "<?php return " . common_Utils::toHumanReadablePhpString($value) . ";" . PHP_EOL
: "<?php return " . common_Utils::toPHPVariableString($value) . ";";
}
/**
* Returns TRUE when the connection is in TTL mode.
*
* @return bool
*/
public function isTtlMode()
{
return $this->ttlMode;
}
/**
* Sets the TTL mode.
*
* @param bool $ttlMode
*/
public function setTtlMode($ttlMode)
{
$this->ttlMode = $ttlMode;
}
/**
* @return array
*/
private function getCachedFiles()
{
try {
$files = helpers_File::scandir($this->directory, [
'recursive' => true,
'only' => helpers_File::SCAN_FILE,
'absolute' => true,
]);
} catch (common_Exception $exception) {
\common_Logger::e($exception->getMessage());
return [];
}
return $files;
}
/**
* @param string $filePath
* @return bool
*/
private function removeCacheFile($filePath)
{
try {
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filePath, true);
}
return helpers_File::remove($filePath);
} catch (common_exception_Error $exception) {
\common_Logger::e($exception->getMessage());
return false;
}
}
}