? .DS_Store ? .cache ? .git ? .project ? .settings ? INSTALL.sqlite.txt ? file_usage.patch ? junk ? logs ? pgsql_349671.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.149 diff -u -p -r1.149 file.inc --- includes/file.inc 2 Jan 2009 21:45:11 -0000 1.149 +++ includes/file.inc 2 Jan 2009 22:25:01 -0000 @@ -349,6 +349,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', 'type', 'id', 'count')) + ->condition('f.fid', $file->fid)->condition('f.count', 0, '>') + ->execute(); + + $return = array(); + foreach ($result as $usage) { + $return[$usage->module][$usage->type] = array('id' => $usage->id, '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 + * $type would be 'node_revision' and $id would be the node's vid. + * - The user module associates an image with a user so the $type would be + * 'user' and the $id would be the user's id. + * + * @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($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(); +} + +/** + * 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 $type + * The name of the table where the file is referenced. + * @param $id + * The id of the row in the table. + * + * @see file_add_usage() + * @see file_get_usage() + */ +function file_remove_usage($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(); +} + +/** * 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 @@ -691,22 +777,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. @@ -716,6 +806,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: includes/database/pgsql/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/pgsql/database.inc,v retrieving revision 1.15 diff -u -p -r1.15 database.inc --- includes/database/pgsql/database.inc 26 Dec 2008 11:48:18 -0000 1.15 +++ includes/database/pgsql/database.inc 2 Jan 2009 22:25:01 -0000 @@ -13,6 +13,14 @@ class DatabaseConnection_pgsql extends DatabaseConnection { + /** + * A cache of information collected on tables by introspecting the database. + * + * @see DatabaseConnection_pgsql->getTableInformation(). + * @var array + */ + protected $tableInformation = array(); + public function __construct(array $connection_options = array()) { // This driver defaults to transaction support, except if explicitly passed FALSE. $this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] === FALSE; @@ -31,6 +39,47 @@ class DatabaseConnection_pgsql extends D )); } + /** + * Introspect the database and fetch information about a table required by + * insert queries. + * + * @param $table_name + * The non-prefixed name of the table. + * @return + * An object with two member variables: + * * 'blob_fields' that lists all the blob fields in the table. + * * 'sequences' that lists the sequences used in that table. + */ + public function getTableInformation($table) { + // Expand the table name and split it in 'schema.table_name'. + $table_name = $this->prefixTables('{' . $table . '}'); + if (strpos($table_name, '.')) { + list($schema, $table_name) = explode('.', $table_name); + } + else { + $schema = 'public'; + } + + $key = $schema . '|' . $table_name; + if (!isset($this->tableInformation[$key])) { + $table_information = (object) array( + 'blob_fields' => array(), + 'sequences' => array(), + ); + $result = db_query("SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = :schema AND table_name = :table AND (data_type = 'bytea' OR (numeric_precision IS NOT NULL AND column_default LIKE :default))", array(':schema' => $schema, ':table' => $table_name, ':default' => '%nextval%')); + foreach ($result as $column) { + if ($column->data_type == 'bytea') { + $table_information->blob_fields[$column->column_name] = TRUE; + } + else if (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) { + $table_information->sequences[] = $matches[1]; + } + } + $this->tableInformation[$key] = $table_information; + } + return $this->tableInformation[$key]; + } + public function query($query, array $args = array(), $options = array()) { $options += $this->defaultOptions(); Index: includes/database/pgsql/query.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/pgsql/query.inc,v retrieving revision 1.7 diff -u -p -r1.7 query.inc --- includes/database/pgsql/query.inc 20 Dec 2008 18:24:34 -0000 1.7 +++ includes/database/pgsql/query.inc 2 Jan 2009 22:25:01 -0000 @@ -1,7 +1,6 @@ table); - $stmt = $this->connection->prepareQuery((string)$this); + $table_information = $this->connection->getTableInformation($this->table); + $max_placeholder = 0; $blobs = array(); $blob_cnt = 0; foreach ($this->insertValues as &$insert_values) { foreach ($this->insertFields as $idx => $field) { - switch ($schema['fields'][$field]['type']) { - case 'blob': - $blobs[$blob_cnt] = fopen('php://memory', 'a'); - fwrite($blobs[$blob_cnt], $insert_values[$idx]); - rewind($blobs[$blob_cnt]); - - $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_cnt], PDO::PARAM_LOB); - - ++$blob_cnt; - - break; - default: - $stmt->bindParam(':db_insert_placeholder_'. $max_placeholder++, $insert_values[$idx]); - break; + if (isset($table_information->blob_fields[$field])) { + $blobs[$blob_cnt] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_cnt], $insert_values[$idx]); + rewind($blobs[$blob_cnt]); + + $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_cnt], PDO::PARAM_LOB); + + ++$blob_cnt; + } + else { + $stmt->bindParam(':db_insert_placeholder_'. $max_placeholder++, $insert_values[$idx]); } } } @@ -58,8 +54,8 @@ class InsertQuery_pgsql extends InsertQu // the options array. $options = $this->queryOptions; - if ($schema['fields'][$schema['primary key'][0]]['type'] == 'serial') { - $options['sequence_name'] = $this->connection->makeSequenceName($this->table, $schema['primary key'][0]); + if (!empty($table_information->sequences)) { + $options['sequence_name'] = $table_information->sequences[0]; $options['return'] = Database::RETURN_INSERT_ID; } $last_insert_id = $this->connection->query($stmt, array(), $options); Index: modules/simpletest/tests/file.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v retrieving revision 1.17 diff -u -p -r1.17 file.test --- modules/simpletest/tests/file.test 2 Jan 2009 21:45:11 -0000 1.17 +++ modules/simpletest/tests/file.test 2 Jan 2009 22:25:03 -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('id' => 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', + '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 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('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 the file_remove_usage() function. + */ + 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.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 22:25:03 -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 22:25:05 -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 22:25:08 -0000 @@ -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' => '', + ), + '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( @@ -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' => '', + ), + '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'), + ), + ); + + $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 22:25:08 -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, type, id, 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.224 diff -u -p -r1.224 upload.module --- modules/upload/upload.module 2 Jan 2009 21:45:11 -0000 1.224 +++ modules/upload/upload.module 2 Jan 2009 22:25:09 -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); } }