Proyectos de Subversion Moodle

Rev

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