PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.13.4
JetBackup – Backup, Restore & Migrate v3.1.13.4
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / ResumableTask / ResumableTask.php
backup / src / JetBackup / ResumableTask Last commit date
.htaccess 9 months ago ResumableTask.php 9 months ago ResumableTaskItem.php 9 months ago index.html 9 months ago web.config 9 months ago
ResumableTask.php
382 lines
1 <?php
2
3 namespace JetBackup\ResumableTask;
4
5 use JetBackup\Cron\Task\Task;
6 use JetBackup\DirIterator\DirIterator;
7 use JetBackup\Exception\DirIteratorException;
8 use JetBackup\Exception\ExecutionTimeException;
9 use JetBackup\Exception\IOException;
10 use JetBackup\Factory;
11 use JetBackup\JetBackup;
12 use JetBackup\Log\LogController;
13 use SleekDB\Exceptions\InvalidArgumentException;
14
15 if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
16
17 class ResumableTask {
18
19 const TYPE_FUNC = 1;
20 const TYPE_FOREACH = 2;
21 const TYPE_SCAN = 3;
22 const TYPE_FILE_READ = 4;
23 const TYPE_FILE_MERGE = 5;
24
25 const PARAMS_EXECUTION_TIME = 'ExecutionTimeCallback';
26
27 private $_swap_filename;
28 private LogController $_logController;
29 private ?Task $_task = null;
30 private $_filename;
31 private array $_items=[];
32
33 public function __construct(string $id, $tmp_dir = null) {
34
35 if (!$tmp_dir) $tmp_dir = Factory::getLocations()->getTempDir();
36 $this->_filename = $tmp_dir . JetBackup::SEP . $id . '.resume';
37 $this->_swap_filename = $this->_filename . '.swp';
38 $this->_logController = new LogController();
39
40 if (!file_exists(dirname($this->_filename))) mkdir(dirname($this->_filename), 0700, true);
41
42 // If swap file exists, validate and use it
43 if (file_exists($this->_swap_filename)) {
44 if (filesize($this->_swap_filename) > 0) {
45 $contents = @file_get_contents($this->_swap_filename);
46 $data = $contents ? @unserialize($contents) : false;
47
48 if ($data !== false && is_array($data)) {
49 $this->_items = $data;
50 rename($this->_swap_filename, $this->_filename); // Promote swap file to main file
51 $this->_logController->logDebug("Swap file found, using it for $this->_filename");
52 } else {
53 unlink($this->_swap_filename); // Invalid swap file, discard it
54 $this->_logController->logError("Invalid or corrupted swap file: {$this->_swap_filename} discarded.");
55 }
56 } else {
57 unlink($this->_swap_filename); // Zero-sized swap file, discard it
58 $this->_logController->logError("Zero-sized swap file: {$this->_swap_filename} discarded.");
59 }
60 }
61
62 // If main file exists, load data
63 if (file_exists($this->_filename)) {
64 $this->_logController->logDebug("[ResumeableTask] resume file found, trying to load data from: {$this->_filename}");
65 $contents = @file_get_contents($this->_filename);
66 $data = $contents ? @unserialize($contents) : false;
67
68 if ($data !== false && is_array($data)) {
69 $this->_items = $data;
70 } else {
71 $this->_items = []; // Start fresh if main file is invalid
72 $this->_logController->logError("Invalid or corrupted main file: {$this->_filename}.");
73 }
74 } else {
75 // Create the main file if it doesn't exist
76 touch($this->_filename);
77 }
78 }
79
80
81 /**
82 * @param LogController $logController
83 *
84 * @return void
85 */
86 public function setLogController(LogController $logController) { $this->_logController = $logController; }
87
88 /**
89 * @param Task $task
90 *
91 * @return void
92 */
93 public function setTask(Task $task) { $this->_task = $task; }
94
95 /**
96 * @return LogController
97 */
98 public function getLogController():LogController { return $this->_logController; }
99
100 public function _getItem($name, $type):ResumableTaskItem {
101
102 $item = $this->_items[$name.'|'.$type] ?? null;
103
104 if(!$item) {
105 $item = new ResumableTaskItem();
106 $this->_items[$name.'|'.$type] = $item;
107 }
108
109 return $item;
110 }
111
112 /**
113 * @throws IOException
114 */
115 private function _update(): void {
116 $temp_file = $this->_swap_filename;
117
118 if (file_put_contents($temp_file, serialize($this->_items), LOCK_EX) === false) {
119 throw new IOException("Failed to write to temporary file: $temp_file");
120 }
121
122 if (!rename($temp_file, $this->_filename)) {
123 unlink($temp_file);
124 throw new IOException("Failed to rename temporary file: $temp_file to $this->_filename");
125 }
126 }
127
128
129 private static function _getCallableName(callable $callable):?string {
130 if(is_string($callable)) return $callable;
131 if(is_array($callable) && is_object($callable[0])) return get_class($callable[0]) . '->' . $callable[1];
132 if(is_array($callable)) return $callable[0] . '::' . $callable[1];
133 return null;
134 }
135
136
137 public function func(callable $func, array $args=[], ?string $name=null) {
138 if(!$name && !($name = self::_getCallableName($func))) throw new \Exception("Can't find callable name");
139
140 $item = $this->_getItem($name, self::TYPE_FUNC);
141 if($item->isCompleted()) return $item->getResult();
142
143 if($item->getData()) $args = $item->getData();
144 else {
145 $item->setData($args);
146 $this->_update();
147 }
148
149 foreach($args as $i => $arg) {
150 if($arg != self::PARAMS_EXECUTION_TIME) continue;
151 $args[$i] = function() { if($this->_task) $this->_task->checkExecutionTime(); };
152 }
153
154 if($this->_task) $this->_task->checkExecutionTime();
155
156 $item->setResult(call_user_func_array($func, $args));
157 $item->setData([]);
158 $item->setCompleted(true);
159 $this->_update();
160
161 return $item->getResult();
162 }
163
164 public function foreachCallable(callable $data, array $args, callable $func, ?string $name=null) {
165 if(!$name) $name = self::_getCallableName($data);
166
167 $item = $this->_getItem($name, self::TYPE_FOREACH);
168 if($item->isCompleted()) return;
169 $data = $item->getData() ? [] : call_user_func_array($data, $args);
170 $this->foreach($data, $func, $name);
171 }
172
173 public function foreach(array $data, callable $func, ?string $name=null):void {
174 if(!$name) $name = sha1(serialize($data));
175
176 $item = $this->_getItem($name, self::TYPE_FOREACH);
177 if($item->isCompleted()) return;
178
179 if($item->getData()) $data = $item->getData();
180 else {
181 $data = ['records' => $data, 'total' => count($data)];
182 $item->setData($data);
183 $this->_update();
184 }
185
186 foreach($data['records'] as $key => $value) {
187
188 if($this->_task) $this->_task->checkExecutionTime();
189
190 call_user_func_array($func, [$key, $value, $data['total']]);
191 unset($data['records'][$key]);
192
193 $item->setData($data);
194 $this->_update();
195 }
196
197 $item->setCompleted(true);
198 $this->_update();
199 }
200
201 public function for(array $data, callable $func, ?string $name=null):void {
202 $this->foreach(array_values($data), $func, $name);
203 }
204
205 /**
206 * @param string $source
207 * @param callable $func
208 * @param array $excludes
209 * @param string|null $name
210 *
211 * @return void
212 * @throws DirIteratorException
213 * @throws IOException
214 * @throws ExecutionTimeException
215 * @throws \SleekDB\Exceptions\IOException
216 * @throws InvalidArgumentException
217 */
218 public function scan(string $source, callable $func, array $excludes=[], ?string $name=null):void {
219
220 $name = sha1($source . ($name ?: ''));
221
222 $item = $this->_getItem($name, self::TYPE_SCAN);
223 if($item->isCompleted()) return;
224 $tree_file_name = $this->_filename . '.' . $name . '.scan';
225 $this->getLogController()->logDebug("[ResumeableTask][scan] Tree file: {$tree_file_name}");
226 $scan = new DirIterator($tree_file_name);
227 $scan->setSource($source);
228 $scan->setLogController($this->getLogController());
229 $scan->setCallBack(function ($type, $filename, $fileCount) {
230
231 if(!$this->_task) return;
232 if($type == 'error') $this->_task->getQueueItem()->addError();
233 if($type != 'file') return;
234
235 $progress = $this->_task->getQueueItem()->getProgress();
236 $progress->setMessage("Scanned [$fileCount] files...");
237 // if we will not 'zero' the percentage, we will see the status bar of the previous item (if < %100)
238 $progress->setTotalSubItems(0);
239 $progress->setCurrentSubItem(0);
240 $this->_task->getQueueItem()->save();
241 $this->_task->checkExecutionTime();
242 //$this->getLogController()->logDebug("[ResumableTask] File count: $fileCount");
243 });
244 if($excludes) $scan->setExcludes($excludes);
245 if($item->getData()) {
246 $data = $item->getData();
247 } else {
248 $data = new \stdClass();
249 $data->total_size = $data->current_pos = $scan->getTotalFiles();
250 $item->setData($data);
251 $this->_update();
252 }
253
254 while($scan->hasNext()) {
255
256 if($this->_task) $this->_task->checkExecutionTime();
257
258 call_user_func_array($func, [$scan, $data]);
259
260 $data->current_pos--;
261 $item->setData($data);
262 $this->_update();
263 }
264
265 $scan->done();
266 }
267
268
269 /**
270 * @throws IOException
271 */
272 public function fileMerge(string $source, string $target, ?string $name=null):void {
273
274 $this->getLogController()->logDebug("[fileMerge] Source: $source");
275 $this->getLogController()->logDebug("[fileMerge] Target: $target");
276
277 $name = sha1($source . $target . ($name ?: ''));
278
279 $item = $this->_getItem($name, self::TYPE_FILE_MERGE);
280 if($item->isCompleted()) return;
281
282 if($item->getData()) {
283 $data = $item->getData();
284 $this->getLogController()->logDebug("[fileMerge] Retrieved data from item->getData()");
285 } else {
286 $this->getLogController()->logDebug("[fileMerge] item->getData() is empty, starting fresh data holder");
287 $data = new \stdClass();
288 $data->read_line = 0;
289 $data->write_line = 0;
290
291 $item->setData($data);
292 $this->_update();
293 }
294
295 $source_fd = fopen($source, 'r');
296 if (!$source_fd) throw new IOException("Failed to open file: $source");
297
298 if(!file_exists($target)) touch($target);
299 $target_fd = fopen($target, 'a+');
300 if (!$target_fd) throw new IOException("Failed to open file: $target");
301
302 // move target pointer to the correct position (by reading line by line and stopping when needed)
303 $line = 0;
304 while($data->write_line > 0 && fgets($target_fd) !== false) if($line++ >= $data->write_line) break;
305 //
306
307 $line = 0;
308
309 while(($buffer = fgets($source_fd)) !== false) {
310 if($line++ <= $data->read_line) continue;
311 if($this->_task) $this->_task->checkExecutionTime(function() use($source_fd, $target_fd) { fclose($source_fd); fclose($target_fd); });
312
313 fwrite($target_fd, $buffer . PHP_EOL);
314
315 $data->write_line++;
316 $data->read_line++;
317
318 $item->setData($data);
319 $this->_update();
320 $this->getLogController()->logDebug("[fileMerge] Merging '" . basename($source) . "' -> '" . basename($target) . "' | Target write position: " . $data->write_line);
321
322 }
323
324 fclose($source_fd);
325 fclose($target_fd);
326
327 }
328
329
330
331
332
333
334 /**
335 * @throws IOException
336 */
337 public function fileRead(string $file_path, callable $func, ?string $name=null) {
338 $name = sha1($file_path . ($name ?: ''));
339
340 $item = $this->_getItem($name, self::TYPE_FILE_READ);
341 if($item->isCompleted()) return;
342
343 if($item->getData()) $data = $item->getData();
344 else {
345 $data = new \stdClass();
346 $data->line = 0;
347
348 $item->setData($data);
349 $this->_update();
350 }
351
352 $fd = fopen($file_path, 'r');
353 if (!$fd) throw new IOException("Failed to open file: $file_path");
354
355 $line = 0;
356
357 while(($buffer = fgets($fd)) !== false) {
358
359 if($data->line > $line) {
360 $line++;
361 continue;
362 }
363
364 if($this->_task) $this->_task->checkExecutionTime(function() use($fd) { fclose($fd); });
365
366 call_user_func_array($func, [$buffer, $data]);
367
368 $line++;
369 $data->line++;
370
371 $item->setData($data);
372 $this->_update();
373 }
374
375 fclose($fd);
376 }
377
378 public function delete():void {
379 if(file_exists($this->_swap_filename)) unlink($this->_swap_filename);
380 if(file_exists($this->_filename)) unlink($this->_filename);
381 }
382 }