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