@@ -26,161 +26,161 @@ |
||
| 26 | 26 | */ |
| 27 | 27 | class ObjectStorePreviewStorage implements IPreviewStorage { |
| 28 | 28 | |
| 29 | - /** |
|
| 30 | - * @var array<string, array<string, IObjectStore>> |
|
| 31 | - */ |
|
| 32 | - private array $objectStoreCache = []; |
|
| 33 | - |
|
| 34 | - private bool $isMultibucketPreviewDistributionEnabled; |
|
| 35 | - |
|
| 36 | - public function __construct( |
|
| 37 | - private readonly PrimaryObjectStoreConfig $objectStoreConfig, |
|
| 38 | - IConfig $config, |
|
| 39 | - readonly private PreviewMapper $previewMapper, |
|
| 40 | - ) { |
|
| 41 | - $this->isMultibucketPreviewDistributionEnabled = $config->getSystemValueBool('objectstore.multibucket.preview-distribution'); |
|
| 42 | - } |
|
| 43 | - |
|
| 44 | - #[Override] |
|
| 45 | - public function writePreview(Preview $preview, mixed $stream): int { |
|
| 46 | - $size = 0; |
|
| 47 | - $countStream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void { |
|
| 48 | - $size = $writtenSize; |
|
| 49 | - }); |
|
| 50 | - |
|
| 51 | - [ |
|
| 52 | - 'urn' => $urn, |
|
| 53 | - 'store' => $store, |
|
| 54 | - ] = $this->getObjectStoreInfoForNewPreview($preview); |
|
| 55 | - |
|
| 56 | - try { |
|
| 57 | - $store->writeObject($urn, $countStream); |
|
| 58 | - } catch (\Exception $exception) { |
|
| 59 | - throw new NotPermittedException('Unable to save preview to object store', previous: $exception); |
|
| 60 | - } |
|
| 61 | - return $size; |
|
| 62 | - } |
|
| 63 | - |
|
| 64 | - #[Override] |
|
| 65 | - public function readPreview(Preview $preview): mixed { |
|
| 66 | - [ |
|
| 67 | - 'urn' => $urn, |
|
| 68 | - 'store' => $store, |
|
| 69 | - ] = $this->getObjectStoreInfoForExistingPreview($preview); |
|
| 70 | - |
|
| 71 | - try { |
|
| 72 | - return $store->readObject($urn); |
|
| 73 | - } catch (\Exception $exception) { |
|
| 74 | - throw new NotPermittedException('Unable to read preview from object store with urn:' . $urn, previous: $exception); |
|
| 75 | - } |
|
| 76 | - } |
|
| 77 | - |
|
| 78 | - #[Override] |
|
| 79 | - public function deletePreview(Preview $preview): void { |
|
| 80 | - if (defined('PHPUNIT_RUN') && $preview->getLocationId() === null) { |
|
| 81 | - // Should only be the case in unit tests when adding dummy previews in the database. |
|
| 82 | - return; |
|
| 83 | - } |
|
| 84 | - |
|
| 85 | - [ |
|
| 86 | - 'urn' => $urn, |
|
| 87 | - 'store' => $store, |
|
| 88 | - ] = $this->getObjectStoreInfoForExistingPreview($preview); |
|
| 89 | - |
|
| 90 | - try { |
|
| 91 | - $store->deleteObject($urn); |
|
| 92 | - } catch (\Exception $exception) { |
|
| 93 | - throw new NotPermittedException('Unable to delete preview from object store', previous: $exception); |
|
| 94 | - } |
|
| 95 | - } |
|
| 96 | - |
|
| 97 | - #[Override] |
|
| 98 | - public function migratePreview(Preview $preview, SimpleFile $file): void { |
|
| 99 | - // Just set the Preview::bucket and Preview::objectStore |
|
| 100 | - $this->getObjectStoreInfoForNewPreview($preview, migration: true); |
|
| 101 | - $this->previewMapper->update($preview); |
|
| 102 | - } |
|
| 103 | - |
|
| 104 | - /** |
|
| 105 | - * @return ObjectStoreDefinition |
|
| 106 | - */ |
|
| 107 | - private function getObjectStoreInfoForExistingPreview(Preview $preview): array { |
|
| 108 | - $objectStoreName = $preview->getObjectStoreName(); |
|
| 109 | - $bucketName = $preview->getBucketName(); |
|
| 110 | - assert(!empty($objectStoreName)); |
|
| 111 | - assert(!empty($bucketName)); |
|
| 112 | - |
|
| 113 | - $config = $this->objectStoreConfig->getObjectStoreConfiguration($objectStoreName); |
|
| 114 | - $config['arguments']['bucket'] = $preview->getBucketName(); |
|
| 115 | - $objectStoreName = $preview->getObjectStoreName(); |
|
| 116 | - |
|
| 117 | - return [ |
|
| 118 | - 'urn' => $this->getUrn($preview, $config), |
|
| 119 | - 'store' => $this->getObjectStore($objectStoreName, $config), |
|
| 120 | - ]; |
|
| 121 | - } |
|
| 122 | - |
|
| 123 | - /** |
|
| 124 | - * @return ObjectStoreDefinition |
|
| 125 | - */ |
|
| 126 | - private function getObjectStoreInfoForNewPreview(Preview $preview, bool $migration = false): array { |
|
| 127 | - // When migrating old previews, use the 'root' object store configuration |
|
| 128 | - $config = $this->objectStoreConfig->getObjectStoreConfiguration($migration ? 'root' : 'preview'); |
|
| 129 | - $objectStoreName = $this->objectStoreConfig->resolveAlias($migration ? 'root' : 'preview'); |
|
| 130 | - |
|
| 131 | - $bucketName = $config['arguments']['bucket']; |
|
| 132 | - if ($config['arguments']['multibucket']) { |
|
| 133 | - if ($this->isMultibucketPreviewDistributionEnabled) { |
|
| 134 | - // Spread the previews on different buckets depending on their corresponding fileId |
|
| 135 | - $oldLocationArray = str_split(substr(md5((string)$preview->getFileId()), 0, 2)); |
|
| 136 | - $bucketNumber = hexdec('0x' . $oldLocationArray[0]) * 16 + hexdec('0x' . $oldLocationArray[0]); |
|
| 137 | - $bucketName .= '-preview-' . $bucketNumber; |
|
| 138 | - } else { |
|
| 139 | - // Put all previews in the root (0) bucket |
|
| 140 | - $bucketName .= '0'; |
|
| 141 | - } |
|
| 142 | - } |
|
| 143 | - $config['arguments']['bucket'] = $bucketName; |
|
| 144 | - |
|
| 145 | - // Get the locationId corresponding to the bucketName and objectStoreName, this will create |
|
| 146 | - // a new one, if no matching location is found in the DB. |
|
| 147 | - $locationId = $this->previewMapper->getLocationId($bucketName, $objectStoreName); |
|
| 148 | - $preview->setLocationId($locationId); |
|
| 149 | - $preview->setObjectStoreName($objectStoreName); |
|
| 150 | - $preview->setBucketName($bucketName); |
|
| 151 | - |
|
| 152 | - return [ |
|
| 153 | - 'urn' => $this->getUrn($preview, $config), |
|
| 154 | - 'store' => $this->getObjectStore($objectStoreName, $config), |
|
| 155 | - ]; |
|
| 156 | - } |
|
| 157 | - |
|
| 158 | - private function getObjectStore(string $objectStoreName, array $config): IObjectStore { |
|
| 159 | - $bucketName = $config['arguments']['bucket']; |
|
| 160 | - |
|
| 161 | - if (!isset($this->objectStoreCache[$objectStoreName])) { |
|
| 162 | - $this->objectStoreCache[$objectStoreName] = []; |
|
| 163 | - $this->objectStoreCache[$objectStoreName][$bucketName] = $this->objectStoreConfig->buildObjectStore($config); |
|
| 164 | - } elseif (!isset($this->objectStoreCache[$objectStoreName][$bucketName])) { |
|
| 165 | - $this->objectStoreCache[$objectStoreName][$bucketName] = $this->objectStoreConfig->buildObjectStore($config); |
|
| 166 | - } |
|
| 167 | - |
|
| 168 | - return $this->objectStoreCache[$objectStoreName][$bucketName]; |
|
| 169 | - } |
|
| 170 | - |
|
| 171 | - public function getUrn(Preview $preview, array $config): string { |
|
| 172 | - if ($preview->getOldFileId()) { |
|
| 173 | - return ($config['arguments']['objectPrefix'] ?? 'urn:oid:') . $preview->getOldFileId(); |
|
| 174 | - } |
|
| 175 | - if (isset($config['arguments']['objectPrefix'])) { |
|
| 176 | - return ($config['arguments']['objectPrefix'] . 'preview:') . $preview->getId(); |
|
| 177 | - } else { |
|
| 178 | - return 'uri:oid:preview:' . $preview->getId(); |
|
| 179 | - } |
|
| 180 | - } |
|
| 181 | - |
|
| 182 | - #[Override] |
|
| 183 | - public function scan(): int { |
|
| 184 | - return 0; |
|
| 185 | - } |
|
| 29 | + /** |
|
| 30 | + * @var array<string, array<string, IObjectStore>> |
|
| 31 | + */ |
|
| 32 | + private array $objectStoreCache = []; |
|
| 33 | + |
|
| 34 | + private bool $isMultibucketPreviewDistributionEnabled; |
|
| 35 | + |
|
| 36 | + public function __construct( |
|
| 37 | + private readonly PrimaryObjectStoreConfig $objectStoreConfig, |
|
| 38 | + IConfig $config, |
|
| 39 | + readonly private PreviewMapper $previewMapper, |
|
| 40 | + ) { |
|
| 41 | + $this->isMultibucketPreviewDistributionEnabled = $config->getSystemValueBool('objectstore.multibucket.preview-distribution'); |
|
| 42 | + } |
|
| 43 | + |
|
| 44 | + #[Override] |
|
| 45 | + public function writePreview(Preview $preview, mixed $stream): int { |
|
| 46 | + $size = 0; |
|
| 47 | + $countStream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void { |
|
| 48 | + $size = $writtenSize; |
|
| 49 | + }); |
|
| 50 | + |
|
| 51 | + [ |
|
| 52 | + 'urn' => $urn, |
|
| 53 | + 'store' => $store, |
|
| 54 | + ] = $this->getObjectStoreInfoForNewPreview($preview); |
|
| 55 | + |
|
| 56 | + try { |
|
| 57 | + $store->writeObject($urn, $countStream); |
|
| 58 | + } catch (\Exception $exception) { |
|
| 59 | + throw new NotPermittedException('Unable to save preview to object store', previous: $exception); |
|
| 60 | + } |
|
| 61 | + return $size; |
|
| 62 | + } |
|
| 63 | + |
|
| 64 | + #[Override] |
|
| 65 | + public function readPreview(Preview $preview): mixed { |
|
| 66 | + [ |
|
| 67 | + 'urn' => $urn, |
|
| 68 | + 'store' => $store, |
|
| 69 | + ] = $this->getObjectStoreInfoForExistingPreview($preview); |
|
| 70 | + |
|
| 71 | + try { |
|
| 72 | + return $store->readObject($urn); |
|
| 73 | + } catch (\Exception $exception) { |
|
| 74 | + throw new NotPermittedException('Unable to read preview from object store with urn:' . $urn, previous: $exception); |
|
| 75 | + } |
|
| 76 | + } |
|
| 77 | + |
|
| 78 | + #[Override] |
|
| 79 | + public function deletePreview(Preview $preview): void { |
|
| 80 | + if (defined('PHPUNIT_RUN') && $preview->getLocationId() === null) { |
|
| 81 | + // Should only be the case in unit tests when adding dummy previews in the database. |
|
| 82 | + return; |
|
| 83 | + } |
|
| 84 | + |
|
| 85 | + [ |
|
| 86 | + 'urn' => $urn, |
|
| 87 | + 'store' => $store, |
|
| 88 | + ] = $this->getObjectStoreInfoForExistingPreview($preview); |
|
| 89 | + |
|
| 90 | + try { |
|
| 91 | + $store->deleteObject($urn); |
|
| 92 | + } catch (\Exception $exception) { |
|
| 93 | + throw new NotPermittedException('Unable to delete preview from object store', previous: $exception); |
|
| 94 | + } |
|
| 95 | + } |
|
| 96 | + |
|
| 97 | + #[Override] |
|
| 98 | + public function migratePreview(Preview $preview, SimpleFile $file): void { |
|
| 99 | + // Just set the Preview::bucket and Preview::objectStore |
|
| 100 | + $this->getObjectStoreInfoForNewPreview($preview, migration: true); |
|
| 101 | + $this->previewMapper->update($preview); |
|
| 102 | + } |
|
| 103 | + |
|
| 104 | + /** |
|
| 105 | + * @return ObjectStoreDefinition |
|
| 106 | + */ |
|
| 107 | + private function getObjectStoreInfoForExistingPreview(Preview $preview): array { |
|
| 108 | + $objectStoreName = $preview->getObjectStoreName(); |
|
| 109 | + $bucketName = $preview->getBucketName(); |
|
| 110 | + assert(!empty($objectStoreName)); |
|
| 111 | + assert(!empty($bucketName)); |
|
| 112 | + |
|
| 113 | + $config = $this->objectStoreConfig->getObjectStoreConfiguration($objectStoreName); |
|
| 114 | + $config['arguments']['bucket'] = $preview->getBucketName(); |
|
| 115 | + $objectStoreName = $preview->getObjectStoreName(); |
|
| 116 | + |
|
| 117 | + return [ |
|
| 118 | + 'urn' => $this->getUrn($preview, $config), |
|
| 119 | + 'store' => $this->getObjectStore($objectStoreName, $config), |
|
| 120 | + ]; |
|
| 121 | + } |
|
| 122 | + |
|
| 123 | + /** |
|
| 124 | + * @return ObjectStoreDefinition |
|
| 125 | + */ |
|
| 126 | + private function getObjectStoreInfoForNewPreview(Preview $preview, bool $migration = false): array { |
|
| 127 | + // When migrating old previews, use the 'root' object store configuration |
|
| 128 | + $config = $this->objectStoreConfig->getObjectStoreConfiguration($migration ? 'root' : 'preview'); |
|
| 129 | + $objectStoreName = $this->objectStoreConfig->resolveAlias($migration ? 'root' : 'preview'); |
|
| 130 | + |
|
| 131 | + $bucketName = $config['arguments']['bucket']; |
|
| 132 | + if ($config['arguments']['multibucket']) { |
|
| 133 | + if ($this->isMultibucketPreviewDistributionEnabled) { |
|
| 134 | + // Spread the previews on different buckets depending on their corresponding fileId |
|
| 135 | + $oldLocationArray = str_split(substr(md5((string)$preview->getFileId()), 0, 2)); |
|
| 136 | + $bucketNumber = hexdec('0x' . $oldLocationArray[0]) * 16 + hexdec('0x' . $oldLocationArray[0]); |
|
| 137 | + $bucketName .= '-preview-' . $bucketNumber; |
|
| 138 | + } else { |
|
| 139 | + // Put all previews in the root (0) bucket |
|
| 140 | + $bucketName .= '0'; |
|
| 141 | + } |
|
| 142 | + } |
|
| 143 | + $config['arguments']['bucket'] = $bucketName; |
|
| 144 | + |
|
| 145 | + // Get the locationId corresponding to the bucketName and objectStoreName, this will create |
|
| 146 | + // a new one, if no matching location is found in the DB. |
|
| 147 | + $locationId = $this->previewMapper->getLocationId($bucketName, $objectStoreName); |
|
| 148 | + $preview->setLocationId($locationId); |
|
| 149 | + $preview->setObjectStoreName($objectStoreName); |
|
| 150 | + $preview->setBucketName($bucketName); |
|
| 151 | + |
|
| 152 | + return [ |
|
| 153 | + 'urn' => $this->getUrn($preview, $config), |
|
| 154 | + 'store' => $this->getObjectStore($objectStoreName, $config), |
|
| 155 | + ]; |
|
| 156 | + } |
|
| 157 | + |
|
| 158 | + private function getObjectStore(string $objectStoreName, array $config): IObjectStore { |
|
| 159 | + $bucketName = $config['arguments']['bucket']; |
|
| 160 | + |
|
| 161 | + if (!isset($this->objectStoreCache[$objectStoreName])) { |
|
| 162 | + $this->objectStoreCache[$objectStoreName] = []; |
|
| 163 | + $this->objectStoreCache[$objectStoreName][$bucketName] = $this->objectStoreConfig->buildObjectStore($config); |
|
| 164 | + } elseif (!isset($this->objectStoreCache[$objectStoreName][$bucketName])) { |
|
| 165 | + $this->objectStoreCache[$objectStoreName][$bucketName] = $this->objectStoreConfig->buildObjectStore($config); |
|
| 166 | + } |
|
| 167 | + |
|
| 168 | + return $this->objectStoreCache[$objectStoreName][$bucketName]; |
|
| 169 | + } |
|
| 170 | + |
|
| 171 | + public function getUrn(Preview $preview, array $config): string { |
|
| 172 | + if ($preview->getOldFileId()) { |
|
| 173 | + return ($config['arguments']['objectPrefix'] ?? 'urn:oid:') . $preview->getOldFileId(); |
|
| 174 | + } |
|
| 175 | + if (isset($config['arguments']['objectPrefix'])) { |
|
| 176 | + return ($config['arguments']['objectPrefix'] . 'preview:') . $preview->getId(); |
|
| 177 | + } else { |
|
| 178 | + return 'uri:oid:preview:' . $preview->getId(); |
|
| 179 | + } |
|
| 180 | + } |
|
| 181 | + |
|
| 182 | + #[Override] |
|
| 183 | + public function scan(): int { |
|
| 184 | + return 0; |
|
| 185 | + } |
|
| 186 | 186 | } |