FileStore.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <?php
  2. namespace Illuminate\Cache;
  3. use Exception;
  4. use Illuminate\Contracts\Cache\LockProvider;
  5. use Illuminate\Contracts\Cache\Store;
  6. use Illuminate\Contracts\Filesystem\LockTimeoutException;
  7. use Illuminate\Filesystem\Filesystem;
  8. use Illuminate\Filesystem\LockableFile;
  9. use Illuminate\Support\InteractsWithTime;
  10. class FileStore implements Store, LockProvider
  11. {
  12. use InteractsWithTime, RetrievesMultipleKeys;
  13. /**
  14. * The Illuminate Filesystem instance.
  15. *
  16. * @var \Illuminate\Filesystem\Filesystem
  17. */
  18. protected $files;
  19. /**
  20. * The file cache directory.
  21. *
  22. * @var string
  23. */
  24. protected $directory;
  25. /**
  26. * The file cache lock directory.
  27. *
  28. * @var string|null
  29. */
  30. protected $lockDirectory;
  31. /**
  32. * Octal representation of the cache file permissions.
  33. *
  34. * @var int|null
  35. */
  36. protected $filePermission;
  37. /**
  38. * Create a new file cache store instance.
  39. *
  40. * @param \Illuminate\Filesystem\Filesystem $files
  41. * @param string $directory
  42. * @param int|null $filePermission
  43. * @return void
  44. */
  45. public function __construct(Filesystem $files, $directory, $filePermission = null)
  46. {
  47. $this->files = $files;
  48. $this->directory = $directory;
  49. $this->filePermission = $filePermission;
  50. }
  51. /**
  52. * Retrieve an item from the cache by key.
  53. *
  54. * @param string|array $key
  55. * @return mixed
  56. */
  57. public function get($key)
  58. {
  59. return $this->getPayload($key)['data'] ?? null;
  60. }
  61. /**
  62. * Store an item in the cache for a given number of seconds.
  63. *
  64. * @param string $key
  65. * @param mixed $value
  66. * @param int $seconds
  67. * @return bool
  68. */
  69. public function put($key, $value, $seconds)
  70. {
  71. $this->ensureCacheDirectoryExists($path = $this->path($key));
  72. $result = $this->files->put(
  73. $path, $this->expiration($seconds).serialize($value), true
  74. );
  75. if ($result !== false && $result > 0) {
  76. $this->ensurePermissionsAreCorrect($path);
  77. return true;
  78. }
  79. return false;
  80. }
  81. /**
  82. * Store an item in the cache if the key doesn't exist.
  83. *
  84. * @param string $key
  85. * @param mixed $value
  86. * @param int $seconds
  87. * @return bool
  88. */
  89. public function add($key, $value, $seconds)
  90. {
  91. $this->ensureCacheDirectoryExists($path = $this->path($key));
  92. $file = new LockableFile($path, 'c+');
  93. try {
  94. $file->getExclusiveLock();
  95. } catch (LockTimeoutException) {
  96. $file->close();
  97. return false;
  98. }
  99. $expire = $file->read(10);
  100. if (empty($expire) || $this->currentTime() >= $expire) {
  101. $file->truncate()
  102. ->write($this->expiration($seconds).serialize($value))
  103. ->close();
  104. $this->ensurePermissionsAreCorrect($path);
  105. return true;
  106. }
  107. $file->close();
  108. return false;
  109. }
  110. /**
  111. * Create the file cache directory if necessary.
  112. *
  113. * @param string $path
  114. * @return void
  115. */
  116. protected function ensureCacheDirectoryExists($path)
  117. {
  118. $directory = dirname($path);
  119. if (! $this->files->exists($directory)) {
  120. $this->files->makeDirectory($directory, 0777, true, true);
  121. // We're creating two levels of directories (e.g. 7e/24), so we check them both...
  122. $this->ensurePermissionsAreCorrect($directory);
  123. $this->ensurePermissionsAreCorrect(dirname($directory));
  124. }
  125. }
  126. /**
  127. * Ensure the created node has the correct permissions.
  128. *
  129. * @param string $path
  130. * @return void
  131. */
  132. protected function ensurePermissionsAreCorrect($path)
  133. {
  134. if (is_null($this->filePermission) ||
  135. intval($this->files->chmod($path), 8) == $this->filePermission) {
  136. return;
  137. }
  138. $this->files->chmod($path, $this->filePermission);
  139. }
  140. /**
  141. * Increment the value of an item in the cache.
  142. *
  143. * @param string $key
  144. * @param mixed $value
  145. * @return int
  146. */
  147. public function increment($key, $value = 1)
  148. {
  149. $raw = $this->getPayload($key);
  150. return tap(((int) $raw['data']) + $value, function ($newValue) use ($key, $raw) {
  151. $this->put($key, $newValue, $raw['time'] ?? 0);
  152. });
  153. }
  154. /**
  155. * Decrement the value of an item in the cache.
  156. *
  157. * @param string $key
  158. * @param mixed $value
  159. * @return int
  160. */
  161. public function decrement($key, $value = 1)
  162. {
  163. return $this->increment($key, $value * -1);
  164. }
  165. /**
  166. * Store an item in the cache indefinitely.
  167. *
  168. * @param string $key
  169. * @param mixed $value
  170. * @return bool
  171. */
  172. public function forever($key, $value)
  173. {
  174. return $this->put($key, $value, 0);
  175. }
  176. /**
  177. * Get a lock instance.
  178. *
  179. * @param string $name
  180. * @param int $seconds
  181. * @param string|null $owner
  182. * @return \Illuminate\Contracts\Cache\Lock
  183. */
  184. public function lock($name, $seconds = 0, $owner = null)
  185. {
  186. $this->ensureCacheDirectoryExists($this->lockDirectory ?? $this->directory);
  187. return new FileLock(
  188. new static($this->files, $this->lockDirectory ?? $this->directory, $this->filePermission),
  189. $name,
  190. $seconds,
  191. $owner
  192. );
  193. }
  194. /**
  195. * Restore a lock instance using the owner identifier.
  196. *
  197. * @param string $name
  198. * @param string $owner
  199. * @return \Illuminate\Contracts\Cache\Lock
  200. */
  201. public function restoreLock($name, $owner)
  202. {
  203. return $this->lock($name, 0, $owner);
  204. }
  205. /**
  206. * Remove an item from the cache.
  207. *
  208. * @param string $key
  209. * @return bool
  210. */
  211. public function forget($key)
  212. {
  213. if ($this->files->exists($file = $this->path($key))) {
  214. return $this->files->delete($file);
  215. }
  216. return false;
  217. }
  218. /**
  219. * Remove all items from the cache.
  220. *
  221. * @return bool
  222. */
  223. public function flush()
  224. {
  225. if (! $this->files->isDirectory($this->directory)) {
  226. return false;
  227. }
  228. foreach ($this->files->directories($this->directory) as $directory) {
  229. $deleted = $this->files->deleteDirectory($directory);
  230. if (! $deleted || $this->files->exists($directory)) {
  231. return false;
  232. }
  233. }
  234. return true;
  235. }
  236. /**
  237. * Retrieve an item and expiry time from the cache by key.
  238. *
  239. * @param string $key
  240. * @return array
  241. */
  242. protected function getPayload($key)
  243. {
  244. $path = $this->path($key);
  245. // If the file doesn't exist, we obviously cannot return the cache so we will
  246. // just return null. Otherwise, we'll get the contents of the file and get
  247. // the expiration UNIX timestamps from the start of the file's contents.
  248. try {
  249. if (is_null($contents = $this->files->get($path, true))) {
  250. return $this->emptyPayload();
  251. }
  252. $expire = substr($contents, 0, 10);
  253. } catch (Exception) {
  254. return $this->emptyPayload();
  255. }
  256. // If the current time is greater than expiration timestamps we will delete
  257. // the file and return null. This helps clean up the old files and keeps
  258. // this directory much cleaner for us as old files aren't hanging out.
  259. if ($this->currentTime() >= $expire) {
  260. $this->forget($key);
  261. return $this->emptyPayload();
  262. }
  263. try {
  264. $data = unserialize(substr($contents, 10));
  265. } catch (Exception) {
  266. $this->forget($key);
  267. return $this->emptyPayload();
  268. }
  269. // Next, we'll extract the number of seconds that are remaining for a cache
  270. // so that we can properly retain the time for things like the increment
  271. // operation that may be performed on this cache on a later operation.
  272. $time = $expire - $this->currentTime();
  273. return compact('data', 'time');
  274. }
  275. /**
  276. * Get a default empty payload for the cache.
  277. *
  278. * @return array
  279. */
  280. protected function emptyPayload()
  281. {
  282. return ['data' => null, 'time' => null];
  283. }
  284. /**
  285. * Get the full path for the given cache key.
  286. *
  287. * @param string $key
  288. * @return string
  289. */
  290. public function path($key)
  291. {
  292. $parts = array_slice(str_split($hash = sha1($key), 2), 0, 2);
  293. return $this->directory.'/'.implode('/', $parts).'/'.$hash;
  294. }
  295. /**
  296. * Get the expiration time based on the given seconds.
  297. *
  298. * @param int $seconds
  299. * @return int
  300. */
  301. protected function expiration($seconds)
  302. {
  303. $time = $this->availableAt($seconds);
  304. return $seconds === 0 || $time > 9999999999 ? 9999999999 : $time;
  305. }
  306. /**
  307. * Get the Filesystem instance.
  308. *
  309. * @return \Illuminate\Filesystem\Filesystem
  310. */
  311. public function getFilesystem()
  312. {
  313. return $this->files;
  314. }
  315. /**
  316. * Get the working directory of the cache.
  317. *
  318. * @return string
  319. */
  320. public function getDirectory()
  321. {
  322. return $this->directory;
  323. }
  324. /**
  325. * Set the cache directory where locks should be stored.
  326. *
  327. * @param string|null $lockDirectory
  328. * @return $this
  329. */
  330. public function setLockDirectory($lockDirectory)
  331. {
  332. $this->lockDirectory = $lockDirectory;
  333. return $this;
  334. }
  335. /**
  336. * Get the cache key prefix.
  337. *
  338. * @return string
  339. */
  340. public function getPrefix()
  341. {
  342. return '';
  343. }
  344. }