comparison filearchiver.module @ 0:ddfec30e1789 tip

Import public
author Franck Deroche <webmaster@defr.org>
date Mon, 01 Sep 2008 19:27:32 +0200
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:ddfec30e1789
1 <?php
2 // $Id$
3
4 /**
5 * @file
6 * File Archiver module
7 *
8 * This module provides a mechanism to move files associated to nodes to
9 * other directories, for example to archive them.
10 */
11 define('FILEARCHIVER_SOURCE_NODE', 1);
12 define('FILEARCHIVER_SOURCE_COMMENT', 2);
13
14 /**
15 * Implementation of hook_help().
16 */
17
18 function filearchiver_help($section) {
19 switch ($section) {
20 case 'admin/settings/filearchiver':
21 return t('This section allows you to choose the views defining which nodes,
22 if any, should see its files archived. In practice, the archiving is done
23 by moving the files of those nodes to a directory mentionning the date
24 and time of the last modification on this node. You are then able to
25 move away this folder if you want to, and then simply restore it should
26 you need to see the attached files afterwards.');
27 break;
28 }
29 }
30
31 /**
32 * Implementation of hook_menu().
33 * The only page we need to expose is the configuration page
34 * of the module.
35 */
36 function filearchiver_menu($may_cache) {
37 $items = array();
38 if ($may_cache) {
39 // The settings form
40 $items[] = array(
41 'path' => 'admin/settings/filearchiver',
42 'title' => t('File archiver'),
43 'access' => user_access('administer filearchiver'),
44 'callback' => 'drupal_get_form',
45 'callback arguments' => array('filearchiver_settings_form')
46 );
47 // The "File has been archived" page
48 $items[] = array(
49 'path' => 'filearchiver/archived',
50 'title' => t('File archived'),
51 'access' => user_access('access content'),
52 'callback' => 'filearchiver_archived_page'
53 );
54 // The status page
55 $items[] = array(
56 'path' => 'filearchiver',
57 'title' => 'File Archiver',
58 'callback' => 'filearchiver_dry_run',
59 'access' => user_access('administer filearchiver')
60 );
61 // Overwrite system default file callback
62 // This is needed to be able to act on non existing files,
63 // even though it's really suboptimal.
64 $items[] = array(
65 'path' => 'system/files',
66 'title' => t('File download'),
67 'callback' => 'filearchiver_download',
68 'access' => TRUE,
69 'type' => MENU_CALLBACK
70 );
71 }
72 return $items;
73 }
74
75 /**
76 * Implementation of hook_perm().
77 */
78 function filearchiver_perm() {
79 return array('administer filearchiver', 'restore archives');
80 }
81
82
83 /**
84 * This function filter the call to non-exising archived files.
85 * For existing files, it's a downright copy & paste of file_download
86 * in file.inc, which should be faster than calling it.
87 */
88 function filearchiver_download() {
89 // Merge remainder of arguments from GET['q'], into relative file path.
90 $args = func_get_args();
91 $filepath = implode('/', $args);
92
93 // Maintain compatibility with old ?file=paths saved in node bodies.
94 if (isset($_GET['file'])) {
95 $filepath = $_GET['file'];
96 }
97
98 if (file_exists(file_create_path($filepath))) {
99 $headers = module_invoke_all('file_download', $filepath);
100 if (in_array(-1, $headers)) {
101 return drupal_access_denied();
102 }
103 if (count($headers)) {
104 file_transfer($filepath, $headers);
105 }
106 }
107 else {
108 // Check if the file has been archived
109 $count = db_result(db_query("
110 SELECT COUNT(fid)
111 FROM {filearchiver_files}
112 WHERE archive='%s'", file_directory_path() .'/'. $filepath));
113 if ($count) {
114 return filearchiver_archived_page();
115 }
116 }
117 return drupal_not_found();
118 }
119 /**
120 * The "File has been archived page".
121 * This is the landing page in case of a file archived
122 * and no longer in the filesystem.
123 */
124 function filearchiver_archived_page() {
125 drupal_set_title('File archived');
126 return "<p>Sorry, this file has been archived and can't be seen
127 online at the moment. If you need to access it, please contact the
128 support who will be able to put it back online.</p>";
129 }
130
131 /**
132 * Dry-run callback implementation.
133 * This should be quite usefull to test that everything's alright.
134 */
135 function filearchiver_dry_run() {
136 $rows = _filearchiver_archive(TRUE);
137 $output = theme('table', array('nid', 'filename', 'source', 'status', 'target'), $rows);
138 $output .= drupal_get_form('filearchiver_archive_form');
139 return $output;
140 }
141
142 /**
143 * Form located on the dry run page and allowing users
144 * to start the archiving process.
145 */
146 function filearchiver_archive_form() {
147 $form = array();
148 $form[] = array(
149 '#type' => 'submit',
150 '#title' => t('Archive now'),
151 '#value' => t('Archive now')
152 );
153 return $form;
154 }
155
156 /**
157 * Callback of the "Archive now" form that actually starts the process.
158 */
159 function filearchiver_archive_form_submit() {
160 _filearchiver_archive(FALSE);
161 }
162
163 /**
164 * Implementation of hook_cron().
165 */
166 function filearchiver_cron() {
167 if (variable_get('filearchiver_run_on_cron', FALSE)) {
168 _filearchiver_archive(FALSE);
169 }
170 }
171
172 /**
173 * Do the real archiving work.
174 */
175 function _filearchiver_archive($dry_run = TRUE) {
176 $rows = array();
177 // First of all, get the list of our sources
178 $sources = variable_get('filearchiver_sources', array());
179 foreach ($sources as $vid => $active) {
180 if ($active) {
181 /* If the view is checked as a provider for the file archiver,
182 then we're going to ask view to get all the nodes that match,
183 in a convenient items array. */
184 $view = views_get_view($vid);
185 $result = views_build_view('items', $view);
186 foreach ($result['items'] as $item) {
187 // Then for each of those nodes, we'll load it
188 $node = node_load($item->nid);
189 // And deal with the files directly attached to the node
190 foreach ($node->files as $file) {
191 if ($dry_run) {
192 $rows[] = _filearchiver_get_infos($node, $file, FILEARCHIVER_SOURCE_NODE);
193 }
194 else {
195 _filearchiver_archive_node_file($node, $file);
196 }
197 }
198 /* Then we need to take care of all the files that might be
199 attached to the comments if the Comment Upload module is
200 active */
201 if (db_table_exists('{comment_upload_files}') &&
202 function_exists('_comment_upload_load_files')) {
203 $result = db_query("SELECT DISTINCT(cid) FROM {comment_upload_files} WHERE nid=%d",
204 $node->nid);
205 while ($row = db_fetch_object($result)) {
206 $files = _comment_upload_load_files($row->cid, $item->nid);
207 foreach ($files as $file) {
208 if ($dry_run) {
209 $rows[] = _filearchiver_get_infos($node, $file, FILEARCHIVER_SOURCE_COMMENT);
210 }
211 else {
212 _filearchiver_archive_comment_file($node, $row->cid, $file);
213 }
214 }
215 }
216 }
217 }
218 }
219 }
220 return $rows;
221 }
222
223 /**
224 * Callback for the settings form.
225 */
226 function filearchiver_settings_form() {
227 $form = array();
228 // Choose if the archiving should be done on cron runs
229 $form['filearchiver_run_on_cron'] = array(
230 '#type' => 'checkbox',
231 '#title' => t('Run on cron'),
232 '#description' => t('If checked, files will be automatically
233 archived on cron when any of the views selected
234 below match the node to which it is attached'),
235 '#default_value' => variable_get('filearchiver_run_on_cron', FALSE)
236 );
237 // Build the list of all available views
238 $form['filearchiver_sources'] = array(
239 '#type' => 'checkboxes',
240 '#title' => t('Sources'),
241 '#description' => t('Please select the views corresponding to the nodes
242 whose files you want to archive'),
243 '#default_value' => variable_get('filearchiver_sources', array())
244 );
245 $views = array();
246 $result = db_query("SELECT vid, name, description FROM {view_view}");
247 while ($view = db_fetch_object($result)) {
248 $views[$view->vid] = theme('filearchiver_show_view', $view);
249 }
250 $form['filearchiver_sources']['#options'] = $views;
251 // Let the user choose where the files will be archived
252 $form['filearchiver_path'] = array(
253 '#title' => t('Pattern for the archive path'),
254 '#type' => 'textfield',
255 '#description' => t('Specify the pattern to prefix to file names that will be
256 archived. It will be appended after the site files directory (!directory) but
257 before the file name itself. Do not include a leading or trailing slash.
258 Spaces will be converted to underscores to avoid file system issues.',
259 array('!directory' => file_directory_path())),
260 '#default_value' => variable_get('filearchiver_path', 'archives/[yyyy]/[mm]')
261 );
262 // Build the token help
263 $form['token_help'] = array(
264 '#title' => t('Replacement patterns'),
265 '#type' => 'fieldset',
266 '#collapsible' => TRUE,
267 '#collapsed' => TRUE,
268 '#description' => t('Prefer raw-text replacements for text to avoid problems with HTML entities!'),
269 );
270 $form['token_help']['help'] = array(
271 '#value' => theme('token_help', 'node'),
272 );
273
274 return system_settings_form($form);
275 }
276
277 /**
278 * Callback for the settings form submission.
279 */
280 function filearchiver_settings_form_submit($form_id, &$form_values) {
281 variable_set('filearchiver_sources', $form_values['filearchiver_sources']);
282 variable_set('filearchiver_path', $form_values['filearchiver_path']);
283 }
284
285 /**
286 * Theme the view name and it's description for display in the view list.
287 */
288 function theme_filearchiver_show_view($view) {
289 return "<strong>{$view->name}</strong>, {$view->description}";
290 }
291
292 /**
293 * Check if the file has already been archived.
294 */
295 function _filearchiver_file_is_archived($fid, $source = FILEARCHIVER_SOURCE_NODE) {
296 return db_result(db_query("
297 SELECT COUNT(fid)
298 FROM {filearchiver_files}
299 WHERE
300 fid=%d AND source=%d", $fid, $source));
301 }
302
303 /**
304 * Get the archive name from the filename
305 */
306 function _filearchiver_get_filepath($node, $filepath) {
307 $basename = basename($filepath);
308 //$dirname = dirname($filepath);
309 $timestamp = max($node->last_comment_timestamp, $node->revision_date);
310 // First get the template from the configuration screen
311 $tpl = variable_get('filearchiver_path', 'archives/[yyyy]/[mm]');
312 // Then replace the tokens in it
313 $archive = '/'. token_replace($tpl, 'node', $node);
314 // Finally get rid of the whitespaces that may not work all that well on filenames
315 $archive = str_replace(array(' ', "\n", "\t"), '_', $archive);
316 $path = _filearchiver_ensure_path_exists($archive);
317 return $path .'/'. $basename;
318 }
319
320 /**
321 * Ensure that a specific path actually exists
322 * @param A path relative to the file_directory_path.
323 * This path will be sanitized.
324 * @return Full path for the directory
325 */
326 function _filearchiver_ensure_path_exists($archive) {
327 $dirs = explode('/', $archive);
328 $directory = file_directory_path();
329 while (count($dirs)) {
330 $current = array_shift($dirs);
331 /* Ban '.' and '..': those values might be contained in the input
332 * provided by the user and transfered in the archive path by the
333 * token module
334 */
335 if (!in_array($current, array('.', '..'))) {
336 $directory .= '/'. $current;
337 file_check_directory($directory, FILE_CREATE_DIRECTORY);
338 }
339 }
340 return $directory;
341 }
342
343 /**
344 * Archive a file attached directly to the node.
345 */
346 function _filearchiver_archive_node_file($node, $file) {
347 $archived = _filearchiver_file_is_archived($file->fid, FILEARCHIVER_SOURCE_NODE);
348 if (!$archived) {
349 // If this file isn't archived yet, we'll deal with it
350 $target = _filearchiver_get_filepath($node, $file->filepath);
351 // Move the file over there
352 file_move($file, $target, FILE_EXISTS_RENAME);
353 // Update the table
354 db_query("
355 UPDATE {files}
356 SET filepath='%s'
357 WHERE nid=%d AND fid=%d",
358 $file->filepath,
359 $node->nid,
360 $file->fid);
361 // Insert into our table to track that we've archived this file
362 db_query("INSERT INTO {filearchiver_files}(fid, source, archive) VALUES(%d, %d, '%s')",
363 $file->fid,
364 FILEARCHIVER_SOURCE_NODE,
365 $target);
366 }
367 }
368
369 /**
370 * Archive a file attached to a comment of the node.
371 * This happen if the module 'Comment Upload' is in
372 * use.
373 */
374 function _filearchiver_archive_comment_file($node, $cid, $file) {
375 $archived = _filearchiver_file_is_archived($file->fid, FILEARCHIVER_SOURCE_COMMENT);
376 if (!$archived) {
377 // Get the destination path
378 $target = _filearchiver_get_filepath($node, $file->filepath);
379 // Move the file over there
380 file_move($file, $target, FILE_EXISTS_RENAME);
381 // Update the table
382 db_query("
383 UPDATE {comment_upload_files}
384 SET filepath='%s'
385 WHERE nid=%d AND cid=%d AND fid=%d",
386 $file->filepath,
387 $node->nid,
388 $cid,
389 $file->fid);
390 // Insert into our table to track that we've archived this file
391 db_query("INSERT INTO {filearchiver_files}(fid, source, archive) VALUES(%d, %d, '%s')",
392 $file->fid,
393 FILEARCHIVER_SOURCE_COMMENT,
394 $target);
395 }
396 }
397
398 /**
399 * Extract the information that will be presented about the file
400 * on a dry-run.
401 */
402 function _filearchiver_get_infos($node, $file, $source) {
403 $archived = _filearchiver_file_is_archived($file->fid, $source);
404 $from = ($source == FILEARCHIVER_SOURCE_NODE) ? t('node') : t('comment');
405 $status = ($archived) ? t('archived') : t('live');
406 $target = _filearchiver_get_filepath($node, $file->filepath);
407 return array(l($node->nid, 'node/'. $node->nid), $file->filename, $from, $status, $target);
408 }