Passed
Push — master ( e692f4...85b8eb )
by Nicolaas
02:20
created

SortOutFolders::removeUnusedFiles()   B

Complexity

Conditions 7
Paths 2

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 21
c 4
b 0
f 0
dl 0
loc 29
rs 8.6506
cc 7
nc 2
nop 2
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
14
use Sunnysideup\PerfectCmsImages\Api\PerfectCMSImages;
15
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Core\Config\Configurable;
18
19
use SilverStripe\Core\ClassInfo;
20
21
use SilverStripe\Core\Injector\Injector;
22
23
24
/**
25
 * the assumption we make here is that a particular group of images (e.g. Page.Image) live
26
 * live in a particular folder.
27
 */
28
class SortOutFolders
29
{
30
31
    use Configurable;
32
33
    /**
34
     * @var Folder
35
     */
36
    protected $unusedImagesFolder = null;
37
38
    /**
39
     * if set to true then dont do it for real!
40
     * @var bool
41
     */
42
    protected $dryRun = false;
43
44
    /**
45
     *
46
     * @var bool
47
     */
48
    protected $verbose = true;
49
50
    private static $unused_images_folder_name = 'unusedimages';
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
65
    /**
66
     * @param string $unusedFolderName
67
     * @param array $data
68
     * Create test jobs for the purposes of testing.
69
     * The array must contains arrays with
70
     * - folder
71
     * - used_by
72
     * used_by is an array that has ClassNames and Relations
73
     * (has_one / has_many / many_many relations)
74
     * e.g. Page.Image, MyDataObject.MyImages
75
     *
76
     * @param HTTPRequest $request
77
     */
78
    public function run(string $unusedFolderName, array $data) // phpcs:ignore
79
    {
80
        $this->unusedImagesFolder = Folder::find_or_make($unusedFolderName);
81
82
        $folderArray = $this->getFolderArray($data);
83
        if ($this->verbose) {
84
            DB::alteration_message('==== List of folders ====');
85
            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

85
            echo '<pre>'./** @scrutinizer ignore-type */ print_r($folderArray, 1).'</pre>';
Loading history...
86
        }
87
88
        $listOfImageIds = $this->getListOfImages($folderArray);
89
90
        // remove
91
        $imagesLeft = [];
92
        foreach($listOfImageIds as $folderName => $listOfIds) {
93
            DB::alteration_message('==== DOING '.$folderName.' of Image IDs ===='. count($listOfIds).' images to keep');
94
            $imagesLeft[$folderName] = $this->removeUnusedFiles($folderName, $listOfIds);
95
        }
96
97
        // reintroduce
98
        foreach($imagesLeft as $folderName => $listOfIds) {
99
            DB::alteration_message('==== DOING '.$folderName.' of Image IDs ===='. count($listOfIds).' images to re-introduce');
100
            $this->moveUsedFilesIntoFolder($folderName, $listOfIds);
101
        }
102
    }
103
104
105
    public function getFolderArray(array $data) :array
106
    {
107
        // check folders
108
        $folderArray = [];
109
        foreach($data as $dataInner) {
110
            $folder = $dataInner['folder'] ?? '';
111
            if($folder) {
112
                $folderArray[$folder] = [];
113
                $classes = $dataInner['used_by'] ?? [];
114
                if(! empty($classes)) {
115
                    if(is_array($classes)) {
116
                        foreach($classes as $classAndMethodList) {
117
                            $folderArray[$folder][$classAndMethodList] = $classAndMethodList;
118
                        }
119
                    } else {
120
                        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

120
                        user_error('Bad definition for: './** @scrutinizer ignore-type */ print_r($dataInner, 1));
Loading history...
121
                    }
122
                }
123
            }
124
        }
125
        return $folderArray;
126
    }
127
128
    public function getListOfImages(array $folderArray) : array
129
    {
130
        $listOfImageIds = [];
131
        foreach($folderArray as $folderName => $classAndMethodList) {
132
133
            // find all images that should be there...
134
            $listOfIds = [];
135
            foreach($classAndMethodList as $classAndMethod) {
136
                list($className, $method) = explode('.', $classAndMethod);
137
                $fieldDetails = $this->getFieldDetails($className, $method);
138
                if(empty($fieldDetails)) {
139
                    user_error('Could not find relation: '.$className.'.'.$method);
140
                }
141
                if($fieldDetails['dataType'] === 'has_one') {
142
                    $list = $className::get()->columnUnique($method.'ID');
143
                } else {
144
                    $dataClassName = $fieldDetails['dataClassName'];
145
                    $list = $dataClassName::get()->relation($method)->columnUnique('ID');
146
                }
147
                $listOfIds = array_unique(
148
                    array_merge(
149
                        $listOfIds,
150
                        $list
151
                    )
152
                );
153
            }
154
            if(count($listOfIds)) {
155
                $listOfImageIds[$folderName] = $listOfIds;
156
            }
157
        }
158
        return $listOfImageIds;
159
    }
160
161
    /**
162
     * returns the images in the ID list that were not found in the folder.
163
     * @param  string $folderName                   Folder moving to
164
     * @param  array  $listOfImageIds               Images that should be in the folder
165
     * @return array                                Unused images
166
     */
167
    public function removeUnusedFiles(string $folderName, array $listOfImageIds) : array
168
    {
169
        $unusedFolderName = $this->unusedImagesFolder->Name;
170
        $folder = Folder::find_or_make($folderName);
171
        $listAsString = implode(',', $listOfImageIds);
172
        $where = ' ParentID = ' . $folder->ID. ' AND File.ID NOT IN('.$listAsString.')';
173
        $unused = Image::get()->where($where);
174
        if ($unused->exists()) {
175
            foreach ($unused as $file) {
176
                if (in_array($file->ID, $listOfImageIds)) {
177
                    unset($array[array_search($file->ID, $listOfImageIds)]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $array seems to be never defined.
Loading history...
178
                }
179
                $oldName = $file->getFileName();
180
                if($this->verbose) {
181
                    DB::alteration_message('moving '.$file->getFileName().' to '.$unusedFolderName);
182
                }
183
                if($this->dryRun === false) {
184
                    $file->ParentID = $this->unusedImagesFolder->ID;
185
                    $file->write();
186
                    $file->doPublish();
187
                    $newName = str_replace($folder->Name, $unusedFolderName, $oldName);
188
                    if($newName !== $file->getFileName()) {
189
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFileName());
190
                    }
191
                    $this->physicallyMovingImage($oldName, $newName);
192
                }
193
            }
194
        }
195
        return $listOfImageIds;
196
    }
197
198
    public function moveUsedFilesIntoFolder(string $folderName, array $listOfImageIds)
199
    {
200
        $folder = Folder::find_or_make($folderName);
201
        $listAsString = implode(',', $listOfImageIds);
202
        $where = ' ParentID <> ' . $folder->ID. ' AND File.ID IN('.$listAsString.')';
203
        $used = Image::get()->where($where);
204
        if ($used->exists()) {
205
            foreach ($used as $file) {
206
                $oldFolderName = $file->Parent()->Name;
207
                $oldName = $file->getFileName();
208
                if($this->verbose) {
209
                    DB::alteration_message('moving '.$file->getFileName().' to '.$folderName);
210
                }
211
                if($this->dryRun === false) {
212
                    $file->ParentID = $folder->ID;
213
                    $file->write();
214
                    $file->doPublish();
215
                    if($oldFolderName === '') {
216
                        $newName = $folder->Name . '/' . $oldName;
217
                    } else {
218
                        $newName = str_replace($oldFolderName, $folder->Name, $oldName);
219
                    }
220
                    if($this->verbose && $newName !== $file->getFileName()) {
221
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFileName(), 'deleted');
222
                    } else {
223
                        $this->physicallyMovingImage($oldName, $newName);
224
                    }
225
                }
226
            }
227
        }
228
    }
229
230
    protected static $my_field_cache = [];
231
232
    protected function getFieldDetails(string $originClassName, string $originMethod) : array
233
    {
234
        $key = $originClassName.'_'.$originMethod;
235
        if(! isset(self::$my_field_cache[$key])) {
236
            $types = ['has_one', 'has_many', 'many_many'];
237
            $classNames = ClassInfo::ancestry($originClassName, true);
238
            foreach ($classNames as $className) {
239
                $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...
240
                foreach ($types as $type) {
241
                    $rels = Config::inst()->get($className, $type, Config::UNINHERITED);
242
                    if (is_array($rels) && ! empty($rels)) {
243
                        foreach ($rels as $relName => $relType) {
244
                            if (Image::class === $relType && $relName === $originMethod) {
245
                                self::$my_field_cache[$key] = [
246
                                    'dataClassName' => $className,
247
                                    'dataType' => $type,
248
                                ];
249
                            }
250
                        }
251
                    }
252
                }
253
            }
254
        }
255
        return self::$my_field_cache[$key];
256
    }
257
258
    protected function physicallyMovingImage(string $oldName, string $newName)
259
    {
260
        if ($oldName !== $newName) {
261
            $oldNameFull = Controller::join_links(ASSETS_PATH, $oldName);
262
            $newNameFull = Controller::join_links(ASSETS_PATH, $newName);
263
            if (file_exists($oldNameFull)) {
264
                if(file_exists($newNameFull)) {
265
                    if ($this->verbose) {
266
                        DB::alteration_message('Deleting '.$newName.' to make place for a new file.', 'deleted');
267
                    }
268
                    unlink($newNameFull);
269
                }
270
                if ($this->verbose) {
271
                    DB::alteration_message('Moving '.$oldNameFull.' to '.$newNameFull, 'created');
272
                }
273
                rename($oldNameFull, $newNameFull);
274
            }
275
        } elseif($this->verbose) {
276
            DB::alteration_message('ERROR: old and new file names are the same '.$oldName, 'deleted');
277
        }
278
    }
279
280
}
281