Passed
Push — master ( 001c32...253646 )
by Nicolaas
18:38 queued 10:38
created

SortOutFolders::moveUsedFilesIntoFolder()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 24
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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