Passed
Push — master ( 85b8eb...91b4c0 )
by Nicolaas
02:24
created

SortOutFolders::checkForRogueFiles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 0
c 1
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 0
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
                    $file->flushCache();
189
                    if($newName !== $file->getFileName()) {
190
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFileName(), 'deleted');
191
                    }
192
                    $this->physicallyMovingImage($oldName, $newName);
193
                }
194
            }
195
        }
196
        return $listOfImageIds;
197
    }
198
199
    public function moveUsedFilesIntoFolder(string $folderName, array $listOfImageIds)
200
    {
201
        $folder = Folder::find_or_make($folderName);
202
        $listAsString = implode(',', $listOfImageIds);
203
        $where = ' ParentID <> ' . $folder->ID. ' AND File.ID IN('.$listAsString.')';
204
        $used = Image::get()->where($where);
205
        if ($used->exists()) {
206
            foreach ($used as $file) {
207
                $oldFolderName = $file->Parent()->Name;
208
                $oldName = $file->getFileName();
209
                if($this->verbose) {
210
                    DB::alteration_message('moving '.$file->getFileName().' to '.$folderName);
211
                }
212
                if($this->dryRun === false) {
213
                    $file->ParentID = $folder->ID;
214
                    $file->write();
215
                    $file->doPublish();
216
                    if($oldFolderName === '') {
217
                        $newName = $folder->Name . '/' . $oldName;
218
                    } else {
219
                        $newName = str_replace($oldFolderName, $folder->Name, $oldName);
220
                    }
221
                    $file->flushCache();
222
                    if($this->verbose && $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
    protected static $my_field_cache = [];
233
234
    protected function getFieldDetails(string $originClassName, string $originMethod) : array
235
    {
236
        $key = $originClassName.'_'.$originMethod;
237
        if(! isset(self::$my_field_cache[$key])) {
238
            $types = ['has_one', 'has_many', 'many_many'];
239
            $classNames = ClassInfo::ancestry($originClassName, true);
240
            foreach ($classNames as $className) {
241
                $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...
242
                foreach ($types as $type) {
243
                    $rels = Config::inst()->get($className, $type, Config::UNINHERITED);
244
                    if (is_array($rels) && ! empty($rels)) {
245
                        foreach ($rels as $relName => $relType) {
246
                            if (Image::class === $relType && $relName === $originMethod) {
247
                                self::$my_field_cache[$key] = [
248
                                    'dataClassName' => $className,
249
                                    'dataType' => $type,
250
                                ];
251
                            }
252
                        }
253
                    }
254
                }
255
            }
256
        }
257
        return self::$my_field_cache[$key];
258
    }
259
260
    protected function physicallyMovingImage(string $oldName, string $newName)
261
    {
262
        if ($oldName !== $newName) {
263
            $oldNameFull = Controller::join_links(ASSETS_PATH, $oldName);
264
            $newNameFull = Controller::join_links(ASSETS_PATH, $newName);
265
            if (file_exists($oldNameFull)) {
266
                if(file_exists($newNameFull)) {
267
                    if ($this->verbose) {
268
                        DB::alteration_message('Deleting '.$newName.' to make place for a new file.', 'deleted');
269
                    }
270
                    unlink($newNameFull);
271
                }
272
                if ($this->verbose) {
273
                    DB::alteration_message('Moving '.$oldNameFull.' to '.$newNameFull, 'created');
274
                }
275
                rename($oldNameFull, $newNameFull);
276
            }
277
        } elseif($this->verbose) {
278
            DB::alteration_message('ERROR: old and new file names are the same '.$oldName, 'deleted');
279
        }
280
    }
281
282
    protected function checkForRogueFiles()
283
    {
284
285
    }
286
287
}
288