disk = $bundle->getDisk(); $root = $bundle->getDataDir(); $this->metadataFilePath = sprintf('%s%s.json', $root, $kind); $this->targetForFiles = sprintf('%s%s/%s', $root, $this->attachmentsDir, $kind); $this->manager = new MetadataManager($this->metadataFilePath, $bundle); } /** * Add a file to a named history and return attachment's reference */ public function addToHistory(string $name, array $data): string { $reference = $this->add($data); $this->manager->merge([ 'history' => [ $name => [ 'reference' => $reference, 'date' => now(), ], ], ]); return $reference; } /** * Provides direct access to underlying manager for better control */ public function manager(): MetadataManager { return $this->manager; } /** * Add a single file to the bundle */ public function add(array $data): string { $reference = $this->generateNewReference(); if (!empty($data['contents'])) { // Adding from an image resource of which $data['contents'] is a // representation $contents = $data['contents']; unset($data['contents']); } elseif (!empty($data['url'])) { // Adding from a URL which implies downloading the resource $contents = Http::throw()->get($data['url'])->body(); $data['filename'] = basename($data['url']); unset($data['url']); } $filename = $this->getFormatedFilename($data['filename']); $fullPath = sprintf('%s/%s', $this->targetForFiles, $filename); $relativePath = sprintf('%s/%s/%s', $this->attachmentsDir, $this->kind, $filename); $data['filename'] = $relativePath; $this->disk->put($fullPath, $contents); $this->manager->set(sprintf('files.%s', $reference), $data); return $reference; } /** * Return a boolean value indicating if the file actually exists on disk */ public function exists() { return $this->disk->exists($this->metadataFilePath); } /** * Load data from disk */ public function load(bool $reload = false) { if (!$this->isLoaded || $reload) { $this->manager->load(); $this->isLoaded = true; } } /** * Store file on disk */ public function save(): bool { return $this->manager->save(); } /** * Replace all attachments declared in specified markdown with appropriate * Blade components */ public function replaceAttachmentsInMarkdown(string $markdown) { $pattern = '//'; preg_match_all($pattern, $markdown, $matches); foreach ($matches[1] as $index => $ref) { try { $component = $this->getComponentByRef($ref, 'article'); } catch (AttachmentNotFound $ex) { continue; } $replacement = Str::squish((string) $component->render()); $markdown = Str::replace($matches[0][$index], $replacement, $markdown); } return $markdown; } /** * Return a Blade component for the attachment we passed the ref off */ public function getComponentByRef(string $ref, ?string $filter, ?array $options = []) { $attachment = $this->getAttachmentData($ref); $variant = null; if (!empty($filter)) { $variant = $this->getVariantData($ref, $filter) ?? null; } // We set the fullname to the full path of the file in the filesystem // so the Blade component does not have to worry about it $attachment['filename'] = $this->getAttachmentFullPath($ref); if (!empty($filter) && !empty($variant)) { $variant['filename'] = $this->getVariantFullPath($ref, $filter); } $component = $this->getBladeComponent($attachment, $variant, $options); return $component; } /** * Remove an attachment from disk and any reference in this file */ public function deleteAttachment(string $ref) { $path = $this->getAttachmentFullPath($ref); $parent = dirname($path); $this->disk->delete($path); if (empty($this->disk->listContents($parent, true))) { $this->disk->delete($parent); } $this->manager->remove([ sprintf('files.%s', $ref), sprintf('variants.%s', $ref), sprintf('history.%s', $ref), ]); } /** * Remove an attachment from disk and any reference in this file */ public function deleteVariant(string $originalRef, string $filter) { $path = $this->getVariantFullPath($originalRef, $filter); $parent = dirname($path); $this->disk->delete($path); if (empty($this->disk->listContents($parent, true))) { $this->disk->delete($parent); } $this->manager->remove([ sprintf('variants.%s.%s', $originalRef, $filter), ]); } /** * Return full path of specified attachment */ public function getAttachmentFullPath(string $ref) { $data = $this->getAttachmentData($ref); return sprintf('%s%s', $this->bundle->getDataDir(), $data['filename']); } /** * Return attachment's relative path to bundle's data directory */ public function getAttachmentRelativePath(string $ref) { $fullPath = $this->getAttachmentFullPath($ref); return Str::remove($this->bundle->getDataDir(), $fullPath); } /** * Return full path of specified attachment's variant */ public function getVariantFullPath(string $originalRef, string $filter) { $originalPath = $this->getAttachmentFullPath($originalRef); $parentDir = dirname($originalPath); $extension = pathinfo($originalPath, PATHINFO_EXTENSION); $variantExtension = $extension === 'gif' ? 'gif' : 'webp'; return sprintf('%s/%s.%s', $parentDir, $filter, $variantExtension); } /** * Return attachment variant's relative path to bundle's data directory */ public function getVariantRelativePath(string $ref, string $filter) { $fullPath = $this->getVariantFullPath($ref, $filter); return Str::remove($this->bundle->getDataDir(), $fullPath); } /** * Return specified attachment data */ public function getAttachmentData(string $ref): array { $data = $this->manager->get(sprintf('files.%s', $ref)); if (empty($data)) { throw new AttachmentNotFound(sprintf('Unknown attachment %s', $ref)); } return $data; } /** * Return specified variant data */ public function getVariantData(string $originalRef, string $filter): array { return $this->manager->get(sprintf('variants.%s.%s', $originalRef, $filter), []); } /** * Repair attachments */ public function repair() { switch ($this->kind) { case self::Images: $this->repairImages(); break; case self::Sounds: $this->repairSounds(); break; case self::Videos: $this->repairVideos(); break; } } /** * Generate new, random reference for a file */ private function generateNewReference(): string { return Str::random(6); } /** * Return a suitable filename */ private function getFormatedFilename(string $path): string { $filename = pathinfo($path, PATHINFO_FILENAME); $extension = pathinfo($path, PATHINFO_EXTENSION); $slug = Str::slug($filename); return sprintf('%s.%s', $slug, $extension); } /** * Return a Blade component corresponding to the kind of data we are * managing, and we pass it attachment data */ private function getBladeComponent(array $data, ?array $variant, ?array $options = []) { switch ($this->kind) { case self::Images: return new Image($data, $variant, $options); case self::Sounds: return new Sound($data); case self::Videos: return new Video($data); default: throw new Exception(sprintf('Unknown Blade Component for attachment kind "%s"', $this->kind)); } } }