# HG changeset patch # User Franck Deroche # Date 1220290052 -7200 # Node ID ddfec30e1789df3109a1fd9da4c4c888dd742f7a Import public diff -r 000000000000 -r ddfec30e1789 filearchiver.info --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/filearchiver.info Mon Sep 01 19:27:32 2008 +0200 @@ -0,0 +1,6 @@ +; $Id: ows.info,v 1.1 2007/08/02 10:16:21 julien Exp $ +name = File Archiver +package = "OWS" +description = "Archive file associated to nodes" +version = "5.x-dev" +dependencies = views token diff -r 000000000000 -r ddfec30e1789 filearchiver.install --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/filearchiver.install Mon Sep 01 19:27:32 2008 +0200 @@ -0,0 +1,35 @@ + 'admin/settings/filearchiver', + 'title' => t('File archiver'), + 'access' => user_access('administer filearchiver'), + 'callback' => 'drupal_get_form', + 'callback arguments' => array('filearchiver_settings_form') + ); + // The "File has been archived" page + $items[] = array( + 'path' => 'filearchiver/archived', + 'title' => t('File archived'), + 'access' => user_access('access content'), + 'callback' => 'filearchiver_archived_page' + ); + // The status page + $items[] = array( + 'path' => 'filearchiver', + 'title' => 'File Archiver', + 'callback' => 'filearchiver_dry_run', + 'access' => user_access('administer filearchiver') + ); + // Overwrite system default file callback + // This is needed to be able to act on non existing files, + // even though it's really suboptimal. + $items[] = array( + 'path' => 'system/files', + 'title' => t('File download'), + 'callback' => 'filearchiver_download', + 'access' => TRUE, + 'type' => MENU_CALLBACK + ); + } + return $items; +} + +/** + * Implementation of hook_perm(). + */ +function filearchiver_perm() { + return array('administer filearchiver', 'restore archives'); +} + + +/** + * This function filter the call to non-exising archived files. + * For existing files, it's a downright copy & paste of file_download + * in file.inc, which should be faster than calling it. + */ +function filearchiver_download() { + // Merge remainder of arguments from GET['q'], into relative file path. + $args = func_get_args(); + $filepath = implode('/', $args); + + // Maintain compatibility with old ?file=paths saved in node bodies. + if (isset($_GET['file'])) { + $filepath = $_GET['file']; + } + + if (file_exists(file_create_path($filepath))) { + $headers = module_invoke_all('file_download', $filepath); + if (in_array(-1, $headers)) { + return drupal_access_denied(); + } + if (count($headers)) { + file_transfer($filepath, $headers); + } + } + else { + // Check if the file has been archived + $count = db_result(db_query(" + SELECT COUNT(fid) + FROM {filearchiver_files} + WHERE archive='%s'", file_directory_path() .'/'. $filepath)); + if ($count) { + return filearchiver_archived_page(); + } + } + return drupal_not_found(); +} +/** + * The "File has been archived page". + * This is the landing page in case of a file archived + * and no longer in the filesystem. + */ +function filearchiver_archived_page() { + drupal_set_title('File archived'); + return "

Sorry, this file has been archived and can't be seen + online at the moment. If you need to access it, please contact the + support who will be able to put it back online.

"; +} + +/** + * Dry-run callback implementation. + * This should be quite usefull to test that everything's alright. + */ +function filearchiver_dry_run() { + $rows = _filearchiver_archive(TRUE); + $output = theme('table', array('nid', 'filename', 'source', 'status', 'target'), $rows); + $output .= drupal_get_form('filearchiver_archive_form'); + return $output; +} + +/** + * Form located on the dry run page and allowing users + * to start the archiving process. + */ +function filearchiver_archive_form() { + $form = array(); + $form[] = array( + '#type' => 'submit', + '#title' => t('Archive now'), + '#value' => t('Archive now') + ); + return $form; +} + +/** + * Callback of the "Archive now" form that actually starts the process. + */ +function filearchiver_archive_form_submit() { + _filearchiver_archive(FALSE); +} + +/** + * Implementation of hook_cron(). + */ +function filearchiver_cron() { + if (variable_get('filearchiver_run_on_cron', FALSE)) { + _filearchiver_archive(FALSE); + } +} + +/** + * Do the real archiving work. + */ +function _filearchiver_archive($dry_run = TRUE) { + $rows = array(); + // First of all, get the list of our sources + $sources = variable_get('filearchiver_sources', array()); + foreach ($sources as $vid => $active) { + if ($active) { + /* If the view is checked as a provider for the file archiver, + then we're going to ask view to get all the nodes that match, + in a convenient items array. */ + $view = views_get_view($vid); + $result = views_build_view('items', $view); + foreach ($result['items'] as $item) { + // Then for each of those nodes, we'll load it + $node = node_load($item->nid); + // And deal with the files directly attached to the node + foreach ($node->files as $file) { + if ($dry_run) { + $rows[] = _filearchiver_get_infos($node, $file, FILEARCHIVER_SOURCE_NODE); + } + else { + _filearchiver_archive_node_file($node, $file); + } + } + /* Then we need to take care of all the files that might be + attached to the comments if the Comment Upload module is + active */ + if (db_table_exists('{comment_upload_files}') && + function_exists('_comment_upload_load_files')) { + $result = db_query("SELECT DISTINCT(cid) FROM {comment_upload_files} WHERE nid=%d", + $node->nid); + while ($row = db_fetch_object($result)) { + $files = _comment_upload_load_files($row->cid, $item->nid); + foreach ($files as $file) { + if ($dry_run) { + $rows[] = _filearchiver_get_infos($node, $file, FILEARCHIVER_SOURCE_COMMENT); + } + else { + _filearchiver_archive_comment_file($node, $row->cid, $file); + } + } + } + } + } + } + } + return $rows; +} + +/** + * Callback for the settings form. + */ +function filearchiver_settings_form() { + $form = array(); + // Choose if the archiving should be done on cron runs + $form['filearchiver_run_on_cron'] = array( + '#type' => 'checkbox', + '#title' => t('Run on cron'), + '#description' => t('If checked, files will be automatically + archived on cron when any of the views selected + below match the node to which it is attached'), + '#default_value' => variable_get('filearchiver_run_on_cron', FALSE) + ); + // Build the list of all available views + $form['filearchiver_sources'] = array( + '#type' => 'checkboxes', + '#title' => t('Sources'), + '#description' => t('Please select the views corresponding to the nodes + whose files you want to archive'), + '#default_value' => variable_get('filearchiver_sources', array()) + ); + $views = array(); + $result = db_query("SELECT vid, name, description FROM {view_view}"); + while ($view = db_fetch_object($result)) { + $views[$view->vid] = theme('filearchiver_show_view', $view); + } + $form['filearchiver_sources']['#options'] = $views; + // Let the user choose where the files will be archived + $form['filearchiver_path'] = array( + '#title' => t('Pattern for the archive path'), + '#type' => 'textfield', + '#description' => t('Specify the pattern to prefix to file names that will be + archived. It will be appended after the site files directory (!directory) but + before the file name itself. Do not include a leading or trailing slash. + Spaces will be converted to underscores to avoid file system issues.', + array('!directory' => file_directory_path())), + '#default_value' => variable_get('filearchiver_path', 'archives/[yyyy]/[mm]') + ); + // Build the token help + $form['token_help'] = array( + '#title' => t('Replacement patterns'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Prefer raw-text replacements for text to avoid problems with HTML entities!'), + ); + $form['token_help']['help'] = array( + '#value' => theme('token_help', 'node'), + ); + + return system_settings_form($form); +} + +/** + * Callback for the settings form submission. + */ +function filearchiver_settings_form_submit($form_id, &$form_values) { + variable_set('filearchiver_sources', $form_values['filearchiver_sources']); + variable_set('filearchiver_path', $form_values['filearchiver_path']); +} + +/** + * Theme the view name and it's description for display in the view list. + */ +function theme_filearchiver_show_view($view) { + return "{$view->name}, {$view->description}"; +} + +/** + * Check if the file has already been archived. + */ +function _filearchiver_file_is_archived($fid, $source = FILEARCHIVER_SOURCE_NODE) { + return db_result(db_query(" + SELECT COUNT(fid) + FROM {filearchiver_files} + WHERE + fid=%d AND source=%d", $fid, $source)); +} + +/** + * Get the archive name from the filename + */ +function _filearchiver_get_filepath($node, $filepath) { + $basename = basename($filepath); + //$dirname = dirname($filepath); + $timestamp = max($node->last_comment_timestamp, $node->revision_date); + // First get the template from the configuration screen + $tpl = variable_get('filearchiver_path', 'archives/[yyyy]/[mm]'); + // Then replace the tokens in it + $archive = '/'. token_replace($tpl, 'node', $node); + // Finally get rid of the whitespaces that may not work all that well on filenames + $archive = str_replace(array(' ', "\n", "\t"), '_', $archive); + $path = _filearchiver_ensure_path_exists($archive); + return $path .'/'. $basename; +} + +/** + * Ensure that a specific path actually exists + * @param A path relative to the file_directory_path. + * This path will be sanitized. + * @return Full path for the directory + */ +function _filearchiver_ensure_path_exists($archive) { + $dirs = explode('/', $archive); + $directory = file_directory_path(); + while (count($dirs)) { + $current = array_shift($dirs); + /* Ban '.' and '..': those values might be contained in the input + * provided by the user and transfered in the archive path by the + * token module + */ + if (!in_array($current, array('.', '..'))) { + $directory .= '/'. $current; + file_check_directory($directory, FILE_CREATE_DIRECTORY); + } + } + return $directory; +} + +/** + * Archive a file attached directly to the node. + */ +function _filearchiver_archive_node_file($node, $file) { + $archived = _filearchiver_file_is_archived($file->fid, FILEARCHIVER_SOURCE_NODE); + if (!$archived) { + // If this file isn't archived yet, we'll deal with it + $target = _filearchiver_get_filepath($node, $file->filepath); + // Move the file over there + file_move($file, $target, FILE_EXISTS_RENAME); + // Update the table + db_query(" + UPDATE {files} + SET filepath='%s' + WHERE nid=%d AND fid=%d", + $file->filepath, + $node->nid, + $file->fid); + // Insert into our table to track that we've archived this file + db_query("INSERT INTO {filearchiver_files}(fid, source, archive) VALUES(%d, %d, '%s')", + $file->fid, + FILEARCHIVER_SOURCE_NODE, + $target); + } +} + +/** + * Archive a file attached to a comment of the node. + * This happen if the module 'Comment Upload' is in + * use. + */ +function _filearchiver_archive_comment_file($node, $cid, $file) { + $archived = _filearchiver_file_is_archived($file->fid, FILEARCHIVER_SOURCE_COMMENT); + if (!$archived) { + // Get the destination path + $target = _filearchiver_get_filepath($node, $file->filepath); + // Move the file over there + file_move($file, $target, FILE_EXISTS_RENAME); + // Update the table + db_query(" + UPDATE {comment_upload_files} + SET filepath='%s' + WHERE nid=%d AND cid=%d AND fid=%d", + $file->filepath, + $node->nid, + $cid, + $file->fid); + // Insert into our table to track that we've archived this file + db_query("INSERT INTO {filearchiver_files}(fid, source, archive) VALUES(%d, %d, '%s')", + $file->fid, + FILEARCHIVER_SOURCE_COMMENT, + $target); + } +} + +/** + * Extract the information that will be presented about the file + * on a dry-run. + */ +function _filearchiver_get_infos($node, $file, $source) { + $archived = _filearchiver_file_is_archived($file->fid, $source); + $from = ($source == FILEARCHIVER_SOURCE_NODE) ? t('node') : t('comment'); + $status = ($archived) ? t('archived') : t('live'); + $target = _filearchiver_get_filepath($node, $file->filepath); + return array(l($node->nid, 'node/'. $node->nid), $file->filename, $from, $status, $target); +}