Flysystem::lock()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 2
b 0
f 0
nc 4
nop 1
dl 0
loc 19
rs 9.9332
1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use League\Flysystem\FileExistsException;
0 ignored issues
show
Bug introduced by
The type League\Flysystem\FileExistsException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use League\Flysystem\FileNotFoundException;
0 ignored issues
show
Bug introduced by
The type League\Flysystem\FileNotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use League\Flysystem\Filesystem;
0 ignored issues
show
Bug introduced by
The type League\Flysystem\Filesystem was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use League\Flysystem\UnableToDeleteFile;
0 ignored issues
show
Bug introduced by
The type League\Flysystem\UnableToDeleteFile was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use League\Flysystem\UnableToReadFile;
0 ignored issues
show
Bug introduced by
The type League\Flysystem\UnableToReadFile was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use League\Flysystem\UnableToWriteFile;
0 ignored issues
show
Bug introduced by
The type League\Flysystem\UnableToWriteFile was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use MatthiasMullie\Scrapbook\Adapters\Collections\Flysystem as Collection;
12
use MatthiasMullie\Scrapbook\KeyValueStore;
13
14
/**
15
 * Flysystem 1.x and 2.x adapter. Data will be written to League\Flysystem\Filesystem.
16
 *
17
 * Flysystem doesn't allow locking files, though. To guarantee interference from
18
 * other processes, we'll create separate lock-files to flag a cache key in use.
19
 *
20
 * @author Matthias Mullie <[email protected]>
21
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
22
 * @license LICENSE MIT
23
 */
24
class Flysystem implements KeyValueStore
25
{
26
    /**
27
     * @var Filesystem
28
     */
29
    protected $filesystem;
30
31
    /**
32
     * @var int
33
     */
34
    protected $version;
35
36
    public function __construct(Filesystem $filesystem)
37
    {
38
        $this->filesystem = $filesystem;
39
        $this->version = class_exists('League\Flysystem\Local\LocalFilesystemAdapter') ? 2 : 1;
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function get($key, &$token = null)
46
    {
47
        $token = null;
48
49
        // let expired-but-not-yet-deleted files be deleted first
50
        if (!$this->exists($key)) {
51
            return false;
52
        }
53
54
        $data = $this->read($key);
55
        if (false === $data) {
0 ignored issues
show
introduced by
The condition false === $data is always false.
Loading history...
56
            return false;
57
        }
58
59
        $value = unserialize($data[1]);
60
        $token = $data[1];
61
62
        return $value;
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function getMulti(array $keys, array &$tokens = null)
69
    {
70
        $results = array();
71
        $tokens = array();
72
        foreach ($keys as $key) {
73
            $token = null;
74
            $value = $this->get($key, $token);
75
76
            if (null !== $token) {
77
                $results[$key] = $value;
78
                $tokens[$key] = $token;
79
            }
80
        }
81
82
        return $results;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function set($key, $value, $expire = 0)
89
    {
90
        // we don't really need a lock for this operation, but we need to make
91
        // sure it's not locked by another operation, which we could overwrite
92
        if (!$this->lock($key)) {
93
            return false;
94
        }
95
96
        $expire = $this->normalizeTime($expire);
97
        if (0 !== $expire && $expire < time()) {
98
            $this->unlock($key);
99
100
            // don't waste time storing (and later comparing expiration
101
            // timestamp) data that is already expired; just delete it already
102
            return !$this->exists($key) || $this->delete($key);
103
        }
104
105
        $path = $this->path($key);
106
        $data = $this->wrap($value, $expire);
107
        try {
108
            if (1 === $this->version) {
109
                $this->filesystem->put($path, $data);
110
            } else {
111
                $this->filesystem->write($path, $data);
112
            }
113
        } catch (FileExistsException $e) {
114
            // v1.x
115
            $this->unlock($key);
116
117
            return false;
118
        } catch (UnableToWriteFile $e) {
119
            // v2.x
120
            $this->unlock($key);
121
122
            return false;
123
        }
124
125
        return $this->unlock($key);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function setMulti(array $items, $expire = 0)
132
    {
133
        $success = array();
134
        foreach ($items as $key => $value) {
135
            $success[$key] = $this->set($key, $value, $expire);
136
        }
137
138
        return $success;
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function delete($key)
145
    {
146
        if (!$this->exists($key)) {
147
            return false;
148
        }
149
150
        if (!$this->lock($key)) {
151
            return false;
152
        }
153
154
        $path = $this->path($key);
155
156
        try {
157
            $this->filesystem->delete($path);
158
159
            return $this->unlock($key);
160
        } catch (FileNotFoundException $e) {
161
            // v1.x
162
            $this->unlock($key);
163
164
            return false;
165
        } catch (UnableToDeleteFile $e) {
166
            // v2.x
167
            $this->unlock($key);
168
169
            return false;
170
        }
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function deleteMulti(array $keys)
177
    {
178
        $success = array();
179
        foreach ($keys as $key) {
180
            $success[$key] = $this->delete($key);
181
        }
182
183
        return $success;
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189
    public function add($key, $value, $expire = 0)
190
    {
191
        if (!$this->lock($key)) {
192
            return false;
193
        }
194
195
        if ($this->exists($key)) {
196
            $this->unlock($key);
197
198
            return false;
199
        }
200
201
        $path = $this->path($key);
202
        $data = $this->wrap($value, $expire);
203
204
        try {
205
            $this->filesystem->write($path, $data);
206
207
            return $this->unlock($key);
208
        } catch (FileExistsException $e) {
209
            // v1.x
210
            $this->unlock($key);
211
212
            return false;
213
        } catch (UnableToWriteFile $e) {
214
            // v2.x
215
            $this->unlock($key);
216
217
            return false;
218
        }
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224
    public function replace($key, $value, $expire = 0)
225
    {
226
        if (!$this->lock($key)) {
227
            return false;
228
        }
229
230
        if (!$this->exists($key)) {
231
            $this->unlock($key);
232
233
            return false;
234
        }
235
236
        $path = $this->path($key);
237
        $data = $this->wrap($value, $expire);
238
239
        try {
240
            if (1 === $this->version) {
241
                $this->filesystem->update($path, $data);
242
            } else {
243
                $this->filesystem->write($path, $data);
244
            }
245
246
            return $this->unlock($key);
247
        } catch (FileNotFoundException $e) {
248
            // v1.x
249
            $this->unlock($key);
250
251
            return false;
252
        } catch (UnableToWriteFile $e) {
253
            // v2.x
254
            $this->unlock($key);
255
256
            return false;
257
        }
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function cas($token, $key, $value, $expire = 0)
264
    {
265
        if (!$this->lock($key)) {
266
            return false;
267
        }
268
269
        $current = $this->get($key);
270
        if ($token !== serialize($current)) {
271
            $this->unlock($key);
272
273
            return false;
274
        }
275
276
        $path = $this->path($key);
277
        $data = $this->wrap($value, $expire);
278
279
        try {
280
            if (1 === $this->version) {
281
                $this->filesystem->update($path, $data);
282
            } else {
283
                $this->filesystem->write($path, $data);
284
            }
285
286
            return $this->unlock($key);
287
        } catch (FileNotFoundException $e) {
288
            // v1.x
289
            $this->unlock($key);
290
291
            return false;
292
        } catch (UnableToWriteFile $e) {
293
            // v2.x
294
            $this->unlock($key);
295
296
            return false;
297
        }
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
304
    {
305
        if ($offset <= 0 || $initial < 0) {
306
            return false;
307
        }
308
309
        return $this->doIncrement($key, $offset, $initial, $expire);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
316
    {
317
        if ($offset <= 0 || $initial < 0) {
318
            return false;
319
        }
320
321
        return $this->doIncrement($key, -$offset, $initial, $expire);
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function touch($key, $expire)
328
    {
329
        if (!$this->lock($key)) {
330
            return false;
331
        }
332
333
        $value = $this->get($key);
334
        if (false === $value) {
335
            $this->unlock($key);
336
337
            return false;
338
        }
339
340
        $path = $this->path($key);
341
        $data = $this->wrap($value, $expire);
342
343
        try {
344
            if (1 === $this->version) {
345
                $this->filesystem->update($path, $data);
346
            } else {
347
                $this->filesystem->write($path, $data);
348
            }
349
350
            return $this->unlock($key);
351
        } catch (FileNotFoundException $e) {
352
            // v1.x
353
            $this->unlock($key);
354
355
            return false;
356
        } catch (UnableToWriteFile $e) {
357
            // v2.x
358
            $this->unlock($key);
359
360
            return false;
361
        }
362
    }
363
364
    /**
365
     * {@inheritdoc}
366
     */
367
    public function flush()
368
    {
369
        $files = $this->filesystem->listContents('.');
370
        foreach ($files as $file) {
371
            try {
372
                if ('dir' === $file['type']) {
373
                    if (1 === $this->version) {
374
                        $this->filesystem->deleteDir($file['path']);
375
                    } else {
376
                        $this->filesystem->deleteDirectory($file['path']);
377
                    }
378
                } else {
379
                    $this->filesystem->delete($file['path']);
380
                }
381
            } catch (FileNotFoundException $e) {
382
                // v1.x
383
                // don't care if we failed to unlink something, might have
384
                // been deleted by another process in the meantime...
385
            } catch (UnableToDeleteFile $e) {
386
                // v2.x
387
                // don't care if we failed to unlink something, might have
388
                // been deleted by another process in the meantime...
389
            }
390
        }
391
392
        return true;
393
    }
394
395
    /**
396
     * {@inheritdoc}
397
     */
398
    public function getCollection($name)
399
    {
400
        /*
401
         * A better solution could be to simply construct a new object for a
402
         * subfolder, but we can't reliably create a new
403
         * `League\Flysystem\Filesystem` object for a subfolder from the
404
         * `$this->filesystem` object we have. I could `->getAdapter` and fetch
405
         * the path from there, but only if we can assume that the adapter is
406
         * `League\Flysystem\Adapter\Local` (1.x) or
407
         * `League\Flysystem\Local\LocalFilesystemAdapter` (2.x), which it may not be.
408
         * But I can just create a new object that changes the path to write at,
409
         * by prefixing it with a subfolder!
410
         */
411
        if (1 === $this->version) {
412
            $this->filesystem->createDir($name);
413
        } else {
414
            $this->filesystem->createDirectory($name);
415
        }
416
417
        return new Collection($this->filesystem, $name);
418
    }
419
420
    /**
421
     * Shared between increment/decrement: both have mostly the same logic
422
     * (decrement just increments a negative value), but need their validation
423
     * split up (increment won't accept negative values).
424
     *
425
     * @param string $key
426
     * @param int    $offset
427
     * @param int    $initial
428
     * @param int    $expire
429
     *
430
     * @return int|bool
431
     */
432
    protected function doIncrement($key, $offset, $initial, $expire)
433
    {
434
        $current = $this->get($key, $token);
435
        if (false === $current) {
436
            $success = $this->add($key, $initial, $expire);
437
438
            return $success ? $initial : false;
439
        }
440
441
        // NaN, doesn't compute
442
        if (!is_numeric($current)) {
443
            return false;
444
        }
445
446
        $value = $current + $offset;
447
        $value = max(0, $value);
448
449
        $success = $this->cas($token, $key, $value, $expire);
450
451
        return $success ? $value : false;
452
    }
453
454
    /**
455
     * @param string $key
456
     *
457
     * @return bool
458
     */
459
    protected function exists($key)
460
    {
461
        $data = $this->read($key);
462
        if (false === $data) {
0 ignored issues
show
introduced by
The condition false === $data is always false.
Loading history...
463
            return false;
464
        }
465
466
        $expire = $data[0];
467
        if (0 !== $expire && $expire < time()) {
468
            // expired, don't keep it around
469
            $path = $this->path($key);
470
            $this->filesystem->delete($path);
471
472
            return false;
473
        }
474
475
        return true;
476
    }
477
478
    /**
479
     * Obtain a lock for a given key.
480
     * It'll try to get a lock for a couple of times, but ultimately give up if
481
     * no lock can be obtained in a reasonable time.
482
     *
483
     * @param string $key
484
     *
485
     * @return bool
486
     */
487
    protected function lock($key)
488
    {
489
        $path = md5($key).'.lock';
490
491
        for ($i = 0; $i < 25; ++$i) {
492
            try {
493
                $this->filesystem->write($path, '');
494
495
                return true;
496
            } catch (FileExistsException $e) {
497
                // v1.x
498
                usleep(200);
499
            } catch (UnableToWriteFile $e) {
500
                // v2.x
501
                usleep(200);
502
            }
503
        }
504
505
        return false;
506
    }
507
508
    /**
509
     * Release the lock for a given key.
510
     *
511
     * @param string $key
512
     *
513
     * @return bool
514
     */
515
    protected function unlock($key)
516
    {
517
        $path = md5($key).'.lock';
518
        try {
519
            $this->filesystem->delete($path);
520
        } catch (FileNotFoundException $e) {
521
            // v1.x
522
            return false;
523
        } catch (UnableToDeleteFile $e) {
524
            // v2.x
525
            return false;
526
        }
527
528
        return true;
529
    }
530
531
    /**
532
     * Times can be:
533
     * * relative (in seconds) to current time, within 30 days
534
     * * absolute unix timestamp
535
     * * 0, for infinity.
536
     *
537
     * The first case (relative time) will be normalized into a fixed absolute
538
     * timestamp.
539
     *
540
     * @param int $time
541
     *
542
     * @return int
543
     */
544
    protected function normalizeTime($time)
545
    {
546
        // 0 = infinity
547
        if (!$time) {
548
            return 0;
549
        }
550
551
        // relative time in seconds, <30 days
552
        if ($time < 30 * 24 * 60 * 60) {
553
            $time += time();
554
        }
555
556
        return $time;
557
    }
558
559
    /**
560
     * Build value, token & expiration time to be stored in cache file.
561
     *
562
     * @param string $value
563
     * @param int    $expire
564
     *
565
     * @return string
566
     */
567
    protected function wrap($value, $expire)
568
    {
569
        $expire = $this->normalizeTime($expire);
570
571
        return $expire."\n".serialize($value);
572
    }
573
574
    /**
575
     * Fetch stored data from cache file.
576
     *
577
     * @param string $key
578
     *
579
     * @return bool|array
580
     */
581
    protected function read($key)
582
    {
583
        $path = $this->path($key);
584
        try {
585
            $data = $this->filesystem->read($path);
586
        } catch (FileNotFoundException $e) {
587
            // v1.x
588
            // unlikely given previous 'exists' check, but let's play safe...
589
            // (outside process may have removed it since)
590
            return false;
591
        } catch (UnableToReadFile $e) {
592
            // v2.x
593
            // unlikely given previous 'exists' check, but let's play safe...
594
            // (outside process may have removed it since)
595
            return false;
596
        }
597
598
        if (false === $data) {
599
            // in theory, a file could still be deleted between Flysystem's
600
            // assertPresent & the time it actually fetched the content
601
            // extremely unlikely though
602
            return false;
603
        }
604
605
        $data = explode("\n", $data, 2);
606
        $data[0] = (int) $data[0];
607
608
        return $data;
609
    }
610
611
    /**
612
     * @param string $key
613
     *
614
     * @return string
615
     */
616
    protected function path($key)
617
    {
618
        return md5($key).'.cache';
619
    }
620
}
621