Implements StoredObjectManager for local storage

This commit is contained in:
2024-12-19 17:09:16 +01:00
parent 1f6de3cb11
commit c1e449f48e
9 changed files with 466 additions and 11 deletions

View File

@@ -11,49 +11,200 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage;
use Base64Url\Base64Url;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
class StoredObjectManager implements StoredObjectManagerInterface
{
private readonly string $baseDir;
private readonly Filesystem $filesystem;
public function __construct(
ParameterBagInterface $parameterBag,
private readonly KeyGenerator $keyGenerator,
) {
$this->baseDir = $parameterBag->get('chill_doc_store')['local_storage']['base_dir'];
$this->filesystem = new Filesystem();
}
public function getLastModified(StoredObject|StoredObjectVersion $document): \DateTimeInterface
{
// TODO: Implement getLastModified() method.
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
if (null === $version) {
throw StoredObjectManagerException::storedObjectDoesNotContainsVersion();
}
$path = $this->buildPath($version->getFilename());
if (false === $ts = filemtime($path)) {
throw StoredObjectManagerException::unableToReadDocumentOnDisk($path);
}
return \DateTimeImmutable::createFromFormat('U', (string) $ts);
}
public function getContentLength(StoredObject|StoredObjectVersion $document): int
{
// TODO: Implement getContentLength() method.
return strlen($this->read($document));
}
public function exists(StoredObject|StoredObjectVersion $document): bool
{
// TODO: Implement exists() method.
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
if (null === $version) {
return false;
}
$path = $this->buildPath($version->getFilename());
return $this->filesystem->exists($path);
}
public function read(StoredObject|StoredObjectVersion $document): string
{
// TODO: Implement read() method.
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
if (null === $version) {
throw StoredObjectManagerException::storedObjectDoesNotContainsVersion();
}
$path = $this->buildPath($version->getFilename());
if (!file_exists($path)) {
throw StoredObjectManagerException::unableToFindDocumentOnDisk($path);
}
if (false === $content = file_get_contents($path)) {
throw StoredObjectManagerException::unableToReadDocumentOnDisk($path);
}
if (!$this->isVersionEncrypted($version)) {
return $content;
}
$clearData = openssl_decrypt(
$content,
self::ALGORITHM,
// TODO: Why using this library and not use base64_decode() ?
Base64Url::decode($version->getKeyInfos()['k']),
\OPENSSL_RAW_DATA,
pack('C*', ...$version->getIv())
);
if (false === $clearData) {
throw StoredObjectManagerException::unableToDecrypt(openssl_error_string());
}
return $clearData;
}
public function write(StoredObject $document, string $clearContent, ?string $contentType = null): StoredObjectVersion
{
// TODO: Implement write() method.
$newIv = $document->isEncrypted() ? $document->getIv() : $this->keyGenerator->generateIv();
$newKey = $document->isEncrypted() ? $document->getKeyInfos() : $this->keyGenerator->generateKey(self::ALGORITHM);
$newType = $contentType ?? $document->getType();
$version = $document->registerVersion(
$newIv,
$newKey,
$newType
);
$encryptedContent = $this->isVersionEncrypted($version)
? openssl_encrypt(
$clearContent,
self::ALGORITHM,
// TODO: Why using this library and not use base64_decode() ?
Base64Url::decode($version->getKeyInfos()['k']),
\OPENSSL_RAW_DATA,
pack('C*', ...$version->getIv())
)
: $clearContent;
if (false === $encryptedContent) {
throw StoredObjectManagerException::unableToEncryptDocument((string) openssl_error_string());
}
$fullPath = $this->buildPath($version->getFilename());
$dir = Path::getDirectory($fullPath);
if (!$this->filesystem->exists($dir)) {
$this->filesystem->mkdir($dir);
}
$result = file_put_contents($fullPath, $encryptedContent);
if (false === $result) {
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
}
return $version;
}
private function buildPath(string $filename): string
{
$dirs = [$this->baseDir];
for ($i = 0; $i < min(strlen($filename), 8); ++$i) {
$dirs[] = $filename[$i];
}
$dirs[] = $filename;
return Path::canonicalize(implode(DIRECTORY_SEPARATOR, $dirs));
}
public function delete(StoredObjectVersion $storedObjectVersion): void
{
// TODO: Implement delete() method.
if (!$this->exists($storedObjectVersion)) {
return;
}
$path = $this->buildPath($storedObjectVersion->getFilename());
$this->filesystem->remove($path);
$this->removeDirectoriesRecursively(Path::getDirectory($path));
}
private function removeDirectoriesRecursively(string $path): void
{
if ($path === $this->baseDir) {
return;
}
$files = scandir($path);
// if it does contains only "." and "..", we can remove the directory
if (2 === count($files) && in_array('.', $files, true) && in_array('..', $files, true)) {
$this->filesystem->remove($path);
$this->removeDirectoriesRecursively(Path::getDirectory($path));
}
}
/**
* @throws StoredObjectManagerException
*/
public function etag(StoredObject|StoredObjectVersion $document): string
{
// TODO: Implement etag() method.
return md5($this->read($document));
}
public function clearCache(): void
{
// TODO: Implement clearCache() method.
// there is no cache: nothing to do here !
}
private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool
{
return $storedObjectVersion->isEncrypted();
}
}

View File

@@ -25,8 +25,6 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
final class StoredObjectManager implements StoredObjectManagerInterface
{
private const ALGORITHM = 'AES-256-CBC';
private array $inMemory = [];
public function __construct(
@@ -362,6 +360,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool
{
return ([] !== $storedObjectVersion->getKeyInfos()) && ([] !== $storedObjectVersion->getIv());
return $storedObjectVersion->isEncrypted();
}
}