Commit fc5a0dbe authored by webchick's avatar webchick

Issue #1797478 by marcingy, chx: Make file usage storage pluggable.

parent e9ca778b
......@@ -921,7 +921,7 @@ function file_create_filename($basename, $directory) {
* The file id.
*
* @see file_unmanaged_delete()
* @see file_usage_list()
* @see file_usage()->listUsage()
*/
function file_delete($fid) {
return file_delete_multiple(array($fid));
......@@ -938,7 +938,7 @@ function file_delete($fid) {
* The file id.
*
* @see file_unmanaged_delete()
* @see file_usage_list()
* @see file_usage()->listUsage()
*/
function file_delete_multiple(array $fids) {
entity_delete_multiple('file', $fids);
......@@ -1052,7 +1052,7 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
* Saves a file upload to a new location.
*
* The file will be added to the {file_managed} table as a temporary file.
* Temporary files are periodically cleaned. Use file_usage_add() to register
* Temporary files are periodically cleaned. Use file_usage()->add() to register
* the usage of the file which will automatically mark it as permanent.
*
* @param $source
......
......@@ -219,7 +219,7 @@ function file_field_prepare_view($entity_type, $entities, $field, $instances, $l
function file_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
// Add a new usage of each uploaded file.
foreach ($items as $item) {
file_usage_add(file_load($item['fid']), 'file', $entity_type, $entity->id());
file_usage()->add(file_load($item['fid']), 'file', $entity_type, $entity->id());
}
}
......@@ -233,7 +233,7 @@ function file_field_update($entity_type, $entity, $field, $instance, $langcode,
// deletion of previous file usages are necessary.
if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
foreach ($items as $item) {
file_usage_add(file_load($item['fid']), 'file', $entity_type, $entity->id());
file_usage()->add(file_load($item['fid']), 'file', $entity_type, $entity->id());
}
return;
}
......@@ -252,7 +252,7 @@ function file_field_update($entity_type, $entity, $field, $instance, $langcode,
$original_fids[] = $original_item['fid'];
if (isset($original_item['fid']) && !in_array($original_item['fid'], $current_fids)) {
// Decrement the file usage count by 1.
file_usage_delete(file_load($original_item['fid']), 'file', $entity_type, $entity->id());
file_usage()->delete(file_load($original_item['fid']), 'file', $entity_type, $entity->id());
}
}
}
......@@ -260,7 +260,7 @@ function file_field_update($entity_type, $entity, $field, $instance, $langcode,
// Add new usage entries for newly added files.
foreach ($items as $item) {
if (!in_array($item['fid'], $original_fids)) {
file_usage_add(file_load($item['fid']), 'file', $entity_type, $entity->id());
file_usage()->add(file_load($item['fid']), 'file', $entity_type, $entity->id());
}
}
}
......@@ -271,7 +271,7 @@ function file_field_update($entity_type, $entity, $field, $instance, $langcode,
function file_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
// Delete all file usages within this entity.
foreach ($items as $delta => $item) {
file_usage_delete(file_load($item['fid']), 'file', $entity_type, $entity->id(), 0);
file_usage()->delete(file_load($item['fid']), 'file', $entity_type, $entity->id(), 0);
}
}
......@@ -281,7 +281,7 @@ function file_field_delete($entity_type, $entity, $field, $instance, $langcode,
function file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
foreach ($items as $delta => $item) {
// Decrement the file usage count by 1.
file_usage_delete(file_load($item['fid']), 'file', $entity_type, $entity->id());
file_usage()->delete(file_load($item['fid']), 'file', $entity_type, $entity->id());
}
}
......
......@@ -9,6 +9,8 @@
use Drupal\file\File;
use Drupal\Core\Template\Attribute;
use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\file\FileUsage\DatabaseFileUsageBackend;
use Drupal\file\FileUsage\FileUsageInterface;
// Load all Field module hooks for File.
require_once DRUPAL_ROOT . '/core/modules/file/file.field.inc';
......@@ -146,132 +148,12 @@ function file_load($fid) {
}
/**
* Determines where a file is used.
* Returns the file usage service.
*
* @param Drupal\file\File $file
* A file entity.
*
* @return
* A nested array with usage data. The first level is keyed by module name,
* the second by object type and the third by the object id. The value
* of the third level contains the usage count.
*
* @see file_usage_add()
* @see file_usage_delete()
*/
function file_usage_list(File $file) {
$result = db_select('file_usage', 'f')
->fields('f', array('module', 'type', 'id', 'count'))
->condition('fid', $file->fid)
->condition('count', 0, '>')
->execute();
$references = array();
foreach ($result as $usage) {
$references[$usage->module][$usage->type][$usage->id] = $usage->count;
}
return $references;
}
/**
* Records that a module is using a file.
*
* Examples:
* - A module that associates files with nodes, so $type would be
* 'node' and $id would be the node's nid. Files for all revisions are stored
* within a single nid.
* - The User module associates an image with a user, so $type would be 'user'
* and the $id would be the user's uid.
*
* @param Drupal\file\File $file
* A file entity.
* @param $module
* The name of the module using the file.
* @param $type
* The type of the object that contains the referenced file.
* @param $id
* The unique, numeric ID of the object containing the referenced file.
* @param $count
* (optional) The number of references to add to the object. Defaults to 1.
*
* @see file_usage_list()
* @see file_usage_delete()
* @return Drupal\file\FileUsage\FileUsageInterface.
*/
function file_usage_add(File $file, $module, $type, $id, $count = 1) {
db_merge('file_usage')
->key(array(
'fid' => $file->fid,
'module' => $module,
'type' => $type,
'id' => $id,
))
->fields(array('count' => $count))
->expression('count', 'count + :count', array(':count' => $count))
->execute();
// Make sure that a used file is permament.
if ($file->status != FILE_STATUS_PERMANENT) {
$file->status = FILE_STATUS_PERMANENT;
$file->save();
}
}
/**
* Removes a record to indicate that a module is no longer using a file.
*
* @param Drupal\file\File $file
* A file entity.
* @param $module
* The name of the module using the file.
* @param $type
* (optional) The type of the object that contains the referenced file. May
* be omitted if all module references to a file are being deleted.
* @param $id
* (optional) The unique, numeric ID of the object containing the referenced
* file. May be omitted if all module references to a file are being deleted.
* @param $count
* (optional) The number of references to delete from the object. Defaults to
* 1. 0 may be specified to delete all references to the file within a
* specific object.
*
* @see file_usage_add()
* @see file_usage_list()
*/
function file_usage_delete(File $file, $module, $type = NULL, $id = NULL, $count = 1) {
// Delete rows that have a exact or less value to prevent empty rows.
$query = db_delete('file_usage')
->condition('module', $module)
->condition('fid', $file->fid);
if ($type && $id) {
$query
->condition('type', $type)
->condition('id', $id);
}
if ($count) {
$query->condition('count', $count, '<=');
}
$result = $query->execute();
// If the row has more than the specified count decrement it by that number.
if (!$result && $count > 0) {
$query = db_update('file_usage')
->condition('module', $module)
->condition('fid', $file->fid);
if ($type && $id) {
$query
->condition('type', $type)
->condition('id', $id);
}
$query->expression('count', 'count - :count', array(':count' => $count));
$query->execute();
}
// If there are no more remaining usages of this file, mark it as temporary,
// which result in a delete through system_cron().
$usage = file_usage_list($file);
if (empty($usage)) {
$file->status = 0;
$file->save();
}
function file_usage() {
return drupal_container()->get('file.usage');
}
/**
......@@ -422,7 +304,7 @@ function file_move(File $source, $destination = NULL, $replace = FILE_EXISTS_REN
module_invoke_all('file_move', $file, $source);
// Delete the original if it's not in use elsewhere.
if ($delete_source && !file_usage_list($source)) {
if ($delete_source && !file_usage()->listUsage($source)) {
$source->delete();
}
......@@ -882,7 +764,7 @@ function file_cron() {
));
foreach ($result as $row) {
if ($file = file_load($row->fid)) {
$references = file_usage_list($file);
$references = file_usage()->listUsage($file);
if (empty($references)) {
if (file_exists($file->uri)) {
$file->delete();
......@@ -1235,7 +1117,7 @@ function file_managed_file_validate(&$element, &$form_state) {
if ($clicked_button != 'remove_button' && !empty($element['fid']['#value'])) {
if ($file = file_load($element['fid']['#value'])) {
if ($file->status == FILE_STATUS_PERMANENT) {
$references = file_usage_list($file);
$references = file_usage()->listUsage($file);
if (empty($references)) {
form_error($element, t('The file used in the !name field may not be referenced.', array('!name' => $element['#title'])));
}
......
<?php
/**
* @file
* Definition of Drupal\file\FileBundle.
*/
namespace Drupal\file;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class FileBundle extends Bundle {
public function build(ContainerBuilder $container) {
$container->register('file.usage', 'Drupal\file\FileUsage\DatabaseFileUsageBackend')
->addArgument(new Reference('database'));
}
}
<?php
/**
* @file
* Definition of Drupal\file\FileUsage\DatabaseFileUsageBackend.
*/
namespace Drupal\file\FileUsage;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\file\File;
/**
* Defines the database file usage backend. This is the default Drupal backend.
*/
class DatabaseFileUsageBackend extends FileUsageBase {
/**
* The database connection used to store file usage information.
*
* @var Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The name of the SQL table used to store file usage information.
*
* @var string
*/
protected $tableName;
/**
* Construct the DatabaseFileUsageBackend.
*
* @param Drupal\Core\Database\Connection $connection
* The database connection which will be used to store the file usage
* information.
* @param string $table
* (optional) The table to store file usage info. Defaults to 'file_usage'.
*/
public function __construct(Connection $connection, $table = 'file_usage') {
$this->connection = $connection;
$this->tableName = $table;
}
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::add().
*/
public function add(File $file, $module, $type, $id, $count = 1) {
$this->connection->merge($this->tableName)
->key(array(
'fid' => $file->fid,
'module' => $module,
'type' => $type,
'id' => $id,
))
->fields(array('count' => $count))
->expression('count', 'count + :count', array(':count' => $count))
->execute();
parent::add($file, $module, $type, $id, $count);
}
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::delete().
*/
public function delete(File $file, $module, $type = NULL, $id = NULL, $count = 1) {
// Delete rows that have a exact or less value to prevent empty rows.
$query = $this->connection->delete($this->tableName)
->condition('module', $module)
->condition('fid', $file->fid);
if ($type && $id) {
$query
->condition('type', $type)
->condition('id', $id);
}
if ($count) {
$query->condition('count', $count, '<=');
}
$result = $query->execute();
// If the row has more than the specified count decrement it by that number.
if (!$result && $count > 0) {
$query = $this->connection->update($this->tableName)
->condition('module', $module)
->condition('fid', $file->fid);
if ($type && $id) {
$query
->condition('type', $type)
->condition('id', $id);
}
$query->expression('count', 'count - :count', array(':count' => $count));
$query->execute();
}
parent::delete($file, $module, $type, $id, $count);
}
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::listUsage().
*/
public function listUsage(File $file) {
$result = $this->connection->select($this->tableName, 'f')
->fields('f', array('module', 'type', 'id', 'count'))
->condition('fid', $file->fid)
->condition('count', 0, '>')
->execute();
$references = array();
foreach ($result as $usage) {
$references[$usage->module][$usage->type][$usage->id] = $usage->count;
}
return $references;
}
}
<?php
/**
* @file
* Definition of Drupal\file\FileUsage\FileUsageBase.
*/
namespace Drupal\file\FileUsage;
use Drupal\file\File;
/**
* Defines the base class for database file usage backend.
*/
abstract class FileUsageBase implements FileUsageInterface {
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::add().
*/
public function add(File $file, $module, $type, $id, $count = 1) {
// Make sure that a used file is permament.
if ($file->status != FILE_STATUS_PERMANENT) {
$file->status = FILE_STATUS_PERMANENT;
$file->save();
}
}
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::delete().
*/
public function delete(File $file, $module, $type = NULL, $id = NULL, $count = 1) {
// If there are no more remaining usages of this file, mark it as temporary,
// which result in a delete through system_cron().
$usage = file_usage()->listUsage($file);
if (empty($usage)) {
$file->status = 0;
$file->save();
}
}
}
<?php
/**
* @file
* Definition of Drupal\file\FileUsage\FileUsageInterface.
*/
namespace Drupal\file\FileUsage;
use Drupal\file\File;
/**
* File usage backend interface.
*/
interface FileUsageInterface {
/**
* Records that a module is using a file.
*
* Examples:
* - A module that associates files with nodes, so $type would be
* 'node' and $id would be the node's nid. Files for all revisions are
* stored within a single nid.
* - The User module associates an image with a user, so $type would be 'user'
* and the $id would be the user's uid.
*
* @param Drupal\file\File $file
* A file entity.
* @param string $module
* The name of the module using the file.
* @param string $type
* The type of the object that contains the referenced file.
* @param int $id
* The unique, numeric ID of the object containing the referenced file.
* @param int $count
* (optional) The number of references to add to the object. Defaults to 1.
*/
public function add(File $file, $module, $type, $id, $count = 1);
/**
* Removes a record to indicate that a module is no longer using a file.
*
* @param Drupal\file\File $file
* A file entity.
* @param string $module
* The name of the module using the file.
* @param string $type
* (optional) The type of the object that contains the referenced file. May
* be omitted if all module references to a file are being deleted. Defaults
* to NULL.
* @param int $id
* (optional) The unique, numeric ID of the object containing the referenced
* file. May be omitted if all module references to a file are being
* deleted. Defaults to NULL.
* @param int $count
* (optional) The number of references to delete from the object. Defaults
* to 1. Zero may be specified to delete all references to the file within a
* specific object.
*/
public function delete(File $file, $module, $type = NULL, $id = NULL, $count = 1);
/**
* Determines where a file is used.
*
* @param Drupal\file\File $file
* A file entity.
*
* @return array
* A nested array with usage data. The first level is keyed by module name,
* the second by object type and the third by the object id. The value of
* the third level contains the usage count.
*
*/
public function listUsage(File $file);
}
......@@ -38,11 +38,11 @@ function testUnused() {
*/
function testInUse() {
$file = $this->createFile();
file_usage_add($file, 'testing', 'test', 1);
file_usage_add($file, 'testing', 'test', 1);
file_usage()->add($file, 'testing', 'test', 1);
file_usage()->add($file, 'testing', 'test', 1);
file_usage_delete($file, 'testing', 'test', 1);
$usage = file_usage_list($file);
file_usage()->delete($file, 'testing', 'test', 1);
$usage = file_usage()->listUsage($file);
$this->assertEqual($usage['testing']['test'], array(1 => 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.'));
......@@ -50,8 +50,8 @@ function testInUse() {
// Clear out the call to hook_file_load().
file_test_reset();
file_usage_delete($file, 'testing', 'test', 1);
$usage = file_usage_list($file);
file_usage()->delete($file, 'testing', 'test', 1);
$usage = file_usage()->listUsage($file);
$this->assertFileHooksCalled(array('load', 'update'));
$this->assertTrue(empty($usage), t('File usage data was removed.'));
$this->assertTrue(file_exists($file->uri), 'File still exists on the disk.');
......
......@@ -20,7 +20,7 @@ public static function getInfo() {
}
/**
* Tests file_usage_list().
* Tests file_usage()->listUsage().
*/
function testGetUsage() {
$file = $this->createFile();
......@@ -43,7 +43,7 @@ function testGetUsage() {
))
->execute();
$usage = file_usage_list($file);
$usage = file_usage()->listUsage($file);
$this->assertEqual(count($usage['testing']), 2, t('Returned the correct number of items.'));
$this->assertTrue(isset($usage['testing']['foo'][1]), t('Returned the correct id.'));
......@@ -53,15 +53,15 @@ function testGetUsage() {
}
/**
* Tests file_usage_add().
* Tests file_usage()->add().
*/
function testAddUsage() {
$file = $this->createFile();
file_usage_add($file, 'testing', 'foo', 1);
file_usage()->add($file, 'testing', 'foo', 1);
// Add the file twice to ensure that the count is incremented rather than
// creating additional records.
file_usage_add($file, 'testing', 'bar', 2);
file_usage_add($file, 'testing', 'bar', 2);
file_usage()->add($file, 'testing', 'bar', 2);
file_usage()->add($file, 'testing', 'bar', 2);
$usage = db_select('file_usage', 'f')
->fields('f')
......@@ -78,7 +78,7 @@ function testAddUsage() {
}
/**
* Tests file_usage_delete().
* Tests file_usage()->delete().
*/
function testRemoveUsage() {
$file = $this->createFile();
......@@ -93,7 +93,7 @@ function testRemoveUsage() {
->execute();
// Normal decrement.
file_usage_delete($file, 'testing', 'bar', 2);
file_usage()->delete($file, 'testing', 'bar', 2);
$count = db_select('file_usage', 'f')
->fields('f', array('count'))
->condition('f.fid', $file->fid)
......@@ -102,7 +102,7 @@ function testRemoveUsage() {
$this->assertEqual(2, $count, t('The count was decremented correctly.'));
// Multiple decrement and removal.
file_usage_delete($file, 'testing', 'bar', 2, 2);
file_usage()->delete($file, 'testing', 'bar', 2, 2);
$count = db_select('file_usage', 'f')
->fields('f', array('count'))
->condition('f.fid', $file->fid)
......@@ -111,7 +111,7 @@ function testRemoveUsage() {
$this->assertIdentical(FALSE, $count, t('The count was removed entirely when empty.'));
// Non-existent decrement.
file_usage_delete($file, 'testing', 'bar', 2);
file_usage()->delete($file, 'testing', 'bar', 2);
$count = db_select('file_usage', 'f')
->fields('f', array('count'))
->condition('f.fid', $file->fid)
......
......@@ -373,7 +373,7 @@ function image_field_delete_field($field) {
// The value of a managed_file element can be an array if #extended == TRUE.
$fid = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']);
if ($fid && ($file = file_load($fid))) {
file_usage_delete($file, 'image', 'default_image', $field['id']);
file_usage()->delete($file, 'image', 'default_image', $field['id']);
}
}
......@@ -397,12 +397,12 @@ function image_field_update_field($field, $prior_field, $has_data) {
if ($file_new) {
$file_new->status = FILE_STATUS_PERMANENT;
$file_new->save();
file_usage_add($file_new, 'image', 'default_image', $field['id']);
file_usage()->add($file_new, 'image', 'default_image', $field['id']);
}
// Is there an old file?
if ($fid_old && ($file_old = file_load($fid_old))) {
file_usage_delete($file_old, 'image', 'default_image', $field['id']);
file_usage()->delete($file_old, 'image', 'default_image', $field['id']);
}
}
......@@ -433,7 +433,7 @@ function image_field_delete_instance($instance) {
// Remove the default image when the instance is deleted.
if ($fid && ($file = file_load($fid))) {
file_usage_delete($file, 'image', 'default_image', $instance['id']);
file_usage()->delete($file, 'image', 'default_image', $instance['id']);
}
}
......@@ -465,11 +465,11 @@ function image_field_update_instance($instance, $prior_instance) {
if ($file_new) {
$file_new->status = FILE_STATUS_PERMANENT;
$file_new->save();
file_usage_add($file_new, 'image', 'default_image', $instance['id']);
file_usage()->add($file_new, 'image', 'default_image', $instance['id']);
}
// Delete the old file, if present.
if ($fid_old && ($file_old = file_load($fid_old))) {
file_usage_delete($file_old, 'image', 'default_image', $instance['id']);
file_usage()->delete($file_old, 'image', 'default_image', $instance['id']);
}
}
......
......@@ -33,6 +33,7 @@ public static function getInfo() {
* Test that services provided by module bundles get registered to the DIC.
*/
function testBundleRegistration() {
$this->assertTrue(drupal_container()->getDefinition('file.usage')->getClass() == 'Drupal\\bundle_test\\TestFileUsage', 'Class has been changed');
$this->assertTrue(drupal_container()->has('bundle_test_class'), 'The bundle_test_class service has been registered to the DIC');
// The event subscriber method in the test class calls drupal_set_message with
// a message saying it has fired. This will fire on every page request so it
......
......@@ -21,6 +21,9 @@ public function build(ContainerBuilder $container) {
$container->register('bundle_test_class', 'Drupal\bundle_test\TestClass')
->addTag('kernel.event_subscriber');
// Override a default bundle used by core to a dummy class.
$container->register('file.usage', 'Drupal\bundle_test\TestFileUsage');
// @todo Remove when the 'kernel.event_subscriber' tag above is made to
// work: http://drupal.org/node/1706064.
$container->get('dispatcher')->addSubscriber($container->get('bundle_test_class'));
......
<?php
/**
* @file
* Definition of Drupal\bundle_test\TestFileUsage.
*/
namespace Drupal\bundle_test;
use Drupal\file\File;
class TestFileUsage extends FileUsageBase {
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::add().
*/
public function add(File $file, $module, $type, $id, $count = 1) {
}
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::delete().
*/
public function delete(File $file, $module, $type = NULL, $id = NULL, $count = 1) {
}
/**
* Implements Drupal\file\FileUsage\FileUsageInterface::listUsage().
*/
public function listUsage(File $file) {
}
}
......@@ -105,7 +105,7 @@ protected function preSave(EntityInterface $entity) {
// and no replacement was submitted.
elseif (!empty($entity->picture_delete)) {
$entity->picture = 0;
file_usage_delete($entity->original->picture, 'user', 'user', $entity->uid);
file_usage()->delete($entity->original->picture, 'user', 'user', $entity->uid);
file_delete($entity->original->picture->fid);