Backup::getOldestFileToDelete()   B
last analyzed

Complexity

Conditions 7
Paths 12

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 12
nop 0
dl 0
loc 26
rs 8.5706
c 0
b 0
f 0
1
<?php
2
3
namespace libraries;
4
5
/**
6
 * Creating DB backups
7
 * getting backup files
8
 * deleting old backup-files
9
 *
10
 * @author kolia
11
 */
12
class Backup
13
{
14
15
    /**
16
     *
17
     * @var Backup
18
     */
19
    protected static $instance;
20
21
    /**
22
     * Backup directory
23
     * @var string
24
     */
25
    protected $directory = BACKUPFOLDER;
26
27
    /**
28
     * Aviable extentions
29
     * @var array
30
     */
31
    protected $ext = [
32
                      'sql',
33
                      'zip',
34
                      'gzip',
35
                     ];
36
37
    /**
38
     * Patterns for backup file names
39
     * key - regex pattern, value - boolean (allow delete)
40
     * @var array
41
     */
42
    protected $filePatterns = [
43
        // для файлів із авт. підбором імені.
44
                               '/^[a-zA-Z]{1,10}_[0-9]{2}-[0-9]{2}-[0-9]{4}_[0-9]{2}.[0-9]{2}.[0-9]{2}.(zip|gzip|sql|txt)$/' => [
45
                                                                                                                                 'allowDelete' => TRUE,
46
                                                                                                                                 'type'        => 'default',
47
                                                                                                                                ],
48
                               // для старіших бекапів із обновлення
49
                               '/^[0-9]{10}.(zip|gzip|sql|txt)$/'                                                            => [
50
                                                                                                                                 'allowDelete' => TRUE,
51
                                                                                                                                 'type'        => 'update',
52
                                                                                                                                ],
53
                               // для новіших бекапів із обновлення
54
                               '/^backup.(zip|gzip|sql|txt)$/'                                                               => [
55
                                                                                                                                 'allowDelete' => FALSE,
56
                                                                                                                                 'type'        => 'update',
57
                                                                                                                                ],
58
                              ];
59
60
    /**
61
     *
62
     * @var CodeIgniter
63
     */
64
    protected $ci;
65
66
    /**
67
     *
68
     * @var string
69
     */
70
    public $error;
71
72
    public function __construct() {
73
        $this->ci = &get_instance();
74
        $this->ci->load->dbutil();
75
    }
76
77
    /**
78
     *
79
     * @return Backup
80
     */
81
    public static function create() {
82
        (null !== self::$instance) OR self::$instance = new self();
83
        return self::$instance;
84
    }
85
86
    /**
87
     * Setting the backup value into the DB
88
     * @param string $key
89
     * @param string|int $value
90
     */
91
    public function setSetting($key, $value) {
92
        $settings = $this->getSetting();
93
        if (!is_array($settings)) { //no settings yet
94
            $settings = [];
95
        }
96
        $settings[$key] = $value;
97
        return $this->ci->db->update('settings', ['backup' => serialize($settings)]);
98
    }
99
100
    /**
101
     * Getting the backup value from the DB
102
     * @param string
103
     * @param string $key
104
     * @return mixed settings array or specified value
105
     */
106
    public function getSetting($key = NULL) {
107
        $result = $this->ci->db->select('backup')->get('settings');
108
        if ($result) {
109
            $row = $result->result_array();
110
        } else {
111
            $row = [];
112
        }
113
114
        $backupSettings = unserialize($row[0]['backup']);
115
        if (!is_array($backupSettings)) { // no settings yet
116
            return NULL;
117
        }
118
        if ($key != null) {
119
            if (!array_key_exists($key, $backupSettings)) {
120
                return NULL;
121
            }
122
            return $backupSettings[$key];
123
        }
124
        return $backupSettings;
125
    }
126
127
    /**
128
     * Creating the backup file
129
     * @param string $ext extention (txt|zip|gzip)
130
     * @param string $prefix
131
     * @param bool|string $fullName (optional) filename
132
     * @param array $params
133
     * @return false|string filename|FALSE
134
     */
135
    public function createBackup($ext, $prefix = NULL, $fullName = FALSE, $params = []) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
136
        if (is_really_writable($this->directory)) {
137
            if ($prefix == null) {
138
                $prefix = 'sql';
139
            }
140
            if ($fullName === TRUE) {
141
                $fileName = $prefix;
142
            } else {
143
                $fileName = $prefix . '_' . date('d-m-Y_H.i.s');
144
            }
145
146
            $params = [
147
                       'format' => $ext == 'sql' ? 'txt' : $ext,
148
                      ];
149
150
            $currentDbInstance = $this->ci->db;
151
152
            $this->initBackupDB();
153
154
            $backup = & $this->ci->dbutil->backup($params);
155
156
            $this->ci->db = $currentDbInstance;
157
158
            if (write_file($this->directory . '/' . $fileName . '.' . $ext, $backup)) {
159
                return $this->directory . '/' . $fileName . '.' . $ext;
160
            }
161
            $this->error = 'Невозможно создать файл, проверте папку ' . BACKUPFOLDER . ' на возможность записи';
162
            return FALSE;
163
        } else {
164
            $this->error = 'Невозможно создать снимок базы, проверте папку ' . BACKUPFOLDER . ' на возможность записи';
165
            return FALSE;
166
        }
167
    }
168
169
    /**
170
     * Init DB for backup with msql driver
171
     */
172
    private function initBackupDB() {
173
        // this is just all config keys
174
        $configNames = [
175
                        'hostname',
176
                        'username',
177
                        'password',
178
                        'database',
179
                        'dbdriver',
180
                        'dbprefix',
181
                        'pconnect',
182
                        'db_debug',
183
                        'cache_on',
184
                        'cachedir',
185
                        'char_set',
186
                        'dbcollat',
187
                        'swap_pre',
188
                        'autoinit',
189
                        'stricton',
190
                       ];
191
192
        $config = [];
193
        foreach ($configNames as $key) {
194
            $config[$key] = $key == 'dbdriver' ? 'mysql' : $this->ci->db->$key;
195
        }
196
197
        $this->ci->db = $this->ci->load->database($config, TRUE);
198
        $this->ci->load->dbutil();
199
    }
200
201
    /**
202
     *
203
     * @return boolean
0 ignored issues
show
Documentation introduced by
Should the return type not be array<string,integer>|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
204
     */
205
    public function deleteOldFiles() {
206
        $term = $this->getSetting('backup_term');
207
        $maxSize = ($this->getSetting('backup_maxsize') * 1024 * 1024);
0 ignored issues
show
Unused Code introduced by
$maxSize is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
208
209
        $maxSize = 5 * 1024 * 1024;
210
        // if time of file will be lower then delete
211
        $time = time() - (60 * 60 * 24 * 30.5 * $term);
212
213
        $files = $this->backupFiles();
214
        // get summary backup size
215
        $size = 0;
216
        foreach ($files as $file) {
217
            $size += $file['size'];
218
        }
219
220
        // start deleting if overload more then max size
221
        if ($size > $maxSize) {
222
            $deleteSize = $size - $maxSize;
223
            $deleteEdOnSize = 0;
224
            $filesCount = 0;
225
            do {
226
                $fileToDelete = $this->getOldestFileToDelete();
227
                if ($fileToDelete == FALSE) { // if FALSE then no more files to delete
228
                    break;
229
                }
230
                if ($fileToDelete['timeUpdate'] > $time) { // check if not overrun max date
231
                    break;
232
                }
233
                $filesCount++;
234
                $deleteEdOnSize += $fileToDelete['size'];
235
                unlink($this->directory . '/' . $fileToDelete['filename']);
236
            } while ($deleteEdOnSize < $deleteSize);
237
238
            return [
239
                    'count' => $filesCount,
240
                    'size'  => $deleteEdOnSize,
241
                   ];
242
        }
243
        return FALSE;
244
    }
245
246
    /**
247
     *
248
     * @return boolean
249
     */
250
    public function getOldestFileToDelete() {
251
        $files_ = $this->backupFiles();
252
        // getting only files that allow to delete by pattern
253
        $files = [];
254
        foreach ($files_ as $file) {
255
            if ($this->checkFileName($file['filename'], 'allowDelete') && $file['locked'] != 1) {
256
                $files[] = $file;
257
            }
258
        }
259
260
        if (!count($files) > 0) {
261
            return FALSE;
262
        }
263
264
        $minKey = 0;
265
        $minTime = $files[0]['timeUpdate'];
266
267
        $countFiles = count($files);
268
        for ($i = 1; $i < $countFiles; $i++) {
269
            if ($minTime > $files[$i]['timeUpdate']) {
270
                $minTime = $files[$i]['timeUpdate'];
271
                $minKey = $i;
272
            }
273
        }
274
        return $files[$minKey];
275
    }
276
277
    /**
278
     * Getting list of backup files
279
     * @return array
280
     */
281
    public function backupFiles() {
282
        $lockedFiles = $this->getSetting('lockedFiles');
283
        if (!is_array($lockedFiles)) {
284
            $lockedFiles = [];
285
        }
286
        $files = [];
287
        if ($dir = opendir($this->directory)) {
288
            while (FALSE !== ($fileName = readdir($dir))) {
289
                if ($fileName != '.' & $fileName !== '..') {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: ($fileName != '.') & $fileName !== '..', Probably Intended Meaning: $fileName != ('.' & $fileName !== '..')

When comparing the result of a bit operation, we suggest to add explicit parenthesis and not to rely on PHP’s built-in operator precedence to ensure the code behaves as intended and to make it more readable.

Let’s take a look at these examples:

// Returns always int(0).
return 0 === $foo & 4;
return (0 === $foo) & 4;

// More likely intended return: true/false
return 0 === ($foo & 4);
Loading history...
290
                    if (TRUE === $this->checkFileName($fileName)) {
291
                        $file = [
292
                                 'filename'    => $fileName,
293
                                 'allowDelete' => $this->checkFileName($fileName, 'allowDelete') == TRUE ? 1 : 0,
294
                                 'type'        => $this->checkFileName($fileName, 'type'),
295
                                 'ext'         => pathinfo($fileName, PATHINFO_EXTENSION),
296
                                 'size'        => filesize($this->directory . '/' . $fileName),
297
                                 'timeUpdate'  => filemtime($this->directory . '/' . $fileName),
298
                                ];
299
                        if ($file['type'] == 'default') {
300
                            $prefIndex = strpos($fileName, '_');
301
                            $file['prefix'] = substr($fileName, 0, $prefIndex);
302
                        } else {
303
                            $file['prefix'] = 'update';
304
                        }
305
                        if (in_array($fileName, $lockedFiles)) {
306
                            $file['locked'] = 1;
307
                        } else {
308
                            $file['locked'] = 0;
309
                        }
310
                        $files[] = $file;
311
                    }
312
                }
313
            }
314
            closedir($dir);
315
        }
316
317
        foreach ($files as $key => $row) {
318
            $backaps[$key] = $row['timeUpdate'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$backaps was never initialized. Although not strictly required by PHP, it is generally a good practice to add $backaps = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
319
        }
320
321
        array_multisort($backaps, SORT_DESC, $files);
322
323
        return $files;
324
    }
325
326
    /**
327
     * Checking file name by pattern
328
     * @param string $fileName
329
     * @param boolean|string $returnValue
330
     * @return boolean|string
331
     */
332
    protected function checkFileName($fileName, $returnValue = FALSE) {
333
        foreach ($this->filePatterns as $pattern => $params) {
334
            if (preg_match($pattern, $fileName)) {
335
                return $returnValue !== FALSE ? $params[$returnValue] : TRUE;
336
            }
337
        }
338
        return FALSE;
339
    }
340
341
    /**
342
     * Deleting backup file
343
     * @param string $file
344
     * @return boolean true on success, false on error
345
     */
346
    public function deleteFile($file) {
347
        if (FALSE === $this->checkFileName($file, 'allowDelete')) {
348
            return FALSE;
349
        }
350
        if (file_exists($this->directory . '/' . $file)) {
351
            return unlink($this->directory . '/' . $file);
352
        }
353
        return FALSE;
354
    }
355
356
}