1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie;
13
14 class FileCache
15 {
16 const T_COMPRESS = 'compress';
17 const T_REPOSITORY = 'repository';
18 const T_REPOSITORY_DELETE_RATIO = 'repository_delete_ratio';
19 const T_REPOSITORY_SIZE = 'repository_size';
20 const T_SERIALIZE = 'serialize';
21 const T_MODIFIED_TIME = 'modified_time';
22
23 public $compress = false;
24 public $repository;
25 public $repository_delete_ratio = .25;
26 public $repository_size = 512;
27 public $serialize = false;
28 public $modified_time;
29
30 protected $root;
31
32 public function __construct(array $tags)
33 {
34 if (empty($tags[self::T_REPOSITORY]))
35 {
36 throw new Exception('The %tag tag is required', [ '%tag' => 'T_REPOSITORY' ]);
37 }
38
39 foreach ($tags as $tag => $value)
40 {
41 $this->$tag = $value;
42 }
43
44 if (strpos($this->repository, DOCUMENT_ROOT) === 0)
45 {
46 $this->repository = substr($this->repository, strlen(DOCUMENT_ROOT) - 1);
47 }
48
49 $this->root = realpath(\ICanBoogie\DOCUMENT_ROOT . $this->repository);
50 }
51
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
68
69 public function get($file, $constructor, $userdata=null)
70 {
71 if (!is_dir($this->root))
72 {
73 throw new Exception('The repository %repository does not exists.', [ '%repository' => $this->repository ], 404);
74 }
75
76 $location = getcwd();
77
78 chdir($this->root);
79
80 if (!is_file($file) || ($this->modified_time && $this->modified_time > filemtime($file)))
81 {
82 $file = call_user_func($constructor, $this, $file, $userdata);
83 }
84
85 chdir($location);
86
87 return $file ? $this->repository . '/' . $file : $file;
88 }
89
90 public function exists($key)
91 {
92 $location = getcwd();
93
94 chdir($this->root);
95
96 $rc = file_exists($key);
97
98 chdir($location);
99
100 return $rc;
101 }
102
103 104 105 106 107 108 109 110 111 112 113 114 115 116
117 public function load($key, $constructor, $userdata=null)
118 {
119
120
121
122
123
124 if (!is_dir($this->root))
125 {
126 throw new Exception('The repository %repository does not exists.', [ '%repository' => $this->repository ], 404);
127
128 return call_user_func($contructor, $userdata, $this, $key);
129 }
130
131
132
133
134
135 $location = getcwd();
136
137 chdir($this->root);
138
139 $contents = null;
140
141 if (is_readable($key))
142 {
143 $contents = file_get_contents($key);
144
145 if ($this->compress)
146 {
147 $contents = gzinflate($contents);
148 }
149
150 if ($this->serialize)
151 {
152 $contents = unserialize($contents);
153 }
154 }
155
156 if ($contents === null)
157 {
158 $contents = call_user_func($constructor, $userdata, $this, $key);
159
160 $this->save($key, $contents);
161 }
162
163 chdir($location);
164
165 return $contents;
166 }
167
168 169 170 171 172 173 174 175
176 protected function save($file, $contents)
177 {
178 if (!is_writable($this->root))
179 {
180 throw new Exception('The repository %repository is not writable.', [ '%repository' => $this->repository ]);
181 }
182
183 $location = getcwd();
184
185 chdir($this->root);
186
187 if ($this->serialize)
188 {
189 $contents = serialize($contents);
190 }
191
192 if ($this->compress)
193 {
194 $contents = gzdeflate($contents);
195 }
196
197 $rc = file_put_contents($file, $contents, LOCK_EX);
198
199 chdir($location);
200
201 return $rc;
202 }
203
204 public function store($key, $data)
205 {
206 return $this->save($key, $data);
207 }
208
209 public function retrieve($key)
210 {
211 $location = getcwd();
212
213 chdir($this->root);
214
215 $rc = file_get_contents($key);
216
217 chdir($location);
218
219 return $rc;
220 }
221
222 public function delete($file)
223 {
224 return $this->unlink([ $file => true ]);
225 }
226
227 228 229 230 231 232 233 234
235
236 protected function read()
237 {
238 $root = $this->root;
239
240 if (!is_dir($root))
241 {
242
243
244 return false;
245 }
246
247 try
248 {
249 $dir = new \DirectoryIterator($root);
250 }
251 catch (\UnexpectedValueException $e)
252 {
253 throw new Exception('Unable to open directory %root', [ '%root' => $root ]);
254 }
255
256
257
258
259
260
261 $files = [];
262
263 foreach ($dir as $file)
264 {
265 if (!$file->isDot())
266 {
267 $files[$file->getFilename()] = [ $file->getCTime(), $file->getSize() ];
268 }
269 }
270
271 return $files;
272 }
273
274 protected function unlink($files)
275 {
276 if (!$files)
277 {
278 return;
279 }
280
281
282
283
284
285 $location = getcwd();
286
287 chdir($this->root);
288
289
290
291
292
293 $lh = fopen('.lock', 'w+');
294
295 if (!$lh)
296 {
297 Debug::trigger('Unable to lock %repository', [ '%repository' => $this->repository ]);
298
299 chdir($location);
300
301 return;
302 }
303
304
305
306
307
308 $n = 10;
309
310 while (!flock($lh, LOCK_EX | LOCK_NB))
311 {
312
313
314
315
316
317
318 usleep(round(rand(0, 100) * 1000));
319
320 if (!--$n)
321 {
322
323
324
325
326
327 chdir($location);
328
329 return;
330 }
331 }
332
333
334
335
336
337 foreach ($files as $file => $dummy)
338 {
339
340
341
342
343
344 if (!file_exists($file))
345 {
346 continue;
347 }
348
349 unlink($file);
350 }
351
352 chdir($location);
353
354
355
356
357
358 fclose($lh);
359 }
360
361 362 363 364 365
366
367 public function clear()
368 {
369 $files = $this->read();
370
371 return $this->unlink($files);
372 }
373
374 375 376 377 378
379
380 public function clean()
381 {
382 $files = $this->read();
383
384 if (!$files)
385 {
386 return;
387 }
388
389 $totalsize = 0;
390
391 foreach ($files as $stat)
392 {
393 $totalsize += $stat[1];
394 }
395
396 $repository_size = $this->repository_size * 1024;
397
398 if ($totalsize < $repository_size)
399 {
400
401
402
403
404 return;
405 }
406
407
408
409
410
411
412
413 asort($files);
414
415 $deletesize = $repository_size * $this->repository_delete_ratio;
416
417 $i = 0;
418
419 foreach ($files as $file => $stat)
420 {
421 $i++;
422
423 $deletesize -= $stat[1];
424
425 if ($deletesize < 0)
426 {
427 break;
428 }
429 }
430
431 $files = array_slice($files, 0, $i);
432
433 return $this->unlink($files);
434 }
435 }