archive.drush.inc

  1. 8.0.x commands/core/archive.drush.inc
  2. 6.x commands/core/archive.drush.inc
  3. 7.x commands/core/archive.drush.inc
  4. 4.x commands/core/archive.drush.inc
  5. 5.x commands/core/archive.drush.inc
  6. master commands/core/archive.drush.inc

An early implementation of Site Archive dump/restore. See http://groups.drupal.org/site-archive-format.

Functions

Namesort descending Description
archive_archive_dump_complete Command argument complete callback.
archive_archive_restore_complete Command argument complete callback.
archive_drush_command
drush_archive_dump Command callback. Generate site archive file.
drush_archive_guess_manifest Try to find docroot and DB dump file in an extracted archive.
drush_archive_restore Command callback. Restore web site(s) from a site archive file.

File

commands/core/archive.drush.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * An early implementation of Site Archive dump/restore. See
  5. * http://groups.drupal.org/site-archive-format.
  6. */
  7. use Drush\Log\LogLevel;
  8. function archive_drush_command() {
  9. $items['archive-dump'] = array(
  10. 'description' => 'Backup your code, files, and database into a single file.',
  11. 'arguments' => array(
  12. 'sites' => 'Optional. Site specifications, delimited by commas. Typically, list subdirectory name(s) under /sites.',
  13. ),
  14. // Most options on sql-dump should not be shown, so just offer a subset.
  15. 'options' => drush_sql_get_table_selection_options() + array(
  16. 'description' => 'Describe the archive contents.',
  17. 'tags' => 'Add tags to the archive manifest. Delimit multiple by commas.',
  18. 'destination' => 'The full path and filename in which the archive should be stored. If omitted, it will be saved to the drush-backups directory and a filename will be generated.',
  19. 'overwrite' => 'Do not fail if the destination file exists; overwrite it instead. Default is --no-overwrite.',
  20. 'generator' => 'The generator name to store in the MANIFEST file. The default is "Drush archive-dump".',
  21. 'generatorversion' => 'The generator version number to store in the MANIFEST file. The default is ' . Drush::getMajorVersion() . '.',
  22. 'pipe' => 'Only print the destination of the archive. Useful for scripts that don\'t pass --destination.',
  23. 'preserve-symlinks' => 'Preserve symbolic links.',
  24. 'no-core' => 'Exclude Drupal core, so the backup only contains the site specific stuff.',
  25. 'tar-options' => 'Options passed thru to the tar command.',
  26. ),
  27. 'examples' => array(
  28. 'drush archive-dump default,example.com,foo.com' => 'Write an archive containing 3 sites in it.',
  29. 'drush archive-dump @sites' => 'Save archive containing all sites in a multi-site.',
  30. 'drush archive-dump default --destination=/backups/mysite.tar' => 'Save archive to custom location.',
  31. 'drush archive-dump --tar-options="--exclude=.git --exclude=sites/default/files"' => 'Omits any .git directories found in the tree as well as sites/default/files.',
  32. 'drush archive-dump --tar-options="--exclude=%files"' => 'Placeholder %files is replaced with the real path for the current site, and that path is excluded.',
  33. ),
  34. 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE,
  35. 'aliases' => array('ard', 'archive-backup', 'arb'),
  36. );
  37. $items['archive-restore'] = array(
  38. 'description' => 'Expand a site archive into a Drupal web site.',
  39. 'arguments' => array(
  40. 'file' => 'The site archive file that should be expanded.',
  41. 'site name' => 'Optional. Which site within the archive you want to restore. Defaults to all.',
  42. ),
  43. 'required-arguments' => 1,
  44. 'options' => array(
  45. 'destination' => 'Specify where the Drupal site should be expanded, including the docroot. Defaults to the current working directory.',
  46. 'db-prefix' => 'An optional table prefix to use during restore.',
  47. 'db-url' => array(
  48. 'description' => 'A Drupal 6 style database URL indicating the target for database restore. If not provided, the archived settings.php is used.',
  49. 'example-value' => 'mysql://root:pass@host/db',
  50. ),
  51. 'db-su' => 'Account to use when creating the new database. Optional.',
  52. 'db-su-pw' => 'Password for the "db-su" account. Optional.',
  53. 'overwrite' => 'Allow drush to overwrite any files in the destination. Default is --no-overwrite.',
  54. 'tar-options' => 'Options passed thru to the tar command.',
  55. ),
  56. 'examples' => array(
  57. 'drush archive-restore ./example.tar.gz' => 'Restore the files and databases for all sites in the archive.',
  58. 'drush archive-restore ./example.tar.gz example.com' => 'Restore the files and database for example.com site.',
  59. 'drush archive-restore ./example.tar.gz --destination=/var/www/example.com/docroot' => 'Restore archive to a custom location.',
  60. 'drush archive-restore ./example.tar.gz --db-url=mysql://root:pass@127.0.0.1/dbname' => 'Restore archive to a new database (and customize settings.php to point there.).',
  61. ),
  62. 'bootstrap' => DRUSH_BOOTSTRAP_NONE,
  63. 'aliases' => array('arr'),
  64. );
  65. return $items;
  66. }
  67. /**
  68. * Command callback. Generate site archive file.
  69. */
  70. function drush_archive_dump($sites_subdirs = '@self') {
  71. $include_platform = !drush_get_option('no-core', FALSE);
  72. $tar = drush_get_tar_executable();
  73. $sites = array();
  74. list($aliases, $not_found) = drush_sitealias_resolve_sitespecs(explode(',', $sites_subdirs));
  75. if (!empty($not_found)) {
  76. drush_log(dt("Some site subdirectories are not valid Drupal sites: @list", array("@list" => implode(', ', $not_found))), LogLevel::WARNING);
  77. }
  78. foreach ($aliases as $key => $alias) {
  79. $sites[$key] = $alias;
  80. if (($db_record = sitealias_get_databases_from_record($alias))) {
  81. $sites[$key]['databases'] = $db_record;
  82. }
  83. else {
  84. $sites[$key]['databases'] = array();
  85. drush_log(dt('DB definition not found for !alias', array('!alias' => $key)), LogLevel::INFO);
  86. }
  87. }
  88. // The user can specify a destination filepath or not. That filepath might
  89. // end with .gz, .tgz, or something else. At the end of this command we will
  90. // gzip a file, and we want it to end up with the user-specified name (if
  91. // any), but gzip renames files and refuses to compress files ending with
  92. // .gz and .tgz, making our lives difficult. Solution:
  93. //
  94. // 1. Create a unique temporary base name to which gzip WILL append .gz.
  95. // 2. If no destination is provided, set $dest_dir to a backup directory and
  96. // $final_destination to be the unique name in that dir.
  97. // 3. If a destination is provided, set $dest_dir to that directory and
  98. // $final_destination to the exact name given.
  99. // 4. Set $destination, the actual working file we will build up, to the
  100. // unqiue name in $dest_dir.
  101. // 5. After gzip'ing $destination, rename $destination.gz to
  102. // $final_destination.
  103. //
  104. // Sheesh.
  105. // Create the unique temporary name.
  106. $prefix = 'none';
  107. if (!empty($sites)) {
  108. $first = current($sites);
  109. if ( !empty($first['databases']['default']['default']['database']) ) {
  110. $prefix = count($sites) > 1 ? 'multiple_sites' : str_replace('/', '-', $first['databases']['default']['default']['database']);
  111. }
  112. }
  113. $date = gmdate('Ymd_His');
  114. $temp_dest_name = "$prefix.$date.tar";
  115. $final_destination = drush_get_option('destination');
  116. if (!$final_destination) {
  117. // No destination provided.
  118. $backup = drush_include_engine('version_control', 'backup');
  119. // TODO: this standard Drush pattern leads to a slightly obtuse directory structure.
  120. $dest_dir = $backup->prepare_backup_dir('archive-dump');
  121. if (empty($dest_dir)) {
  122. $dest_dir = drush_tempdir();
  123. }
  124. $final_destination = "$dest_dir/$temp_dest_name.gz";
  125. }
  126. else {
  127. // Use the supplied --destination. If it is relative, resolve it
  128. // relative to the directory in which drush was invoked.
  129. $command_cwd = getcwd();
  130. drush_op('chdir', drush_get_context('DRUSH_OLDCWD', getcwd()));
  131. // This doesn't perform realpath on the basename, but that's okay. This is
  132. // not path-based security. We just use it for checking for perms later.
  133. drush_mkdir(dirname($final_destination));
  134. $dest_dir = realpath(dirname($final_destination));
  135. $final_destination = $dest_dir . '/' . basename($final_destination);
  136. drush_op('chdir', $command_cwd);
  137. }
  138. // $dest_dir is either the backup directory or specified directory. Set our
  139. // working file.
  140. $destination = "$dest_dir/$temp_dest_name";
  141. // Validate the FINAL destination. It should be a file that does not exist
  142. // (unless --overwrite) in a writable directory (and a writable file if
  143. // it exists). We check all this up front to avoid failing after a long
  144. // dump process.
  145. $overwrite = drush_get_option('overwrite');
  146. $dest_dir = dirname($final_destination);
  147. $dt_args = array('!file' => $final_destination, '!dir' => $dest_dir);
  148. if (is_dir($final_destination)) {
  149. drush_set_error('DRUSH_ARCHIVE_DEST_IS_DIR', dt('Destination !file must be a file, not a directory.', $dt_args));
  150. return;
  151. }
  152. else if (file_exists($final_destination)) {
  153. if (!$overwrite) {
  154. drush_set_error('DRUSH_ARCHIVE_DEST_EXISTS', dt('Destination !file exists; specify --overwrite to overwrite.', $dt_args));
  155. return;
  156. }
  157. else if (!is_writable($final_destination)) {
  158. drush_set_error('DRUSH_ARCHIVE_DEST_FILE_NOT_WRITEABLE', dt('Destination !file is not writable.', $dt_args));
  159. return;
  160. }
  161. }
  162. else if (!is_writable($dest_dir)) {
  163. drush_set_error('DRUSH_ARCHIVE_DEST_DIR_NOT_WRITEABLE', dt('Destination directory !dir is not writable.', $dt_args));
  164. return;
  165. }
  166. // Get the extra options for tar, if any
  167. $tar_extra_options = drush_sitealias_evaluate_paths_in_options(drush_get_option('tar-options', ''));
  168. // Start adding codebase to the archive.
  169. $docroot_path = realpath(drush_get_context('DRUSH_DRUPAL_ROOT'));
  170. $docroot = basename($docroot_path);
  171. $workdir = dirname($docroot_path);
  172. if ($include_platform) {
  173. $dereference = (drush_get_option('preserve-symlinks', FALSE)) ? '' : '--dereference ';
  174. // Convert destination path to Unix style for tar on MinGW - see http://drupal.org/node/1844224
  175. if (drush_is_mingw()) {
  176. $destination_orig = $destination;
  177. $destination = str_replace('\\', '/', $destination);
  178. $destination = preg_replace('$^([a-zA-Z]):$', '/$1', $destination);
  179. }
  180. // Archive Drupal core, excluding sites dir.
  181. drush_shell_cd_and_exec($workdir, "$tar {$tar_extra_options} --exclude \"{$docroot}/sites\" {$dereference}-cf %s %s", $destination, $docroot);
  182. // Add sites/all to the same archive.
  183. drush_shell_cd_and_exec($workdir, "$tar {$tar_extra_options} {$dereference}-rf %s %s", $destination, "{$docroot}/sites/all");
  184. // Add special files in sites/ to the archive. Last 2 items are new in Drupal8.
  185. $files_to_add = array('sites/README.txt', 'sites/sites.php', 'sites/example.sites.php', 'sites/development.services.yml', 'sites/example.settings.local.php');
  186. foreach ($files_to_add as $file_to_add) {
  187. if (file_exists($file_to_add)) {
  188. drush_shell_cd_and_exec($workdir, "$tar {$dereference}-rf %s %s", $destination, $docroot . '/' . $file_to_add);
  189. }
  190. }
  191. }
  192. $tmp = drush_tempdir();
  193. $all_dbs = array();
  194. // Dump the default database for each site and add to the archive.
  195. foreach ($sites as $key => $alias) {
  196. if (isset($alias['databases']['default']['default'])) {
  197. $db_spec = $alias['databases']['default']['default'];
  198. // Use a subdirectory name matching the docroot name.
  199. drush_mkdir("{$tmp}/{$docroot}");
  200. // Ensure uniqueness by prefixing key if needed. Remove path delimiters.
  201. $dbname = str_replace(DIRECTORY_SEPARATOR, '-', $db_spec['database']);
  202. $result_file = count($sites) == 1 ? "$tmp/$dbname.sql" : str_replace('@', '', "$tmp/$key-$dbname.sql");
  203. $all_dbs[$key] = array(
  204. 'file' => basename($result_file),
  205. 'driver' => $db_spec['driver'],
  206. );
  207. $sql = drush_sql_get_class($db_spec);
  208. $sql->dump($result_file);
  209. drush_shell_cd_and_exec($tmp, "$tar {$tar_extra_options} --dereference -rf %s %s", $destination, basename($result_file));
  210. }
  211. }
  212. // Build a manifest file AND add sites/$subdir to archive as we go.
  213. $platform = array(
  214. 'datestamp' => time(),
  215. 'formatversion' => '1.0',
  216. 'generator' => drush_get_option('generator', 'Drush archive-dump'),
  217. 'generatorversion' => drush_get_option('generatorversion', Drush::getMajorVersion()),
  218. 'description' => drush_get_option('description', ''),
  219. 'tags' => drush_get_option('tags', ''),
  220. 'archiveformat' => ($include_platform ? 'platform' : 'site'),
  221. );
  222. $contents = drush_export_ini(array('Global' => $platform));
  223. $i=0;
  224. foreach ($sites as $key => $alias) {
  225. $status = drush_invoke_process($alias, 'core-status', array(), array(), array('integrate' => FALSE));
  226. if (!$status || $status['error_log']) {
  227. drush_log(dt('Unable to determine sites directory for !alias', array('!alias' => $key)), LogLevel::WARNING);
  228. }
  229. // Add the site specific directory to archive.
  230. if (!empty($status['object']['%paths']['%site'])) {
  231. drush_shell_cd_and_exec($workdir, "$tar {$tar_extra_options} --dereference -rf %s %s", $destination, "{$docroot}/sites/" . basename($status['object']['%paths']['%site']));
  232. }
  233. $site = array(
  234. 'docroot' => DRUPAL_ROOT,
  235. 'sitedir' => @$status['object']['%paths']['%site'],
  236. 'files-public' => @$status['object']['%paths']['%files'],
  237. 'files-private' => @$status['object']['%paths']['%private'],
  238. );
  239. $site["database-default-file"] = $all_dbs[$key]['file'];
  240. $site["database-default-driver"] = $all_dbs[$key]['driver'];
  241. // The section title is the sites subdirectory name.
  242. $info[basename($site['sitedir'])] = $site;
  243. $contents .= "\n" . drush_export_ini($info);
  244. unset($info);
  245. $i++;
  246. }
  247. file_put_contents("{$tmp}/MANIFEST.ini", $contents);
  248. // Add manifest to archive.
  249. drush_shell_cd_and_exec($tmp, "$tar --dereference -rf %s %s", $destination, 'MANIFEST.ini');
  250. // Ensure that default/default.settings.php is in the archive. This is needed
  251. // by site-install when restoring a site without any DB.
  252. // NOTE: Windows tar file replace operation is broken so we have to check if file already exists.
  253. // Otherwise it will corrupt the archive.
  254. $res = drush_shell_cd_and_exec($workdir, "$tar -tf %s %s", $destination, $docroot . '/sites/default/default.settings.php');
  255. $output = drush_shell_exec_output();
  256. if (!$res || !isset($output[0]) || empty($output[0])) {
  257. drush_shell_cd_and_exec($workdir, "$tar --dereference -vrf %s %s", $destination, $docroot . '/sites/default/default.settings.php');
  258. }
  259. // Switch back to original destination in case it was modified for tar on MinGW.
  260. if (!empty($destination_orig)) {
  261. $destination = $destination_orig;
  262. }
  263. // Compress the archive
  264. if (!drush_shell_exec("gzip --no-name -f %s", $destination)) {
  265. // Clean up the tar file
  266. drush_register_file_for_deletion($destination);
  267. return drush_set_error(DRUSH_APPLICATION_ERROR, dt('Failed to gzip !destination', ['!destination' => $destination]));
  268. }
  269. // gzip appends .gz unless the name already ends in .gz, .tgz, or .taz.
  270. if ("{$destination}.gz" != $final_destination) {
  271. drush_move_dir("{$destination}.gz", $final_destination, $overwrite);
  272. }
  273. drush_log(dt('Archive saved to !dest', array('!dest' => $final_destination)), LogLevel::OK);
  274. return $final_destination;
  275. }
  276. /**
  277. * Command argument complete callback.
  278. *
  279. * @return
  280. * List of site names/aliases for archival.
  281. */
  282. function archive_archive_dump_complete() {
  283. return array('values' => array_keys(_drush_sitealias_all_list()));
  284. }
  285. /**
  286. * Command callback. Restore web site(s) from a site archive file.
  287. */
  288. function drush_archive_restore($file, $site_id = NULL) {
  289. $tmp = drush_tempdir();
  290. // Get the extra options for tar, if any
  291. $tar_extra_options = drush_sitealias_evaluate_paths_in_options(drush_get_option('tar-options', ''));
  292. if (!$files = drush_tarball_extract($file, $tmp, FALSE, $tar_extra_options)) {
  293. return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_EXTRACT', dt('Unable to extract site archive tarball to !tmp.', array('!tmp' => $tmp)));
  294. }
  295. $manifest = $tmp . '/MANIFEST.ini';
  296. if (file_exists($manifest)) {
  297. if (!$ini = parse_ini_file($manifest, TRUE)) {
  298. return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_PARSE_MANIFEST', dt('Unable to parse MANIFEST.ini in the archive.'));
  299. }
  300. }
  301. else {
  302. $ini = drush_archive_guess_manifest($tmp);
  303. }
  304. // Backward compatibility: 'archiveformat' did not exist
  305. // in older versions of archive-dump.
  306. if (!isset( $ini['Global']['archiveformat'])) {
  307. $ini['Global']['archiveformat'] = 'platform';
  308. }
  309. // Grab the first site in the Manifest and move docroot to destination.
  310. $ini_tmp = $ini;
  311. unset($ini_tmp['Global']);
  312. $first = array_shift($ini_tmp);
  313. $docroot = basename($first['docroot']);
  314. $destination = drush_get_option('destination', realpath('.') . "/$docroot");
  315. if ($ini['Global']['archiveformat'] == 'platform') {
  316. // Move the whole platform in-place at once.
  317. if (!drush_move_dir("$tmp/$docroot", $destination, drush_get_option('overwrite'))) {
  318. return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_RESTORE_FILES', dt('Unable to restore platform to !dest', array('!dest' => $destination)));
  319. }
  320. }
  321. else {
  322. // When no platform is included we do this on a per-site basis.
  323. }
  324. // Loop over sites and restore databases and append to settings.php.
  325. foreach ($ini as $section => $site) {
  326. if ($section != 'Global' && (!isset($site_id) || $section == $site_id) && !empty($site['database-default-file'])) {
  327. $site_destination = $destination . '/' . $site['sitedir'];
  328. // Restore site, in case not already done above.
  329. if ($ini['Global']['archiveformat'] == 'site') {
  330. if (!drush_move_dir("$tmp/$docroot/" . $site['sitedir'], $site_destination, drush_get_option('overwrite'))) {
  331. return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_RESTORE_FILES', dt('Unable to restore site to !dest', array('!dest' => $site_destination)));
  332. }
  333. }
  334. // Restore database.
  335. $sql_file = $tmp . '/' . $site['database-default-file'];
  336. if ($db_url = drush_get_option('db-url')) {
  337. if (empty($site_id) && count($ini) >= 3) {
  338. // TODO: Use drushrc to provide multiple db-urls for multi-restore?
  339. return drush_set_error('DRUSH_ARCHIVE_UNABLE_MULTI_RESTORE', dt('You must specify a site to restore when the archive contains multiple sites and a db-url is provided.'));
  340. }
  341. $db_spec = drush_convert_db_from_db_url($db_url);
  342. }
  343. else {
  344. $site_specification = $destination . '#' . $section;
  345. if ($return = drush_invoke_process($site_specification, 'sql-conf', array(), array('all' => TRUE), array('integrate' => FALSE, 'override-simulated' => TRUE))) {
  346. $databases = $return['object'];
  347. $db_spec = $databases['default']['default'];
  348. }
  349. else {
  350. return drush_set_error('DRUSH_ARCHIVE_UNABLE_DISCOVER_DB', dt('Unable to get database details from db-url option or settings.php', array('!key' => $key)));
  351. }
  352. }
  353. $sql = drush_sql_get_class($db_spec);
  354. $sql->drop_or_create();
  355. $sql->query(NULL, $sql_file);
  356. // Append new DB info to settings.php.
  357. if ($db_url) {
  358. $settingsfile = $destination . '/' . $site['sitedir'] . '/settings.php';
  359. //If settings.php doesn't exist in the archive, create it from default.settings.php.
  360. if (!file_exists($settingsfile)) {
  361. drush_op('copy', $destination . '/sites/default/default.settings.php', $settingsfile);
  362. }
  363. // Need to do something here or else we can't write.
  364. chmod($settingsfile, 0664);
  365. file_put_contents($settingsfile, "\n// Appended by drush archive-restore command.\n", FILE_APPEND);
  366. if (drush_drupal_major_version($destination) >= 7) {
  367. file_put_contents($settingsfile, "\n" . '$databases = ' . var_export(drush_sitealias_convert_db_from_db_url($db_url), TRUE) . ";\n", FILE_APPEND);
  368. }
  369. else {
  370. file_put_contents($settingsfile, "\n" . '$db_url = \'' . $db_url . "';\n", FILE_APPEND);
  371. }
  372. drush_log(dt('Drush appended the new database configuration at settings.php. Optionally remove the old configuration manually.'), LogLevel::OK);
  373. }
  374. }
  375. }
  376. drush_log(dt('Archive restored to !dest', array('!dest' => $destination)), LogLevel::OK);
  377. return $destination;
  378. }
  379. /**
  380. * Command argument complete callback.
  381. *
  382. * @return
  383. * Strong glob of files to complete on.
  384. */
  385. function archive_archive_restore_complete() {
  386. return array(
  387. 'files' => array(
  388. 'directories' => array(
  389. 'pattern' => '*',
  390. 'flags' => GLOB_ONLYDIR,
  391. ),
  392. 'tar' => array(
  393. 'pattern' => '*.tar.gz',
  394. ),
  395. ),
  396. );
  397. }
  398. /**
  399. * Try to find docroot and DB dump file in an extracted archive.
  400. *
  401. * @param string $path The location of the extracted archive.
  402. * @return array The manifest data.
  403. */
  404. function drush_archive_guess_manifest($path) {
  405. $db_file = drush_scan_directory($path, '/\.sql$/', array('.', '..', 'CVS'), 0, 0);
  406. if (file_exists($path . '/index.php')) {
  407. $docroot = './';
  408. }
  409. else {
  410. $directories = glob($path . '/*' , GLOB_ONLYDIR);
  411. $docroot = reset($directories);
  412. }
  413. $ini = array(
  414. 'Global' => array(
  415. // Very crude detection of a platform...
  416. 'archiveformat' => (drush_drupal_version($docroot) ? 'platform' : 'site'),
  417. ),
  418. 'default' => array(
  419. 'docroot' => $docroot,
  420. 'sitedir' => 'sites/default',
  421. 'database-default-file' => key($db_file),
  422. ),
  423. );
  424. return $ini;
  425. }