mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #6484 from open-sausages/pulls/unique-folder-enforced
Enforce unique Folder names, use AssetNameGenerator
This commit is contained in:
commit
e6ae532998
@ -808,6 +808,7 @@ specific functions.
|
||||
* `Injector::load` given a `src` parameter will no longer guess the
|
||||
service name from the filename. Now the service name will either
|
||||
by the array key, or the `class` parameter value.
|
||||
* Uniqueness checks for `File.Name` is performed on write only (not in `setName()`)
|
||||
|
||||
#### <a name="overview-general-removed"></a>General and Core Removed API
|
||||
|
||||
|
@ -10,6 +10,7 @@ use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Forms\DatetimeField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
@ -639,12 +640,65 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
|
||||
$this->OwnerID = Member::currentUserID();
|
||||
}
|
||||
|
||||
// Set default name
|
||||
if (!$this->getField('Name')) {
|
||||
$this->Name ="new-" . strtolower($this->class);
|
||||
$name = $this->getField('Name');
|
||||
$title = $this->getField('Title');
|
||||
|
||||
$changed = $this->isChanged('Name');
|
||||
|
||||
// Name can't be blank, default to Title
|
||||
if (!$name) {
|
||||
$changed = true;
|
||||
$name = $title;
|
||||
}
|
||||
|
||||
// Propegate changes to the AssetStore and update the DBFile field
|
||||
$filter = FileNameFilter::create();
|
||||
if ($name) {
|
||||
// Fix illegal characters
|
||||
$name = $filter->filter($name);
|
||||
} else {
|
||||
// Default to file name
|
||||
$changed = true;
|
||||
$name = $this->i18n_singular_name();
|
||||
$name = $filter->filter($name);
|
||||
}
|
||||
|
||||
// Check for duplicates when the name has changed (or is set for the first time)
|
||||
if ($changed) {
|
||||
$nameGenerator = $this->getNameGenerator($name);
|
||||
// Defaults to returning the original filename on first iteration
|
||||
foreach ($nameGenerator as $newName) {
|
||||
// This logic is also used in the Folder subclass, but we're querying
|
||||
// for duplicates on the File base class here (including the Folder subclass).
|
||||
|
||||
// TODO Add read lock to avoid other processes creating files with the same name
|
||||
// before this process has a chance to persist in the database.
|
||||
$existingFile = File::get()->filter(array(
|
||||
'Name' => $newName,
|
||||
'ParentID' => (int) $this->ParentID
|
||||
))->exclude(array(
|
||||
'ID' => $this->ID
|
||||
))->first();
|
||||
if (!$existingFile) {
|
||||
$name = $newName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update actual field value
|
||||
$this->setField('Name', $name);
|
||||
|
||||
// Update title
|
||||
if (!$title) {
|
||||
// Generate a readable title, dashes and underscores replaced by whitespace,
|
||||
// and any file extensions removed.
|
||||
$this->setField(
|
||||
'Title',
|
||||
str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $name))
|
||||
);
|
||||
}
|
||||
|
||||
// Propagate changes to the AssetStore and update the DBFile field
|
||||
$this->updateFilesystem();
|
||||
|
||||
parent::onBeforeWrite();
|
||||
@ -715,62 +769,14 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter function for Name. Automatically sets a default title,
|
||||
* and removes characters that might be invalid on the filesystem.
|
||||
* Also adds a suffix to the name if the filename already exists
|
||||
* on the filesystem, and is associated to a different {@link File} database record
|
||||
* in the same folder. This means "myfile.jpg" might become "myfile-1.jpg".
|
||||
* Get an asset renamer for the given filename.
|
||||
*
|
||||
* Does not change the filesystem itself, please use {@link write()} for this.
|
||||
*
|
||||
* @param string $name
|
||||
* @return $this
|
||||
* @param string $filename Path name
|
||||
* @return AssetNameGenerator
|
||||
*/
|
||||
public function setName($name)
|
||||
protected function getNameGenerator($filename)
|
||||
{
|
||||
$oldName = $this->Name;
|
||||
|
||||
// It can't be blank, default to Title
|
||||
if (!$name) {
|
||||
$name = $this->Title;
|
||||
}
|
||||
|
||||
// Fix illegal characters
|
||||
$filter = FileNameFilter::create();
|
||||
$name = $filter->filter($name);
|
||||
|
||||
// We might have just turned it blank, so check again.
|
||||
if (!$name) {
|
||||
$name = 'new-folder';
|
||||
}
|
||||
|
||||
// If it's changed, check for duplicates
|
||||
if ($oldName && $oldName != $name) {
|
||||
$base = pathinfo($name, PATHINFO_FILENAME);
|
||||
$ext = self::get_file_extension($name);
|
||||
$suffix = 1;
|
||||
|
||||
while (File::get()->filter(array(
|
||||
'Name' => $name,
|
||||
'ParentID' => (int) $this->ParentID
|
||||
))->exclude(array(
|
||||
'ID' => $this->ID
|
||||
))->first()
|
||||
) {
|
||||
$suffix++;
|
||||
$name = "$base-$suffix.$ext";
|
||||
}
|
||||
}
|
||||
|
||||
// Update actual field value
|
||||
$this->setField('Name', $name);
|
||||
|
||||
// Update title
|
||||
if (!$this->Title) {
|
||||
$this->Title = str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $name));
|
||||
}
|
||||
|
||||
return $this;
|
||||
return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,7 +77,8 @@ class FileNameFilter extends Object
|
||||
// Safeguard against empty file names
|
||||
$nameWithoutExt = pathinfo($name, PATHINFO_FILENAME);
|
||||
if (empty($nameWithoutExt)) {
|
||||
$name = $this->getDefaultName() . '.' . $ext;
|
||||
$name = $this->getDefaultName();
|
||||
$name .= $ext ? '.' . $ext : '';
|
||||
}
|
||||
|
||||
return $name;
|
||||
|
@ -129,7 +129,9 @@ class Folder extends File
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->setName($title);
|
||||
$this->setField('Title', $title);
|
||||
$this->setField('Name', $title);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -143,21 +145,6 @@ class Folder extends File
|
||||
return $this->Name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override setting the Title of Folders to that Name and Title are always in sync.
|
||||
* Note that this is not appropriate for files, because someone might want to create a human-readable name
|
||||
* of a file that is different from its name on disk. But folders should always match their name on disk.
|
||||
*
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
parent::setName($name);
|
||||
$this->setField('Title', $this->Name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A folder doesn't have a (meaningful) file size.
|
||||
*
|
||||
|
@ -142,4 +142,11 @@ class FileNameFilterTest extends SapphireTest
|
||||
$filter = new FileNameFilter();
|
||||
$this->assertEquals('test-document.txt', $filter->filter($name));
|
||||
}
|
||||
|
||||
public function testDoesntAddExtensionWhenMissing()
|
||||
{
|
||||
$name = 'no-extension';
|
||||
$filter = new FileNameFilter();
|
||||
$this->assertEquals('no-extension', $filter->filter($name));
|
||||
}
|
||||
}
|
||||
|
@ -535,6 +535,88 @@ class FileTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testRenamesDuplicateFilesInSameFolder()
|
||||
{
|
||||
$original = new File();
|
||||
$original->update([
|
||||
'Name' => 'file1.txt',
|
||||
'ParentID' => 0
|
||||
]);
|
||||
$original->write();
|
||||
|
||||
$duplicate = new File();
|
||||
$duplicate->update([
|
||||
'Name' => 'file1.txt',
|
||||
'ParentID' => 0
|
||||
]);
|
||||
$duplicate->write();
|
||||
|
||||
$original = File::get()->byID($original->ID);
|
||||
|
||||
$this->assertEquals($original->Name, 'file1.txt');
|
||||
$this->assertEquals($original->Title, 'file1');
|
||||
$this->assertEquals($duplicate->Name, 'file1-v2.txt');
|
||||
$this->assertEquals($duplicate->Title, 'file1 v2');
|
||||
}
|
||||
|
||||
public function testSetsEmptyTitleToNameWithoutExtensionAndSpecialCharacters()
|
||||
{
|
||||
$fileWithTitle = new File();
|
||||
$fileWithTitle->update([
|
||||
'Name' => 'file1-with-title.txt',
|
||||
'Title' => 'Some Title'
|
||||
]);
|
||||
$fileWithTitle->write();
|
||||
|
||||
$this->assertEquals($fileWithTitle->Name, 'file1-with-title.txt');
|
||||
$this->assertEquals($fileWithTitle->Title, 'Some Title');
|
||||
|
||||
$fileWithoutTitle = new File();
|
||||
$fileWithoutTitle->update([
|
||||
'Name' => 'file1-without-title.txt',
|
||||
]);
|
||||
$fileWithoutTitle->write();
|
||||
|
||||
$this->assertEquals($fileWithoutTitle->Name, 'file1-without-title.txt');
|
||||
$this->assertEquals($fileWithoutTitle->Title, 'file1 without title');
|
||||
}
|
||||
|
||||
public function testSetsEmptyNameToSingularNameWithoutTitle()
|
||||
{
|
||||
$fileWithTitle = new File();
|
||||
$fileWithTitle->update([
|
||||
'Name' => '',
|
||||
'Title' => 'Some Title',
|
||||
]);
|
||||
$fileWithTitle->write();
|
||||
|
||||
$this->assertEquals($fileWithTitle->Name, 'Some-Title');
|
||||
$this->assertEquals($fileWithTitle->Title, 'Some Title');
|
||||
|
||||
$fileWithoutTitle = new File();
|
||||
$fileWithoutTitle->update([
|
||||
'Name' => '',
|
||||
'Title' => '',
|
||||
]);
|
||||
$fileWithoutTitle->write();
|
||||
|
||||
$this->assertEquals($fileWithoutTitle->Name, $fileWithoutTitle->i18n_singular_name());
|
||||
$this->assertEquals($fileWithoutTitle->Title, $fileWithoutTitle->i18n_singular_name());
|
||||
}
|
||||
|
||||
public function testSetsEmptyNameToTitleIfPresent()
|
||||
{
|
||||
$file = new File();
|
||||
$file->update([
|
||||
'Name' => '',
|
||||
'Title' => 'file1',
|
||||
]);
|
||||
$file->write();
|
||||
|
||||
$this->assertEquals($file->Name, 'file1');
|
||||
$this->assertEquals($file->Title, 'file1');
|
||||
}
|
||||
|
||||
public function testSetsOwnerOnFirstWrite()
|
||||
{
|
||||
Session::set('loggedInAs', null);
|
||||
|
@ -76,6 +76,30 @@ class FolderTest extends SapphireTest
|
||||
$this->assertEquals($folder1->Filename . 'CreateFromNameAndParentID/', $newFolder->Filename);
|
||||
}
|
||||
|
||||
public function testRenamesDuplicateFolders()
|
||||
{
|
||||
$original = new Folder();
|
||||
$original->update([
|
||||
'Name' => 'folder1',
|
||||
'ParentID' => 0
|
||||
]);
|
||||
$original->write();
|
||||
|
||||
$duplicate = new Folder();
|
||||
$duplicate->update([
|
||||
'Name' => 'folder1',
|
||||
'ParentID' => 0
|
||||
]);
|
||||
$duplicate->write();
|
||||
|
||||
$original = Folder::get()->byID($original->ID);
|
||||
|
||||
$this->assertEquals($original->Name, 'folder1');
|
||||
$this->assertEquals($original->Title, 'folder1');
|
||||
$this->assertEquals($duplicate->Name, 'folder1-v2');
|
||||
$this->assertEquals($duplicate->Title, 'folder1-v2');
|
||||
}
|
||||
|
||||
public function testAllChildrenIncludesFolders()
|
||||
{
|
||||
$folder1 = $this->objFromFixture(Folder::class, 'folder1');
|
||||
@ -255,14 +279,18 @@ class FolderTest extends SapphireTest
|
||||
|
||||
$newFolder->Name = 'TestNameCopiedToTitle';
|
||||
$this->assertEquals($newFolder->Name, $newFolder->Title);
|
||||
$this->assertEquals($newFolder->Title, 'TestNameCopiedToTitle');
|
||||
|
||||
$newFolder->Title = 'TestTitleCopiedToName';
|
||||
$this->assertEquals($newFolder->Name, $newFolder->Title);
|
||||
$this->assertEquals($newFolder->Title, 'TestTitleCopiedToName');
|
||||
|
||||
$newFolder->Name = 'TestNameWithIllegalCharactersCopiedToTitle <!BANG!>';
|
||||
$this->assertEquals($newFolder->Name, $newFolder->Title);
|
||||
$this->assertEquals($newFolder->Title, 'TestNameWithIllegalCharactersCopiedToTitle <!BANG!>');
|
||||
|
||||
$newFolder->Title = 'TestTitleWithIllegalCharactersCopiedToName <!BANG!>';
|
||||
$this->assertEquals($newFolder->Name, $newFolder->Title);
|
||||
$this->assertEquals($newFolder->Title, 'TestTitleWithIllegalCharactersCopiedToName <!BANG!>');
|
||||
}
|
||||
}
|
||||
|
@ -104,4 +104,32 @@ class DefaultAssetNameGeneratorTest extends SapphireTest
|
||||
$this->assertEquals('folder/MyFile-v99.jpg', $suggestions[98]);
|
||||
$this->assertNotEquals('folder/MyFile-v100.jpg', $suggestions[99]);
|
||||
}
|
||||
|
||||
public function testFolderWithoutDefaultPrefix()
|
||||
{
|
||||
Config::inst()->update(DefaultAssetNameGenerator::class, 'version_prefix', '');
|
||||
$generator = new DefaultAssetNameGenerator('folder/subfolder');
|
||||
$suggestions = iterator_to_array($generator);
|
||||
|
||||
// Expect 100 suggestions
|
||||
$this->assertEquals(100, count($suggestions));
|
||||
|
||||
// First item is always the same as input
|
||||
$this->assertEquals('folder/subfolder', $suggestions[0]);
|
||||
$this->assertEquals('folder/subfolder2', $suggestions[1]);
|
||||
}
|
||||
|
||||
public function testFolderWithDefaultPrefix()
|
||||
{
|
||||
Config::inst()->update(DefaultAssetNameGenerator::class, 'version_prefix', '-v');
|
||||
$generator = new DefaultAssetNameGenerator('folder/subfolder');
|
||||
$suggestions = iterator_to_array($generator);
|
||||
|
||||
// Expect 100 suggestions
|
||||
$this->assertEquals(100, count($suggestions));
|
||||
|
||||
// First item is always the same as input
|
||||
$this->assertEquals('folder/subfolder', $suggestions[0]);
|
||||
$this->assertEquals('folder/subfolder-v2', $suggestions[1]);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user