Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Add TUS file upload #106

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5ebff00
FEATURE: Add first draft of tus upload backend
daniellienert Nov 10, 2021
54632b7
TASK: Add tus-php to composer.json
daniellienert Nov 10, 2021
f87a494
TASK: Fullfill cache backend method interfaces
daniellienert Nov 10, 2021
86f15f3
TASK: Change method headers to please PHPStan
daniellienert Nov 10, 2021
d7dbfca
TASK: Optinally accept rsource identifier
daniellienert Nov 10, 2021
c3dc792
TASK: Set upload directorxy path
daniellienert Nov 10, 2021
2ee5eb9
TASK: adjust upload files hook to use tus client
andrehoffmann30 Nov 10, 2021
391cea2
TASK: Split maximimFileUploadSize from MaxChunkUploadSize
daniellienert Nov 10, 2021
cf770ee
TASK: Unify configuration setting keys
daniellienert Nov 10, 2021
df48123
TASK: Remove complete uploaded file after importing
daniellienert Nov 10, 2021
cf8bbbf
TASK: adjust media ui graphql schema for new config variables
andrehoffmann30 Nov 10, 2021
57c394e
TASK: Mute PhpStan complaint
daniellienert Nov 10, 2021
b3e889d
TASK: Refactor configuration into own service
daniellienert Nov 10, 2021
553a3bf
BUGFIX: Fix logical error and conversion in getMaximumUploadChunkSize
daniellienert Nov 11, 2021
d7d90a6
BUGFIX: Correct Status code and do not create asset twice
daniellienert Nov 12, 2021
6d7cd01
FEATURE: refactor upload files hook, add progress info and indicator
andrehoffmann30 Nov 17, 2021
df7b3c2
TASK: add progress bar animation, add production build of assets
andrehoffmann30 Nov 18, 2021
bfefdff
TASK: Use flow cache adapter again
daniellienert Nov 19, 2021
d5f78ab
BUGFIX: Fix maximumUploadFileSize variable
daniellienert Nov 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions Classes/Controller/MediaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,38 @@
* source code.
*/

use Flowpack\Media\Ui\Service\ConfigurationService;
use Flowpack\Media\Ui\Tus\PartialUploadFileCacheAdapter;
use Flowpack\Media\Ui\Tus\TusEventHandler;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Log\Utility\LogEnvironment;
use Neos\Flow\Utility\Environment;
use Neos\Flow\Utility\Exception;
use Neos\Fusion\View\FusionView;
use Neos\Neos\Controller\Module\AbstractModuleController;
use Neos\Utility\Exception\FilesException;
use Neos\Utility\Files;
use Psr\Log\LoggerInterface;
use TusPhp\Events\TusEvent;
use TusPhp\Tus\Server;

/**
* @Flow\Scope("singleton")
*/
class MediaController extends AbstractModuleController
{
/**
* @Flow\Inject
* @var TusEventHandler
*/
protected $tusEventHandler;

/**
* @Flow\Inject
* @var Environment
*/
protected $environment;

/**
* @var FusionView
*/
Expand All @@ -33,6 +56,24 @@ class MediaController extends AbstractModuleController
*/
protected $defaultViewObjectName = FusionView::class;

/**
* @Flow\Inject
* @var PartialUploadFileCacheAdapter
*/
protected $partialUploadFileCacheAdapater;

/**
* @Flow\Inject
* @var ConfigurationService
*/
protected $configurationService;

/**
* @Flow\Inject
* @var LoggerInterface
*/
protected $logger;

/**
* @var array
*/
Expand All @@ -46,4 +87,40 @@ class MediaController extends AbstractModuleController
public function indexAction(): void
{
}

/**
* @throws Exception
* @throws FilesException
* @Flow\SkipCsrfProtection
*/
public function uploadAction(): string
{
$uploadDirectory = Files::concatenatePaths([$this->environment->getPathToTemporaryDirectory(), 'TusUpload']);
if (!file_exists($uploadDirectory)) {
Files::createDirectoryRecursively($uploadDirectory);
}

$server = new Server($this->partialUploadFileCacheAdapater);
$server->setApiPath($this->controllerContext->getRequest()->getHttpRequest()->getUri()->getPath())/** @phpstan-ignore-line */
->setUploadDir($uploadDirectory)
->setMaxUploadSize($this->configurationService->getMaximumUploadFileSize())
->event()
->addListener('tus-server.upload.complete', function (TusEvent $event) {
$this->tusEventHandler->processUploadedFile($event);
});

$server->event()->addListener('tus-server.upload.created', function (TusEvent $event) {
$this->logger->debug(sprintf('A new TUS file upload session was started for file "%s"', $event->getFile()->getName()), LogEnvironment::fromMethodName(__METHOD__));
});

$server->event()->addListener('tus-server.upload.progress', function (TusEvent $event) {
$this->logger->debug(sprintf('Resumed TUS file upload for file "%s"', $event->getFile()->getName()), LogEnvironment::fromMethodName(__METHOD__));
});

$response = $server->serve();
$this->controllerContext->getResponse()->setStatusCode($response->getStatusCode());

$response->send();
return '';
}
}
45 changes: 14 additions & 31 deletions Classes/GraphQL/Resolver/Type/QueryResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Flowpack\Media\Ui\Exception as MediaUiException;
use Flowpack\Media\Ui\GraphQL\Context\AssetSourceContext;
use Flowpack\Media\Ui\Service\AssetChangeLog;
use Flowpack\Media\Ui\Service\ConfigurationService;
use Flowpack\Media\Ui\Service\SimilarityService;
use Flowpack\Media\Ui\Service\UsageDetailsService;
use Neos\Flow\Annotations as Flow;
Expand Down Expand Up @@ -94,6 +95,12 @@ class QueryResolver implements ResolverInterface
*/
protected $persistenceManager;

/**
* @Flow\Inject
* @var ConfigurationService
*/
protected $configurationService;

/**
* @Flow\InjectConfiguration(package="Flowpack.Media.Ui")
* @var array
Expand Down Expand Up @@ -136,7 +143,8 @@ public function assetCount($_, array $variables, AssetSourceContext $assetSource
protected function createAssetProxyQuery(
array $variables,
AssetSourceContext $assetSourceContext
): ?AssetProxyQueryInterface {
): ?AssetProxyQueryInterface
{
[
'assetSourceId' => $assetSourceId,
'tagId' => $tagId,
Expand Down Expand Up @@ -260,39 +268,13 @@ public function assetUsageCount($_, array $variables, AssetSourceContext $assetS
public function config($_): array
{
return [
'uploadMaxFileSize' => $this->getMaximumFileUploadSize(),
'uploadMaxFileUploadLimit' => $this->getMaximumFileUploadLimit(),
'maximumUploadChunkSize' => $this->configurationService->getMaximumUploadChunkSize(),
'maximumUploadFileSize' => $this->configurationService->getMaximumUploadFileSize(),
'maximumUploadFileCount' => $this->configurationService->getMaximumUploadFileCount(),
'currentServerTime' => (new \DateTime())->format(DATE_W3C),
];
}

/**
* Returns the lowest configured maximum upload file size
*
* @return int
*/
protected function getMaximumFileUploadSize(): int
{
try {
return (int)min(
Files::sizeStringToBytes(ini_get('post_max_size')),
Files::sizeStringToBytes(ini_get('upload_max_filesize'))
);
} catch (FilesException $e) {
return 0;
}
}

/**
* Returns the maximum number of files that can be uploaded
*
* @return int
*/
protected function getMaximumFileUploadLimit(): int
{
return (int)($this->settings['maximumFileUploadLimit'] ?? 10);
}

/**
* Provides a filterable list of asset proxies. These are the main entities for media management.
*
Expand All @@ -305,7 +287,8 @@ public function assets(
$_,
array $variables,
AssetSourceContext $assetSourceContext
): ?AssetProxyQueryResultInterface {
): ?AssetProxyQueryResultInterface
{
['limit' => $limit, 'offset' => $offset] = $variables + ['limit' => 20, 'offset' => 0];
$query = $this->createAssetProxyQuery($variables, $assetSourceContext);

Expand Down
69 changes: 69 additions & 0 deletions Classes/Service/ConfigurationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);

namespace Flowpack\Media\Ui\Service;

/*
* This file is part of the Flowpack.Media.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\Utility\Exception\FilesException;
use Neos\Utility\Files;

class ConfigurationService
{
/**
* @Flow\InjectConfiguration(package="Flowpack.Media.Ui")
* @var array
*/
protected $configuration;

/**
* Returns the maximum size of files that can be uploaded
*
* @return int
*/
public function getMaximumUploadFileSize(): int
{
try {
return (int)Files::sizeStringToBytes($this->configuration['maximumUploadFileSize'] ?? '100MB');
} catch (FilesException $e) {
return 0;
}
}

/**
* Returns the maximum of server capable upload size and configured maximum chunk size
*
* @return int
*/
public function getMaximumUploadChunkSize(): int
{
try {
return min(
(int)(Files::sizeStringToBytes($this->configuration['maximumUploadChunkSize']) ?? '5MB'),
(int)Files::sizeStringToBytes(ini_get('post_max_size')),
(int)Files::sizeStringToBytes(ini_get('upload_max_filesize'))
);
} catch (FilesException $e) {
return 5 * 1024 * 1024;
}
}

/**
* Returns the maximum number of files that can be uploaded
*
* @return int
*/
public function getMaximumUploadFileCount(): int
{
return (int)($this->configuration['maximumUploadFileCount'] ?? 10);
}
}
4 changes: 4 additions & 0 deletions Classes/Service/UsageDetailsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Flowpack\Media\Ui\Domain\Model\Dto\AssetUsageDetails;
use Flowpack\Media\Ui\Exception;
use GuzzleHttp\Psr7\ServerRequest;
Expand Down Expand Up @@ -353,6 +355,8 @@ protected function getAssetVariantFilterClause(string $alias): string
* Returns number of assets which have no usage reference provided by `Flowpack.EntityUsage`
*
* @throws Exception
* @throws NoResultException
* @throws NonUniqueResultException
*/
public function getUnusedAssetCount(): int
{
Expand Down
108 changes: 108 additions & 0 deletions Classes/Tus/PartialUploadFileCacheAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);

namespace Flowpack\Media\Ui\Tus;

/*
* This file is part of the Flowpack.Media.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Carbon\Carbon;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dependency doesn't seem to be included in the composer.json. Maybe we can get rid of this and replace the one method used?

use Neos\Cache\Frontend\StringFrontend;
use Neos\Utility\Exception\PropertyNotAccessibleException;
use Neos\Utility\ObjectAccess;
use TusPhp\Cache\Cacheable;

class PartialUploadFileCacheAdapter implements Cacheable
{

/**
* @var StringFrontend
*/
protected $partialUploadFileCache;

/**
* @param string $key
* @param bool $withExpired
* @return mixed|null
* @throws \JsonException
*/
public function get(string $key, bool $withExpired = false)
{
$contents = $this->partialUploadFileCache->get($key);
if (!is_string($contents)) {
return null;
}

$contents = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);

if ($withExpired) {
return $contents;
}

if (!$contents) {
return null;
}

$isExpired = Carbon::parse($contents['expires_at'])->lt(Carbon::now());

return $isExpired ? null : $contents;
}

public function set(string $key, $value)
{
$contents = $this->get($key) ?? [];

if (\is_array($value)) {
$contents = $value + $contents;
} else {
$contents[] = $value;
}

$this->partialUploadFileCache->set($this->getPrefix() . $key, json_encode($contents));

return true;
}

public function delete(string $key): bool
{
return $this->partialUploadFileCache->remove($key);
}

public function deleteAll(array $keys): bool
{
$this->partialUploadFileCache->flush();
return true;
}

/**
* @throws PropertyNotAccessibleException
*/
public function getTtl(): int
{
return 60*60*24;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should go right?

return (int)ObjectAccess::getProperty($this->partialUploadFileCache->getBackend(), 'defaultLifetime', true);
}

public function keys(): array
{
// @todo implement a replacement for keys() for flow cache backends
return [];
}

public function setPrefix(string $prefix): Cacheable
{
return $this;
}

public function getPrefix(): string
{
return '';
}
}
Loading