view filearchiver.module @ 0:ddfec30e1789 tip

Import public
author Franck Deroche <webmaster@defr.org>
date Mon, 01 Sep 2008 19:27:32 +0200
parents
children
line wrap: on
line source
<?php
// $Id$

/**
 * @file
 * File Archiver module
 *
 * This module provides a mechanism to move files associated to nodes to
 * other directories, for example to archive them.
 */
define('FILEARCHIVER_SOURCE_NODE', 1);
define('FILEARCHIVER_SOURCE_COMMENT', 2);

/**
 * Implementation of hook_help().
 */

function filearchiver_help($section) {
  switch ($section) {
    case 'admin/settings/filearchiver':
      return t('This section allows you to choose the views defining which nodes,
      if any, should see its files archived. In practice, the archiving is done
      by moving the files of those nodes to a directory mentionning the date
      and time of the last modification on this node. You are then able to
      move away this folder if you want to, and then simply restore it should
      you need to see the attached files afterwards.');
    break;
  }
}

/**
 * Implementation of hook_menu().
 * The only page we need to expose is the configuration page
 * of the module.
 */
function filearchiver_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    // The settings form
    $items[] = array(
      'path'               => '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 "<p>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.</p>";
}

/**
 * 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 "<strong>{$view->name}</strong>, {$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);
}