Skip to content

Commit

Permalink
Merge pull request #2007 from bedita/feat/v4/clone-stream
Browse files Browse the repository at this point in the history
Add endpoint to clone an existing stream - BEdita 4
  • Loading branch information
batopa committed Apr 28, 2023
2 parents f0b3201 + 498667b commit c9f1dcf
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 7 deletions.
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -33,7 +33,7 @@
"cakephp/cakephp-codesniffer": "~3.2.1",
"psy/psysh": "@stable",
"bedita/dev-tools": "1.5.*",
"phpstan/phpstan": "^1.5",
"phpstan/phpstan": "^1.5, < 1.10.12 || ^1.11",
"phpunit/phpunit": "^6.5"
},
"autoload": {
Expand Down
5 changes: 5 additions & 0 deletions plugins/BEdita/API/config/routes.php
Expand Up @@ -118,6 +118,11 @@ function (RouteBuilder $routes) {
['controller' => 'Streams', 'action' => 'upload'],
['_name' => 'streams:upload', 'pass' => ['fileName']]
);
$routes->connect(
'/streams/clone/{uuid}',
['controller' => 'Streams', 'action' => 'clone'],
['_name' => 'streams:clone']
)->setPass(['uuid']);
$routes->connect(
'/media/thumbs/{id}',
['controller' => 'Media', 'action' => 'thumbs'],
Expand Down
28 changes: 28 additions & 0 deletions plugins/BEdita/API/src/Controller/StreamsController.php
Expand Up @@ -105,6 +105,34 @@ public function upload($fileName): void
);
}

/**
* Clone a Stream by its UUID.
*
* @param string $uuid ID of the Stream to clone.
* @return void
*/
public function clone(string $uuid): void
{
$data = $this->Table->clone($this->Table->get($uuid));

$this->set(compact('data'));
$this->setSerialize(['data']);

$this->response = $this->response
->withStatus(201)
->withHeader(
'Location',
Router::url(
[
'_name' => 'api:resources:resource',
'controller' => $this->name,
'id' => $data->get('uuid'),
],
true
)
);
}

/**
* Download a stream.
*
Expand Down
Expand Up @@ -242,4 +242,43 @@ public function testDownload(): void
$response = (string)$this->_response->getBody();
static::assertEquals(trim($response), 'Sample uploaded file.');
}

/**
* Test {@see \BEdita\API\Controller\StreamsController::clone()} action.
*
* @return void
* @covers ::clone()
*/
public function testClone(): void
{
$attributes = [
'file_name' => 'bedita-logo-gray.gif',
'mime_type' => 'image/gif',
];
$meta = [
'version' => 1,
'file_size' => 927,
'hash_md5' => 'a714dbb31ca89d5b1257245dfa5c5153',
'hash_sha1' => '444b2b42b48b0b815d70f6648f8a7a23d5faf54b',
];

$this->configRequestHeaders('POST', $this->getUserAuthHeader());
$this->post('/streams/clone/6aceb0eb-bd30-4f60-ac74-273083b921b6');

$this->assertResponseCode(201);
$this->assertContentType('application/vnd.api+json');

$response = json_decode((string)$this->_response->getBody(), true);
static::assertArrayHasKey('data', $response);

$id = $response['data']['id'];
$url = sprintf('http://api.example.com/streams/%s', $id);
static::assertTrue(Validation::uuid($id));
static::assertSame('streams', $response['data']['type']);
static::assertEquals($attributes, $response['data']['attributes']);
static::assertArraySubset($meta, $response['data']['meta']);
static::assertSame(sprintf('https://static.example.org/files/%s-%s', $id, $attributes['file_name']), $response['data']['meta']['url']);

$this->assertHeader('Location', $url);
}
}
29 changes: 27 additions & 2 deletions plugins/BEdita/Core/src/Model/Behavior/UploadableBehavior.php
Expand Up @@ -37,6 +37,9 @@ class UploadableBehavior extends Behavior
'contents' => 'contents',
],
],
'implementedMethods' => [
'copyFiles' => 'copyFiles',
],
];

/**
Expand Down Expand Up @@ -70,12 +73,15 @@ protected function write(MountManager $manager, $path, $contents)
*/
protected function processUpload(Entity $entity, $pathField, $contentsField)
{
if (!$entity->isDirty($pathField) && !$entity->isDirty($contentsField)) {
$manager = FilesystemRegistry::getMountManager();
if (
(!$entity->isDirty($pathField) || !$manager->has($entity->getOriginal($pathField)))
&& !$entity->isDirty($contentsField)
) {
// Nothing to do.
return true;
}

$manager = FilesystemRegistry::getMountManager();
$path = $entity->get($pathField);
$originalPath = $entity->getOriginal($pathField);
if ($entity->isDirty($pathField) && $originalPath !== $path) {
Expand Down Expand Up @@ -152,4 +158,23 @@ public function afterDelete(Event $event, Entity $entity)
$this->processDelete($entity, $file['path']);
}
}

/**
* Copy files from an entity to another.
*
* @param \Cake\ORM\Entity $src Source entity. It must have path fields set and referenced files must exist.
* @param \Cake\ORM\Entity $dest Destination entity. It must have path fields set.
* @return void
* @throws \League\Flysystem\FilesystemException
*/
public function copyFiles(Entity $src, Entity $dest): void
{
$manager = FilesystemRegistry::getMountManager();
foreach ($this->getConfig('files') as $file) {
$srcPath = $src->get($file['path']);
$destPath = $dest->get($file['path']);

$manager->copy($srcPath, $destPath);
}
}
}
13 changes: 13 additions & 0 deletions plugins/BEdita/Core/src/Model/Entity/Stream.php
Expand Up @@ -55,6 +55,19 @@ class Stream extends Entity implements JsonApiSerializable
use JsonApiTrait;
use LogTrait;

public const FILE_PROPERTIES = [
'file_name',
'mime_type',
'file_size',
'hash_md5',
'hash_sha1',
'width',
'height',
'duration',
'file_metadata',
'private_url',
];

/**
* @inheritDoc
*/
Expand Down
36 changes: 36 additions & 0 deletions plugins/BEdita/Core/src/Model/Table/StreamsTable.php
Expand Up @@ -32,6 +32,7 @@
* @method \BEdita\Core\Model\Entity\Stream[] patchEntities($entities, array $data, array $options = [])
* @method \BEdita\Core\Model\Entity\Stream findOrCreate($search, callable $callback = null, $options = [])
* @mixin \Cake\ORM\Behavior\TimestampBehavior
* @mixin \BEdita\Core\Model\Behavior\UploadableBehavior
* @since 4.0.0
*/
class StreamsTable extends Table
Expand Down Expand Up @@ -120,6 +121,19 @@ public function validationDefault(Validator $validator): Validator
return $validator;
}

/**
* Validator for cloning streams.
*
* @param \Cake\Validation\Validator $validator Validator instance.
* @return \Cake\Validation\Validator
* @codeCoverageIgnore
*/
public function validationClone(Validator $validator): Validator
{
return $this->validationDefault($validator)
->requirePresence('contents', false);
}

/**
* {@inheritDoc}
*
Expand Down Expand Up @@ -163,4 +177,26 @@ public function afterDelete(Event $event, Stream $stream)
{
Thumbnail::delete($stream);
}

/**
* Clone a stream.
*
* @param \BEdita\Core\Model\Entity\Stream $stream Stream to clone.
* @return \BEdita\Core\Model\Entity\Stream
*/
public function clone(Stream $stream): Stream
{
$clone = $this->newEntity($stream->extract(Stream::FILE_PROPERTIES), [
'accessibleFields' => array_fill_keys(Stream::FILE_PROPERTIES, true),
'validate' => 'clone',
]);
$clone->uri = $clone->filesystemPath();

return $this->getConnection()->transactional(function () use ($clone, $stream): Stream {
$clone = $this->saveOrFail($clone, ['atomic' => false]);
$this->copyFiles($stream, $clone);

return $this->get($clone->get('uuid'));
});
}
}
Expand Up @@ -30,7 +30,7 @@ class UploadableBehaviorTest extends TestCase
*
* @var \BEdita\Core\Model\Table\StreamsTable
*/
public $Streams;
protected $Streams;

/**
* Fixtures
Expand Down Expand Up @@ -70,7 +70,7 @@ public function tearDown(): void
*
* @return array
*/
public function afterSaveProvider()
public function afterSaveProvider(): array
{
$originalContents = "Sample uploaded file.\n";
$newContents = 'Modified contents.';
Expand Down Expand Up @@ -136,7 +136,7 @@ public function afterSaveProvider()
* @covers ::setVisibility()
* @covers ::write()
*/
public function testAfterSave(array $expected, array $data)
public function testAfterSave(array $expected, array $data): void
{
$manager = FilesystemRegistry::getMountManager();

Expand Down Expand Up @@ -167,7 +167,7 @@ public function testAfterSave(array $expected, array $data)
* @covers ::afterDelete()
* @covers ::processDelete()
*/
public function testAfterDelete()
public function testAfterDelete(): void
{
$manager = FilesystemRegistry::getMountManager();

Expand All @@ -178,4 +178,24 @@ public function testAfterDelete()

static::assertFalse($manager->has($path));
}

/**
* Test [@see \BEdita\Core\Model\Behavior\UploadableBehavior::copyFiles()} method..
*
* @return void
* @covers ::copyFiles()
*/
public function testCopyFiles(): void
{
$manager = FilesystemRegistry::getMountManager();

$stream = $this->Streams->get('9e58fa47-db64-4479-a0ab-88a706180d59');
$copy = $this->Streams->newEntity([]);
$copy->uri = $copy->filesystemPath();

$this->Streams->copyFiles($stream, $copy);

static::assertTrue($manager->has($copy->uri));
static::assertSame($manager->read($stream->uri), $manager->read($copy->uri));
}
}
Expand Up @@ -13,6 +13,7 @@

namespace BEdita\Core\Test\TestCase\Model\Table;

use BEdita\Core\Model\Entity\Stream;
use BEdita\Core\Model\Table\ObjectsTable;
use BEdita\Core\Test\Utility\TestFilesystemTrait;
use Cake\ORM\Association\BelongsTo;
Expand Down Expand Up @@ -251,4 +252,30 @@ public function testBeforeSaveNotNew()

static::assertSame($expected, $result);
}

/**
* Test {@see \BEdita\Core\Model\Table\StreamsTable::clone()} method.
*
* @param string $uuid UUID of the Stream to clone.
* @return void
* @testWith ["e5afe167-7341-458d-a1e6-042e8791b0fe"]
* ["9e58fa47-db64-4479-a0ab-88a706180d59"]
* ["6aceb0eb-bd30-4f60-ac74-273083b921b6"]
* @covers ::clone()
*/
public function testClone(string $uuid): void
{
$src = $this->Streams->get($uuid);
$expected = $src->extract(Stream::FILE_PROPERTIES);

$clone = $this->Streams->clone($src);
$actual = $clone->extract(Stream::FILE_PROPERTIES);

static::assertNotSame($src, $clone, 'Cloned stream is the same entity as the source stream');
static::assertTrue($this->Streams->exists(['uuid' => $clone->uuid]), 'Cloned stream has not been persisted');
static::assertNotSame($src->uuid, $clone->uuid, 'Cloned stream has the same UUID as the source stream');
static::assertNull($clone->object_id, 'Cloned stream must not be linked to any object');
static::assertSame($src->contents->getContents(), $clone->contents->getContents(), 'Cloned stream must have the same file contents');
static::assertSame($expected, $actual, 'Cloned stream must preserve property values');
}
}

0 comments on commit c9f1dcf

Please sign in to comment.