PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
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 / Schedule / Schedule.php
backup / src / JetBackup / Schedule Last commit date
.htaccess 1 year ago Schedule.php 3 months ago ScheduleItem.php 1 year ago index.html 1 year ago web.config 1 year ago
Schedule.php
527 lines
1 <?php
2
3 namespace JetBackup\Schedule;
4
5 use DateTime;
6 use DateTimeZone;
7 use Exception;
8 use JetBackup\Alert\Alert;
9 use JetBackup\BackupJob\BackupJob;
10 use JetBackup\Data\DBObject;
11 use JetBackup\Data\SleekStore;
12 use JetBackup\Entities\Util;
13 use JetBackup\Exception\DBException;
14 use JetBackup\Exception\FieldsValidationException;
15 use JetBackup\Exception\ScheduleException;
16 use JetBackup\Factory;
17 use JetBackup\JetBackup;
18 use SleekDB\Exceptions\InvalidArgumentException;
19 use SleekDB\Exceptions\IOException;
20 use SleekDB\QueryBuilder;
21
22 if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
23
24 /**
25 * The Schedules class is responsible for handling scheduling operations.
26 * It provides functionality to add new schedules and retrieve all schedules.
27 */
28 class Schedule extends DBObject {
29
30 const COLLECTION = 'schedules';
31
32 const UNIQUE_ID = 'unique_id';
33 const NAME = 'name';
34 const TYPE = 'type';
35 const TYPE_NAME = 'type_name';
36 const INTERVALS = 'intervals';
37 const BACKUP_ID = 'backup_id';
38 const JOB_COUNT = 'job_count';
39 const JOB_ASSIGNED = 'jobs_assigned';
40 const JOB_NAMES = 'job_names';
41 const HIDDEN = 'hidden';
42 const DEFAULT = 'default';
43
44 const TYPE_HOURLY = 1;
45 const TYPE_DAILY = 2;
46 const TYPE_WEEKLY = 3;
47 const TYPE_MONTHLY = 4;
48 const TYPE_MANUALLY = 5;
49 const TYPE_AFTER_JOB_DONE = 6;
50 const TYPE_IMPORTED = 7;
51
52 const DEFAULT_SCHEDULE_TYPES = [self::TYPE_HOURLY,self::TYPE_DAILY,self::TYPE_WEEKLY,self::TYPE_MONTHLY];
53 const DEFAULT_SCHEDULE_INTERVALS = [
54 self::TYPE_HOURLY => 1,
55 self::TYPE_DAILY => [1,2,3,4,5,6,7],
56 self::TYPE_WEEKLY => 1,
57 self::TYPE_MONTHLY => 1
58 ];
59
60 CONST ALLOWED_HOURLY_INTERVALS = [1,2,3,4,6,8,12];
61 const ALLOWED_INTERVALS = [
62 self::TYPE_DAILY => [1,2,3,4,5,6,7],
63 self::TYPE_MONTHLY => [1,7,14,21,28],
64 ];
65
66 const TYPE_NAMES = [
67 self::TYPE_HOURLY => 'Hourly',
68 self::TYPE_DAILY => 'Daily',
69 self::TYPE_WEEKLY => 'Weekly',
70 self::TYPE_MONTHLY => 'Monthly',
71 self::TYPE_MANUALLY => 'Manually',
72 self::TYPE_AFTER_JOB_DONE => 'After Job Done',
73 self::TYPE_IMPORTED => 'Imported'
74 ];
75
76 const TYPE_ALLOWED = [
77 self::TYPE_HOURLY,
78 self::TYPE_DAILY,
79 self::TYPE_WEEKLY,
80 self::TYPE_MONTHLY,
81 self::TYPE_AFTER_JOB_DONE
82 ];
83
84 const DEFAULT_CONFIG_SCHEDULE_NAME = 'Default Daily Config';
85
86 public function __construct($_id=null) {
87 parent::__construct(self::COLLECTION);
88 if($_id) $this->_loadById((int) $_id);
89 }
90 public function setDefault(bool $default) { $this->set(self::DEFAULT, $default); }
91 public function isDefault():bool { return $this->get(self::DEFAULT, false); }
92 public function setUniqueId($id) { $this->set(self::UNIQUE_ID, $id); }
93 public function getUniqueId():string { return $this->get(self::UNIQUE_ID); }
94
95 public function setName(string $name) { $this->set(self::NAME, $name); }
96 public function getName():string { return $this->get(self::NAME); }
97
98 public function setHidden(bool $hidden) { $this->set(self::HIDDEN, $hidden); }
99 public function isHidden():bool { return !!$this->get(self::HIDDEN, false); }
100
101 public function setType(int $type) { $this->set(self::TYPE, $type); }
102 public function getType():int {return (int) $this->get(self::TYPE, 0);}
103 public function setIntervals($intervals) { $this->set(self::INTERVALS, $intervals); }
104 public function getIntervals() {
105 return $this->get(self::INTERVALS, 0);
106 }
107
108 public function setBackupId(int $id) { $this->set(self::BACKUP_ID, $id); }
109 public function getBackupId():int { return (int) $this->get(self::BACKUP_ID, 0); }
110
111 public function setJobsCount(int $count):void { $this->set(self::JOB_COUNT, $count); }
112
113 public function addJobsCount():void { $this->setJobsCount($this->getJobsCount() + 1); }
114
115 public function reduceJobsCount():void {
116 $count = $this->getJobsCount();
117 $this->setJobsCount($count > 0 ? $count - 1 : 0);
118 }
119
120 public function getJobsCount():int { return $this->get(self::JOB_COUNT, 0); }
121
122 public static function db():SleekStore {
123 return new SleekStore(self::COLLECTION);
124 }
125
126 public static function query():QueryBuilder {
127 return self::db()->createQueryBuilder();
128 }
129
130 public function save():void {
131 if(!$this->getUniqueId()) $this->setUniqueId(Util::generateUniqueId());
132 if ($this->getType() == Schedule::TYPE_AFTER_JOB_DONE) $this->setIntervals(null);
133
134 parent::save();
135
136 // Update backup next run only after updating the schedule itself
137 $list = BackupJob::query()
138 ->select([JetBackup::ID_FIELD])
139 ->where([ BackupJob::SCHEDULES, 'contains', $this->getId()])
140 ->getQuery()
141 ->fetch();
142
143 foreach($list as $details) {
144 $config = new BackupJob( $details[ JetBackup::ID_FIELD]);
145 $config->calculateNextRun();
146 $config->save();
147 }
148 }
149
150 public function delete():void {
151
152 if($details = BackupJob::query()
153 ->select([ BackupJob::NAME])
154 ->where([ BackupJob::SCHEDULES, 'contains', $this->getId()])
155 ->getQuery()
156 ->first()) throw new ScheduleException('Schedule is assigned to a job: ' . $details[ BackupJob::NAME]);
157
158 // if we want to remove the schedule from jobs on delete
159 /*
160 $list = BackupConfig::query()
161 ->createQueryBuilder()
162 ->select([JetBackup::ID_FIELD])
163 ->where([BackupConfig::SCHEDULES, 'contains', $this->getId()])
164 ->getQuery()
165 ->fetch();
166
167 foreach($list as $details) {
168 $config = new BackupConfig($details[JetBackup::ID_FIELD]);
169 $config->removeSchedule($this->getId());
170 $config->calculateNextRun();
171 $config->save();
172 }
173 */
174
175 parent::delete();
176 }
177
178 public function calculateNextRun(string $time):int {
179
180 try {
181
182 list($hour, $minute) = explode(':', $time);
183
184 switch ($this->getType()) {
185 default: return 0;
186
187 case self::TYPE_HOURLY:
188
189 /*
190 *
191 * Moves the $Target time to the next interval hour while keeping the minutes (and seconds) consistent with your specified $time
192 *
193 * Splits the provided $time into hours and minutes.
194 * Sets $Target as a clone of the current time $Now.
195 * Modifies $Target by adding the interval hours to it.
196 * Sets the minutes (and resets seconds to 00) based on the provided $time,
197 * ensuring that $Target is aligned with the specific minute mark you want.
198 *
199 */
200
201 $target = Util::getDateTime();
202 $target->modify("+" . ((int) $this->getIntervals()) . " hours");
203 $target->setTime($target->format('H'), $minute, 00);
204 return $target->getTimestamp();
205
206
207 case self::TYPE_DAILY:
208 $now = Util::getDateTime();
209
210 // Get the days when the schedule should run
211 $runDays = array_values($this->getIntervals()); // Ensure it's an indexed array
212 $target = null;
213 $maxIterations = 14; // Max iterations for two weeks
214 $dayIndex = 0;
215
216 // Iterate through the days until the next run day is found
217 while ($target === null && $maxIterations > 0) {
218
219 $testDate = Util::getDateTime()->modify("+$dayIndex day");
220 $testDate->setTime($hour, $minute); // Set to the specific time of day
221
222 // Correct the target time for time zone offset if necessary
223 $timezoneOffset = $testDate->getOffset() - $now->getOffset();
224 if ($timezoneOffset != 0) $testDate->modify($timezoneOffset . ' seconds');
225
226 $dayName = $testDate->format('w'); // 'l' returns the full textual representation of the day of the week
227
228 if (in_array($dayName, $runDays) && $testDate > $now) $target = $testDate;
229
230 $dayIndex++;
231 $maxIterations--;
232 }
233
234 if ($maxIterations === 0) Alert::add('calcNextRun Error', 'Daily loop did not find a valid date', Alert::LEVEL_WARNING);
235
236
237 return $target->getTimestamp();
238
239 case self::TYPE_WEEKLY:
240
241 $runDay = $this->getIntervals() ?? 0; // Expected weekday number (0-6 if using `w`, 1-7 if using `N`)
242 $target = null;
243 $maxIterations = 8; // Max iterations for 8 days
244 $dayIndex = 0;
245
246 $now = Util::getDateTime();
247 $todayDayName = (int) $now->format('w'); // 0 (Sunday) to 6 (Saturday)
248
249
250 $scheduledTimeToday = (clone $now)->setTime($hour, $minute);
251
252
253 // Check if today is the run day
254 if ($todayDayName === (int) $runDay) {
255 if ($scheduledTimeToday > $now) {
256 // If the scheduled time today is still in the future
257 return $scheduledTimeToday->getTimestamp();
258 } else {
259 // If the scheduled time today has already passed, start checking from the next day
260 $dayIndex = 1;
261 }
262 }
263
264 // Iterate through the next days to find the correct scheduled weekday
265 while ($target === null && $maxIterations > 0) {
266 $testDate = (clone $now)->modify("+$dayIndex day")->setTime($hour, $minute);
267
268 $testDay = (int) $testDate->format('w'); // 0 (Sunday) to 6 (Saturday)
269
270 if ($testDay === (int) $runDay) {
271 $target = $testDate;
272 break;
273 }
274
275 $dayIndex++;
276 $maxIterations--;
277 }
278
279 if ($target === null) {
280 Alert::add('calcNextRun Error', 'Weekly loop did not find a valid date', Alert::LEVEL_WARNING);
281 }
282
283 return $target ? $target->getTimestamp() : 0;
284
285
286 case self::TYPE_MONTHLY:
287
288 $now = Util::getDateTime();
289 $runDays = $this->getIntervals(); // Days of the month to run
290 if (!is_array($runDays) || empty($runDays)) {
291 Alert::add('calcNextRun Error', 'No valid run days provided for monthly schedule', Alert::LEVEL_WARNING);
292 return 0;
293 }
294
295 $target = null;
296 $currentYear = (int)$now->format('Y');
297 $currentMonth = (int)$now->format('m');
298 $currentDay = (int)$now->format('d');
299 $maxIterations = 12; // Max iterations to prevent endless loop
300
301 // Check if today is a run day and the time has not yet passed
302 $now = Util::getDateTime();
303 if (in_array($currentDay, $runDays)) {
304 $scheduledTimeToday = (clone $now)->setTime($hour, $minute);
305 if ($scheduledTimeToday > $now) return $scheduledTimeToday->getTimestamp();
306 }
307
308 // Iterate through the months to find the next occurrence of the specified day
309 while ($target === null && $maxIterations > 0) {
310 foreach ($runDays as $day) {
311 try {
312
313 $testDate = (clone $now)->setDate($currentYear, $currentMonth, $day);
314 $testDate->setTime($hour, $minute);
315
316 // Correct the target time for time zone offset if necessary
317 $timezoneOffset = $testDate->getOffset() - $now->getOffset();
318 if ($timezoneOffset != 0) $testDate->modify($timezoneOffset . ' seconds');
319
320 if ($testDate > $now) {
321 $target = $testDate;
322 break 2;
323 }
324 } catch (Exception $e) {
325 continue; // Skip to the next day in $runDays
326 }
327 }
328
329 // Increment the month and adjust the year if needed
330 $currentMonth++;
331 if ($currentMonth > 12) {
332 $currentYear++;
333 $currentMonth = 1;
334 }
335
336 $maxIterations--;
337 }
338
339 if ($maxIterations === 0) {
340 Alert::add('calcNextRun Error', 'Monthly loop did not find a valid date', Alert::LEVEL_WARNING);
341 return 0;
342 }
343
344 return $target->getTimestamp() ?? 0;
345 }
346
347 } catch (Exception $e) {
348 Throw new ScheduleException($e->getMessage());
349 }
350 }
351
352 /**
353 * @throws InvalidArgumentException
354 * @throws IOException
355 */
356 public static function createDefaultSchedule():void {
357
358 foreach(self::DEFAULT_SCHEDULE_TYPES as $type) {
359
360 $res = self::query()
361 ->select([JetBackup::ID_FIELD])
362 ->where([ self::TYPE, "=", $type ])
363 ->where([ self::HIDDEN, "=", false ])
364 ->where([ self::DEFAULT, "=", true ])
365 ->getQuery()
366 ->first();
367
368 if (empty($res)) {
369 $schedule = new Schedule();
370 $schedule->setName(self::TYPE_NAMES[$type]);
371 $schedule->setType($type);
372 $schedule->setIntervals(self::DEFAULT_SCHEDULE_INTERVALS[$type]);
373 $schedule->setHidden(false);
374 $schedule->setDefault(true);
375 $schedule->save();
376 }
377
378 }
379 }
380
381 /**
382 * @throws DBException
383 * @throws IOException
384 * @throws InvalidArgumentException
385 */
386 public static function getDefaultConfigSchedule():Schedule {
387 $result = self::query()
388 ->select([JetBackup::ID_FIELD])
389 ->where([ self::NAME, "=", self::DEFAULT_CONFIG_SCHEDULE_NAME ])
390 ->where([ self::HIDDEN, "=", true ])
391 ->where([ self::DEFAULT, "=", true ])
392 ->getQuery()
393 ->first();
394
395 if($result) return new Schedule($result[JetBackup::ID_FIELD]);
396
397 $schedule = new Schedule();
398 $schedule->setName(Schedule::DEFAULT_CONFIG_SCHEDULE_NAME);
399 $schedule->setType(Schedule::TYPE_DAILY);
400 $schedule->setIntervals(Schedule::DEFAULT_SCHEDULE_INTERVALS[Schedule::TYPE_DAILY]);
401 $schedule->setHidden(true);
402 $schedule->setDefault(true);
403 $schedule->save();
404
405 return $schedule;
406 }
407
408 /**
409 * @throws InvalidArgumentException
410 * @throws IOException
411 */
412 public function getDisplay():array {
413
414 $jobs = BackupJob::query()
415 ->select([BackupJob::NAME, BackupJob::SCHEDULES])
416 ->getQuery()
417 ->fetch();
418
419 $names = [];
420
421 foreach ($jobs as $job) {
422 if (!isset($job[BackupJob::SCHEDULES])) continue;
423 foreach ($job[BackupJob::SCHEDULES] as $schedule) {
424 if ($schedule['_id'] === $this->getId()) {
425 $names[] = $job[BackupJob::NAME];
426 break;
427 }
428 }
429 }
430
431 return [
432 JetBackup::ID_FIELD => $this->getId(),
433 self::UNIQUE_ID => $this->getUniqueId(),
434 self::NAME => $this->getName(),
435 self::TYPE => $this->getType(),
436 self::TYPE_NAME => self::TYPE_NAMES[$this->getType()] ?? '',
437 self::INTERVALS => $this->getIntervals(),
438 self::BACKUP_ID => $this->getBackupId(),
439 self::JOB_ASSIGNED => count($names),
440 self::JOB_NAMES => $names,
441 self::DEFAULT => $this->isDefault(),
442 ];
443 }
444
445 public function getDisplayCLI():array {
446
447 $jobs = BackupJob::query()
448 ->select([BackupJob::NAME, BackupJob::SCHEDULES])
449 ->getQuery()
450 ->fetch();
451
452 $names = 0;
453
454 foreach ($jobs as $job) {
455 if (!isset($job[BackupJob::SCHEDULES])) continue;
456 foreach ($job[BackupJob::SCHEDULES] as $schedule) {
457 if ($schedule['_id'] === $this->getId()) {
458 $names++;
459 break;
460 }
461 }
462 }
463
464 return [
465 'ID' => $this->getId(),
466 'Name' => $this->getName(),
467 'Type' => $this->getType(),
468 'Intervals' => $this->getIntervals(),
469 'Backup ID' => $this->getBackupId(),
470 'Default' => $this->isDefault(),
471 'Backup Jobs Assigned' => $names,
472 ];
473 }
474
475 /**
476 * @return void
477 * @throws DBException
478 * @throws FieldsValidationException
479 * @throws IOException
480 * @throws InvalidArgumentException
481 */
482 public function validateFields():void {
483
484 if(!$this->getName()) throw new FieldsValidationException("Schedule name must be set");
485 if(!$this->getType()) throw new FieldsValidationException("Schedule type must be set");
486 if(!in_array($this->getType(), self::TYPE_ALLOWED)) throw new FieldsValidationException("Invalid schedule type, allowed types are: " . implode(',', self::TYPE_ALLOWED));
487 if($this->getIntervals() === null && $this->getType() != Schedule::TYPE_AFTER_JOB_DONE) throw new FieldsValidationException("Schedule intervals must be set");
488
489 if(isset(Schedule::ALLOWED_INTERVALS[$this->getType()])) {
490 if(!is_array($this->getIntervals())) throw new FieldsValidationException("Schedule types must be array for " . Schedule::TYPE_NAMES[$this->getType()]);
491 if (array_diff($this->getIntervals(), Schedule::ALLOWED_INTERVALS[$this->getType()])) {
492 throw new FieldsValidationException("Invalid intervals detected. Allowed intervals: " . implode(", ", Schedule::ALLOWED_INTERVALS[$this->getType()]));
493 }
494 }
495 if ($this->getType() == Schedule::TYPE_WEEKLY) {
496 $interval = $this->getIntervals(); // 0 = Sunday (using DateTime::format('w')
497
498 if ($interval < 0 || $interval > 6) {
499 throw new FieldsValidationException(
500 "Schedule intervals must be a numeric value between 0 (Sunday) and 6 (Saturday) for " .
501 Schedule::TYPE_NAMES[Schedule::TYPE_WEEKLY]
502 );
503 }
504 }
505 if ($this->getType() == Schedule::TYPE_HOURLY) {
506 $interval = $this->getIntervals();
507 if (!in_array($interval, self::ALLOWED_HOURLY_INTERVALS, true)) {
508 throw new FieldsValidationException(
509 "Schedule interval '{$interval}' is invalid for " .
510 Schedule::TYPE_NAMES[Schedule::TYPE_HOURLY] .
511 ". Allowed intervals: " .implode(',',self::ALLOWED_HOURLY_INTERVALS)
512 );
513 }
514 }
515
516
517 if ($this->getType() == Schedule::TYPE_AFTER_JOB_DONE) {
518 if(!$this->getBackupId()) throw new FieldsValidationException("Backup ID must be set");
519 $backup = new BackupJob($this->getBackupId());
520 if (!$backup->getId() || $backup->isHidden()) throw new FieldsValidationException("Backup job not found");
521 if (!$backup->isEnabled()) throw new FieldsValidationException("Backup job is disabled");
522
523 }
524
525 }
526
527 }