archive.action.inc 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php
  2. /**
  3. * @file
  4. * Provides an action for creating a zip archive of selected files.
  5. *
  6. * An entry in the {file_managed} table is created for the newly created archive,
  7. * and it is marked as permanent or temporary based on the operation settings.
  8. */
  9. /**
  10. * Implements hook_action_info().
  11. */
  12. function views_bulk_operations_archive_action_info() {
  13. $actions = array();
  14. if (function_exists('zip_open')) {
  15. $actions['views_bulk_operations_archive_action'] = array(
  16. 'type' => 'file',
  17. 'label' => t('Create an archive of selected files'),
  18. // This action only works when invoked through VBO. That's why it's
  19. // declared as non-configurable to prevent it from being shown in the
  20. // "Create an advanced action" dropdown on admin/config/system/actions.
  21. 'configurable' => FALSE,
  22. 'vbo_configurable' => TRUE,
  23. 'behavior' => array('views_property'),
  24. 'triggers' => array('any'),
  25. );
  26. }
  27. return $actions;
  28. }
  29. /**
  30. * Since Drupal's Archiver doesn't abstract properly the archivers it implements
  31. * (Archive_Tar and ZipArchive), it can't be used here.
  32. */
  33. function views_bulk_operations_archive_action($file, $context) {
  34. global $user;
  35. static $archive_contents = array();
  36. // Adding a non-existent file to the archive crashes ZipArchive on close().
  37. if (file_exists($file->uri)) {
  38. $destination = $context['destination'];
  39. $zip = new ZipArchive();
  40. // If the archive already exists, open it. If not, create it.
  41. if (file_exists($destination)) {
  42. $opened = $zip->open(drupal_realpath($destination));
  43. }
  44. else {
  45. $opened = $zip->open(drupal_realpath($destination), ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
  46. }
  47. if ($opened) {
  48. // Create a list of all files in the archive. Used for duplicate checking.
  49. if (empty($archive_contents)) {
  50. for ($i = 0; $i < $zip->numFiles; $i++) {
  51. $archive_contents[] = $zip->getNameIndex($i);
  52. }
  53. }
  54. // Make sure that the target filename is unique.
  55. $filename = _views_bulk_operations_archive_action_create_filename(basename($file->uri), $archive_contents);
  56. // Note that the actual addition happens on close(), hence the need
  57. // to open / close the archive each time the action runs.
  58. $zip->addFile(drupal_realpath($file->uri), $filename);
  59. $zip->close();
  60. $archive_contents[] = $filename;
  61. }
  62. }
  63. // The operation is complete, create a file entity and provide a download
  64. // link to the user.
  65. if ($context['progress']['current'] == $context['progress']['total']) {
  66. $archive_file = new stdClass();
  67. $archive_file->uri = $destination;
  68. $archive_file->filename = basename($destination);
  69. $archive_file->filemime = file_get_mimetype($destination);
  70. $archive_file->uid = $user->uid;
  71. $archive_file->status = $context['settings']['temporary'] ? FALSE : FILE_STATUS_PERMANENT;
  72. // Clear filesize() cache to avoid private file system differences in
  73. // filesize.
  74. // @see https://www.drupal.org/node/2743999
  75. clearstatcache();
  76. file_save($archive_file);
  77. $url = file_create_url($archive_file->uri);
  78. $url = l($url, $url, array('absolute' => TRUE));
  79. _views_bulk_operations_log(t('An archive has been created and can be downloaded from: !url', array('!url' => $url)));
  80. }
  81. }
  82. /**
  83. * Configuration form shown to the user before the action gets executed.
  84. */
  85. function views_bulk_operations_archive_action_form($context) {
  86. // Pass the scheme as a value, so that the submit callback can access it.
  87. $form['scheme'] = array(
  88. '#type' => 'value',
  89. '#value' => $context['settings']['scheme'],
  90. );
  91. $form['filename'] = array(
  92. '#type' => 'textfield',
  93. '#title' => t('Filename'),
  94. '#default_value' => 'vbo_archive_' . date('Ymd'),
  95. '#field_suffix' => '.zip',
  96. '#description' => t('The name of the archive file.'),
  97. );
  98. return $form;
  99. }
  100. /**
  101. * Assembles a sanitized and unique URI for the archive.
  102. *
  103. * @returns array
  104. * A URI array used by the action callback
  105. * (views_bulk_operations_archive_action).
  106. */
  107. function views_bulk_operations_archive_action_submit($form, $form_state) {
  108. // Validate the scheme, fallback to public if it's somehow invalid.
  109. $scheme = $form_state['values']['scheme'];
  110. if (!file_stream_wrapper_valid_scheme($scheme)) {
  111. $scheme = 'public';
  112. }
  113. $destination = $scheme . '://' . basename($form_state['values']['filename']) . '.zip';
  114. // If the chosen filename already exists, file_destination() will append
  115. // an integer to it in order to make it unique.
  116. $destination = file_destination($destination, FILE_EXISTS_RENAME);
  117. return array(
  118. 'destination' => $destination,
  119. );
  120. }
  121. /**
  122. * Settings form (embedded into the VBO field settings in the Views UI).
  123. */
  124. function views_bulk_operations_archive_action_views_bulk_operations_form($options) {
  125. $scheme_options = array();
  126. foreach (file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL_NORMAL) as $scheme => $stream_wrapper) {
  127. $scheme_options[$scheme] = $stream_wrapper['name'];
  128. }
  129. if (count($scheme_options) > 1) {
  130. $form['scheme'] = array(
  131. '#type' => 'radios',
  132. '#title' => t('Storage'),
  133. '#options' => $scheme_options,
  134. '#default_value' => !empty($options['scheme']) ? $options['scheme'] : variable_get('file_default_scheme', 'public'),
  135. '#description' => t('Select where the archive should be stored. Private file storage has significantly more overhead than public files, but allows restricted access.'),
  136. );
  137. }
  138. else {
  139. $scheme_option_keys = array_keys($scheme_options);
  140. $form['scheme'] = array(
  141. '#type' => 'value',
  142. '#value' => reset($scheme_option_keys),
  143. );
  144. }
  145. $form['temporary'] = array(
  146. '#type' => 'checkbox',
  147. '#title' => t('Temporary'),
  148. '#default_value' => isset($options['temporary']) ? $options['temporary'] : TRUE,
  149. '#description' => t('Temporary files older than 6 hours are removed when cron runs.'),
  150. );
  151. return $form;
  152. }
  153. /**
  154. * Create a sanitized and unique version of the provided filename.
  155. *
  156. * @param string $filename
  157. * The filename to create.
  158. * @param array $archive_list
  159. * The list of files already in the archive.
  160. *
  161. * @return string
  162. * The new filename.
  163. */
  164. function _views_bulk_operations_archive_action_create_filename($filename, $archive_list) {
  165. // Strip control characters (ASCII value < 32). Though these are allowed in
  166. // some filesystems, not many applications handle them well.
  167. $filename = preg_replace('/[\x00-\x1F]/u', '_', $filename);
  168. if (substr(PHP_OS, 0, 3) == 'WIN') {
  169. // These characters are not allowed in Windows filenames.
  170. $filename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $filename);
  171. }
  172. if (in_array($filename, $archive_list)) {
  173. // Destination file already exists, generate an alternative.
  174. $pos = strrpos($filename, '.');
  175. if ($pos !== FALSE) {
  176. $name = substr($filename, 0, $pos);
  177. $ext = substr($filename, $pos);
  178. }
  179. else {
  180. $name = $filename;
  181. $ext = '';
  182. }
  183. $counter = 0;
  184. do {
  185. $filename = $name . '_' . $counter++ . $ext;
  186. } while (in_array($filename, $archive_list));
  187. }
  188. return $filename;
  189. }