Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
1441 ariadna 17
use core_cache\configurable_cache_interface;
18
use core_cache\definition;
19
use core_cache\key_aware_cache_interface;
20
use core_cache\lockable_cache_interface;
21
use core_cache\searchable_cache_interface;
22
use core_cache\store;
1 efrain 23
 
24
/**
25
 * The file store class.
26
 *
27
 * Configuration options
28
 *      path:           string: path to the cache directory, if left empty one will be created in the cache directory
29
 *      autocreate:     true, false
30
 *      prescan:        true, false
31
 *
1441 ariadna 32
 * @package    cachestore_file
1 efrain 33
 * @copyright  2012 Sam Hemelryk
34
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
1441 ariadna 36
class cachestore_file extends store implements
37
    key_aware_cache_interface,
38
    configurable_cache_interface,
39
    searchable_cache_interface,
40
    lockable_cache_interface
41
{
42
    /**
43
     * Value to represent use of the PHP serializer.
44
     */
45
    public const SERIALIZER_PHP = 'php';
1 efrain 46
 
47
    /**
1441 ariadna 48
     * Value to represent use of the Igbinary serializer.
49
     */
50
    public const SERIALIZER_IGBINARY = 'igbinary';
51
 
52
    /**
1 efrain 53
     * The name of the store.
54
     * @var string
55
     */
56
    protected $name;
57
 
58
    /**
59
     * The path used to store files for this store and the definition it was initialised with.
60
     * @var string
61
     */
62
    protected $path = false;
63
 
64
    /**
65
     * The path in which definition specific sub directories will be created for caching.
66
     * @var string
67
     */
68
    protected $filestorepath = false;
69
 
70
    /**
71
     * Set to true when a prescan has been performed.
72
     * @var bool
73
     */
74
    protected $prescan = false;
75
 
76
    /**
77
     * Set to true if we should store files within a single directory.
78
     * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system
79
     * limitations such as maximum files per directory.
80
     * @var bool
81
     */
82
    protected $singledirectory = false;
83
 
84
    /**
85
     * Set to true when the path should be automatically created if it does not yet exist.
86
     * @var bool
87
     */
88
    protected $autocreate = false;
89
 
90
    /**
91
     * Set to true if new cache revision directory needs to be created. Old directory will be purged asynchronously
92
     * via Schedule task.
93
     * @var bool
94
     */
95
    protected $asyncpurge = false;
96
 
97
    /**
98
     * Set to true if a custom path is being used.
99
     * @var bool
100
     */
101
    protected $custompath = false;
102
 
103
    /**
104
     * An array of keys we are sure about presently.
105
     * @var array
106
     */
107
    protected $keys = array();
108
 
109
    /**
110
     * True when the store is ready to be initialised.
111
     * @var bool
112
     */
113
    protected $isready = false;
114
 
115
    /**
116
     * The cache definition this instance has been initialised with.
1441 ariadna 117
     * @var definition
1 efrain 118
     */
119
    protected $definition;
120
 
121
    /**
122
     * Bytes read or written by last call to set()/get() or set_many()/get_many().
123
     *
124
     * @var int
125
     */
126
    protected $lastiobytes = 0;
127
 
128
    /**
129
     * A reference to the global $CFG object.
130
     *
131
     * You may be asking yourself why on earth this is here, but there is a good reason.
132
     * By holding onto a reference of the $CFG object we can be absolutely sure that it won't be destroyed before
133
     * we are done with it.
134
     * This makes it possible to use a cache within a destructor method for the purposes of
135
     * delayed writes. Like how the session mechanisms work.
136
     *
137
     * @var stdClass
138
     */
139
    private $cfg = null;
140
 
141
    /** @var int Maximum number of seconds to wait for a lock before giving up. */
142
    protected $lockwait = 60;
143
 
144
    /**
145
     * Instance of file_lock_factory configured to create locks in the cache directory.
146
     *
147
     * @var \core\lock\file_lock_factory $lockfactory
148
     */
149
    protected $lockfactory = null;
150
 
151
    /**
152
     * List of current locks.
153
     *
154
     * @var array $locks
155
     */
156
    protected $locks = [];
157
 
158
    /**
1441 ariadna 159
     * Serializer for this store.
160
     *
161
     * @var string
162
     */
163
    protected $serializer = self::SERIALIZER_PHP;
164
 
165
    /**
166
     * Determine if igbinary functions are available for use.
167
     *
168
     * @return boolean
169
     */
170
    public static function igbinary_available(): bool {
171
        return function_exists('igbinary_serialize');
172
    }
173
 
174
    /**
175
     * Gets an array of options to use as the serialiser.
176
     *
177
     * @return array
178
     */
179
    public static function config_get_serializer_options(): array {
180
        $options = [
181
            self::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_file'),
182
        ];
183
        if (self::igbinary_available()) {
184
            $options[self::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_file');
185
        }
186
        return $options;
187
    }
188
 
189
    /**
1 efrain 190
     * Constructs the store instance.
191
     *
192
     * Noting that this function is not an initialisation. It is used to prepare the store for use.
1441 ariadna 193
     * The store will be initialised when required and will be provided with a definition at that time.
1 efrain 194
     *
195
     * @param string $name
196
     * @param array $configuration
197
     */
198
    public function __construct($name, array $configuration = array()) {
199
        global $CFG;
200
 
201
        if (isset($CFG)) {
202
            // Hold onto a reference of the global $CFG object.
203
            $this->cfg = $CFG;
204
        }
205
 
206
        $this->name = $name;
207
        if (array_key_exists('path', $configuration) && $configuration['path'] !== '') {
208
            $this->custompath = true;
209
            $this->autocreate = !empty($configuration['autocreate']);
210
            $path = (string)$configuration['path'];
211
            if (!is_dir($path)) {
212
                if ($this->autocreate) {
213
                    if (!make_writable_directory($path, false)) {
214
                        $path = false;
215
                        debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
216
                    }
217
                } else {
218
                    $path = false;
219
                    debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
220
                }
221
            }
222
            if ($path !== false && !is_writable($path)) {
223
                $path = false;
224
                debugging('The file cache store path is not writable for `'.$name.'`', DEBUG_DEVELOPER);
225
            }
226
        } else {
227
            $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
228
        }
229
        $this->isready = $path !== false;
230
        $this->filestorepath = $path;
231
        // This will be updated once the store has been initialised for a definition.
232
        $this->path = $path;
233
 
234
        // Check if we should prescan the directory.
235
        if (array_key_exists('prescan', $configuration)) {
236
            $this->prescan = (bool)$configuration['prescan'];
237
        } else {
238
            // Default is no, we should not prescan.
239
            $this->prescan = false;
240
        }
241
        // Check if we should be storing in a single directory.
242
        if (array_key_exists('singledirectory', $configuration)) {
243
            $this->singledirectory = (bool)$configuration['singledirectory'];
244
        } else {
245
            // Default: No, we will use multiple directories.
246
            $this->singledirectory = false;
247
        }
248
        // Check if directory needs to be purged asynchronously.
249
        if (array_key_exists('asyncpurge', $configuration)) {
250
            $this->asyncpurge = (bool)$configuration['asyncpurge'];
251
        } else {
252
            $this->asyncpurge = false;
253
        }
254
 
255
        // Leverage cachelock_file to provide native locking, to avoid duplicating logic.
256
        // This will store locks alongside the cache, so local cache uses local locks.
257
        $lockdir = $path . '/filelocks';
258
        if (!file_exists($lockdir)) {
259
            make_writable_directory($lockdir);
260
        }
261
        if (array_key_exists('lockwait', $configuration)) {
262
            $this->lockwait = (int)$configuration['lockwait'];
263
        }
264
        $this->lockfactory = new \core\lock\file_lock_factory('cachestore_file', $lockdir);
265
        if (!$this->lockfactory->is_available()) {
266
            // File locking is disabled in config, fall back to default lock factory.
267
            $this->lockfactory = \core\lock\lock_config::get_lock_factory('cachestore_file');
268
        }
1441 ariadna 269
 
270
        // Set the serializer to use based on configuration.
271
        if (array_key_exists('serializer', $configuration)) {
272
            $this->serializer = (string)$configuration['serializer'];
273
        }
1 efrain 274
    }
275
 
276
    /**
277
     * Performs any necessary operation when the file store instance has been created.
278
     */
279
    public function instance_created() {
280
        if ($this->isready && !$this->prescan) {
281
            // It is supposed the store instance to expect an empty folder.
282
            $this->purge_all_definitions();
283
        }
284
    }
285
 
286
    /**
287
     * Returns true if this store instance is ready to be used.
288
     * @return bool
289
     */
290
    public function is_ready() {
291
        return $this->isready;
292
    }
293
 
294
    /**
295
     * Returns true once this instance has been initialised.
296
     *
297
     * @return bool
298
     */
299
    public function is_initialised() {
300
        return true;
301
    }
302
 
303
    /**
304
     * Returns the supported features as a combined int.
305
     *
306
     * @param array $configuration
307
     * @return int
308
     */
309
    public static function get_supported_features(array $configuration = array()) {
310
        $supported = self::SUPPORTS_DATA_GUARANTEE +
311
                     self::SUPPORTS_NATIVE_TTL +
312
                     self::IS_SEARCHABLE +
313
                     self::DEREFERENCES_OBJECTS;
314
        return $supported;
315
    }
316
 
317
    /**
318
     * Returns false as this store does not support multiple identifiers.
319
     * (This optional function is a performance optimisation; it must be
320
     * consistent with the value from get_supported_features.)
321
     *
322
     * @return bool False
323
     */
324
    public function supports_multiple_identifiers() {
325
        return false;
326
    }
327
 
328
    /**
329
     * Returns the supported modes as a combined int.
330
     *
331
     * @param array $configuration
332
     * @return int
333
     */
334
    public static function get_supported_modes(array $configuration = array()) {
335
        return self::MODE_APPLICATION + self::MODE_SESSION;
336
    }
337
 
338
    /**
339
     * Returns true if the store requirements are met.
340
     *
341
     * @return bool
342
     */
343
    public static function are_requirements_met() {
344
        return true;
345
    }
346
 
347
    /**
348
     * Returns true if the given mode is supported by this store.
349
     *
1441 ariadna 350
     * @param int $mode One of store::MODE_*
1 efrain 351
     * @return bool
352
     */
353
    public static function is_supported_mode($mode) {
1441 ariadna 354
        return ($mode === static::MODE_APPLICATION || $mode === static::MODE_SESSION);
1 efrain 355
    }
356
 
357
    /**
358
     * Initialises the cache.
359
     *
360
     * Once this has been done the cache is all set to be used.
361
     *
1441 ariadna 362
     * @param definition $definition
1 efrain 363
     */
1441 ariadna 364
    public function initialise(definition $definition) {
1 efrain 365
        global $CFG;
366
 
367
        $this->definition = $definition;
368
        $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
369
        $this->path = $this->filestorepath.'/'.$hash;
370
        make_writable_directory($this->path, false);
371
 
372
        if ($this->asyncpurge) {
373
            $timestampfile = $this->path . '/.lastpurged';
374
            if (!file_exists($timestampfile)) {
375
                touch($timestampfile);
376
                @chmod($timestampfile, $CFG->filepermissions);
377
            }
378
            $cacherev = gmdate("YmdHis", filemtime($timestampfile));
379
            // Update file path with new cache revision.
380
            $this->path .= '/' . $cacherev;
381
            make_writable_directory($this->path, false);
382
        }
383
 
384
        if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
385
            $this->prescan = false;
386
        }
387
        if ($this->prescan) {
388
            $this->prescan_keys();
389
        }
390
    }
391
 
392
    /**
393
     * Pre-scan the cache to see which keys are present.
394
     */
395
    protected function prescan_keys() {
396
        $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
397
        if (is_array($files)) {
398
            foreach ($files as $filename) {
399
                $this->keys[basename($filename)] = filemtime($filename);
400
            }
401
        }
402
    }
403
 
404
    /**
405
     * Gets a pattern suitable for use with glob to find all keys in the cache.
406
     *
407
     * @param string $prefix A prefix to use.
408
     * @return string The pattern.
409
     */
410
    protected function glob_keys_pattern($prefix = '') {
411
        if ($this->singledirectory) {
412
            return $this->path . '/'.$prefix.'*.cache';
413
        } else {
414
            return $this->path . '/*/'.$prefix.'*.cache';
415
        }
416
    }
417
 
418
    /**
419
     * Returns the file path to use for the given key.
420
     *
421
     * @param string $key The key to generate a file path for.
422
     * @param bool $create If set to the true the directory structure the key requires will be created.
423
     * @return string The full path to the file that stores a particular cache key.
424
     */
425
    protected function file_path_for_key($key, $create = false) {
426
        if ($this->singledirectory) {
427
            // Its a single directory, easy, just the store instances path + the file name.
428
            return $this->path . '/' . $key . '.cache';
429
        } else {
430
            // We are using a single subdirectory to achieve 1 level.
431
           // We suffix the subdir so it does not clash with any windows
432
           // reserved filenames like 'con'.
433
            $subdir = substr($key, 0, 3) . '-cache';
434
            $dir = $this->path . '/' . $subdir;
435
            if ($create) {
436
                // Create the directory. This function does it recursivily!
437
                make_writable_directory($dir, false);
438
            }
439
            return $dir . '/' . $key . '.cache';
440
        }
441
    }
442
 
443
    /**
444
     * Retrieves an item from the cache store given its key.
445
     *
446
     * @param string $key The key to retrieve
447
     * @return mixed The data that was associated with the key, or false if the key did not exist.
448
     */
449
    public function get($key) {
450
        $this->lastiobytes = 0;
451
        $filename = $key.'.cache';
452
        $file = $this->file_path_for_key($key);
453
        $ttl = $this->definition->get_ttl();
454
        $maxtime = 0;
455
        if ($ttl) {
456
            $maxtime = cache::now() - $ttl;
457
        }
458
        $readfile = false;
459
        if ($this->prescan && array_key_exists($filename, $this->keys)) {
460
            if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) {
461
                $readfile = true;
462
            } else {
463
                $this->delete($key);
464
            }
465
        } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
466
            $readfile = true;
467
        }
468
        if (!$readfile) {
469
            return false;
470
        }
471
        // Open ensuring the file for reading in binary format.
472
        if (!$handle = fopen($file, 'rb')) {
473
            return false;
474
        }
475
 
476
        // Note: There is no need to perform any file locking here.
477
        // The cache file is only ever written to in the `write_file` function, where it does so by writing to a temp
478
        // file and performing an atomic rename of that file. The target file is never locked, so there is no benefit to
479
        // obtaining a lock (shared or exclusive) here.
480
 
481
        $data = '';
482
        // Read the data in 1Mb chunks. Small caches will not loop more than once.  We don't use filesize as it may
483
        // be cached with a different value than what we need to read from the file.
484
        do {
485
            $data .= fread($handle, 1048576);
486
        } while (!feof($handle));
487
        $this->lastiobytes = strlen($data);
488
 
489
        if ($this->lastiobytes == 0) {
490
            // Potentially statcache is stale. File can be deleted, let's clear cache and recheck.
491
            clearstatcache(true, $file);
492
            if (!file_exists($file)) {
493
                // It's a completely normal condition. Just ignore and keep going.
494
                return false;
495
            }
496
        }
497
 
498
        // Return it unserialised.
499
        return $this->prep_data_after_read($data, $file);
500
    }
501
 
502
    /**
503
     * Retrieves several items from the cache store in a single transaction.
504
     *
505
     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
506
     *
507
     * @param array $keys The array of keys to retrieve
508
     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
509
     *      be set to false.
510
     */
511
    public function get_many($keys) {
512
        $result = array();
513
        $total = 0;
514
        foreach ($keys as $key) {
515
            $result[$key] = $this->get($key);
516
            $total += $this->lastiobytes;
517
        }
518
        $this->lastiobytes = $total;
519
        return $result;
520
    }
521
 
522
    /**
523
     * Gets bytes read by last get() or get_many(), or written by set() or set_many().
524
     *
525
     * @return int Bytes read or written
526
     * @since Moodle 4.0
527
     */
528
    public function get_last_io_bytes(): int {
529
        return $this->lastiobytes;
530
    }
531
 
532
    /**
533
     * Deletes an item from the cache store.
534
     *
535
     * @param string $key The key to delete.
536
     * @return bool Returns true if the operation was a success, false otherwise.
537
     */
538
    public function delete($key) {
539
        $filename = $key.'.cache';
540
        $file = $this->file_path_for_key($key);
541
        if (file_exists($file) && @unlink($file)) {
542
            unset($this->keys[$filename]);
543
            return true;
544
        }
545
 
546
        return false;
547
    }
548
 
549
    /**
550
     * Deletes several keys from the cache in a single action.
551
     *
552
     * @param array $keys The keys to delete
553
     * @return int The number of items successfully deleted.
554
     */
555
    public function delete_many(array $keys) {
556
        $count = 0;
557
        foreach ($keys as $key) {
558
            if ($this->delete($key)) {
559
                $count++;
560
            }
561
        }
562
        return $count;
563
    }
564
 
565
    /**
566
     * Sets an item in the cache given its key and data value.
567
     *
568
     * @param string $key The key to use.
569
     * @param mixed $data The data to set.
570
     * @return bool True if the operation was a success false otherwise.
571
     */
572
    public function set($key, $data) {
573
        $this->ensure_path_exists();
574
        $filename = $key.'.cache';
575
        $file = $this->file_path_for_key($key, true);
576
        $serialized = $this->prep_data_before_save($data);
577
        $this->lastiobytes = strlen($serialized);
578
        $result = $this->write_file($file, $serialized);
579
        if (!$result) {
580
            // Couldn't write the file.
581
            return false;
582
        }
583
        // Record the key if required.
584
        if ($this->prescan) {
585
            $this->keys[$filename] = cache::now() + 1;
586
        }
587
        // Return true.. it all worked **miracles**.
588
        return true;
589
    }
590
 
591
    /**
592
     * Prepares data to be stored in a file.
593
     *
594
     * @param mixed $data
595
     * @return string
596
     */
597
    protected function prep_data_before_save($data) {
1441 ariadna 598
        return $this->serialize($data);
1 efrain 599
    }
600
 
601
    /**
602
     * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
603
     *
604
     * @param string $data
605
     * @param string $path
606
     * @return mixed
607
     */
608
    protected function prep_data_after_read($data, $path) {
1441 ariadna 609
        $result = @$this->unserialize($data);
610
        if ($result === false && $data != @$this->serialize(false)) {
1 efrain 611
            debugging('Failed to unserialise data from cache file: ' . $path . '. Data: ' . $data, DEBUG_DEVELOPER);
612
            return false;
613
        }
614
        return $result;
615
    }
616
 
617
    /**
618
     * Sets many items in the cache in a single transaction.
619
     *
620
     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
621
     *      keys, 'key' and 'value'.
622
     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
623
     *      sent ... if they care that is.
624
     */
625
    public function set_many(array $keyvaluearray) {
626
        $count = 0;
627
        $totaliobytes = 0;
628
        foreach ($keyvaluearray as $pair) {
629
            if ($this->set($pair['key'], $pair['value'])) {
630
                $totaliobytes += $this->lastiobytes;
631
                $count++;
632
            }
633
        }
634
        $this->lastiobytes = $totaliobytes;
635
        return $count;
636
    }
637
 
638
    /**
639
     * Checks if the store has a record for the given key and returns true if so.
640
     *
641
     * @param string $key
642
     * @return bool
643
     */
644
    public function has($key) {
645
        $filename = $key.'.cache';
646
        $maxtime = cache::now() - $this->definition->get_ttl();
647
        if ($this->prescan) {
648
            return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
649
        }
650
        $file = $this->file_path_for_key($key);
651
        return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
652
    }
653
 
654
    /**
655
     * Returns true if the store contains records for all of the given keys.
656
     *
657
     * @param array $keys
658
     * @return bool
659
     */
660
    public function has_all(array $keys) {
661
        foreach ($keys as $key) {
662
            if (!$this->has($key)) {
663
                return false;
664
            }
665
        }
666
        return true;
667
    }
668
 
669
    /**
670
     * Returns true if the store contains records for any of the given keys.
671
     *
672
     * @param array $keys
673
     * @return bool
674
     */
675
    public function has_any(array $keys) {
676
        foreach ($keys as $key) {
677
            if ($this->has($key)) {
678
                return true;
679
            }
680
        }
681
        return false;
682
    }
683
 
684
    /**
685
     * Purges the cache definition deleting all the items within it.
686
     *
687
     * @return boolean True on success. False otherwise.
688
     */
689
    public function purge() {
690
        global $CFG;
691
        if ($this->isready) {
692
            // If asyncpurge = true, create a new cache revision directory and adhoc task to delete old directory.
693
            if ($this->asyncpurge && isset($this->definition)) {
694
                $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
695
                $filepath = $this->filestorepath . '/' . $hash;
696
                $timestampfile = $filepath . '/.lastpurged';
697
                if (file_exists($timestampfile)) {
698
                    $oldcacherev = gmdate("YmdHis", filemtime($timestampfile));
699
                    $oldcacherevpath = $filepath . '/' . $oldcacherev;
700
                    // Delete old cache revision file.
701
                    @unlink($timestampfile);
702
 
703
                    // Create adhoc task to delete old cache revision folder.
704
                    $purgeoldcacherev = new \cachestore_file\task\asyncpurge();
705
                    $purgeoldcacherev->set_custom_data(['path' => $oldcacherevpath]);
706
                    \core\task\manager::queue_adhoc_task($purgeoldcacherev);
707
                }
708
                touch($timestampfile, time());
709
                @chmod($timestampfile, $CFG->filepermissions);
710
                $newcacherev = gmdate("YmdHis", filemtime($timestampfile));
711
                $filepath .= '/' . $newcacherev;
712
                make_writable_directory($filepath, false);
713
            } else {
714
                $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
715
                if (is_array($files)) {
716
                    foreach ($files as $filename) {
717
                        @unlink($filename);
718
                    }
719
                }
720
                $this->keys = [];
721
            }
722
        }
723
        return true;
724
    }
725
 
726
    /**
727
     * Purges all the cache definitions deleting all items within them.
728
     *
729
     * @return boolean True on success. False otherwise.
730
     */
731
    protected function purge_all_definitions() {
732
        // Warning: limit the deletion to what file store is actually able
733
        // to create using the internal {@link purge()} providing the
734
        // {@link $path} with a wildcard to perform a purge action over all the definitions.
735
        $currpath = $this->path;
736
        $this->path = $this->filestorepath.'/*';
737
        $result = $this->purge();
738
        $this->path = $currpath;
739
        return $result;
740
    }
741
 
742
    /**
743
     * Given the data from the add instance form this function creates a configuration array.
744
     *
745
     * @param stdClass $data
746
     * @return array
747
     */
748
    public static function config_get_configuration_array($data) {
749
        $config = array();
750
 
751
        if (isset($data->path)) {
752
            $config['path'] = $data->path;
753
        }
754
        if (isset($data->autocreate)) {
755
            $config['autocreate'] = $data->autocreate;
756
        }
757
        if (isset($data->singledirectory)) {
758
            $config['singledirectory'] = $data->singledirectory;
759
        }
760
        if (isset($data->prescan)) {
761
            $config['prescan'] = $data->prescan;
762
        }
763
        if (isset($data->asyncpurge)) {
764
            $config['asyncpurge'] = $data->asyncpurge;
765
        }
766
        if (isset($data->lockwait)) {
767
            $config['lockwait'] = $data->lockwait;
768
        }
1441 ariadna 769
        if (isset($data->serializer)) {
770
            $config['serializer'] = $data->serializer;
771
        }
1 efrain 772
 
773
        return $config;
774
    }
775
 
776
    /**
777
     * Allows the cache store to set its data against the edit form before it is shown to the user.
778
     *
779
     * @param moodleform $editform
780
     * @param array $config
781
     */
782
    public static function config_set_edit_form_data(moodleform $editform, array $config) {
783
        $data = array();
784
        if (!empty($config['path'])) {
785
            $data['path'] = $config['path'];
786
        }
787
        if (isset($config['autocreate'])) {
788
            $data['autocreate'] = (bool)$config['autocreate'];
789
        }
790
        if (isset($config['singledirectory'])) {
791
            $data['singledirectory'] = (bool)$config['singledirectory'];
792
        }
793
        if (isset($config['prescan'])) {
794
            $data['prescan'] = (bool)$config['prescan'];
795
        }
796
        if (isset($config['asyncpurge'])) {
797
            $data['asyncpurge'] = (bool)$config['asyncpurge'];
798
        }
799
        if (isset($config['lockwait'])) {
800
            $data['lockwait'] = (int)$config['lockwait'];
801
        }
1441 ariadna 802
        if (isset($config['serializer'])) {
803
            $data['serializer'] = (string)$config['serializer'];
804
        }
1 efrain 805
        $editform->set_data($data);
806
    }
807
 
808
    /**
809
     * Checks to make sure that the path for the file cache exists.
810
     *
811
     * @return bool
812
     * @throws coding_exception
813
     */
814
    protected function ensure_path_exists() {
815
        global $CFG;
816
        if (!is_writable($this->path)) {
817
            if ($this->custompath && !$this->autocreate) {
818
                throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
819
            }
820
            $createdcfg = false;
821
            if (!isset($CFG)) {
822
                // This can only happen during destruction of objects.
823
                // A cache is being used within a destructor, php is ending a request and $CFG has
824
                // already being cleaned up.
825
                // Rebuild $CFG with directory permissions just to complete this write.
826
                $CFG = $this->cfg;
827
                $createdcfg = true;
828
            }
829
            if (!make_writable_directory($this->path, false)) {
830
                throw new coding_exception('File store path does not exist and can not be created.');
831
            }
832
            if ($createdcfg) {
833
                // We re-created it so we'll clean it up.
834
                unset($CFG);
835
            }
836
        }
837
        return true;
838
    }
839
 
840
    /**
841
     * Performs any necessary clean up when the file store instance is being deleted.
842
     *
843
     * 1. Purges the cache directory.
844
     * 2. Deletes the directory we created for the given definition.
845
     */
846
    public function instance_deleted() {
847
        $this->purge_all_definitions();
848
        @rmdir($this->filestorepath);
849
    }
850
 
851
    /**
852
     * Generates an instance of the cache store that can be used for testing.
853
     *
854
     * Returns an instance of the cache store, or false if one cannot be created.
855
     *
1441 ariadna 856
     * @param definition $definition
1 efrain 857
     * @return cachestore_file
858
     */
1441 ariadna 859
    public static function initialise_test_instance(definition $definition) {
1 efrain 860
        $name = 'File test';
861
        $path = make_cache_directory('cachestore_file_test');
862
        $cache = new cachestore_file($name, array('path' => $path));
863
        if ($cache->is_ready()) {
864
            $cache->initialise($definition);
865
        }
866
        return $cache;
867
    }
868
 
869
    /**
870
     * Generates the appropriate configuration required for unit testing.
871
     *
872
     * @return array Array of unit test configuration data to be used by initialise().
873
     */
874
    public static function unit_test_configuration() {
875
        return array();
876
    }
877
 
878
    /**
879
     * Writes your madness to a file.
880
     *
881
     * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
882
     *   1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
883
     *   2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
884
     *
885
     * @param string $file Absolute file path
886
     * @param string $content The content to write.
887
     * @return bool
888
     */
889
    protected function write_file($file, $content) {
890
        // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
891
        // in this way we avoid partial writes.
892
        $path = dirname($file);
893
        while (true) {
894
            $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
895
            if (!file_exists($tempfile)) {
896
                break;
897
            }
898
        }
899
 
900
        // Open the file with mode=x. This acts to create and open the file for writing only.
901
        // If the file already exists this will return false.
902
        // We also force binary.
903
        $handle = @fopen($tempfile, 'xb+');
904
        if ($handle === false) {
905
            // File already exists... lock already exists, return false.
906
            return false;
907
        }
908
        fwrite($handle, $content);
909
        fflush($handle);
910
        // Close the handle, we're done.
911
        fclose($handle);
912
 
913
        if (md5_file($tempfile) !== md5($content)) {
914
            // The md5 of the content of the file must match the md5 of the content given to be written.
915
            @unlink($tempfile);
916
            return false;
917
        }
918
 
919
        // Finally rename the temp file to the desired file, returning the true|false result.
920
        $result = rename($tempfile, $file);
921
        @chmod($file, $this->cfg->filepermissions);
922
        if (!$result) {
923
            // Failed to rename, don't leave files lying around.
924
            @unlink($tempfile);
925
        }
926
        return $result;
927
    }
928
 
929
    /**
930
     * Returns the name of this instance.
931
     * @return string
932
     */
933
    public function my_name() {
934
        return $this->name;
935
    }
936
 
937
    /**
938
     * Finds all of the keys being used by this cache store instance.
939
     *
940
     * @return array
941
     */
942
    public function find_all() {
943
        $this->ensure_path_exists();
944
        $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
945
        $return = array();
946
        if ($files === false) {
947
            return $return;
948
        }
949
        foreach ($files as $file) {
950
            $return[] = substr(basename($file), 0, -6);
951
        }
952
        return $return;
953
    }
954
 
955
    /**
956
     * Finds all of the keys whose keys start with the given prefix.
957
     *
958
     * @param string $prefix
959
     */
960
    public function find_by_prefix($prefix) {
961
        $this->ensure_path_exists();
962
        $prefix = preg_replace('#(\*|\?|\[)#', '[$1]', $prefix);
963
        $files = glob($this->glob_keys_pattern($prefix), GLOB_MARK | GLOB_NOSORT);
964
        $return = array();
965
        if ($files === false) {
966
            return $return;
967
        }
968
        foreach ($files as $file) {
969
            // Trim off ".cache" from the end.
970
            $return[] = substr(basename($file), 0, -6);
971
        }
972
        return $return;
973
    }
974
 
975
    /**
976
     * Gets total size for the directory used by the cache store.
977
     *
978
     * @return int Total size in bytes
979
     */
980
    public function store_total_size(): ?int {
981
        return get_directory_size($this->filestorepath);
982
    }
983
 
984
    /**
985
     * Gets total size for a specific cache.
986
     *
987
     * With the file cache we can just look at the directory listing without having to
988
     * actually load any files, so the $samplekeys parameter is ignored.
989
     *
990
     * @param int $samplekeys Unused
991
     * @return stdClass Cache details
992
     */
993
    public function cache_size_details(int $samplekeys = 50): stdClass {
994
        $result = (object)[
995
            'supported' => true,
996
            'items' => 0,
997
            'mean' => 0,
998
            'sd' => 0,
999
            'margin' => 0
1000
        ];
1001
 
1002
        // Find all the files in this cache.
1003
        $this->ensure_path_exists();
1004
        $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
1005
        if ($files === false || count($files) === 0) {
1006
            return $result;
1007
        }
1008
 
1009
        // Get the sizes and count of files.
1010
        $sizes = [];
1011
        foreach ($files as $file) {
1012
            $result->items++;
1013
            $sizes[] = filesize($file);
1014
        }
1015
 
1016
        // Work out mean and standard deviation.
1017
        $total = array_sum($sizes);
1018
        $result->mean = $total / $result->items;
1019
        $squarediff = 0;
1020
        foreach ($sizes as $size) {
1021
            $squarediff += ($size - $result->mean) ** 2;
1022
        }
1023
        $squarediff /= $result->items;
1024
        $result->sd = sqrt($squarediff);
1025
        return $result;
1026
    }
1027
 
1028
    /**
1029
     * Use lock factory to determine the lock state.
1030
     *
1031
     * @param string $key Lock identifier
1032
     * @param string $ownerid Cache identifier
1033
     * @return bool|null
1034
     */
1035
    public function check_lock_state($key, $ownerid): ?bool {
1036
        if (!array_key_exists($key, $this->locks)) {
1037
            return null; // Lock does not exist.
1038
        }
1039
        if (!array_key_exists($ownerid, $this->locks[$key])) {
1040
            return false; // Lock exists, but belongs to someone else.
1041
        }
1042
        if ($this->locks[$key][$ownerid] instanceof \core\lock\lock) {
1043
            return true; // Lock exists, and we own it.
1044
        }
1045
        // Try to get the lock with an immediate timeout. If this succeeds, the lock does not currently exist.
1046
        $lock = $this->lockfactory->get_lock($key, 0);
1047
        if ($lock) {
1048
            // Lock was not already held.
1049
            $lock->release();
1050
            return null;
1051
        } else {
1052
            // Lock is held by someone else.
1053
            return false;
1054
        }
1055
    }
1056
 
1057
    /**
1058
     * Use lock factory to acquire a lock.
1059
     *
1060
     * @param string $key Lock identifier
1061
     * @param string $ownerid Cache identifier
1062
     * @return bool
1063
     */
1064
    public function acquire_lock($key, $ownerid): bool {
1065
        $lock = $this->lockfactory->get_lock($key, $this->lockwait);
1066
        if ($lock) {
1067
            $this->locks[$key][$ownerid] = $lock;
1068
        }
1069
        return (bool)$lock;
1070
    }
1071
 
1072
    /**
1073
     * Use lock factory to release a lock.
1074
     *
1075
     * @param string $key Lock identifier
1076
     * @param string $ownerid Cache identifier
1077
     * @return bool
1078
     */
1079
    public function release_lock($key, $ownerid): bool {
1080
        if (!array_key_exists($key, $this->locks)) {
1081
            return false; // No lock to release.
1082
        }
1083
        if (!array_key_exists($ownerid, $this->locks[$key])) {
1084
            return false; // Tried to release someone else's lock.
1085
        }
1086
        $unlocked = $this->locks[$key][$ownerid]->release();
1087
        if ($unlocked) {
1088
            unset($this->locks[$key]);
1089
        }
1090
        return $unlocked;
1091
    }
1441 ariadna 1092
 
1093
    /**
1094
     * Serializes the data according to the configured serializer.
1095
     *
1096
     * @param mixed $value
1097
     * @return string
1098
     */
1099
    protected function serialize($value): string {
1100
        switch ($this->serializer) {
1101
            case self::SERIALIZER_PHP:
1102
                return serialize($value);
1103
            case self::SERIALIZER_IGBINARY:
1104
                if (self::igbinary_available()) {
1105
                    return igbinary_serialize($value);
1106
                }
1107
        }
1108
        debugging("Unknown or unavailable serializer {$this->serializer}");
1109
        return serialize($value);
1110
    }
1111
 
1112
    /**
1113
     * Unserializes the data according to the configured serializer.
1114
     *
1115
     * @param mixed $value
1116
     * @return mixed
1117
     */
1118
    protected function unserialize($value) {
1119
        switch ($this->serializer) {
1120
            case self::SERIALIZER_PHP:
1121
                return unserialize($value);
1122
            case self::SERIALIZER_IGBINARY:
1123
                if (self::igbinary_available()) {
1124
                    return igbinary_unserialize($value);
1125
                }
1126
        }
1127
        debugging("Unknown or unavailable serializer: {$this->serializer}");
1128
        return unserialize($value);
1129
    }
1 efrain 1130
}