Mercurial > defr > drupal > filearchiver
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 } |