1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* Provides an action for creating a zip archive of selected files.
|
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
|
function views_bulk_operations_archive_action_info() {
|
11
|
$actions = array();
|
12
|
if (function_exists('zip_open')) {
|
13
|
$actions['views_bulk_operations_archive_action'] = array(
|
14
|
'type' => 'file',
|
15
|
'label' => t('Create an archive of selected files'),
|
16
|
// This action only works when invoked through VBO. That's why it's
|
17
|
// declared as non-configurable to prevent it from being shown in the
|
18
|
// "Create an advanced action" dropdown on admin/config/system/actions.
|
19
|
'configurable' => FALSE,
|
20
|
'vbo_configurable' => TRUE,
|
21
|
'triggers' => array('any'),
|
22
|
);
|
23
|
}
|
24
|
return $actions;
|
25
|
}
|
26
|
|
27
|
/**
|
28
|
* Since Drupal's Archiver doesn't abstract properly the archivers it implements
|
29
|
* (Archive_Tar and ZipArchive), it can't be used here.
|
30
|
*/
|
31
|
function views_bulk_operations_archive_action($file, $context) {
|
32
|
global $user;
|
33
|
static $archive_contents = array();
|
34
|
|
35
|
// Adding a non-existent file to the archive crashes ZipArchive on close().
|
36
|
if (file_exists($file->uri)) {
|
37
|
$destination = $context['destination'];
|
38
|
$zip = new ZipArchive();
|
39
|
// If the archive already exists, open it. If not, create it.
|
40
|
if (file_exists($destination)) {
|
41
|
$opened = $zip->open(drupal_realpath($destination));
|
42
|
}
|
43
|
else {
|
44
|
$opened = $zip->open(drupal_realpath($destination), ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
|
45
|
}
|
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
|
|
64
|
// The operation is complete, create a file entity and provide a download
|
65
|
// link to the user.
|
66
|
if ($context['progress']['current'] == $context['progress']['total']) {
|
67
|
$archive_file = new stdClass();
|
68
|
$archive_file->uri = $destination;
|
69
|
$archive_file->filename = basename($destination);
|
70
|
$archive_file->filemime = file_get_mimetype($destination);
|
71
|
$archive_file->uid = $user->uid;
|
72
|
$archive_file->status = $context['settings']['temporary'] ? FALSE : FILE_STATUS_PERMANENT;
|
73
|
file_save($archive_file);
|
74
|
|
75
|
$url = file_create_url($archive_file->uri);
|
76
|
$url = l($url, $url, array('absolute' => TRUE));
|
77
|
_views_bulk_operations_log(t('An archive has been created and can be downloaded from: !url', array('!url' => $url)));
|
78
|
}
|
79
|
}
|
80
|
|
81
|
/**
|
82
|
* Configuration form shown to the user before the action gets executed.
|
83
|
*/
|
84
|
function views_bulk_operations_archive_action_form($context) {
|
85
|
// Pass the scheme as a value, so that the submit callback can access it.
|
86
|
$form['scheme'] = array(
|
87
|
'#type' => 'value',
|
88
|
'#value' => $context['settings']['scheme'],
|
89
|
);
|
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
|
/**
|
102
|
* Assembles a sanitized and unique URI for the archive, and returns it for
|
103
|
* usage by the action callback (views_bulk_operations_archive_action).
|
104
|
*/
|
105
|
function views_bulk_operations_archive_action_submit($form, $form_state) {
|
106
|
// Validate the scheme, fallback to public if it's somehow invalid.
|
107
|
$scheme = $form_state['values']['scheme'];
|
108
|
if (!file_stream_wrapper_valid_scheme($scheme)) {
|
109
|
$scheme = 'public';
|
110
|
}
|
111
|
$destination = $scheme . '://' . basename($form_state['values']['filename']) . '.zip';
|
112
|
// If the chosen filename already exists, file_destination() will append
|
113
|
// an integer to it in order to make it unique.
|
114
|
$destination = file_destination($destination, FILE_EXISTS_RENAME);
|
115
|
|
116
|
return array(
|
117
|
'destination' => $destination,
|
118
|
);
|
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
|
|
146
|
$form['temporary'] = array(
|
147
|
'#type' => 'checkbox',
|
148
|
'#title' => t('Temporary'),
|
149
|
'#default_value' => isset($options['temporary']) ? $options['temporary'] : TRUE,
|
150
|
'#description' => t('Temporary files older than 6 hours are removed when cron runs.'),
|
151
|
);
|
152
|
return $form;
|
153
|
}
|
154
|
|
155
|
/**
|
156
|
* Create a sanitized and unique version of the provided filename.
|
157
|
*
|
158
|
* @param $filename
|
159
|
* String filename
|
160
|
*
|
161
|
* @return
|
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
|
|
173
|
if (in_array($filename, $archive_list)) {
|
174
|
// Destination file already exists, generate an alternative.
|
175
|
$pos = strrpos($filename, '.');
|
176
|
if ($pos !== FALSE) {
|
177
|
$name = substr($filename, 0, $pos);
|
178
|
$ext = substr($filename, $pos);
|
179
|
}
|
180
|
else {
|
181
|
$name = $filename;
|
182
|
$ext = '';
|
183
|
}
|
184
|
|
185
|
$counter = 0;
|
186
|
do {
|
187
|
$filename = $name . '_' . $counter++ . $ext;
|
188
|
} while (in_array($filename, $archive_list));
|
189
|
}
|
190
|
|
191
|
return $filename;
|
192
|
}
|