Skip to content

Commit

Permalink
Add asset proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
apfelbox committed Nov 21, 2024
1 parent 9ce7e1e commit 4dc239e
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ storyblok.webhook:
controller: Torr\Storyblok\Webhook\Controller\WebhookController::webhookEndpoint
defaults:
urlSecret: ~

storyblok.asset-proxy:
path: /asset/{width}x{height}/{assetId}/{filename}
controller: Torr\Storyblok\Assets\Controller\AssetProxyController::proxyAsset
requirements:
width: \d*
height: \d*
assetId: \w+
3 changes: 3 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ services:
$localeLevel: !abstract set via config
$webhookSecret: !abstract set via config
$allowUrlWebhookSecret: !abstract set via config

Torr\Storyblok\Assets\Proxy\AssetProxy:
$storagePath: '%kernel.project_dir%/var/storyblok/assets'
40 changes: 40 additions & 0 deletions src/Assets/Controller/AssetProxyController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);

namespace Torr\Storyblok\Assets\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Torr\Storyblok\Assets\Proxy\AssetProxy;

/**
* @final
*/
class AssetProxyController extends AbstractController
{
public function proxyAsset (
AssetProxy $assetProxy,
Request $request,
string $width,
string $height,
string $assetId,
string $filename,
) : Response
{
$path = \sprintf("%sx%s/%s/%s", $width, $height, $assetId, $filename);
$filePath = $assetProxy->getFilePath($path);

if (null === $filePath)
{
throw $this->createNotFoundException("File not found");
}

return $this->file(
$filePath,
disposition: $request->query->has("download")
? ResponseHeaderBag::DISPOSITION_ATTACHMENT
: ResponseHeaderBag::DISPOSITION_INLINE,
);
}
}
126 changes: 126 additions & 0 deletions src/Assets/Prox/AssetProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php declare(strict_types=1);

namespace Torr\Storyblok\Assets\Proxy;

use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Torr\Storyblok\Api\ContentApi;
use Torr\Storyblok\Config\StoryblokConfig;

/**
* @final
*/
readonly class AssetProxy
{
public function __construct (
private StoryblokConfig $config,
private HttpClientInterface $client,
private ContentApi $api,
private Filesystem $filesystem,
private string $storagePath,
private LoggerInterface $logger,
) {}

/**
* Returns the file path of the proxied file
*/
public function getFilePath (string $path) : ?string
{
$targetPath = Path::join($this->storagePath, $path);

if (!is_file($targetPath))
{
$originUrl = \sprintf(
"https://a.storyblok.com/f/%s/%s?cv=%s",
$this->config->getSpaceId(),
ltrim($path, "/"),
$this->api->getSpaceInfo()->getCacheVersion(),
);

$this->logger->debug("Fetch proxied storyblok asset", [
"targetPath" => $targetPath,
"originUrl" => $originUrl,
]);

$success = $this->fetchFileFromOrigin($originUrl, $targetPath);

return $success
? $targetPath
: null;
}

$this->logger->debug("Found existing proxied storyblok asset", [
"targetPath" => $targetPath,
]);

return $targetPath;
}

/**
* Fetches a fresh file from the origin
*
* @return bool whether the file was download correctly
*/
private function fetchFileFromOrigin (
string $originUrl,
string $targetPath,
) : bool
{
// ensure directory exists
$this->filesystem->mkdir(\dirname($targetPath));

// open file handle
$fileHandle = fopen($targetPath, "wb+");

if (!$fileHandle)
{
$this->logger->critical("Could not create proxied asset file on disk: {targetPath}", [
"targetPath" => $targetPath,
]);

return false;
}

try
{
// start request
$response = $this->client->request(
"GET",
$originUrl,
(new HttpOptions())
->buffer(false)
->toArray(),
);

// stream to file
foreach ($this->client->stream($response) as $chunk)
{
fwrite($fileHandle, $chunk->getContent());
}

fclose($fileHandle);

return true;
}
catch (TransportExceptionInterface|HttpExceptionInterface $exception)
{
$this->logger->error("Failed to fetch Storyblok asset {originUrl}: {message}", [
"message" => $exception->getMessage(),
"originUrl" => $originUrl,
"targetPath" => $targetPath,
"exception" => $exception,
]);

// be sure to close the file handle and clean up the incomplete file
fclose($fileHandle);
$this->filesystem->remove($targetPath);

return false;
}
}
}

0 comments on commit 4dc239e

Please sign in to comment.