PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.18.8
JetBackup – Backup, Restore & Migrate v3.1.18.8
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 5 months ago ResumableTask.php 5 months ago ResumableTaskItem.php 5 months ago index.html 5 months ago web.config 5 months ago
ResumableTask.php
408 lines
1 <?php
2
3 namespace JetBackup\ResumableTask;
4
5 use JetBackup\Cron\Task\Task;
6 use JetBackup\DirIterator\DirIterator;
7 use JetBackup\Exception\DBException;
8 use JetBackup\Exception\DirIteratorException;
9 use JetBackup\Exception\ExecutionTimeException;
10 use JetBackup\Exception\IOException;
11 use JetBackup\Exception\JBException;
12 use JetBackup\Factory;
13 use JetBackup\Filesystem\AtomicWrite;
14 use JetBackup\JetBackup;
15 use JetBackup\Log\LogController;
16 use SleekDB\Exceptions\InvalidArgumentException;
17
18 if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
19
20 class ResumableTask {
21
22 const TYPE_FUNC = 1;
23 const TYPE_FOREACH = 2;
24 const TYPE_SCAN = 3;
25 const TYPE_FILE_READ = 4;
26 const TYPE_FILE_MERGE = 5;
27
28 const PARAMS_EXECUTION_TIME = 'ExecutionTimeCallback';
29 private LogController $_logController;
30 private ?Task $_task = null;
31 private string $_filename;
32 private array $_items=[];
33
34 public function __construct(string $id, $tmp_dir = null) {
35
36 if (!$tmp_dir) $tmp_dir = Factory::getLocations()->getTempDir();
37 $this->_filename = $tmp_dir . JetBackup::SEP . $id . '.resume';
38 $this->_logController = new LogController();
39
40 $dir = dirname($this->_filename);
41 if (!is_dir($dir)) @mkdir($dir, 0700, true);
42
43 // Check for orphaned swap file from crashed write operation
44 $swapFile = $this->_filename . '.swap';
45 if (file_exists($swapFile)) {
46 if (filesize($swapFile) > 0) {
47 $contents = @file_get_contents($swapFile);
48 $data = $contents ? @unserialize($contents) : false;
49 if ($data !== false && is_array($data)) {
50 // Valid swap file, promote it to main file
51 @rename($swapFile, $this->_filename);
52 $this->_logController->logDebug("[ResumableTask] Recovered from swap file: {$swapFile}");
53 } else {
54 @unlink($swapFile);
55 $this->_logController->logError("[ResumableTask] Invalid swap file discarded: {$swapFile}");
56 }
57 } else {
58 @unlink($swapFile);
59 $this->_logController->logDebug("[ResumableTask] Zero-sized swap file discarded: {$swapFile}");
60 }
61 }
62
63 // If main file exists, load data
64 if (file_exists($this->_filename)) {
65
66 $this->_logController->logDebug( "[ResumableTask] resume file found, trying to load data from: {$this->_filename}" );
67 $contents = @file_get_contents($this->_filename);
68 $data = $contents ? @unserialize($contents) : false;
69
70 if ($data !== false && is_array($data)) {
71 $this->_items = $data;
72 } else {
73 $this->_items = []; // Start fresh if main file is invalid
74 $this->_logController->logError( "[ResumableTask] Invalid or corrupted main file: {$this->_filename}." );
75 }
76
77 } else {
78 // Best-effort create, not critical
79 @touch($this->_filename);
80 }
81
82 }
83
84 /**
85 * @param LogController $logController
86 *
87 * @return void
88 */
89 public function setLogController(LogController $logController) { $this->_logController = $logController; }
90
91 /**
92 * @param Task $task
93 *
94 * @return void
95 */
96 public function setTask(Task $task) { $this->_task = $task; }
97
98 /**
99 * @return LogController
100 */
101 public function getLogController():LogController { return $this->_logController; }
102
103 public function _getItem($name, $type):ResumableTaskItem {
104
105 $item = $this->_items[$name.'|'.$type] ?? null;
106
107 if(!$item) {
108 $item = new ResumableTaskItem();
109 $this->_items[$name.'|'.$type] = $item;
110 }
111
112 return $item;
113 }
114
115 /**
116 * @throws IOException
117 */
118 private function _update(): void
119 {
120 try {
121 AtomicWrite::write($this->_filename, serialize($this->_items), $this->_logController);
122 } catch (\Exception $e) {
123 $msg = "[ResumableTask][_update] Atomic write failed for {$this->_filename}: " . $e->getMessage();
124 $this->_logController->logError($msg);
125 throw new IOException($msg, $e->getCode(), $e);
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 ExecutionTimeException
214 * @throws IOException
215 * @throws InvalidArgumentException
216 * @throws DBException
217 * @throws JBException
218 * @throws \SleekDB\Exceptions\IOException
219 */
220 public function scan(string $source, callable $func, array $excludes=[], ?string $name=null):void {
221
222 $name = sha1($source . ($name ?: ''));
223
224 $item = $this->_getItem($name, self::TYPE_SCAN);
225 if($item->isCompleted()) return;
226 $tree_file_name = $this->_filename . '.' . $name . '.scan';
227 $this->getLogController()->logDebug("[ResumeableTask][scan] Tree file: {$tree_file_name}");
228 $scan = new DirIterator($tree_file_name);
229 $scan->setSource($source);
230 $scan->setLogController($this->getLogController());
231 $scan->setCallBack(function ($type, $filename, $fileCount) {
232
233 if(!$this->_task) return;
234 if($type == 'error') $this->_task->getQueueItem()->addError();
235 if($type != 'file') return;
236
237 $progress = $this->_task->getQueueItem()->getProgress();
238 $progress->setMessage("Scanned [$fileCount] files...");
239 // if we will not 'zero' the percentage, we will see the status bar of the previous item (if < %100)
240 $progress->setTotalSubItems(0);
241 $progress->setCurrentSubItem(0);
242 $this->_task->getQueueItem()->save();
243 $this->_task->checkExecutionTime();
244 //$this->getLogController()->logDebug("[ResumableTask] File count: $fileCount");
245 });
246 if($excludes) $scan->setExcludes($excludes);
247 if($item->getData()) {
248 $data = $item->getData();
249 } else {
250 $data = new \stdClass();
251 $data->total_size = $data->current_pos = $scan->getTotalFiles();
252 $item->setData($data);
253 $this->_update();
254 }
255
256 while($scan->hasNext()) {
257
258 if($this->_task) $this->_task->checkExecutionTime();
259
260 call_user_func_array($func, [$scan, $data]);
261
262 $data->current_pos--;
263 $item->setData($data);
264 $this->_update();
265 }
266
267 $scan->done();
268 }
269
270
271 /**
272 * @param string $source
273 * @param string $target
274 * @param string|null $name
275 *
276 * @return void
277 * @throws DBException
278 * @throws ExecutionTimeException
279 * @throws IOException
280 * @throws InvalidArgumentException
281 * @throws JBException
282 * @throws \SleekDB\Exceptions\IOException
283 */
284 public function fileMerge(string $source, string $target, ?string $name=null):void {
285
286 $this->getLogController()->logDebug("[fileMerge] Source: $source");
287 $this->getLogController()->logDebug("[fileMerge] Target: $target");
288
289 $name = sha1($source . $target . ($name ?: ''));
290
291 $item = $this->_getItem($name, self::TYPE_FILE_MERGE);
292 if($item->isCompleted()) return;
293
294 if($item->getData()) {
295 $data = $item->getData();
296 $this->getLogController()->logDebug("[fileMerge] Retrieved data from item->getData()");
297 } else {
298 $this->getLogController()->logDebug("[fileMerge] item->getData() is empty, starting fresh data holder");
299 $data = new \stdClass();
300 $data->read_line = 0;
301 $data->write_line = 0;
302
303 $item->setData($data);
304 $this->_update();
305 }
306
307 $source_fd = null;
308 $target_fd = null;
309 try {
310 $source_fd = fopen($source, 'r');
311 if (!$source_fd) throw new IOException("Failed to open file: $source");
312
313 if(!file_exists($target)) touch($target);
314 $target_fd = fopen($target, 'a+');
315 if (!$target_fd) throw new IOException("Failed to open file: $target");
316
317 // move target pointer to the correct position (by reading line by line and stopping when needed)
318 $line = 0;
319 while($data->write_line > 0 && fgets($target_fd) !== false) if($line++ >= $data->write_line) break;
320 //
321
322 $line = 0;
323
324 while(($buffer = fgets($source_fd)) !== false) {
325 if($line++ <= $data->read_line) continue;
326 if($this->_task) $this->_task->checkExecutionTime(function() use($source_fd, $target_fd) { fclose($source_fd); fclose($target_fd); });
327
328 fwrite($target_fd, $buffer . PHP_EOL);
329
330 $data->write_line++;
331 $data->read_line++;
332
333 $item->setData($data);
334 $this->_update();
335 $this->getLogController()->logDebug("[fileMerge] Merging '" . basename($source) . "' -> '" . basename($target) . "' | Target write position: " . $data->write_line);
336
337 }
338 } finally {
339 if (is_resource($source_fd)) fclose($source_fd);
340 if (is_resource($target_fd)) fclose($target_fd);
341 }
342
343 }
344
345
346 /**
347 * @param string $file_path
348 * @param callable $func
349 * @param string|null $name
350 *
351 * @return void
352 * @throws DBException
353 * @throws ExecutionTimeException
354 * @throws IOException
355 * @throws InvalidArgumentException
356 * @throws JBException
357 * @throws \SleekDB\Exceptions\IOException
358 */
359 public function fileRead(string $file_path, callable $func, ?string $name=null) {
360 $name = sha1($file_path . ($name ?: ''));
361
362 $item = $this->_getItem($name, self::TYPE_FILE_READ);
363 if($item->isCompleted()) return;
364
365 if($item->getData()) $data = $item->getData();
366 else {
367 $data = new \stdClass();
368 $data->line = 0;
369
370 $item->setData($data);
371 $this->_update();
372 }
373
374 $fd = null;
375 try {
376 $fd = fopen($file_path, 'r');
377 if (!$fd) throw new IOException("Failed to open file: $file_path");
378
379 $line = 0;
380
381 while(($buffer = fgets($fd)) !== false) {
382
383 if($data->line > $line) {
384 $line++;
385 continue;
386 }
387
388 if($this->_task) $this->_task->checkExecutionTime(function() use($fd) { fclose($fd); });
389
390 call_user_func_array($func, [$buffer, $data]);
391
392 $line++;
393 $data->line++;
394
395 $item->setData($data);
396 $this->_update();
397 }
398 } finally {
399 if (is_resource($fd)) fclose($fd);
400 }
401 }
402
403 public function delete():void {
404 $swapFile = $this->_filename . '.swap';
405 if(file_exists($swapFile)) @unlink($swapFile);
406 if(file_exists($this->_filename)) @unlink($this->_filename);
407 }
408 }