Issues (35)

src/Files/AllFilesInfo.php (11 issues)

1
<?php
2
3
namespace Sunnysideup\AssetsOverview\Files;
4
5
use FilesystemIterator;
6
use RecursiveDirectoryIterator;
7
use RecursiveIteratorIterator;
8
use SilverStripe\Assets\File;
9
use SilverStripe\Assets\Folder;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Config\Configurable;
12
use SilverStripe\Core\Injector\Injectable;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\Versioned\Versioned;
16
use SilverStripe\View\ArrayData;
17
use Sunnysideup\AssetsOverview\Interfaces\FileInfo;
18
use Sunnysideup\AssetsOverview\Traits\Cacher;
19
use Sunnysideup\AssetsOverview\Traits\FilesystemRelatedTraits;
20
use Sunnysideup\Flush\FlushNow;
21
22
class AllFilesInfo implements FileInfo
23
{
24
    use FilesystemRelatedTraits;
25
    use Injectable;
26
    use Configurable;
27
    use Cacher;
28
    use FlushNow;
29
30
    public static function inst(): AllFilesInfo
31
    {
32
        return Injector::inst()
33
            ->get(AllFilesInfo::class, true, [ASSETS_PATH]);
34
    }
35
36
    public function setVerbose(bool $b): static
37
    {
38
        $this->verbose = $b;
39
        return $this;
40
    }
41
42
    public function setNoCache(bool $b): static
43
    {
44
        $this->noCache = $b;
45
        return $this;
46
    }
47
48
    private static array $not_real_file_substrings = [
49
        DIRECTORY_SEPARATOR . '_resampled',
50
        DIRECTORY_SEPARATOR . '__',
51
        DIRECTORY_SEPARATOR . '.',
52
        '__Fit',
53
        '__Pad',
54
        '__Fill',
55
        '__Focus',
56
        '__Scale',
57
        '__ResizedImage',
58
    ];
59
60
    public function getAllFiles(?bool $verbose = true, ?bool $noCache = true): array
61
    {
62
        $this->setLimit(99999999999);
63
        $this->setVerbose($verbose);
0 ignored issues
show
It seems like $verbose can also be of type null; however, parameter $b of Sunnysideup\AssetsOvervi...FilesInfo::setVerbose() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

63
        $this->setVerbose(/** @scrutinizer ignore-type */ $verbose);
Loading history...
64
        $this->setNoCache($noCache);
0 ignored issues
show
It seems like $noCache can also be of type null; however, parameter $b of Sunnysideup\AssetsOvervi...FilesInfo::setNoCache() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

64
        $this->setNoCache(/** @scrutinizer ignore-type */ $noCache);
Loading history...
65
        return $this->getFilesAsArrayList()->toArray();
66
    }
67
68
    protected bool $noCache = false;
69
    protected bool $verbose = false;
70
71
    /**
72
     * @var string
73
     */
74
    protected string $path = '';
75
76
    /**
77
     * @var array
78
     */
79
    protected array $dataStaging = [];
80
81
    /**
82
     * @var array
83
     */
84
    protected array $dataLive = [];
85
86
    /**
87
     * @var array
88
     */
89
    protected array $listOfFiles = [];
90
91
    /**
92
     * @var array
93
     */
94
    protected array $databaseLookupListStaging = [];
95
96
97
    /**
98
     * @var array
99
     */
100
    protected array $databaseLookupListLive = [];
101
102
103
    /**
104
     * @var ArrayList
105
     */
106
    protected ArrayList $filesAsArrayList;
107
108
    /**
109
     * @var ArrayList
110
     */
111
    protected ArrayList $filesAsSortedArrayList;
112
113
    /**
114
     * @var array
115
     */
116
    protected array $availableExtensions = [];
117
118
119
120
    /**
121
     * @var int
122
     */
123
    protected int $totalFileCountRaw = 0;
124
125
126
    /**
127
     * @var int
128
     */
129
    protected int $totalFileCountFiltered = 0;
130
131
    /**
132
     * @var int
133
     */
134
    protected int $totalFileSizeFiltered = 0;
135
136
    /**
137
     * @var int
138
     */
139
    protected int $limit = 1000;
140
141
    /**
142
     * @var int
143
     */
144
    protected int $startLimit = 0;
145
146
    /**
147
     * @var int
148
     */
149
    protected int $endLimit = 0;
150
151
    /**
152
     * @var int
153
     */
154
    protected int $pageNumber = 1;
155
156
    /**
157
     * @var string
158
     */
159
    protected string $sorter = 'byfolder';
160
    protected array $sorters = [];
161
162
    /**
163
     * @var string
164
     */
165
    protected string $filter = '';
166
167
    /**
168
     * @var string
169
     */
170
    protected array $filters = [];
171
172
    /**
173
     * @var string
174
     */
175
    protected string $displayer = 'thumbs';
176
177
    /**
178
     * @var array
179
     */
180
    protected array $allowedExtensions = [];
181
182
183
    public function __construct(?string $path = ASSETS_PATH)
184
    {
185
        $this->path = $path;
186
    }
187
188
    public function getTotalFilesCount(): int
189
    {
190
        return (int) count($this->listOfFiles);
191
    }
192
193
    /**
194
     * does the file exists in the database on staging?
195
     */
196
    public function existsOnStaging(int $id): bool
197
    {
198
        return isset($this->dataStaging[$id]);
199
    }
200
201
    /**
202
     * does the file exists in the database on live?
203
     */
204
    public function existsOnLive(int $id): bool
205
    {
206
        return isset($this->dataLive[$id]);
207
    }
208
209
    /**
210
     * get data from staging database row.
211
     */
212
    public function getAnyData(string $pathFromAssets, ?int $id = 0): array
213
    {
214
        $data = self::getStagingData($pathFromAssets, $id);
0 ignored issues
show
Bug Best Practice introduced by
The method Sunnysideup\AssetsOvervi...sInfo::getStagingData() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

214
        /** @scrutinizer ignore-call */ 
215
        $data = self::getStagingData($pathFromAssets, $id);
Loading history...
215
        if (empty($data)) {
216
            $data = self::getLiveData($pathFromAssets, $id);
0 ignored issues
show
Bug Best Practice introduced by
The method Sunnysideup\AssetsOvervi...ilesInfo::getLiveData() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

216
            /** @scrutinizer ignore-call */ 
217
            $data = self::getLiveData($pathFromAssets, $id);
Loading history...
217
        }
218
219
        return $data;
220
    }
221
222
    /**
223
     * get data from staging database row.
224
     *
225
     * @param string $pathFromAssets from the root of assets
226
     * @param int    $id
227
     */
228
    public function getStagingData(string $pathFromAssets, ?int $id = 0): array
229
    {
230
        if (! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
231
            $id = $this->databaseLookupListStaging[$pathFromAssets] ?? 0;
232
        }
233
234
        return $this->dataStaging[$id] ?? [];
235
    }
236
237
    /**
238
     * get data from live database row.
239
     *
240
     * @param string $pathFromAssets - full lookup list
241
     */
242
    public function getLiveData(string $pathFromAssets, ?int $id = 0): array
243
    {
244
        if (! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
245
            $id = $this->databaseLookupListLive[$pathFromAssets] ?? 0;
246
        }
247
248
        return $this->dataLive[$id] ?? [];
249
    }
250
251
    /**
252
     * find a value in a field in staging
253
     * returns ID of row.
254
     *
255
     * @param mixed $value
256
     */
257
    public function findInStagingData(string $fieldName, $value): int
258
    {
259
        return self::findInData($this->dataStaging, $fieldName, $value);
0 ignored issues
show
Bug Best Practice introduced by
The method Sunnysideup\AssetsOvervi...FilesInfo::findInData() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

259
        return self::/** @scrutinizer ignore-call */ findInData($this->dataStaging, $fieldName, $value);
Loading history...
260
    }
261
262
    /**
263
     * find a value in a field in live
264
     * returns ID of row.
265
     *
266
     * @param mixed $value
267
     */
268
    public function findInLiveData(string $fieldName, $value): int
269
    {
270
        return self::findInData($this->dataLive, $fieldName, $value);
0 ignored issues
show
Bug Best Practice introduced by
The method Sunnysideup\AssetsOvervi...FilesInfo::findInData() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

270
        return self::/** @scrutinizer ignore-call */ findInData($this->dataLive, $fieldName, $value);
Loading history...
271
    }
272
273
    public function getTotalFileSizesRaw()
274
    {
275
        $bytestotal = 0;
276
        $absoluteAssetPath = realpath(ASSETS_PATH);
277
        if (false !== $absoluteAssetPath && '' !== $absoluteAssetPath && file_exists($absoluteAssetPath)) {
278
            foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absoluteAssetPath, FilesystemIterator::SKIP_DOTS)) as $object) {
279
                $bytestotal += $object->getSize();
280
            }
281
        }
282
283
        return $bytestotal;
284
    }
285
286
    public function toArray(): array
287
    {
288
        if ([] === $this->listOfFiles) {
289
            $cachekey = $this->getCacheKey();
290
            if ($this->hasCacheKey($cachekey) && $this->noCache === false) {
291
                $this->listOfFiles = $this->getCacheValue($cachekey);
292
                $this->availableExtensions = $this->getCacheValue($cachekey . 'availableExtensions');
293
                $this->dataStaging = $this->getCacheValue($cachekey . 'dataStaging');
294
                $this->dataLive = $this->getCacheValue($cachekey . 'dataLive');
295
                $this->databaseLookupListStaging = $this->getCacheValue($cachekey . 'databaseLookupStaging');
296
                $this->databaseLookupListLive = $this->getCacheValue($cachekey . 'databaseLookupLive');
297
            } else {
298
                //disk
299
                $diskArray = $this->getArrayOfFilesOnDisk();
300
                foreach ($diskArray as $path) {
301
                    $this->registerFile($path, true);
302
                }
303
304
                //database
305
                $databaseArray = $this->getArrayOfFilesInDatabase();
306
                foreach ($databaseArray as $path) {
307
                    $location = trim(str_replace(ASSETS_PATH, '', $path), '/');
308
                    $this->registerFile($location, false);
309
                }
310
311
                asort($this->listOfFiles);
312
                asort($this->availableExtensions);
313
                $this->setCacheValue($cachekey, $this->listOfFiles);
314
                $this->setCacheValue($cachekey . 'availableExtensions', $this->availableExtensions);
315
                $this->setCacheValue($cachekey . 'dataStaging', $this->dataStaging);
316
                $this->setCacheValue($cachekey . 'dataLive', $this->dataLive);
317
                $this->setCacheValue($cachekey . 'databaseLookupStaging', $this->databaseLookupListStaging);
318
                $this->setCacheValue($cachekey . 'databaseLookupLive', $this->databaseLookupListLive);
319
            }
320
        }
321
322
        return $this->listOfFiles;
323
    }
324
325
    public function getFilesAsArrayList(): ArrayList
326
    {
327
        if (!isset($this->filesAsArrayList)) {
328
            $rawArray = $this->toArray();
329
            //prepare loop
330
            $this->totalFileCountRaw = self::getTotalFilesCount();
0 ignored issues
show
Bug Best Practice introduced by
The method Sunnysideup\AssetsOvervi...o::getTotalFilesCount() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

330
            /** @scrutinizer ignore-call */ 
331
            $this->totalFileCountRaw = self::getTotalFilesCount();
Loading history...
331
            $this->filesAsArrayList = ArrayList::create();
332
            $filterFree = true;
333
            $filterField = null;
334
            $filterValues = [];
335
            if (isset($this->filters[$this->filter])) {
336
                $filterFree = false;
337
                $filterField = $this->filters[$this->filter]['Field'];
338
                $filterValues = $this->filters[$this->filter]['Values'];
339
            }
340
341
            foreach ($rawArray as $location => $fileExists) {
342
                if ($this->isPathWithAllowedExtension($this->allowedExtensions, $location)) {
343
                    $intel = $this->getDataAboutOneFile($location, $fileExists);
344
                    if ($filterFree || in_array($intel[$filterField], $filterValues, 1)) {
345
                        ++$this->totalFileCountFiltered;
346
                        $this->totalFileSizeFiltered += $intel['PathFileSize'];
347
                        $this->filesAsArrayList->push(
348
                            ArrayData::create($intel)
349
                        );
350
                    }
351
                }
352
            }
353
        }
354
355
        return $this->filesAsArrayList;
356
    }
357
358
359
    public function getFilesAsSortedArrayList(): ArrayList
360
    {
361
        if (!isset($this->filesAsSortedArrayList)) {
362
            $sortField = $this->sorters[$this->sorter]['Sort'];
363
            $headerField = $this->sorters[$this->sorter]['Group'];
364
            $this->filesAsSortedArrayList = ArrayList::create();
365
            $this->filesAsArrayList = $this->filesAsArrayList->Sort($sortField);
366
367
            $count = 0;
368
369
            $innerArray = ArrayList::create();
370
            $prevHeader = 'nothing here....';
371
            $newHeader = '';
372
            foreach ($this->filesAsArrayList as $file) {
373
                $newHeader = $file->{$headerField};
374
                if ($newHeader !== $prevHeader) {
375
                    $this->addTofilesAsSortedArrayList(
376
                        $prevHeader, //correct! important ...
377
                        $innerArray
378
                    );
379
                    $prevHeader = $newHeader;
380
                    unset($innerArray);
381
                    $innerArray = ArrayList::create();
382
                }
383
384
                if ($count >= $this->startLimit && $count < $this->endLimit) {
385
                    $innerArray->push($file);
386
                } elseif ($count >= $this->endLimit) {
387
                    break;
388
                }
389
390
                ++$count;
391
            }
392
393
            //last one!
394
            $this->addTofilesAsSortedArrayList(
395
                $newHeader,
396
                $innerArray
397
            );
398
        }
399
400
        return $this->filesAsSortedArrayList;
401
    }
402
403
404
    public function getAvailableExtensions(): array
405
    {
406
        return $this->availableExtensions;
407
    }
408
409
410
    public function getTotalFileCountRaw(): int
411
    {
412
        return $this->totalFileCountRaw;
413
    }
414
415
416
    public function getTotalFileCountFiltered(): int
417
    {
418
        return $this->totalFileCountFiltered;
419
    }
420
421
422
    public function getTotalFileSizeFiltered(): int
423
    {
424
        return $this->totalFileSizeFiltered;
425
    }
426
427
    public function setAvailableExtensions(array $availableExtensions): static
428
    {
429
        $this->availableExtensions = $availableExtensions;
430
        return $this;
431
    }
432
433
    public function setFilters(array $filters): static
434
    {
435
        $this->filters = $filters;
0 ignored issues
show
Documentation Bug introduced by
It seems like $filters of type array is incompatible with the declared type string of property $filters.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
436
        return $this;
437
    }
438
439
    public function setSorters(array $sorters): static
440
    {
441
        $this->sorters = $sorters;
442
        return $this;
443
    }
444
445
    public function setLimit(int $limit): static
446
    {
447
        $this->limit = $limit;
448
        return $this;
449
    }
450
451
    public function setStartLimit(int $startLimit): static
452
    {
453
        $this->startLimit = $startLimit;
454
        return $this;
455
    }
456
457
    public function setEndLimit(int $endLimit): static
458
    {
459
        $this->endLimit = $endLimit;
460
        return $this;
461
    }
462
463
    public function setPageNumber(int $pageNumber): static
464
    {
465
        $this->pageNumber = $pageNumber;
466
        return $this;
467
    }
468
469
    public function setSorter(string $sorter): static
470
    {
471
        $this->sorter = $sorter;
472
        return $this;
473
    }
474
475
    public function setFilter(string $filter): static
476
    {
477
        $this->filter = $filter;
478
        return $this;
479
    }
480
481
    public function setDisplayer(string $displayer): static
482
    {
483
        $this->displayer = $displayer;
484
        return $this;
485
    }
486
487
    public function setAllowedExtensions(array $allowedExtensions): static
488
    {
489
        $this->allowedExtensions = $allowedExtensions;
490
        return $this;
491
    }
492
493
494
    protected function addTofilesAsSortedArrayList(string $header, ArrayList $arrayList)
495
    {
496
        if ($arrayList->exists()) {
497
            $count = $this->filesAsSortedArrayList->count();
498
            $this->filesAsSortedArrayList->push(
499
                ArrayData::create(
500
                    [
501
                        'Number' => $count,
502
                        'SubTitle' => $header,
503
                        'Items' => $arrayList,
504
                    ]
505
                )
506
            );
507
        }
508
    }
509
510
    protected function getDataAboutOneFile(string $location, ?bool $fileExists): array
0 ignored issues
show
The parameter $fileExists is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

510
    protected function getDataAboutOneFile(string $location, /** @scrutinizer ignore-unused */ ?bool $fileExists): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
511
    {
512
        $obj = OneFileInfo::inst($location);
513
        $obj->setNoCache($this->noCache);
514
        return $obj->toArray();
515
    }
516
517
    /**
518
     * @param string $path - does not have to be full path
519
     */
520
    protected function isPathWithAllowedExtension(array $allowedExtensions, string $path): bool
521
    {
522
        $count = count($allowedExtensions);
523
        if (0 === $count) {
524
            return true;
525
        }
526
527
        $extension = strtolower($this->getExtension($path));
528
        if ($extension === '') {
529
            $extension = 'n/a';
530
        }
531
532
        return in_array($extension, $allowedExtensions, true);
533
    }
534
535
    protected function registerFile($path, ?bool $inFileSystem = true)
536
    {
537
        if ($path) {
538
            if (! isset($this->listOfFiles[$path])) {
539
                $this->listOfFiles[$path] = $inFileSystem;
540
                if ($this->verbose) {
541
                    if ($inFileSystem) {
542
                        echo '✓ ';
543
                    } else {
544
                        echo PHP_EOL . 'x NOT IN FILE SYSTEM: ' . $path . ' ' . PHP_EOL;
545
                    }
546
                }
547
548
                $extension = strtolower($this->getExtension($path));
549
                $this->availableExtensions[$extension] = $extension;
550
            }
551
        }
552
    }
553
554
555
    /**
556
     * @param mixed $value
557
     */
558
    protected function findInData(array $data, string $fieldName, $value): int
559
    {
560
        foreach ($data as $id => $row) {
561
            if (isset($row[$fieldName])) {
562
                if ($row[$fieldName] === $value) {
563
                    return (int) $id;
564
                }
565
            }
566
        }
567
568
        return 0;
569
    }
570
571
    protected function isRealFile(string $absolutePath): bool
572
    {
573
        $listOfItemsToSearchFor = Config::inst()->get(self::class, 'not_real_file_substrings');
574
        foreach ($listOfItemsToSearchFor as $test) {
575
            if (strpos($absolutePath, $test)) {
576
                return false;
577
            }
578
        }
579
580
        $fileName = basename($absolutePath);
581
        $isErrorPage = ('error' === substr((string) $fileName, 0, 5) && '.html' === substr((string) $fileName, -5));
582
        return ! $isErrorPage;
583
    }
584
585
    protected function getArrayOfFilesOnDisk(): array
586
    {
587
        $finalArray = [];
588
        $arrayRaw = new RecursiveIteratorIterator(
589
            new RecursiveDirectoryIterator($this->path),
590
            RecursiveIteratorIterator::SELF_FIRST
591
        );
592
        foreach ($arrayRaw as $src) {
593
            $absolutePath = $src->getPathName();
594
            if (false === $this->isRealFile($absolutePath)) {
595
                continue;
596
            }
597
            if ($src->isDir()) {
598
                continue;
599
            }
600
            $location = trim(str_replace(ASSETS_PATH, '', $absolutePath), '/');
601
            $finalArray[$location] = $location;
602
        }
603
604
        return $finalArray;
605
    }
606
607
    /**
608
     *
609
     * returns all the files in the database except for folders.
610
     * @return array
611
     */
612
    protected function getArrayOfFilesInDatabase(): array
613
    {
614
        $finalArray = [];
615
        foreach (['Stage', 'Live'] as $stage) {
616
            Versioned::set_stage($stage);
617
            $files = File::get()->exclude(['ClassName' => Folder::class]);
618
            foreach ($files as $file) {
619
                $row = $file->toMap();
620
                $location = trim($file->getFilename(), '/');
621
                if (!$location) {
622
                    $location = $file->generateFilename();
623
                }
624
                if ('Stage' === $stage) {
625
                    $this->dataStaging[$row['ID']] = $row;
626
                    $this->databaseLookupListStaging[$location] = $row['ID'];
627
                } elseif ('Live' === $stage) {
628
                    $this->dataLive[$row['ID']] = $row;
629
                    $this->databaseLookupListLive[$location] = $row['ID'];
630
                } else {
631
                    user_error('Can not find stage');
632
                }
633
634
                $finalArray[$location] = $location;
635
            }
636
        }
637
        Versioned::set_stage('Stage');
638
        return $finalArray;
639
    }
640
641
    //#############################################
642
    // CACHE
643
    //#############################################
644
645
    protected function getCacheKey(): string
646
    {
647
        return 'allfiles';
648
    }
649
}
650