? drupal.file-usage.353458.51.patch
Index: includes/file.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/file.inc,v
retrieving revision 1.205
diff -u -p -r1.205 file.inc
--- includes/file.inc	1 Feb 2010 19:07:07 -0000	1.205
+++ includes/file.inc	17 Feb 2010 15:58:41 -0000
@@ -524,6 +524,101 @@ function file_save(stdClass $file) {
   return $file;
 }
 
+/** Determine where a file is used.
+ *
+ * @param $file
+ *   A file object.
+ * @return
+ *   An nested array with usage data. The first level is keyed by module name,
+ *   the second by table name, the third has 'id' and 'count' keys.
+ *
+ * @see file_add_usage()
+ * @see file_remove_usage()
+ */
+function file_get_usage(stdClass $file) {
+  $result = db_query('SELECT f.module, f.type, f.id, f.count FROM {file_usage} f WHERE f.fid = :fid AND count > 0', array(':fid' => $file->fid));
+  $references = array();
+  foreach ($result as $usage) {
+    $references[$usage->module][$usage->type] = array('id' => $usage->id, 'count' => $usage->count);
+  }
+  return $references;
+}
+
+/**
+ * Record 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 $type would
+ *   be 'node_revision' and $id would be the node's vid.
+ * - The User module associates an image with a user, so $type would be 'users'
+ *   and the $id would be the user's uid.
+ *
+ * @param $file
+ *   A file object.
+ * @param $module
+ *   The name of the module using the file.
+ * @param $type
+ *   The name of the table where the file is referenced.
+ * @param $id
+ *   The id of the row in the table.
+ *
+ * @see file_get_usage()
+ * @see file_remove_usage()
+ */
+function file_add_usage(stdClass $file, $module, $type, $id) {
+  db_merge('file_usage')
+    ->key(array(
+      'fid' => $file->fid,
+      'module' => $module,
+      'type' => $type,
+      'id' => $id,
+    ))
+    ->fields(array('count' => 1))
+    ->expression('count', 'count + 1')
+    ->execute();
+}
+
+/**
+ * Remove a record to indicate that a module is no longer using a file.
+ *
+ * If no usage records for a file remain in the database, the file will be
+ * deleted using file_delete().
+ *
+ * @param $file
+ *   A file object.
+ * @param $module
+ *   The name of the module using the file.
+ * @param $type
+ *   The name of the table where the file is referenced.
+ * @param $id
+ *   The id of the row in the table.
+ * @return
+ *   TRUE for success, or FALSE if no usages remain but the file still could
+ *   not be deleted.
+ *
+ * @see file_add_usage()
+ * @see file_get_usage()
+ * @see file_delete()
+ */
+function file_remove_usage(stdClass $file, $module, $type, $id) {
+  db_update('file_usage')
+    ->expression('count', 'count - 1')
+    ->condition('fid', $file->fid)
+    ->condition('module', $module)
+    ->condition('type', $type)
+    ->condition('id', $id)
+    ->execute();
+
+  // Delete the file, unless it is still in use somewhere.
+  $references = db_result(db_query('SELECT COUNT(*) FROM {file_usage} f WHERE f.fid = :fid AND count > 0', array(':fid' => $file->fid)));
+  if (empty($references)) {
+    return file_delete($file);
+  }
+  return TRUE;
+}
+
 /**
  * Copy a file to a new location and adds a file record to the database.
  *
@@ -941,25 +1036,15 @@ function file_create_filename($basename,
 /**
  * Delete a file and its database record.
  *
- * If the $force parameter is not TRUE hook_file_references() will be called
- * to determine if the file is being used by any modules. If the file is being
- * used is the delete will be canceled.
- *
- * @param $file
- *   A file object.
- * @param $force
- *   Boolean indicating that the file should be deleted even if
- *   hook_file_references() reports that the file is in use.
- * @return mixed
- *   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.
+ * @return
+ * TRUE for success, or FALSE in the event of an error.
  *
  * @see file_unmanaged_delete()
- * @see hook_file_references()
+ * @see file_get_usage()
+ * @see file_remove_usage()
  * @see hook_file_delete()
  */
-function file_delete(stdClass $file, $force = FALSE) {
+function file_delete(stdClass $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.
@@ -974,6 +1059,7 @@ function file_delete(stdClass $file, $fo
   // database, so UIs can still find the file in the database.
   if (file_unmanaged_delete($file->uri)) {
     db_delete('file')->condition('fid', $file->fid)->execute();
+    db_delete('file_usage')->condition('fid', $file->fid)->execute();
     return TRUE;
   }
   return FALSE;
Index: modules/file/file.field.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.field.inc,v
retrieving revision 1.22
diff -u -p -r1.22 file.field.inc
--- modules/file/file.field.inc	17 Feb 2010 05:39:51 -0000	1.22
+++ modules/file/file.field.inc	17 Feb 2010 15:58:41 -0000
@@ -215,6 +215,7 @@ function file_field_load($entity_type, $
 
     foreach ($items[$id] as $delta => $item) {
       // If the file does not exist, mark the entire item as empty.
+      // @todo NULL will break array_merge() below for multiple field values.
       if (empty($item['fid']) || !isset($files[$item['fid']])) {
         $items[$id][$delta] = NULL;
       }
@@ -263,8 +264,14 @@ function file_field_presave($obj_type, $
  * Check for files that have been removed from the object.
  */
 function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
   // On new revisions, old files are always maintained in the previous revision.
   if (!empty($entity->revision)) {
+    foreach ($items as $item) {
+      $file = (object) $item;
+      file_add_usage($file, 'file', $entity_type . '_revision', $vid);
+    }
     return;
   }
 
@@ -275,14 +282,12 @@ function file_field_update($entity_type,
   }
 
   // Delete items from original object.
-  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
   $load_function = $entity_type . '_load';
-
   $original = $load_function($id);
   if (!empty($original->{$field['field_name']}[$langcode])) {
     foreach ($original->{$field['field_name']}[$langcode] as $original_item) {
       if (isset($original_item['fid']) && !in_array($original_item['fid'], $fids)) {
-        // For hook_file_references, remember that this is being deleted.
+        // For file usage references, remember that this is being deleted.
         $original_item['file_field_name'] = $field['field_name'];
         // Delete the file if possible.
         file_field_delete_file($original_item, $field);
@@ -297,10 +302,9 @@ function file_field_update($entity_type,
 function file_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
   list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
   foreach ($items as $delta => $item) {
-    // For hook_file_references(), remember that this is being deleted.
+    // For file usage references, remember that this is being deleted.
     $item['file_field_name'] = $field['field_name'];
-    // Pass in the ID of the object that is being removed so all references can
-    // be counted in hook_file_references().
+    // Pass in the ID of the object that is being removed.
     $item['file_field_type'] = $entity_type;
     $item['file_field_id'] = $id;
     file_field_delete_file($item, $field);
@@ -312,7 +316,7 @@ function file_field_delete($entity_type,
  */
 function file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
   foreach ($items as $delta => $item) {
-    // For hook_file_references, remember that this file is being deleted.
+    // For file usage references, remember that this is being deleted.
     $item['file_field_name'] = $field['field_name'];
     if (file_field_delete_file($item, $field)) {
       $items[$delta] = NULL;
@@ -334,15 +338,9 @@ function file_field_delete_file($item, $
   // the file reference count. Temporary files can be deleted because they
   // are not yet associated with any content at all.
   $file = (object) $item;
-  if ($file->status == 0 || file_get_file_reference_count($file, $field) > 0) {
-    $file->file_field_name = $field_name;
-    $file->file_field_id = $field_id;
-    return file_delete($file);
+  if ($file->status == 0) {
+    return file_remove_usage($file, 'file', $field_name, $field_id);
   }
-
-  // Even if the file is not deleted, return TRUE to indicate the file field
-  // record can be removed from the field database tables.
-  return TRUE;
 }
 
 /**
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.19
diff -u -p -r1.19 file.module
--- modules/file/file.module	11 Feb 2010 17:44:47 -0000	1.19
+++ modules/file/file.module	17 Feb 2010 15:58:41 -0000
@@ -320,21 +320,6 @@ function file_progress_implementation() 
 }
 
 /**
- * Implements hook_file_references().
- */
-function file_file_references($file) {
-  $count = file_get_file_reference_count($file);
-  return $count ? array('file' => $count) : NULL;
-}
-
-/**
- * Implements hook_file_delete().
- */
-function file_file_delete($file) {
-  // TODO: Remove references to a file that is in-use.
-}
-
-/**
  * Process function to expand the managed_file element type.
  *
  * Expands the file type to include Upload and Remove buttons, as well as
@@ -540,11 +525,8 @@ function file_managed_file_validate(&$el
   if ($clicked_button != 'remove_button' && !empty($element['fid']['#value'])) {
     if ($file = file_load($element['fid']['#value'])) {
       if ($file->status == FILE_STATUS_PERMANENT) {
-        $reference_count = 0;
-        foreach (module_invoke_all('file_references', $file) as $module => $references) {
-          $reference_count += $references;
-        }
-        if ($reference_count == 0) {
+        $references = file_get_usage($file);
+        if (empty($references)) {
           form_error($element, t('Referencing to the file used in the !name field is not allowed.', array('!name' => $element['#title'])));
         }
       }
@@ -870,72 +852,6 @@ function file_icon_map($file) {
  */
 
 /**
- * Count the number of times the file is referenced.
- *
- * @param $file
- *   A file object.
- * @param $field
- *   (optional) A CCK field array or field name as a string. If provided,
- *   limits the reference check to the given field.
- * @param $field_type
- *   (optional) The name of a field type. If provided, limits the reference
- *   check to fields of the given type.
- * @return
- *   An integer value.
- */
-function file_get_file_reference_count($file, $field = NULL, $field_type = 'file') {
-  // Determine the collection of fields to check.
-  if (isset($field)) {
-    // Support $field as 'field name'.
-    if (is_string($field)) {
-      $field = field_info_field($field);
-    }
-    $fields = array($field['field_name'] => $field);
-  }
-  else {
-    $fields = field_info_fields();
-  }
-
-  $types = entity_get_info();
-  $reference_count = 0;
-
-  foreach ($fields as $field) {
-    if (empty($field_type) || $field['type'] == $field_type) {
-      // TODO: Use a more efficient mechanism rather than actually retrieving
-      // all the references themselves, such as using a COUNT() query.
-      $references = file_get_file_references($file, $field, FIELD_LOAD_REVISION, $field_type);
-      foreach ($references as $entity_type => $type_references) {
-        $reference_count += count($type_references);
-      }
-
-      // If a field_name is present in the file object, the file is being deleted
-      // from this field.
-      if (isset($file->file_field_name) && $field['field_name'] == $file->file_field_name) {
-        // If deleting the entire piece of content, decrement references.
-        if (isset($file->file_field_type) && isset($file->file_field_id)) {
-          if ($file->file_field_type == $entity_type) {
-            $info = entity_get_info($entity_type);
-            $id = $types[$entity_type]['object keys']['id'];
-            foreach ($type_references as $reference) {
-              if ($file->file_field_id == $reference->$id) {
-                $reference_count--;
-              }
-            }
-          }
-        }
-        // Otherwise we're just deleting a single reference in this field.
-        else {
-          $reference_count--;
-        }
-      }
-    }
-  }
-
-  return $reference_count;
-}
-
-
-/**
  * Get a list of references to a file.
  *
  * @param $file
Index: modules/image/image.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/image/image.module,v
retrieving revision 1.32
diff -u -p -r1.32 image.module
--- modules/image/image.module	1 Feb 2010 07:08:49 -0000	1.32
+++ modules/image/image.module	17 Feb 2010 15:58:42 -0000
@@ -307,14 +307,6 @@ function image_file_delete($file) {
 }
 
 /**
- * Implements hook_file_references().
- */
-function image_file_references($file) {
-  $count = file_get_file_reference_count($file, NULL, 'image');
-  return $count ? array('image' => $count) : NULL;
-}
-
-/**
  * Implements hook_image_default_styles().
  */
 function image_image_default_styles() {
Index: modules/node/node.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.api.php,v
retrieving revision 1.63
diff -u -p -r1.63 node.api.php
--- modules/node/node.api.php	11 Feb 2010 08:58:55 -0000	1.63
+++ modules/node/node.api.php	17 Feb 2010 15:58:42 -0000
@@ -97,7 +97,7 @@
  *   above):
  *   - hook_prepare() (node-type-specific)
  *   - hook_node_prepare() (all); if translation.module is enabled, this will
- *     also invoke hook_node_prepare_translation() on all modules. 
+ *     also invoke hook_node_prepare_translation() on all modules.
  *   - hook_form() (node-type-specific)
  *   - field_attach_form()
  * - Validating a node during editing form submit (calling
@@ -412,7 +412,7 @@ function hook_node_revision_delete($node
     return;
   }
   foreach ($node->files as $file) {
-    file_delete($file);
+    file_remove_usage($file);
   }
 }
 
Index: modules/simpletest/tests/file.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v
retrieving revision 1.51
diff -u -p -r1.51 file.test
--- modules/simpletest/tests/file.test	29 Jan 2010 22:40:41 -0000	1.51
+++ modules/simpletest/tests/file.test	17 Feb 2010 15:58:42 -0000
@@ -230,13 +230,13 @@ class FileHookTestCase extends FileTestC
       $this->assertTrue(FALSE, t('Expected hooks %expected to be called but %uncalled was not called.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled))));
     }
     else {
-      $this->assertTrue(TRUE, t('All the expected hooks were called: %expected', array('%expected' => implode(', ', $expected))));
+      $this->assertTrue(TRUE, t('All the expected hooks were called: %expected', array('%expected' => empty($expected) ? t('(none)') : implode(', ', $expected))));
     }
 
     // Determine if there were any unexpected calls.
     $unexpected = array_diff($actual, $expected);
     if (count($unexpected)) {
-      $this->assertTrue(FALSE, t('Unexpected hooks were called: %unexpected.', array('%unexpected' => implode(', ', $unexpected))));
+      $this->assertTrue(FALSE, t('Unexpected hooks were called: %unexpected.', array('%unexpected' => empty($unexpected) ? t('(none)') : implode(', ', $unexpected))));
     }
     else {
       $this->assertTrue(TRUE, t('No unexpected hooks were called.'));
@@ -1241,18 +1241,42 @@ 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->uri), t("File exists."));
-    $this->assertIdentical(file_delete($file), TRUE, t("Delete worked."));
-    $this->assertFileHooksCalled(array('references', 'delete'));
-    $this->assertFalse(file_exists($file->uri), t("Test file has actually been deleted."));
+    $this->assertTrue(is_file($file->uri), t('File exists.'));
+    $this->assertIdentical(file_delete($file), TRUE, t('Delete worked.'));
+    $this->assertFileHooksCalled(array('delete'));
+    $this->assertFalse(file_exists($file->uri), 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);
+    file_add_usage($file, 'testing', 'test', 1);
+
+    file_remove_usage($file, 'testing', 'test', 1);
+    $usage = file_get_usage($file);
+    $this->assertFileHooksCalled(array());
+    $this->assertEqual($usage['testing']['test'], array('id' => 1, 'count' => 1), t('Test file is still in use.'));
+    $this->assertTrue(file_exists($file->uri), t('File still exists on the disk.'));
+    $this->assertTrue(file_load($file->fid), t('File still exists in the database.'));
 
-    // TODO: implement hook_file_references() in file_test.module and report a
-    // file in use and test the $force parameter.
+    // Clear out the call to hook_file_load().
+    file_test_reset();
+
+    file_remove_usage($file, 'testing', 'test', 1);
+    $usage = file_get_usage($file);
+    $this->assertFileHooksCalled(array('delete'));
+    $this->assertTrue(empty($usage), t('File usage data was removed.'));
+    $this->assertFalse(file_exists($file->uri), t('File has been deleted after its last usage was removed.'));
+    $this->assertFalse(file_load($file->fid), t('File was removed from the database.'));
   }
 }
 
@@ -1703,6 +1727,109 @@ class FileSaveTest extends FileHookTestC
   }
 }
 
+/**
+ * Tests file usage functions.
+ */
+class FileUsageTest extends FileTestCase {
+  function getInfo() {
+    return array(
+      'name' => 'File usage',
+      'description' => 'Tests the file usage functions.',
+      'group' => 'File',
+    );
+  }
+
+  /**
+   * Test file_get_usage().
+   */
+  function testGetUsage() {
+    $file = $this->createFile();
+    db_insert('file_usage')
+      ->fields(array(
+        'fid' => $file->fid,
+        'module' => 'testing',
+        'type' => 'foo',
+        'id' => 1,
+        'count' => 1
+      ))
+      ->execute();
+    db_insert('file_usage')
+      ->fields(array(
+        'fid' => $file->fid,
+        'module' => 'testing',
+        'type' => 'bar',
+        'id' => 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']['id'], 1, t('Returned the correct id.'));
+    $this->assertEqual($usage['testing']['bar']['id'], 2, t('Returned the correct id.'));
+    $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 file_add_usage().
+   */
+  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('id');
+    $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]->type, 'foo', t('Correct type'));
+    $this->assertEqual($usage[2]->type, 'bar', t('Correct type'));
+    $this->assertEqual($usage[1]->count, 1, t('Correct count'));
+    $this->assertEqual($usage[2]->count, 2, t('Correct count'));
+  }
+
+  /**
+   * Test file_remove_usage().
+   */
+  function testRemoveUsage() {
+    $file = $this->createFile();
+    db_insert('file_usage')
+      ->fields(array(
+        'fid' => $file->fid,
+        'module' => 'testing',
+        'type' => 'bar',
+        'id' => 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.20
diff -u -p -r1.20 file_test.module
--- modules/simpletest/tests/file_test.module	29 Jan 2010 01:59:32 -0000	1.20
+++ modules/simpletest/tests/file_test.module	17 Feb 2010 15:58:42 -0000
@@ -107,7 +107,6 @@ function file_test_reset() {
     'load' => array(),
     'validate' => array(),
     'download' => array(),
-    'references' => array(),
     'insert' => array(),
     'update' => array(),
     'copy' => array(),
@@ -120,7 +119,6 @@ function file_test_reset() {
   $return = array(
     'validate' => array(),
     'download' => NULL,
-    'references' => NULL,
   );
   variable_set('file_test_return', $return);
 }
@@ -131,7 +129,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'.
  * @return
  *   Array of the parameters passed to each call.
  * @see _file_test_log_call() and file_test_reset()
@@ -145,9 +143,9 @@ function file_test_get_calls($op) {
  * Get an array with the calls for all hooks.
  *
  * @return
- *   An array keyed by hook name ('load', 'validate', 'download',
- *   'references', 'insert', 'update', 'copy', 'move', 'delete') with values
- *   being arrays of parameters passed to each call.
+ *   An array keyed by hook name ('load', 'validate', 'download', 'insert',
+ *   'update', 'copy', 'move', 'delete') with values being arrays of parameters
+ *   passed to each call.
  */
 function file_test_get_all_calls() {
   return variable_get('file_test_results', array());
@@ -158,7 +156,7 @@ function file_test_get_all_calls() {
  *
  * @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()
@@ -173,7 +171,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().
@@ -187,7 +185,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().
@@ -227,14 +225,6 @@ function file_test_file_download($uri) {
 }
 
 /**
- * Implements hook_file_references().
- */
-function file_test_file_references($file) {
-  _file_test_log_call('references', array($file));
-  return _file_test_get_return('references');
-}
-
-/**
  * Implements 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.132
diff -u -p -r1.132 system.api.php
--- modules/system/system.api.php	15 Feb 2010 22:11:21 -0000	1.132
+++ modules/system/system.api.php	17 Feb 2010 15:58:42 -0000
@@ -1568,7 +1568,7 @@ function hook_modules_uninstalled($modul
  *   - 'description' A string with a short description of what the wrapper does.
  *   - 'type' A bitmask of flags indicating what type of streams this wrapper
  *     will access - local or remote, readable and/or writeable, etc. Many
- *     shortcut constants are defined in stream_wrappers.inc. 
+ *     shortcut constants are defined in stream_wrappers.inc.
  *
  * @see file_get_stream_wrappers()
  * @see hook_stream_wrappers_alter()
@@ -1702,6 +1702,14 @@ function hook_file_copy($file, $source) 
 /**
  * Respond to a file that has been moved.
  *
+ * $file->fid stays the same unless the moved file replaced a previously
+ * existing one, in which case the fid and filename of the existing target file
+ * is adopted for the new file object.
+ *
+ * Implement this hook if your module keeps track of files in a module-specific
+ * table, so that file references remain valid.
+ *
+ *
  * @param $file
  *   The updated file object after the move.
  * @param $source
@@ -1710,32 +1718,14 @@ function hook_file_copy($file, $source) 
  * @see file_move()
  */
 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.
-  $file_used = (bool) db_query_range('SELECT 1 FROM {upload} WHERE fid = :fid', 0, 1, array(':fid' => $file->fid))->fetchField();
-  if ($file_used) {
-    // Return the name of the module and how many references it has to the file.
-    return array('upload' => $count);
-  }
+  if ($file->fid != $source->fid) {
+    db_update('upload')
+      ->fields(array(
+        'fid' => $file->fid,
+      ))
+      ->condition('fid', $source->fid)
+      ->execute();
+   }
 }
 
 /**
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.447
diff -u -p -r1.447 system.install
--- modules/system/system.install	17 Feb 2010 03:55:45 -0000	1.447
+++ modules/system/system.install	17 Feb 2010 15:58:43 -0000
@@ -794,6 +794,50 @@ function system_schema() {
     ),
   );
 
+  $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' => '',
+      ),
+      'type' => array(
+        'description' => 'The name of the table where the file is used.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'id' => 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', 'type', 'id', 'module'),
+    'indexes' => array(
+      'type_id' => array('type', 'id'),
+    ),
+  );
+
   $schema['flood'] = array(
     'description' => 'Flood controls the threshold of events, such as the number of contact attempts.',
     'fields' => array(
@@ -2427,6 +2471,56 @@ function system_update_7050() {
 }
 
 /**
+ * Create the file_usage table.
+ */
+function system_update_7051() {
+  $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' => '',
+      ),
+      'type' => array(
+        'description' => 'The name of the table where the file is used.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'id' => 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', 'type', 'id', 'module'),
+    'indexes' => array(
+      'type_id' => array('type', 'id'),
+    ),
+  );
+  db_create_table('file_usage', $schema['file_usage']);
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.890
diff -u -p -r1.890 system.module
--- modules/system/system.module	17 Feb 2010 09:09:30 -0000	1.890
+++ modules/system/system.module	17 Feb 2010 15:58:43 -0000
@@ -2707,8 +2707,14 @@ function system_cron() {
   ));
   foreach ($result as $row) {
     if ($file = file_load($row->fid)) {
-      if (!file_delete($file)) {
-        watchdog('file system', 'Could not delete temporary file "%path" during garbage collection', array('%path' => $file->uri), WATCHDOG_ERROR);
+      $references = file_get_usage($file);
+      if (empty($references)) {
+        if (!file_delete($file)) {
+          watchdog('file system', 'Could not delete temporary file "%path" during garbage collection', array('%path' => $file->uri), WATCHDOG_ERROR);
+        }
+      }
+      else {
+        watchdog('file system', 'Could not delete temporary file "%path" during garbage collection, because it is still used.', array('%path' => $file->uri), WATCHDOG_ERROR);
       }
     }
   }
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.1126
diff -u -p -r1.1126 user.module
--- modules/user/user.module	17 Feb 2010 08:54:51 -0000	1.1126
+++ modules/user/user.module	17 Feb 2010 15:58:43 -0000
@@ -415,9 +415,15 @@ function user_save($account, $edit = arr
           if ($picture = file_move($picture, $destination, FILE_EXISTS_REPLACE)) {
             $picture->status |= FILE_STATUS_PERMANENT;
             $edit['picture'] = file_save($picture);
+            file_add_usage($picture, 'user', 'user', $account->uid);
           }
         }
       }
+      // If the picture existed before and was unset, remove our reference to it.
+      elseif (!empty($account->picture->fid)) {
+        file_remove_usage($account->picture, 'user', 'user', $account->uid);
+      }
+
       $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid;
 
       $edit['data'] = $data;
@@ -431,13 +437,6 @@ function user_save($account, $edit = arr
         return FALSE;
       }
 
-      // If the picture changed or was unset, remove the old one. This step needs
-      // to occur after updating the {users} record so that user_file_references()
-      // doesn't report it in use and block the deletion.
-      if (!empty($account->picture->fid) && ($edit['picture'] != $account->picture->fid)) {
-        file_delete($account->picture);
-      }
-
       // Reload user roles if provided.
       if (isset($edit['roles']) && is_array($edit['roles'])) {
         db_delete('users_roles')
@@ -805,14 +804,16 @@ function user_file_download($uri) {
 }
 
 /**
- * Implements hook_file_references().
+ * Implement hook_file_move().
  */
-function user_file_references($file) {
-  // Determine if the file is used by this module.
-  $file_used = (bool) db_query_range('SELECT 1 FROM {users} WHERE picture = :fid', 0, 1, array(':fid' => $file->fid))->fetchField();
-  if ($file_used) {
-    // Return the name of the module and how many references it has to the file.
-    return array('user' => $count);
+function user_file_move($file, $source) {
+  if ($file->fid != $source->fid) {
+    db_update('users')
+      ->fields(array(
+        'picture' => $file->fid,
+      ))
+      ->condition('picture', $source->fid)
+      ->execute();
   }
 }
 
