@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Interface to a directory-based disk cache. Storage persists across requests.
5 *
6 * This cache is very very slow, and most suitable for command line scripts
7 * which need to build large caches derived from sources like working copies
8 * (for example, Diviner). This cache performs better for large amounts of
9 * data than @{class:PhutilOnDiskKeyValueCache} because each key is serialized
10 * individually, but this comes at the cost of having even slower reads and
11 * writes.
12 *
13 * In addition to having slow reads and writes, this entire cache locks for
14 * any read or write activity.
15 *
16 * Keys for this cache treat the character "/" specially, and encode it as
17 * a new directory on disk. This can help keep the cache organized and keep the
18 * number of items in any single directory under control, by using keys like
19 * "ab/cd/efghijklmn".
20 *
21 * @task kvimpl Key-Value Cache Implementation
22 * @task storage Cache Storage
23 */
24final class PhutilDirectoryKeyValueCache extends PhutilKeyValueCache {
25
26 private $lock;
27 private $cacheDirectory;
28
29
30/* -( Key-Value Cache Implementation )------------------------------------- */
31
32
33 public function isAvailable() {
34 return true;
35 }
36
37
38 public function getKeys(array $keys) {
39 $this->validateKeys($keys);
40
41 try {
42 $this->lockCache();
43 } catch (PhutilLockException $ex) {
44 return array();
45 }
46
47 $now = time();
48
49 $results = array();
50 foreach ($keys as $key) {
51 $key_file = $this->getKeyFile($key);
52 try {
53 $data = Filesystem::readFile($key_file);
54 } catch (FilesystemException $ex) {
55 continue;
56 }
57
58 $data = unserialize($data);
59 if (!$data) {
60 continue;
61 }
62
63 if (isset($data['ttl']) && $data['ttl'] < $now) {
64 continue;
65 }
66
67 $results[$key] = $data['value'];
68 }
69
70 $this->unlockCache();
71
72 return $results;
73 }
74
75
76 public function setKeys(array $keys, $ttl = null) {
77 $this->validateKeys(array_keys($keys));
78
79 $this->lockCache(15);
80
81 if ($ttl) {
82 $ttl_epoch = time() + $ttl;
83 } else {
84 $ttl_epoch = null;
85 }
86
87 foreach ($keys as $key => $value) {
88 $dict = array(
89 'value' => $value,
90 );
91 if ($ttl_epoch) {
92 $dict['ttl'] = $ttl_epoch;
93 }
94
95 try {
96 $key_file = $this->getKeyFile($key);
97 $key_dir = dirname($key_file);
98 if (!Filesystem::pathExists($key_dir)) {
99 Filesystem::createDirectory(
100 $key_dir,
101 $mask = 0755,
102 $recursive = true);
103 }
104
105 $new_file = $key_file.'.new';
106 Filesystem::writeFile($new_file, serialize($dict));
107 Filesystem::rename($new_file, $key_file);
108 } catch (FilesystemException $ex) {
109 phlog($ex);
110 }
111 }
112
113 $this->unlockCache();
114
115 return $this;
116 }
117
118
119 public function deleteKeys(array $keys) {
120 $this->validateKeys($keys);
121
122 $this->lockCache(15);
123
124 foreach ($keys as $key) {
125 $path = $this->getKeyFile($key);
126 Filesystem::remove($path);
127
128 // If removing this key leaves the directory empty, clean it up. Then
129 // clean up any empty parent directories.
130 $path = dirname($path);
131 do {
132 if (!Filesystem::isDescendant($path, $this->getCacheDirectory())) {
133 break;
134 }
135 if (Filesystem::listDirectory($path, true)) {
136 break;
137 }
138 Filesystem::remove($path);
139 $path = dirname($path);
140 } while (true);
141 }
142
143 $this->unlockCache();
144
145 return $this;
146 }
147
148
149 public function destroyCache() {
150 Filesystem::remove($this->getCacheDirectory());
151 return $this;
152 }
153
154
155/* -( Cache Storage )------------------------------------------------------ */
156
157
158 /**
159 * @task storage
160 */
161 public function setCacheDirectory($directory) {
162 $this->cacheDirectory = rtrim($directory, '/').'/';
163 return $this;
164 }
165
166
167 /**
168 * @task storage
169 */
170 private function getCacheDirectory() {
171 if (!$this->cacheDirectory) {
172 throw new PhutilInvalidStateException('setCacheDirectory');
173 }
174 return $this->cacheDirectory;
175 }
176
177
178 /**
179 * @task storage
180 */
181 private function getKeyFile($key) {
182 // Colon is a drive separator on Windows.
183 $key = str_replace(':', '_', $key);
184
185 // NOTE: We add ".cache" to each file so we don't get a collision if you
186 // set the keys "a" and "a/b". Without ".cache", the file "a" would need
187 // to be both a file and a directory.
188 return $this->getCacheDirectory().$key.'.cache';
189 }
190
191
192 /**
193 * @task storage
194 */
195 private function validateKeys(array $keys) {
196 foreach ($keys as $key) {
197 // NOTE: Use of "." is reserved for ".lock", "key.new" and "key.cache".
198 // Use of "_" is reserved for converting ":".
199 if (!preg_match('@^[a-zA-Z0-9/:-]+$@', $key)) {
200 throw new Exception(
201 pht(
202 "Invalid key '%s': directory caches may only contain letters, ".
203 "numbers, hyphen, colon and slash.",
204 $key));
205 }
206 }
207 }
208
209
210 /**
211 * @task storage
212 */
213 private function lockCache($wait = 0) {
214 if ($this->lock) {
215 throw new Exception(
216 pht(
217 'Trying to %s with a lock!',
218 __FUNCTION__.'()'));
219 }
220
221 if (!Filesystem::pathExists($this->getCacheDirectory())) {
222 Filesystem::createDirectory($this->getCacheDirectory(), 0755, true);
223 }
224
225 $lock = PhutilFileLock::newForPath($this->getCacheDirectory().'.lock');
226 $lock->lock($wait);
227
228 $this->lock = $lock;
229 }
230
231
232 /**
233 * @task storage
234 */
235 private function unlockCache() {
236 if (!$this->lock) {
237 throw new PhutilInvalidStateException('lockCache');
238 }
239
240 $this->lock->unlock();
241 $this->lock = null;
242 }
243
244}