schedules.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <?php
  2. /**
  3. * @file
  4. * All of the schedule handling code needed for Backup and Migrate.
  5. */
  6. backup_migrate_include('crud');
  7. /**
  8. * Run the preconfigured schedules. Called on cron.
  9. */
  10. function backup_migrate_schedules_run() {
  11. backup_migrate_include('profiles');
  12. foreach (backup_migrate_get_schedules() as $schedule) {
  13. $schedule->cron();
  14. }
  15. backup_migrate_cleanup();
  16. }
  17. /**
  18. * Get all the available backup schedules.
  19. */
  20. function backup_migrate_get_schedules() {
  21. static $schedules = NULL;
  22. // Get the list of schedules and cache them locally.
  23. if ($schedules === NULL) {
  24. $schedules = backup_migrate_crud_get_items('schedule');
  25. }
  26. return $schedules;
  27. }
  28. /**
  29. * Get the schedule info for the schedule with the given ID, or NULL if none exists.
  30. */
  31. function backup_migrate_get_schedule($schedule_id) {
  32. $schedules = backup_migrate_get_schedules();
  33. return @$schedules[$schedule_id];
  34. }
  35. /**
  36. * A schedule class for crud operations.
  37. */
  38. class backup_migrate_schedule extends backup_migrate_item {
  39. var $db_table = "backup_migrate_schedules";
  40. var $type_name = 'schedule';
  41. var $singular = 'schedule';
  42. var $plural = 'schedules';
  43. var $default_values = array();
  44. /**
  45. * This function is not supposed to be called. It is just here to help the po extractor out.
  46. */
  47. function strings() {
  48. // Help the pot extractor find these strings.
  49. t('Schedule');
  50. t('Schedules');
  51. t('schedule');
  52. t('schedules');
  53. }
  54. /**
  55. * Get the default values for this item.
  56. */
  57. function get_default_values() {
  58. return array(
  59. 'name' => t("Untitled Schedule"),
  60. 'source_id' => 'db',
  61. 'enabled' => 1,
  62. 'keep' => 0,
  63. 'period' => 60 * 60 * 24,
  64. 'storage' => BACKUP_MIGRATE_STORAGE_NONE
  65. );
  66. }
  67. /**
  68. * Get the columns needed to list the type.
  69. */
  70. function get_list_column_info() {
  71. $out = parent::get_list_column_info();
  72. $out = array(
  73. 'name' => array('title' => t('Name')),
  74. 'destination_name' => array('title' => t('Destination'), 'html' => TRUE),
  75. 'profile_name' => array('title' => t('Profile'), 'html' => TRUE),
  76. 'frequency_description' => array('title' => t('Frequency')),
  77. 'keep_description' => array('title' => t('Keep')),
  78. 'enabled_description' => array('title' => t('Enabled')),
  79. 'last_run_description' => array('title' => t('Last run')),
  80. ) + $out;
  81. return $out;
  82. }
  83. /**
  84. * Get a row of data to be used in a list of items of this type.
  85. */
  86. function get_list_row() {
  87. drupal_add_css(drupal_get_path('module', 'backup_migrate') .'/backup_migrate.css');
  88. $row = parent::get_list_row();
  89. if (!$this->is_enabled()) {
  90. foreach ($row as $key => $field) {
  91. $row[$key] = array('data' => $field, 'class' => 'schedule-list-disabled');
  92. }
  93. }
  94. return $row;
  95. }
  96. /**
  97. * Is the schedule enabled and valid.
  98. */
  99. function is_enabled() {
  100. $destination = $this->get_destination();
  101. $profile = $this->get_profile();
  102. return (!empty($this->enabled) && !empty($destination) && !empty($profile));
  103. }
  104. /**
  105. * Get the destination object of the schedule.
  106. */
  107. function get_destination() {
  108. backup_migrate_include('destinations');
  109. return backup_migrate_get_destination($this->get('destination_id'));
  110. }
  111. /**
  112. * Get the name of the destination.
  113. */
  114. function get_destination_name() {
  115. if ($destination = $this->get_destination()) {
  116. return check_plain($destination->get_name());
  117. }
  118. return '<div class="row-error">'. t("Missing") .'</div>';
  119. }
  120. /**
  121. * Get the destination of the schedule.
  122. */
  123. function get_profile() {
  124. backup_migrate_include('profiles');
  125. return backup_migrate_get_profile($this->get('profile_id'));
  126. }
  127. /**
  128. * Get the name of the source.
  129. */
  130. function get_profile_name() {
  131. if ($profile = $this->get_profile()) {
  132. return check_plain($profile->get_name());
  133. }
  134. return '<div class="row-error">'. t("Missing") .'</div>';
  135. }
  136. /**
  137. * Format a frequency in human-readable form.
  138. */
  139. function get_frequency_description() {
  140. $period = $this->get_frequency_period();
  141. $out = format_plural(($this->period / $period['seconds']), $period['singular'], $period['plural']);
  142. return $out;
  143. }
  144. /**
  145. * Format the number to keep in human-readable form.
  146. */
  147. function get_keep_description() {
  148. return !empty($this->keep) ? $this->keep : t('All');
  149. }
  150. /**
  151. * Format the enabled status in human-readable form.
  152. */
  153. function get_enabled_description() {
  154. return !empty($this->enabled) ? t('Enabled') : t('Disabled');
  155. }
  156. /**
  157. * Format the enabled status in human-readable form.
  158. */
  159. function get_last_run_description() {
  160. $last_run = $this->get('last_run');
  161. return !empty($last_run) ? format_date($last_run, 'small') : t('Never');
  162. }
  163. /**
  164. * Get the number of excluded tables.
  165. */
  166. function get_exclude_tables_count() {
  167. return count($this->exclude_tables) ? count($this->exclude_tables) : t("No tables excluded");
  168. }
  169. /**
  170. * Get the number of excluded tables.
  171. */
  172. function get_nodata_tables_count() {
  173. return count($this->nodata_tables) ? count($this->nodata_tables) : t("No data omitted");
  174. }
  175. /**
  176. * Get the edit form.
  177. */
  178. function edit_form() {
  179. $form = parent::edit_form();
  180. backup_migrate_include('destinations', 'profiles');
  181. $form['enabled'] = array(
  182. "#type" => "checkbox",
  183. "#title" => t("Enabled"),
  184. "#default_value" => $this->get('enabled'),
  185. );
  186. $form['name'] = array(
  187. "#type" => "textfield",
  188. "#title" => t("Schedule Name"),
  189. "#default_value" => $this->get('name'),
  190. );
  191. $form += _backup_migrate_get_source_form($this->get('source_id'));
  192. $form['profile_id'] = array(
  193. "#type" => "select",
  194. "#title" => t("Settings Profile"),
  195. "#options" => _backup_migrate_get_profile_form_item_options(),
  196. "#default_value" => $this->get('profile_id'),
  197. );
  198. $form['profile_id']['#description'] = ' '. l(t("Create new profile"), BACKUP_MIGRATE_MENU_PATH . "/profile/add");
  199. if (!$form['profile_id']['#options']) {
  200. $form['profile_id']['#options'] = array('0' => t('-- None Available --'));
  201. }
  202. $period_options = array();
  203. foreach ($this->frequency_periods() as $type => $period) {
  204. $period_options[$type] = $period['title'];
  205. }
  206. $default_period = $this->get_frequency_period();
  207. $default_period_num = $this->get('period') / $default_period['seconds'];
  208. $form['period'] = array(
  209. "#type" => "item",
  210. "#title" => t("Backup every"),
  211. "#prefix" => '<div class="container-inline">',
  212. "#suffix" => '</div>',
  213. "#tree" => TRUE,
  214. );
  215. $form['period']['number'] = array(
  216. "#type" => "textfield",
  217. "#size" => 6,
  218. "#default_value" => $default_period_num,
  219. );
  220. $form['period']['type'] = array(
  221. "#type" => "select",
  222. "#options" => $period_options,
  223. "#default_value" => $default_period['type'],
  224. );
  225. $form['keep'] = array(
  226. "#type" => "textfield",
  227. "#size" => 6,
  228. "#title" => t("Number of Backup files to keep"),
  229. "#description" => t("The number of backup files to keep before deleting old ones. Use 0 to never delete backups. <strong>Other files in the destination directory will get deleted if you specify a limit.</strong>"),
  230. "#default_value" => $this->get('keep'),
  231. );
  232. $destination_options = _backup_migrate_get_destination_form_item_options('scheduled backup');
  233. $form['destination_id'] = array(
  234. "#type" => "select",
  235. "#title" => t("Destination"),
  236. "#description" => t("Choose where the backup file will be saved. Backup files contain sensitive data, so be careful where you save them."),
  237. "#options" => $destination_options,
  238. "#default_value" => $this->get('destination_id'),
  239. );
  240. $form['destination_id']['#description'] .= ' '. l(t("Create new destination"), BACKUP_MIGRATE_MENU_PATH . "/destination/add");
  241. return $form;
  242. }
  243. /**
  244. * Submit the edit form.
  245. */
  246. function edit_form_validate($form, &$form_state) {
  247. if (!is_numeric($form_state['values']['period']['number']) || $form_state['values']['period']['number'] <= 0) {
  248. form_set_error('period][number', t('Backup period must be a number greater than 0.'));
  249. }
  250. if (!is_numeric($form_state['values']['keep']) || $form_state['values']['keep'] < 0) {
  251. form_set_error('keep', t('Number to keep must be an integer greater than or equal to 0.'));
  252. }
  253. parent::edit_form_validate($form, $form_state);
  254. }
  255. /**
  256. * Submit the edit form.
  257. */
  258. function edit_form_submit($form, &$form_state) {
  259. $periods = $this->frequency_periods();
  260. $period = $periods[$form_state['values']['period']['type']];
  261. $form_state['values']['period'] = $form_state['values']['period']['number'] * $period['seconds'];
  262. parent::edit_form_submit($form, $form_state);
  263. }
  264. /**
  265. * Get the period of the frequency (ie: seconds, minutes etc.)
  266. */
  267. function get_frequency_period() {
  268. foreach (array_reverse($this->frequency_periods()) as $period) {
  269. if ($period['seconds'] && ($this->period % $period['seconds']) === 0) {
  270. return $period;
  271. }
  272. }
  273. }
  274. /**
  275. * Get a list of available backup periods. Only returns time periods which have a
  276. * (reasonably) consistent number of seconds (ie: no months).
  277. */
  278. function frequency_periods() {
  279. return array(
  280. 'seconds' => array('type' => 'seconds', 'seconds' => 1, 'title' => t('Seconds'), 'singular' => t('Once a second'), 'plural' => t('Every @count seconds')),
  281. 'minutes' => array('type' => 'minutes', 'seconds' => 60, 'title' => t('Minutes'), 'singular' => t('Once a minute'), 'plural' => t('Every @count minutes')),
  282. 'hours' => array('type' => 'hours', 'seconds' => 3600, 'title' => t('Hours'), 'singular' => t('Once an hour'), 'plural' => t('Every @count hours')),
  283. 'days' => array('type' => 'days', 'seconds' => 86400, 'title' => t('Days'), 'singular' => t('Once a day'), 'plural' => t('Every @count days')),
  284. 'weeks' => array('type' => 'weeks', 'seconds' => 604800, 'title' => t('Weeks'), 'singular' => t('Once a week'), 'plural' => t('Every @count weeks')),
  285. );
  286. }
  287. /**
  288. * Get the message to send to the user when confirming the deletion of the item.
  289. */
  290. function delete_confirm_message() {
  291. return t('Are you sure you want to delete the profile %name? Any schedules using this profile will be disabled.', array('%name' => $this->get('name')));
  292. }
  293. /**
  294. * Perform the cron action. Run the backup if enough time has elapsed.
  295. */
  296. function cron() {
  297. $now = time();
  298. // Add a small negative buffer (1% of the entire period) to the time to account for slight difference in cron run length.
  299. $wait_time = $this->period - ($this->period * variable_get('backup_migrate_schedule_buffer', 0.01));
  300. if ($this->is_enabled() && ($now - $this->get('last_run')) >= $wait_time) {
  301. if ($settings = $this->get_profile()) {
  302. $settings->destination_id = $this->destination_id;
  303. $settings->source_id = $this->source_id;
  304. $this->update_last_run($now);
  305. backup_migrate_perform_backup($settings);
  306. $this->remove_expired_backups();
  307. }
  308. else {
  309. backup_migrate_backup_fail("Schedule '%schedule' could not be run because requires a profile which is missing.", array('%schedule' => $schedule->get_name()), $settings);
  310. }
  311. }
  312. }
  313. /**
  314. * Set the last run time of a schedule to the given timestamp, or now if none specified.
  315. */
  316. function update_last_run($timestamp = NULL) {
  317. if ($timestamp === NULL) {
  318. $timestamp = time();
  319. }
  320. variable_set('backup_migrate_schedule_last_run_' . $this->get('id'), $timestamp);
  321. }
  322. /**
  323. * Set the last run time of a schedule to the given timestamp, or now if none specified.
  324. */
  325. function get_last_run($timestamp = NULL) {
  326. return variable_get('backup_migrate_schedule_last_run_' . $this->get('id'), 0);
  327. }
  328. /**
  329. * Remove older backups keeping only the number specified by the aministrator.
  330. */
  331. function remove_expired_backups() {
  332. $files = array();
  333. backup_migrate_include('destinations');
  334. $num_to_keep = $this->keep;
  335. // If num to keep is not 0 (0 is infinity).
  336. if ($num_to_keep && ($destination = $this->get_destination())) {
  337. $i = 0;
  338. if ($destination->op('delete') && $destination_files = $destination->list_files()) {
  339. // Sort the files by modified time.
  340. foreach ($destination_files as $file) {
  341. if ($file->is_recognized_type() && $destination->can_delete_file($file->file_id())) {
  342. $files[str_pad($file->info('filetime'), 10, "0", STR_PAD_LEFT) ."-". $i++] = $file;
  343. }
  344. }
  345. // If we are beyond our limit, remove as many as we need.
  346. $num_files = count($files);
  347. if ($num_files > $num_to_keep) {
  348. $num_to_delete = $num_files - $num_to_keep;
  349. // Sort by date.
  350. ksort($files);
  351. // Delete from the start of the list (earliest).
  352. for ($i = 0; $i < $num_to_delete; $i++) {
  353. $file = array_shift($files);
  354. $destination->delete_file($file->file_id());
  355. }
  356. }
  357. }
  358. }
  359. }
  360. }