? .DS_Store
? .cache
? .git
? .project
? .settings
? INSTALL.sqlite.txt
? junk
? logs
? remove-file_status_temp.patch
? sites/.DS_Store
? sites/all/modules
? sites/default/.DS_Store
? sites/default/files
? sites/default/settings.php
? sites/default/test
Index: includes/file.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/file.inc,v
retrieving revision 1.148
diff -u -p -r1.148 file.inc
--- includes/file.inc	31 Dec 2008 11:08:47 -0000	1.148
+++ includes/file.inc	2 Jan 2009 11:01:18 -0000
@@ -357,6 +357,92 @@ function file_save($file) {
 }
 
 /**
+ * Determine if and where a file is used.
+ *
+ * @param $file
+ *   A file object.
+ * @return
+ *   An array of with usage data.
+ *
+ * @see file_add_usage()
+ * @see file_remove_usage()
+ */
+function file_get_usage($file) {
+  $file = (object)$file;
+
+  $result = db_select('file_usage', 'f')->fields('f', array('module', 'object', 'oid', 'count'))
+    ->condition('f.fid', $file->fid)->condition('f.count', 0, '>')
+    ->execute();
+
+  $return = array();
+  foreach ($result as $usage) {
+    $return[$usage->module][$usage->object] = array('oid' => $usage-> oid, 'count' => $usage->count);
+  }
+
+  return $return;
+}
+
+/**
+ * Inform Drupal that a module is using a file.
+ *
+ * This usage information will be queried during file_delete().
+ *
+ * Examples:
+ * - The upload module that associates files with node revisions so the
+ *   $object would be 'node_revision' and $oid would be the node's vid.
+ * - The user module associates an image with a user so the $object would be
+ *   'user' and the $oid would be the user's id.
+ *
+ * @param $file
+ *   A file object.
+ * @param $module
+ *   The name of the module using the file.
+ * @param $object
+ *   The name of the table where the file is referenced.
+ * @param $oid
+ *   The id of the row in the table.
+ *
+ * @see file_get_usage()
+ * @see file_remove_usage()
+ */
+function file_add_usage($file, $module, $object, $oid) {
+  db_merge('file_usage')
+    ->key(array(
+      'fid' => $file->fid,
+      'module' => $module,
+      'object' => $object,
+      'oid' => $oid,
+    ))
+    ->fields(array('count' => 1))
+    ->expression('count', 'count + 1')
+    ->execute();
+}
+
+/**
+ * Inform Drupal that a module is no longer using a file.
+ *
+ * @param $file
+ *   A file object.
+ * @param $module
+ *   The name of the module using the file.
+ * @param $object
+ *   The name of the table where the file is referenced.
+ * @param $oid
+ *   The id of the row in the table.
+ *
+ * @see file_add_usage()
+ * @see file_get_usage()
+ */
+function file_remove_usage($file, $module, $object, $oid) {
+  db_update('file_usage')->expression('count', 'count - 1')
+    ->condition('fid', $file->fid)
+    ->condition('module', $module)
+    ->condition('object', $object)
+    ->condition('oid', $oid)
+    ->execute();
+}
+
+/**
  * Copy a file to a new location and adds a file record to the database.
  *
  * This function should be used when manipulating files that have records
@@ -699,22 +785,26 @@ function file_create_filename($basename,
  *   Boolean indicating that the file should be deleted even if
  *   hook_file_references() reports that the file is in use.
  * @return mixed
+# TODO UPDATE THIS:
  *   TRUE for success, FALSE in the event of an error, or an array if the file
  *   is being used by another module. The array keys are the module's name and
  *   the values are the number of references.
  *
  * @see file_unmanaged_delete()
- * @see hook_file_references()
+ * @see file_get_usage()
  * @see hook_file_delete()
  */
 function file_delete($file, $force = FALSE) {
   $file = (object)$file;
 
-  // If any module returns a value from the reference hook, the file will not
-  // be deleted from Drupal, but file_delete will return a populated array that
-  // tests as TRUE.
-  if (!$force && ($references = module_invoke_all('file_references', $file))) {
-    return $references;
+  // If file is still in use and $force is FALSE, the file will not be deleted
+  // from Drupal, but file_delete will return a populated array that tests as
+  // TRUE.
+  if (!$force) {
+    $references = file_get_usage($file);
+    if (count($references)) {
+      return $references;
+    }
   }
 
   // Let other modules clean up any references to the deleted file.
@@ -724,6 +814,7 @@ function file_delete($file, $force = FAL
   // database, so UIs can still find the file in the database.
   if (file_unmanaged_delete($file->filepath)) {
     db_delete('files')->condition('fid', $file->fid)->execute();
+    db_delete('file_usage')->condition('fid', $file->fid)->execute();
     return TRUE;
   }
   return FALSE;
Index: modules/simpletest/tests/file.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v
retrieving revision 1.16
diff -u -p -r1.16 file.test
--- modules/simpletest/tests/file.test	31 Dec 2008 11:08:47 -0000	1.16
+++ modules/simpletest/tests/file.test	2 Jan 2009 11:01:20 -0000
@@ -788,19 +788,36 @@ class FileDeleteTest extends FileHookTes
   /**
    * Try deleting a normal file (as opposed to a directory, symlink, etc).
    */
-  function testNormal() {
+  function testUnused() {
     $file = $this->createFile();
 
     // Check that deletion removes the file and database record.
-    $this->assertTrue(is_file($file->filepath), t("File exists."));
     $this->assertIdentical(file_delete($file), TRUE, t("Delete worked."));
-    $this->assertFileHookCalled('references');
     $this->assertFileHookCalled('delete');
     $this->assertFalse(file_exists($file->filepath), t("Test file has actually been deleted."));
     $this->assertFalse(file_load($file->fid), t('File was removed from the database.'));
+    $this->assertFalse(file_get_usage($file), t('File usage data was removed.'));
+  }
+
+  /**
+   * Try deleting a file that is in use.
+   */
+  function testInUse() {
+    $file = $this->createFile();
+    file_add_usage($file, 'testing', 'test', 1);
 
-    // TODO: implement hook_file_references() in file_test.module and report a
-    // file in use and test the $force parameter.
+    $usage = file_delete($file, FALSE);
+    $this->assertEqual($usage['testing']['test'], array('oid' => 1, 'count' => 1), t("Delete failed and returned usage data."));
+    $this->assertFileHookCalled('delete', 0);
+    $this->assertTrue(file_exists($file->filepath), t("Test file still exists on the disk."));
+    $this->assertTrue(file_load($file->fid), t('File still exists in the database.'));
+    $this->assertTrue(file_get_usage($file), t('File usage data still exists.'));
+
+    $this->assertIdentical(file_delete($file, TRUE), TRUE, t("Delete worked."));
+    $this->assertFileHookCalled('delete');
+    $this->assertFalse(file_exists($file->filepath), t("Test file has actually been deleted."));
+    $this->assertFalse(file_load($file->fid), t('File was removed from the database.'));
+    $this->assertFalse(file_get_usage($file), t('File usage data was removed.'));
   }
 }
 
@@ -1021,6 +1038,88 @@ class FileSaveTest extends FileHookTestC
   }
 }
 
+/**
+ * Tests the file_get_usage(), file_add_usage() and file_remove_usage()
+ * functions.
+ */
+class FileUsageTest extends FileTestCase {
+  function getInfo() {
+    return array(
+      'name' => t('File usage'),
+      'description' => t('Tests the file usage functions.'),
+      'group' => t('File'),
+    );
+  }
+
+  function testGetUsage() {
+    $file = $this->createFile();
+    db_insert('file_usage')->fields(array(
+      'fid' => $file->fid,
+      'module' => 'testing',
+      'object' => 'foo',
+      'oid' => 1,
+      'count' => 1
+    ))->execute();
+    db_insert('file_usage')->fields(array(
+      'fid' => $file->fid,
+      'module' => 'testing',
+      'object' => 'bar',
+      'oid' => 2,
+      'count' => 2
+    ))->execute();
+
+    $usage = file_get_usage($file);
+
+    $this->assertEqual(count($usage['testing']), 2, t('Returned the correct number of items.'));
+    $this->assertEqual($usage['testing']['foo']['oid'], 1, t('Returned the correct oid.'));
+    $this->assertEqual($usage['testing']['bar']['oid'], 2, t('Returned the correct oid.'));
+    $this->assertEqual($usage['testing']['foo']['count'], 1, t('Returned the correct count.'));
+    $this->assertEqual($usage['testing']['bar']['count'], 2, t('Returned the correct count.'));
+  }
+
+  /**
+   * Test the file_add_usage() function.
+   */
+  function testAddUsage() {
+    $file = $this->createFile();
+    file_add_usage($file, 'testing', 'foo', 1);
+    // Add the file twice to ensure that the count is incremented rather than
+    // creating additional records.
+    file_add_usage($file, 'testing', 'bar', 2);
+    file_add_usage($file, 'testing', 'bar', 2);
+
+    $usage = db_select('file_usage', 'f')->fields('f')->condition('f.fid', $file->fid)->execute()->fetchAllAssoc('oid');
+    $this->assertEqual(count($usage), 2, t('Created two records'));
+    $this->assertEqual($usage[1]->module, 'testing', t('Correct module'));
+    $this->assertEqual($usage[2]->module, 'testing', t('Correct module'));
+    $this->assertEqual($usage[1]->object, 'foo', t('Correct object'));
+    $this->assertEqual($usage[2]->object, 'bar', t('Correct object'));
+    $this->assertEqual($usage[1]->count, 1, t('Correct count'));
+    $this->assertEqual($usage[2]->count, 2, t('Correct count'));
+  }
+
+  /**
+   * Test the file_remove_usage() function.
+   */
+  function testRemoveUsage() {
+    $file = $this->createFile();
+    db_insert('file_usage')->fields(array(
+      'fid' => $file->fid,
+      'module' => 'testing',
+      'object' => 'bar',
+      'oid' => 2,
+      'count' => 2
+    ))->execute();
+
+    file_remove_usage($file, 'testing', 'bar', 2);
+    $count = db_select('file_usage', 'f')->fields('f', array('count'))->condition('f.fid', $file->fid)->execute()->fetchField();
+    $this->assertEqual(1, $count, t('The count was decremented correctly.'));
+
+    file_remove_usage($file, 'testing', 'bar', 2);
+    $count = db_select('file_usage', 'f')->fields('f', array('count'))->condition('f.fid', $file->fid)->execute()->fetchField();
+    $this->assertEqual(0, $count, t('The count was decremented correctly.'));
+  }
+}
 
 /**
  * Tests the file_validate() function..
Index: modules/simpletest/tests/file_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file_test.module,v
retrieving revision 1.6
diff -u -p -r1.6 file_test.module
--- modules/simpletest/tests/file_test.module	31 Dec 2008 11:08:47 -0000	1.6
+++ modules/simpletest/tests/file_test.module	2 Jan 2009 11:01:20 -0000
@@ -66,7 +66,6 @@ function file_test_reset() {
     'load' => array(),
     'validate' => array(),
     'download' => array(),
-    'references' => array(),
     'insert' => array(),
     'update' => array(),
     'copy' => array(),
@@ -79,7 +78,6 @@ function file_test_reset() {
   $return = array(
     'validate' => array(),
     'download' => NULL,
-    'references' => NULL,
   );
   variable_set('file_test_return', $return);
 }
@@ -90,7 +88,7 @@ function file_test_reset() {
  *
  * @param $op
  *   One of the hook_file_* operations: 'load', 'validate', 'download',
- *   'references', 'insert', 'update', 'copy', 'move', 'delete'.
+ *   'insert', 'update', 'copy', 'move', 'delete'.
  * @returns
  *   Array of the parameters passed to each call.
  * @see _file_test_log_call() and file_test_reset()
@@ -105,7 +103,7 @@ function file_test_get_calls($op) {
  *
  * @param $op
  *   One of the hook_file_* operations: 'load', 'validate', 'download',
- *   'references', 'insert', 'update', 'copy', 'move', 'delete'.
+ *   'insert', 'update', 'copy', 'move', 'delete'.
  * @param $args
  *   Values passed to hook.
  * @see file_test_get_calls() and file_test_reset()
@@ -120,7 +118,7 @@ function _file_test_log_call($op, $args)
  * Load the appropriate return value.
  *
  * @param $op
- *   One of the hook_file_[validate,download,references] operations.
+ *   One of the hook_file_[validate,download] operations.
  * @return
  *   Value set by file_test_set_return().
 * @see file_test_set_return() and file_test_reset().
@@ -134,7 +132,7 @@ function _file_test_get_return($op) {
  * Assign a return value for a given operation.
  *
  * @param $op
- *   One of the hook_file_[validate,download,references] operations.
+ *   One of the hook_file_[validate,download] operations.
  * @param $value
  *   Value for the hook to return.
  * @see _file_test_get_return() and file_test_reset().
@@ -174,14 +172,6 @@ function file_test_file_download($file) 
 }
 
 /**
- * Implementation of hook_file_references().
- */
-function file_test_file_references($file) {
-  _file_test_log_call('references', array($file));
-  return _file_test_get_return('references');
-}
-
-/**
  * Implementation of hook_file_insert().
  */
 function file_test_file_insert($file) {
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.8
diff -u -p -r1.8 system.api.php
--- modules/system/system.api.php	31 Dec 2008 11:08:47 -0000	1.8
+++ modules/system/system.api.php	2 Jan 2009 11:01:21 -0000
@@ -1098,31 +1098,6 @@ function hook_file_move($file, $source) 
 }
 
 /**
- * Report the number of times a file is referenced by a module.
- *
- * This hook is called to determine if a files is in use. Multiple modules may
- * be referencing the same file and to prevent one from deleting a file used by
- * another this hook is called.
- *
- * @param $file
- *   The file object being checked for references.
- * @return
- *   If the module uses this file return an array with the module name as the
- *   key and the value the number of times the file is used.
- *
- * @see file_delete()
- * @see upload_file_references()
- */
-function hook_file_references($file) {
-  // If upload.module is still using a file, do not let other modules delete it.
-  $count = db_query('SELECT COUNT(*) FROM {upload} WHERE fid = :fid', array(':fid' => $file->fid))->fetchField();
-  if ($count) {
-    // Return the name of the module and how many references it has to the file.
-    return array('upload' => $count);
-  }
-}
-
-/**
  * Respond to a file being deleted.
  *
  * @param $file
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.295
diff -u -p -r1.295 system.install
--- modules/system/system.install	30 Dec 2008 16:43:19 -0000	1.295
+++ modules/system/system.install	2 Jan 2009 11:01:24 -0000
@@ -291,7 +291,7 @@ function system_install() {
   if (db_driver() == 'pgsql') {
     // We create some functions using global names instead of prefixing them
     // like we do with table names. If this function is ever called again (for
-    // example, by the test framework when creating prefixed test databases), 
+    // example, by the test framework when creating prefixed test databases),
     // the global names will already exist. We therefore avoid trying to create
     // them again in that case.
 
@@ -665,6 +665,50 @@ function system_schema() {
     'primary key' => array('fid'),
   );
 
+  $schema['file_usage'] = array(
+    'description' => 'Track where a file is used.',
+    'fields' => array(
+      'fid' => array(
+        'description' => 'File ID.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'module' => array(
+        'description' => 'The name of the module that is using the file.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'object' => array(
+        'description' => 'The name of the table where the file is used.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'oid' => array(
+        'description' => 'The primary key of the object using the file.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'count' => array(
+        'description' => 'The number of times this file is used by this object.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('fid', 'object', 'oid', 'module'),
+    'indexes' => array(
+      'object_oid' => array('object', 'oid'),
+    ),
+  );
+
   $schema['flood'] = array(
     'description' => 'Flood controls the threshold of events, such as the number of contact attempts.',
     'fields' => array(
@@ -3138,11 +3182,11 @@ function system_update_7016() {
   $ret = array();
   // Only run these queries if the driver used is pgsql.
   if (db_driver() == 'pgsql') {
-    $result = db_query("SELECT c.relname AS table, a.attname AS field, 
-                        pg_catalog.format_type(a.atttypid, a.atttypmod) AS type 
-                        FROM pg_catalog.pg_attribute a 
-                        LEFT JOIN pg_class c ON (c.oid =  a.attrelid) 
-                        WHERE pg_catalog.pg_table_is_visible(c.oid) AND c.relkind = 'r' 
+    $result = db_query("SELECT c.relname AS table, a.attname AS field,
+                        pg_catalog.format_type(a.atttypid, a.atttypmod) AS type
+                        FROM pg_catalog.pg_attribute a
+                        LEFT JOIN pg_class c ON (c.oid =  a.attrelid)
+                        WHERE pg_catalog.pg_table_is_visible(c.oid) AND c.relkind = 'r'
                         AND pg_catalog.format_type(a.atttypid, a.atttypmod) LIKE '%unsigned%'");
     while ($row = db_fetch_object($result)) {
       switch ($row->type) {
@@ -3155,7 +3199,7 @@ function system_update_7016() {
           $datatype = 'bigint';
           break;
       }
-      $ret[] = update_sql('ALTER TABLE ' . $row->table . ' ALTER COLUMN ' . $row->field . ' TYPE ' . $datatype); 
+      $ret[] = update_sql('ALTER TABLE ' . $row->table . ' ALTER COLUMN ' . $row->field . ' TYPE ' . $datatype);
       $ret[] = update_sql('ALTER TABLE ' . $row->table . ' ADD CHECK (' . $row->field . ' >= 0)');
     }
     $ret[] = update_sql('DROP DOMAIN smallint_unsigned');
@@ -3166,6 +3210,59 @@ function system_update_7016() {
 }
 
 /**
+ * Create the file_usage table.
+ */
+function system_update_7017() {
+  $schema['file_usage'] = array(
+    'description' => 'Track where a file is used.',
+    'fields' => array(
+      'fid' => array(
+        'description' => 'File ID.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'module' => array(
+        'description' => 'The name of the module that is using the file.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'object' => array(
+        'description' => 'The name of the table where the file is used.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'oid' => array(
+        'description' => 'The primary key of the object using the file.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'count' => array(
+        'description' => 'The number of times this file is used by this object.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('fid', 'object', 'oid', 'module'),
+    'indexes' => array(
+      'object_oid' => array('object', 'oid'),
+    ),
+  );
+
+  $ret = array();
+  db_create_table($ret, 'file_usage', $schema['file_usage']);
+  return $ret;
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
Index: modules/upload/upload.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/upload/upload.install,v
retrieving revision 1.8
diff -u -p -r1.8 upload.install
--- modules/upload/upload.install	15 Nov 2008 13:01:11 -0000	1.8
+++ modules/upload/upload.install	2 Jan 2009 11:01:25 -0000
@@ -82,4 +82,11 @@ function upload_schema() {
   return $schema;
 }
 
-
+/**
+ * Create file_usage records for our files.
+ */
+function upload_update_7000() {
+  $ret = array();
+  $ret[] = update_sql("INSERT INTO {file_usage} (fid, module, object, oid, count) SELECT fid, 'upload', 'node_revision', vid, 1 FROM {upload}");
+  return $ret;
+}
Index: modules/upload/upload.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/upload/upload.module,v
retrieving revision 1.223
diff -u -p -r1.223 upload.module
--- modules/upload/upload.module	31 Dec 2008 11:08:47 -0000	1.223
+++ modules/upload/upload.module	2 Jan 2009 11:01:26 -0000
@@ -280,18 +280,6 @@ function upload_file_load($files) {
 }
 
 /**
- * Implementation of hook_file_references().
- */
-function upload_file_references(&$file) {
-  // If upload.module is still using a file, do not let other modules delete it.
-  $count = db_query('SELECT COUNT(*) FROM {upload} WHERE fid = :fid', array(':fid' => $file->fid))->fetchField();
-  if ($count) {
-    // Return the name of the module and how many references it has to the file.
-    return array('upload' => $count);
-  }
-}
-
-/**
  * Implementation of hook_file_delete().
  */
 function upload_file_delete(&$file) {
@@ -494,6 +482,7 @@ function upload_save(&$node) {
     if (!empty($file->remove)) {
       // Remove the reference from this revision.
       db_delete('upload')->condition('fid', $file->fid)->condition('vid', $node->vid)->execute();
+      file_remove_usage($file, 'upload', 'node_revision', $node->vid);
       // Try a soft delete, if the file isn't used elsewhere it'll be deleted.
       file_delete($file);
       // Remove it from the session in the case of new uploads,
@@ -530,6 +519,8 @@ function upload_save(&$node) {
     }
     $file->status &= FILE_STATUS_PERMANENT;
     $file = file_save($file);
+
+    file_add_usage($file, 'upload', 'node_revision', $node->vid);
   }
 }
 
