Passed
Push — master ( 18f79d...e076c9 )
by Nicolaas
02:42 queued 12s
created

SortOutFolders::writeFolder()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
namespace Sunnysideup\PerfectCmsImages\Api;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\Assets\Folder;
7
use SilverStripe\Assets\Image;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Config\Configurable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\Versioned\Versioned;
15
16
/**
17
 * the assumption we make here is that a particular group of images (e.g. Page.Image) live
18
 * live in a particular folder.
19
 */
20
class SortOutFolders
21
{
22
    use Configurable;
23
24
    /**
25
     * the folder where we move images that are not in use.
26
     *
27
     * @var Folder
28
     */
29
    protected $unusedImagesFolder;
30
31
    /**
32
     * if set to true then dont do it for real!
33
     *
34
     * @var bool
35
     */
36
    protected $dryRun = false;
37
38
    /**
39
     * @var bool
40
     */
41
    protected $verbose = true;
42
43
    protected static $my_field_cache = [];
44
45
    public function setVerbose(?bool $b = true)
46
    {
47
        $this->verbose = $b;
48
49
        return $this;
50
    }
51
52
    public function setDryRun(?bool $b = true)
53
    {
54
        $this->dryRun = $b;
55
56
        return $this;
57
    }
58
59
    public function runStandard()
60
    {
61
        $this->runAdvanced(
62
            Config::inst()->get(PerfectCMSImages::class, 'unused_images_folder_name'),
63
            PerfectCMSImages::get_all_values_for_images()
64
        );
65
    }
66
67
    /**
68
     * @param array $data
69
     *                    Create test jobs for the purposes of testing.
70
     *                    The array must contains arrays with
71
     *                    - folder
72
     *                    - used_by
73
     *                    used_by is an array that has ClassNames and Relations
74
     *                    (has_one / has_many / many_many relations)
75
     *                    e.g. Page.Image, MyDataObject.MyImages
76
     */
77
    public function runAdvanced(string $unusedFolderName, array $data)
78
    {
79
        $this->unusedImagesFolder = Folder::find_or_make($unusedFolderName);
80
81
        $folderArray = $this->getFolderArray($data);
82
        if ($this->verbose) {
83
            DB::alteration_message('==== List of folders ====');
84
            echo '<pre>' . print_r($folderArray, 1) . '</pre>';
0 ignored issues
show
Bug introduced by
Are you sure print_r($folderArray, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

84
            echo '<pre>' . /** @scrutinizer ignore-type */ print_r($folderArray, 1) . '</pre>';
Loading history...
85
        }
86
87
        DB::alteration_message('==========================================');
88
89
        $listOfImageIds = $this->getListOfImages($folderArray);
90
91
        // remove
92
        foreach ($listOfImageIds as $folderName => $listOfIds) {
93
            if ($this->verbose) {
94
                DB::alteration_message('<br /><br /><br />==== Checking for images to remove from <u>' . $folderName . '</u>; there are ' . count($listOfIds) . ' images to keep');
95
            }
96
            $imagesLeft[$folderName] = $this->removeUnusedFiles($folderName, $listOfIds);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $imagesLeft[$folderName] is correct as $this->removeUnusedFiles($folderName, $listOfIds) targeting Sunnysideup\PerfectCmsIm...rs::removeUnusedFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
97
        }
98
99
        DB::alteration_message('==========================================');
100
        // move to right folder
101
        foreach ($listOfImageIds as $folderName => $listOfIds) {
102
            if ($this->verbose) {
103
                DB::alteration_message('<br /><br /><br />==== Checking for images to move to <u>' . $folderName . '</u>');
104
            }
105
            $this->moveUsedFilesIntoFolder($folderName, $listOfIds);
106
        }
107
108
        DB::alteration_message('==========================================');
109
110
        // check for rogue files
111
        foreach (array_keys($listOfImageIds) as $folderName) {
112
            if ($this->verbose) {
113
                DB::alteration_message('<br /><br /><br />==== Checking for rogue FILES in <u>' . $folderName . '</u>');
114
            }
115
            $this->findRoqueFilesInFolder($folderName);
116
        }
117
    }
118
119
    public function getFolderArray(array $data): array
120
    {
121
        // check folders
122
        $folderArray = [];
123
        foreach ($data as $dataInner) {
124
            $folderName = $dataInner['folder'] ?? '';
125
            if ($folderName) {
126
                $folderArray[$folderName] = [];
127
                $folderArray[$folderName]['classesAndMethods'] = [];
128
                // $folderArray[$folderName]['resize'] = isset($dataInner['force_resize']) && $dataInner['force_resize'] === true  ? true : false;
129
                $classes = $dataInner['used_by'] ?? [];
130
                if (! empty($classes)) {
131
                    if (is_array($classes)) {
132
                        foreach ($classes as $classAndMethod) {
133
                            $folderArray[$folderName]['classesAndMethods'][$classAndMethod] = $classAndMethod;
134
                        }
135
                    } else {
136
                        user_error('Bad definition for: ' . print_r($dataInner, 1));
0 ignored issues
show
Bug introduced by
Are you sure print_r($dataInner, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

136
                        user_error('Bad definition for: ' . /** @scrutinizer ignore-type */ print_r($dataInner, 1));
Loading history...
137
                    }
138
                }
139
            }
140
        }
141
        $test = [];
142
        foreach ($folderArray as $folderName => $folderData) {
143
            $classAndMethodList = $folderData['classesAndMethods'];
144
            foreach ($classAndMethodList as $classAndMethod) {
145
                if (! isset($test[$classAndMethod])) {
146
                    $test[$classAndMethod] = true;
147
                } else {
148
                    user_error('You have doubled up on folder for Class and Method: ' . $classAndMethod);
149
                }
150
            }
151
        }
152
153
        return $folderArray;
154
    }
155
156
    public function getListOfImages(array $folderArray): array
157
    {
158
        $listOfImageIds = [];
159
        foreach ($folderArray as $folderName => $folderData) {
160
            $classAndMethodList = $folderData['classesAndMethods'];
161
162
            // find all images that should be there...
163
            $listOfIds = [];
164
            foreach ($classAndMethodList as $classAndMethod) {
165
                $dataClassName = '';
166
                list($className, $method) = explode('.', $classAndMethod);
167
                $fieldDetails = $this->getFieldDetails($className, $method);
168
                if (empty($fieldDetails)) {
169
                    user_error('Could not find relation: ' . $className . '.' . $method);
170
                }
171
                if ('has_one' === $fieldDetails['dataType']) {
172
                    $list = $className::get()->columnUnique($method . 'ID');
173
                } else {
174
                    $dataClassName = $fieldDetails['dataClassName'];
0 ignored issues
show
Unused Code introduced by
The assignment to $dataClassName is dead and can be removed.
Loading history...
175
                    $outerList = $className::get();
176
                    $list = [];
177
                    foreach ($outerList as $obj) {
178
                        $list = array_merge($list, $obj->{$method}()->columnUnique('ID'));
179
                    }
180
                    $list = array_unique($list);
181
                }
182
                DB::alteration_message($className . '::' . $method . ' resulted in ' . count($list));
183
                $listOfIds = array_unique(
184
                    array_merge(
185
                        $listOfIds,
186
                        $list
187
                    )
188
                );
189
            }
190
            if (count($listOfIds)) {
191
                $listOfImageIds[$folderName] = $listOfIds;
192
            }
193
        }
194
195
        return $listOfImageIds;
196
    }
197
198
    /**
199
     * returns the images in the ID list that were not found in the folder.
200
     *
201
     * @param string $folderName     Folder moving to
202
     * @param array  $listOfImageIds Images that should be in the folder
203
     */
204
    public function removeUnusedFiles(string $folderName, array $listOfImageIds)
205
    {
206
        $unusedFolderName = $this->unusedImagesFolder->Name;
207
        $folder = Folder::find_or_make($folderName);
208
        $this->writeFolder($folder);
209
        $listAsString = implode(',', $listOfImageIds);
210
        $where = ' ParentID = ' . $folder->ID . ' AND File.ID NOT IN(' . $listAsString . ')';
211
        $unused = Image::get()->where($where);
212
        if ($unused->exists()) {
213
            foreach ($unused as $file) {
214
                echo '.';
215
                $oldName = $file->getFilename();
216
                if ($this->verbose) {
217
                    DB::alteration_message('moving ' . $file->getFilename() . ' to ' . $unusedFolderName);
218
                }
219
                if (false === $this->dryRun) {
220
                    $newName = Controller::join_links($this->unusedImagesFolder->getFileName(), $file->Name);
221
                    $file = $this->moveToNewFolder($file, $this->unusedImagesFolder, $newName);
222
                    if ($newName !== $file->getFilename()) {
223
                        DB::alteration_message('ERROR: file names do not match. Compare: ' . $newName . ' with ' . $file->getFilename(), 'deleted');
224
                    } else {
225
                        $this->physicallyMovingImage($oldName, $newName);
226
                    }
227
                }
228
            }
229
        }
230
    }
231
232
    public function moveUsedFilesIntoFolder(string $folderName, array $listOfImageIds)
233
    {
234
        $folder = Folder::find_or_make($folderName);
235
        $this->writeFolder($folder);
236
        $listAsString = implode(',', $listOfImageIds);
237
        $where = ' ParentID <> ' . $folder->ID . ' AND File.ID IN(' . $listAsString . ')';
238
        $used = Image::get()->where($where);
239
        if ($used->exists()) {
240
            foreach ($used as $file) {
241
                $oldName = $file->getFilename();
242
243
                $oldFolderName = $file->Parent()->getFilename();
0 ignored issues
show
Unused Code introduced by
The assignment to $oldFolderName is dead and can be removed.
Loading history...
244
                $newFolderName = $folder->getFilename();
245
246
                if ($this->verbose) {
247
                    DB::alteration_message('moving ' . $file->getFilename() . ' to ' . $newFolderName, 'created');
248
                }
249
                if (false === $this->dryRun) {
250
                    $newName = Controller::join_links($newFolderName, $file->Name);
251
                    $file = $this->moveToNewFolder($file, $folder, $newName);
252
                    if ($newName !== $file->getFilename()) {
253
                        DB::alteration_message('ERROR: file names do not match. Compare: ' . $newName . ' with ' . $file->getFilename(), 'deleted');
254
                    } else {
255
                        $this->physicallyMovingImage($oldName, $newName);
256
                    }
257
                }
258
            }
259
        }
260
    }
261
262
    public function findRoqueFilesInFolder(string $folderName)
263
    {
264
        $unusedFolderName = $this->unusedImagesFolder->Name;
265
        $folder = Folder::find_or_make($folderName);
266
        $this->writeFolder($folder);
267
        $fullFolderPath = Controller::join_links(ASSETS_PATH, $folder->getFilename());
268
        $excludeArray = Image::get()->filter(['ParentID' => $folder->ID])->columnUnique('Name');
269
        if (is_dir($fullFolderPath)) {
270
            $files = array_diff(scandir($fullFolderPath), ['.', '..']);
271
            foreach ($files as $fileName) {
272
                if (! in_array($fileName, $excludeArray, true)) {
273
                    $associatedClassName = File::get_class_for_file_extension(pathinfo($fileName, PATHINFO_EXTENSION));
274
                    if (Image::class === $associatedClassName) {
275
                        $filePath = Controller::join_links($fullFolderPath, $fileName);
276
                        if (is_file($filePath)) {
277
                            $oldName = $folderName . '/' . $fileName;
278
                            $newName = $unusedFolderName . '/' . $fileName;
279
                            if ($this->verbose) {
280
                                DB::alteration_message('moving ' . $oldName . ' to ' . $unusedFolderName);
281
                            }
282
                            if (false === $this->dryRun) {
283
                                $this->physicallyMovingImage($oldName, $newName);
284
                            }
285
                        } elseif ($this->verbose) {
286
                            DB::alteration_message('skippping ' . $fileName . ', because it is not a valid file.');
287
                        }
288
                    } elseif ($this->verbose) {
289
                        DB::alteration_message('skippping ' . $fileName . ', because it is not an image.');
290
                    }
291
                }
292
            }
293
        } elseif ($this->verbose) {
294
            DB::alteration_message('skippping ' . $fullFolderPath . ', because it is not a valid directory.');
295
        }
296
    }
297
298
    protected function getFieldDetails(string $originClassName, string $originMethod): array
299
    {
300
        $key = $originClassName . '_' . $originMethod;
301
        if (! isset(self::$my_field_cache[$key])) {
302
            $types = ['has_one', 'has_many', 'many_many'];
303
            $classNames = ClassInfo::ancestry($originClassName, true);
304
            foreach ($classNames as $className) {
305
                $obj = Injector::inst()->get($className);
0 ignored issues
show
Unused Code introduced by
The assignment to $obj is dead and can be removed.
Loading history...
306
                foreach ($types as $type) {
307
                    $rels = Config::inst()->get($className, $type, Config::UNINHERITED);
308
                    if (is_array($rels) && ! empty($rels)) {
309
                        foreach ($rels as $relName => $relType) {
310
                            if (Image::class === $relType && $relName === $originMethod) {
311
                                self::$my_field_cache[$key] = [
312
                                    'dataClassName' => $className,
313
                                    'dataType' => $type,
314
                                ];
315
                            }
316
                        }
317
                    }
318
                }
319
            }
320
        }
321
322
        return self::$my_field_cache[$key];
323
    }
324
325
    protected function physicallyMovingImage(string $oldName, string $newName)
326
    {
327
        if ($oldName !== $newName) {
328
            $oldNameFull = Controller::join_links(ASSETS_PATH, $oldName);
329
            $newNameFull = Controller::join_links(ASSETS_PATH, $newName);
330
            if (file_exists($oldNameFull)) {
331
                if (file_exists($newNameFull)) {
332
                    if ($this->verbose) {
333
                        DB::alteration_message('... ... Deleting ' . $newName . ' to make place for a new file.', 'deleted');
334
                    }
335
                    if (false === $this->dryRun) {
336
                        unlink($newNameFull);
337
                    }
338
                }
339
                if ($this->verbose) {
340
                    DB::alteration_message('... Moving ' . $oldNameFull . ' to ' . $newNameFull . ' (file only)', 'created');
341
                }
342
                if (false === $this->dryRun) {
343
                    rename($oldNameFull, $newNameFull);
344
                }
345
            } elseif ($this->verbose && ! file_exists($newNameFull)) {
346
                DB::alteration_message('... Error: could not find:  ' . $oldNameFull . ' and it is also not here: ' . $newNameFull, 'created');
347
            }
348
        } elseif ($this->verbose) {
349
            DB::alteration_message('... ERROR: old and new file names are the same ' . $oldName, 'deleted');
350
        }
351
    }
352
353
    protected function writeFileOrFolder($fileOrFolder)
354
    {
355
        $fileOrFolder->writeToStage(Versioned::DRAFT);
356
        $fileOrFolder->publishSingle();
357
358
        return $fileOrFolder;
359
    }
360
361
    protected function writeFolder($folder) {
362
        if(! $folder->exists()) {
363
            $folder->write();
364
        }
365
        if(! $folder->isPublished()) {
366
            //publish single is SUPER SLOW!
367
            $folder->doPublish();
368
        }
369
    }
370
371
    protected function moveToNewFolder($image, Folder $newFolder, string $newName)
372
    {
373
        $beforePath = (Controller::join_links(ASSETS_PATH, $image->getFilename()));
374
        $afterPath = (Controller::join_links(ASSETS_PATH, $newFolder->getFileName(), $image->Name));
375
        if (file_exists($afterPath)) {
376
            unlink($afterPath);
377
        }
378
        $image->ParentID = $newFolder->ID;
379
        $image->setFilename($newName);
380
        $image = $this->writeFileOrFolder($image);
381
        $image->flushCache();
382
        if (file_exists($beforePath) && ! file_exists($afterPath)) {
383
            rename($beforePath, $afterPath);
384
        }
385
386
        return $image;
387
    }
388
}
389