PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.11.1
JetBackup – Backup, Restore & Migrate v3.1.11.1
3.1.23.6 3.1.23.5 3.1.23.3 3.1.22.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 / Schedule / Schedule.php
backup / src / JetBackup / Schedule Last commit date
.htaccess 1 year ago Schedule.php 1 year ago ScheduleItem.php 1 year ago index.html 1 year ago web.config 1 year ago
Schedule.php
502 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 $scheduledTimeToday = (clone $now)->setTime($hour, $minute);
246
247
248 // Check if today is the run day
249 if ($todayDayName === (int) $runDay) {
250 if ($scheduledTimeToday > $now) {
251 // If the scheduled time today is still in the future
252 return $scheduledTimeToday->getTimestamp();
253 } else {
254 // If the scheduled time today has already passed, start checking from the next day
255 $dayIndex = 1;
256 }
257 }
258
259 // Iterate through the next days to find the correct scheduled weekday
260 while ($target === null && $maxIterations > 0) {
261 $testDate = (clone $now)->modify("+$dayIndex day")->setTime($hour, $minute);
262
263 $testDay = (int) $testDate->format('w'); // 0 (Sunday) to 6 (Saturday)
264
265 if ($testDay === (int) $runDay) {
266 $target = $testDate;
267 break;
268 }
269
270 $dayIndex++;
271 $maxIterations--;
272 }
273
274 if ($target === null) {
275 Alert::add('calcNextRun Error', 'Weekly loop did not find a valid date', Alert::LEVEL_WARNING);
276 }
277
278 return $target ? $target->getTimestamp() : 0;
279
280
281 case self::TYPE_MONTHLY:
282
283 $now = Util::getDateTime();
284 $runDays = $this->getIntervals(); // Days of the month to run
285 if (!is_array($runDays) || empty($runDays)) {
286 Alert::add('calcNextRun Error', 'No valid run days provided for monthly schedule', Alert::LEVEL_WARNING);
287 return 0;
288 }
289
290 $target = null;
291 $currentYear = (int)$now->format('Y');
292 $currentMonth = (int)$now->format('m');
293 $currentDay = (int)$now->format('d');
294 $maxIterations = 12; // Max iterations to prevent endless loop
295
296 // Check if today is a run day and the time has not yet passed
297 $now = Util::getDateTime();
298 if (in_array($currentDay, $runDays)) {
299 $scheduledTimeToday = (clone $now)->setTime($hour, $minute);
300 if ($scheduledTimeToday > $now) return $scheduledTimeToday->getTimestamp();
301 }
302
303 // Iterate through the months to find the next occurrence of the specified day
304 while ($target === null && $maxIterations > 0) {
305 foreach ($runDays as $day) {
306 try {
307
308 $testDate = (clone $now)->setDate($currentYear, $currentMonth, $day);
309 $testDate->setTime($hour, $minute);
310
311 // Correct the target time for time zone offset if necessary
312 $timezoneOffset = $testDate->getOffset() - $now->getOffset();
313 if ($timezoneOffset != 0) $testDate->modify($timezoneOffset . ' seconds');
314
315 if ($testDate > $now) {
316 $target = $testDate;
317 break 2;
318 }
319 } catch (Exception $e) {
320 continue; // Skip to the next day in $runDays
321 }
322 }
323
324 // Increment the month and adjust the year if needed
325 $currentMonth++;
326 if ($currentMonth > 12) {
327 $currentYear++;
328 $currentMonth = 1;
329 }
330
331 $maxIterations--;
332 }
333
334 if ($maxIterations === 0) {
335 Alert::add('calcNextRun Error', 'Monthly loop did not find a valid date', Alert::LEVEL_WARNING);
336 return 0;
337 }
338
339 return $target->getTimestamp() ?? 0;
340 }
341
342 } catch (Exception $e) {
343 Throw new ScheduleException($e->getMessage());
344 }
345 }
346
347 /**
348 * @throws InvalidArgumentException
349 * @throws IOException
350 */
351 public static function createDefaultSchedule():void {
352
353 foreach(self::DEFAULT_SCHEDULE_TYPES as $type) {
354
355 $res = self::query()
356 ->select([JetBackup::ID_FIELD])
357 ->where([ self::TYPE, "=", $type ])
358 ->where([ self::HIDDEN, "=", false ])
359 ->where([ self::DEFAULT, "=", true ])
360 ->getQuery()
361 ->first();
362
363 if (empty($res)) {
364 $schedule = new Schedule();
365 $schedule->setName(self::TYPE_NAMES[$type]);
366 $schedule->setType($type);
367 $schedule->setIntervals(self::DEFAULT_SCHEDULE_INTERVALS[$type]);
368 $schedule->setHidden(false);
369 $schedule->setDefault(true);
370 $schedule->save();
371 }
372
373 }
374 }
375
376 /**
377 * @throws DBException
378 * @throws IOException
379 * @throws InvalidArgumentException
380 */
381 public static function getDefaultConfigSchedule():Schedule {
382 $result = self::query()
383 ->select([JetBackup::ID_FIELD])
384 ->where([ self::NAME, "=", self::DEFAULT_CONFIG_SCHEDULE_NAME ])
385 ->where([ self::HIDDEN, "=", true ])
386 ->where([ self::DEFAULT, "=", true ])
387 ->getQuery()
388 ->first();
389
390 if($result) return new Schedule($result[JetBackup::ID_FIELD]);
391
392 $schedule = new Schedule();
393 $schedule->setName(Schedule::DEFAULT_CONFIG_SCHEDULE_NAME);
394 $schedule->setType(Schedule::TYPE_DAILY);
395 $schedule->setIntervals(Schedule::DEFAULT_SCHEDULE_INTERVALS[Schedule::TYPE_DAILY]);
396 $schedule->setHidden(true);
397 $schedule->setDefault(true);
398 $schedule->save();
399
400 return $schedule;
401 }
402
403 /**
404 * @throws InvalidArgumentException
405 * @throws IOException
406 */
407 public function getDisplay():array {
408
409 $jobs = BackupJob::query()
410 ->select([BackupJob::NAME, BackupJob::SCHEDULES])
411 ->getQuery()
412 ->fetch();
413
414 $names = [];
415
416 foreach ($jobs as $job) {
417 if (!isset($job[BackupJob::SCHEDULES])) continue;
418 foreach ($job[BackupJob::SCHEDULES] as $schedule) {
419 if ($schedule['_id'] === $this->getId()) {
420 $names[] = $job[BackupJob::NAME];
421 break;
422 }
423 }
424 }
425
426 return [
427 JetBackup::ID_FIELD => $this->getId(),
428 self::UNIQUE_ID => $this->getUniqueId(),
429 self::NAME => $this->getName(),
430 self::TYPE => $this->getType(),
431 self::TYPE_NAME => self::TYPE_NAMES[$this->getType()] ?? '',
432 self::INTERVALS => $this->getIntervals(),
433 self::BACKUP_ID => $this->getBackupId(),
434 self::JOB_ASSIGNED => count($names),
435 self::JOB_NAMES => $names,
436 self::DEFAULT => $this->isDefault(),
437 ];
438 }
439
440 public function getDisplayCLI():array {
441
442 $jobs = BackupJob::query()
443 ->select([BackupJob::NAME, BackupJob::SCHEDULES])
444 ->getQuery()
445 ->fetch();
446
447 $names = 0;
448
449 foreach ($jobs as $job) {
450 if (!isset($job[BackupJob::SCHEDULES])) continue;
451 foreach ($job[BackupJob::SCHEDULES] as $schedule) {
452 if ($schedule['_id'] === $this->getId()) {
453 $names++;
454 break;
455 }
456 }
457 }
458
459 return [
460 'ID' => $this->getId(),
461 'Name' => $this->getName(),
462 'Type' => $this->getType(),
463 'Intervals' => $this->getIntervals(),
464 'Backup ID' => $this->getBackupId(),
465 'Default' => $this->isDefault(),
466 'Backup Jobs Assigned' => $names,
467 ];
468 }
469
470 /**
471 * @throws FieldsValidationException
472 */
473 public function validateFields():void {
474 if(!$this->getName()) throw new FieldsValidationException("Schedule name must be set");
475 if(!$this->getType()) throw new FieldsValidationException("Schedule type must be set");
476 if(!in_array($this->getType(), self::TYPE_ALLOWED)) throw new FieldsValidationException("Invalid schedule type, allowed types are: " . implode(',', self::TYPE_ALLOWED));
477 if(!$this->getIntervals() && $this->getType() != Schedule::TYPE_AFTER_JOB_DONE) throw new FieldsValidationException("Schedule intervals must be set");
478
479 if(isset(Schedule::ALLOWED_INTERVALS[$this->getType()])) {
480 if(!is_array($this->getIntervals())) throw new FieldsValidationException("Schedule types must be array for " . Schedule::TYPE_NAMES[$this->getType()]);
481 if (array_diff($this->getIntervals(), Schedule::ALLOWED_INTERVALS[$this->getType()])) {
482 throw new FieldsValidationException("Invalid intervals detected. Allowed intervals: " . implode(", ", Schedule::ALLOWED_INTERVALS[$this->getType()]));
483 }
484 }
485 if ($this->getType() == Schedule::TYPE_WEEKLY) {
486 $interval = $this->getIntervals();
487 if (!(is_int($interval) && $interval > 0 && $interval < 8)) {
488 throw new FieldsValidationException("Schedule intervals must be a numeric value between 1 and 7 for " . Schedule::TYPE_NAMES[Schedule::TYPE_WEEKLY]);
489 }
490 }
491
492 if ($this->getType() == Schedule::TYPE_AFTER_JOB_DONE) {
493 if(!$this->getBackupId()) throw new FieldsValidationException("Backup ID must be set");
494 $backup = new BackupJob($this->getBackupId());
495 if (!$backup->getId() || $backup->isHidden()) throw new FieldsValidationException("Backup job not found");
496 if (!$backup->isEnabled()) throw new FieldsValidationException("Backup job is disabled");
497
498 }
499
500 }
501
502 }