Passed
Push — master ( 28167b...9ffca7 )
by Nicolaas
03:02
created

SortOutFolders::moveToNewFolder()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

90
            echo '<pre>'./** @scrutinizer ignore-type */ print_r($folderArray, 1).'</pre>';
Loading history...
91
        }
92
93
        $listOfImageIds = $this->getListOfImages($folderArray);
94
95
        // remove
96
        foreach($listOfImageIds as $folderName => $listOfIds) {
97
            if($this->verbose) {
98
                DB::alteration_message('<br /><br /><br />==== Checking for images to remove from <u>'.$folderName.'</u>; there are '. count($listOfIds).' images to keep');
99
            }
100
            $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...
101
        }
102
103
        DB::alteration_message('==========================================');
104
        // move to right folder
105
        foreach($listOfImageIds as $folderName => $listOfIds) {
106
            if($this->verbose) {
107
                DB::alteration_message('<br /><br /><br />==== Checking for images to move to <u>'.$folderName.'</u>');
108
            }
109
            $this->moveUsedFilesIntoFolder($folderName, $listOfIds);
110
        }
111
112
        DB::alteration_message('==========================================');
113
114
        // check for rogue files
115
        foreach(array_keys($listOfImageIds) as $folderName) {
116
            if($this->verbose) {
117
                DB::alteration_message('<br /><br /><br />==== Checking for rogue FILES in <u>'.$folderName.'</u>');
118
            }
119
            $this->findRoqueFilesInFolder($folderName);
120
        }
121
    }
122
123
124
    public function getFolderArray(array $data) :array
125
    {
126
        // check folders
127
        $folderArray = [];
128
        foreach($data as $dataInner) {
129
            $folderName = $dataInner['folder'] ?? '';
130
            if($folderName) {
131
                $folderArray[$folderName] = [];
132
                $folderArray[$folderName]['classesAndMethods'] = [];
133
                // $folderArray[$folderName]['resize'] = isset($dataInner['force_resize']) && $dataInner['force_resize'] === true  ? true : false;
134
                $classes = $dataInner['used_by'] ?? [];
135
                if(! empty($classes)) {
136
                    if(is_array($classes)) {
137
                        foreach($classes as $classAndMethod) {
138
                            $folderArray[$folderName]['classesAndMethods'][$classAndMethod] = $classAndMethod;
139
                        }
140
                    } else {
141
                        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

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