Source of file CacheAdapter.php
Size: 13,427 Bytes - Last Modified: 2021-03-22T10:33:08+00:00
/var/www/docs.ssmods.com/process/src/src/Adapter/CacheAdapter.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 | <?php namespace SilverStripe\S3\Adapter; use finfo; use League\Flysystem\AdapterInterface; use League\Flysystem\Config; use SilverStripe\S3\Cache\ContentCache; use Psr\SimpleCache\CacheInterface; use SilverStripe\S3\Cache\ContentWarmer; use SplFileInfo; /** * Wrapper adapter over a backend */ class CacheAdapter implements AdapterInterface { /** * Cache prefix for metadata array */ const METADATA = 'metadata_'; /** * Cache prefix for gnostic exists (null means unknown) */ const HAS = 'has_'; /** * Cache to use for metadata * * @var CacheInterface */ protected $metadataCache = null; /** * Cache to use for content (optional) * * @var ContentWarmer */ protected $contentCache = null; /** * @var AdapterInterface */ protected $backend = null; /** * @return CacheInterface */ public function getMetadataCache() { return $this->metadataCache; } /** * @param CacheInterface $metadataCache */ public function setMetadataCache(CacheInterface $metadataCache) { $this->metadataCache = $metadataCache; } /** * Cache of local cached file paths. * This service will be both used to intercept uploaded content and extract metadata, * and can be used to bypass upstream content request calls for existant content. * * @return ContentWarmer */ public function getContentCache() { return $this->contentCache; } /** * @param ContentWarmer $contentCache * @return $this */ public function setContentCache(ContentWarmer $contentCache = null) { $this->contentCache = $contentCache; return $this; } /** * Failover adapter * * @return AdapterInterface */ public function getBackend() { return $this->backend; } /** * Set failover adapter * * @param AdapterInterface $backend * @return $this */ public function setBackend(AdapterInterface $backend) { $this->backend = $backend; return $this; } public function read($path) { // check if content is available $fileKey = sha1($path); $localPath = $this->getContentCache()->get($fileKey); if ($localPath) { $contents = file_get_contents($localPath); return ['type' => 'file', 'path' => $path, 'contents' => $contents]; } // Cache miss, call and warm $result = $this->getBackend()->read($path); if ($result) { $this->getContentCache()->warmFromString($fileKey, $result['contents']); $this->setCachedHas($path, true); } return $result; } public function readStream($path) { // check if content is available $fileKey = sha1($path); $localPath = $this->getContentCache()->get($fileKey); if ($localPath) { $stream = fopen($localPath, 'rb'); return ['type' => 'file', 'path' => $path, 'stream' => $stream]; } // Cache miss, call and warm $result = $this->getBackend()->readStream($path); if ($result) { $this->getContentCache()->warmFromStream($fileKey, $result['stream']); $this->setCachedHas($path, true); } return $result; } public function updateStream($path, $resource, Config $config) { return $this->writeStream($path, $resource, $config); } public function writeStream($path, $resource, Config $config) { // Warm content cache $fileKey = sha1($path); $this->getContentCache()->warmFromStream($fileKey, $resource); // Update metadata cache $localPath = $this->getContentCache()->get($fileKey); $metadata = $this->getCachedMetadata($path) ?: null; if ($localPath) { $metadata = $this->approximateMetadata($path, $localPath); } // Warm / reset caches $this->setCachedHas($path, true); $this->setCachedMetadata($path, $metadata); return $this->getBackend()->writeStream($path, $resource, $config); } public function update($path, $contents, Config $config) { return $this->write($path, $contents, $config); } public function write($path, $contents, Config $config) { // Warm content cache $fileKey = sha1($path); $this->getContentCache()->warmFromString($fileKey, $contents); // Update metadata cache $localPath = $this->getContentCache()->get($fileKey); $metadata = $this->getCachedMetadata($path) ?: null; if ($localPath) { $metadata = $this->approximateMetadata($path, $localPath); } // Warm / reset caches $this->setCachedHas($path, true); $this->setCachedMetadata($path, $metadata); return $this->getBackend()->update($path, $contents, $config); } public function copy($path, $newpath) { $this->copyMetadata($path, $newpath); return $this->getBackend()->copy($path, $newpath); } public function rename($path, $newpath) { $this->copyMetadata($path, $newpath); $this->deleteMetadata($path); return $this->getBackend()->rename($path, $newpath); } public function delete($path) { $this->deleteMetadata($path); return $this->getBackend()->delete($path); } public function getVisibility($path) { // Not cached since subclasses hard-code return $this->getBackend()->getVisibility($path); } public function setVisibility($path, $visibility) { return $this->getBackend()->setVisibility($path, $visibility); } public function getSize($path) { return $this->getMetadata($path); } public function getMimeType($path) { return $this->getMetadata($path); } public function getTimestamp($path) { return $this->getMetadata($path); } /** * Get metadata. * Note: This method makes no assumption about existance in the backend, * and may find metadata for records that don't physically exist * * @param string $path * @return array|false */ public function getMetadata($path) { // Check cached metadata $metadata = $this->getCachedMetadata($path); if (isset($metadata)) { return $metadata ?: false; // Convert empty arrays to false } // Check if we can approximate from local cache, or fail over to backend $localPath = $this->getContentCache()->get(sha1($path)); if ($localPath) { $metadata = $this->approximateMetadata($path, $localPath); } else { // Fail over to call backend, and pre-warm has cache $metadata = $this->getBackendMetadata($path); $this->setCachedHas($path, !empty($metadata)); } // Save metadata for next time $this->setCachedMetadata($path, $metadata); return $metadata; } /** * Determine existence of this record in the cached backend. * Note: this method will also cache metadat, but uses a slightly more * discerning implementation of getMetadata() * * @param string $path * @return bool */ public function has($path) { // Check has-cache $exists = $this->getCachedHas($path); if (isset($exists)) { return $exists; } // Live request the backend cache $metadata = $this->getBackendMetadata($path); $has = !empty($metadata); $this->setCachedMetadata($path, $metadata); $this->setCachedHas($path, $has); return $has; } public function deleteDir($dirname) { return $this->getBackend()->deleteDir($dirname); } public function createDir($dirname, Config $config) { return $this->createDir($dirname, $config); } public function listContents($directory = '', $recursive = false) { return $this->getBackend()->listContents($directory, $recursive); } /** * Copy metadata from one path to another * * @param string $path * @param string $newPath */ protected function copyMetadata($path, $newPath) { // Share content cache $fileKey = sha1($path); $newKey = sha1($newPath); $localPath = $this->getContentCache()->get($fileKey); if ($localPath) { $this->getContentCache()->set($newKey, $localPath); } // Share metadata cache (or infer from cached content) $metadata = $this->getCachedMetadata($path) ?: []; if ($metadata || $localPath) { // Combine metadata from source with destination // Either an existing $metadata or $localPath should be enough // to generate enough data to cache $newMetadata = array_merge( $metadata, $this->approximateMetadata($newPath, $localPath) ); $newMetadata['timestamp'] = time(); $this->setCachedMetadata($newPath, $newMetadata); } // Mark new location as has = true $this->setCachedHas($newPath, true); } /** * Remove metadata from a file we know doesn't exist anymore * * @param string $path */ public function deleteMetadata($path) { $this->setCachedMetadata($path, false); $this->setCachedHas($path, false); $this->getContentCache()->delete(sha1($path)); } /** * Build metadata from local path. * Note: If $cachePath is not provided, only minimal metadata can be inferred from * the local path. * * @param string $path * @param string $cachePath Optional local file to get extra information from * @return array */ protected function approximateMetadata($path, $cachePath = null) { // Get metadata from path $pathInfo = pathinfo($path); unset($pathInfo['filename']); $metadata = array_merge($pathInfo, [ 'type' => 'file', 'path' => $path, ]); if (!$cachePath) { return $metadata; } // Get metadata from physical file $info = new SplFileInfo($cachePath); $metadata = array_merge($metadata, [ 'type' => $info->getType(), 'timestamp' => $info->getMTime() ]); // Get file specific metadata if ($metadata['type'] === 'file') { $finfo = new Finfo(FILEINFO_MIME_TYPE); $metadata['mimetype'] = $finfo->file($cachePath); $metadata['size'] = $info->getSize(); } return $metadata; } /** * Get metadata for the cache * * @param string $path * @return array|false|null */ protected function getCachedMetadata($path) { $key = self::METADATA . sha1($path); return $this->getMetadataCache()->get($key); } /** * Get full metadata for the backend item. * Note that for some backends, a lot of extra work can be done, * but it does ensure a complete metadata is cached for each entry. * * @param string $path * @return array|false */ protected function getBackendMetadata($path) { // Fail over to call backend $metadata = $this->getBackend()->getMetadata($path); if (!$metadata) { return false; } // Some backends need extra calls to merge in certain fields if (!isset($metadata['size'])) { $metadata = array_merge($metadata, $this->getBackend()->getSize($path)); } if (!isset($metadata['mimetype'])) { $metadata = array_merge($metadata, $this->getBackend()->getMimetype($path)); } if (!isset($metadata['timestamp'])) { $metadata = array_merge($metadata, $this->getBackend()->getTimestamp($path)); } return $metadata; } /** * Set metadata in cache * * @param string $path * @param array|false|null $metadata * @return $this */ protected function setCachedMetadata($path, $metadata) { $key = self::METADATA . sha1($path); if (isset($metadata)) { $this->getMetadataCache()->set($key, $metadata); } else { $this->getMetadataCache()->delete($key); } return $this; } /** * Check if cache knows if this file exists. * Null means it doesn't know. * * @param string $path * @return bool|null */ protected function getCachedHas($path) { $key = self::HAS . sha1($path); return $this->getMetadataCache()->get($key); } /** * Tell cache that we know if this file exists * * @param string $path * @param bool|null $exists * @return $this */ protected function setCachedHas($path, $exists) { $key = self::HAS . sha1($path); if (isset($exists)) { $this->getMetadataCache()->set($key, $exists); } else { $this->getMetadataCache()->delete($key); } return $this; } } |