402 lines
11 KiB
PHP
402 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Classes;
|
|
|
|
use App\Classes\Traits\ManagesMetadata;
|
|
use App\Classes\Traits\Repairs\RepairsImages;
|
|
use App\Classes\Traits\Repairs\RepairsSounds;
|
|
use App\Classes\Traits\Repairs\RepairsVideos;
|
|
use App\Exceptions\AttachmentNotFound;
|
|
use App\View\Components\Image;
|
|
use App\View\Components\Sound;
|
|
use App\View\Components\Video;
|
|
use Exception;
|
|
use Illuminate\Filesystem\FilesystemAdapter;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
|
|
class AttachmentsManager
|
|
{
|
|
use ManagesMetadata, RepairsImages, RepairsSounds, RepairsVideos;
|
|
|
|
/**
|
|
* Manages images
|
|
*/
|
|
const Images = 'images';
|
|
|
|
/**
|
|
* Manages sounds
|
|
*/
|
|
const Sounds = 'sounds';
|
|
|
|
/**
|
|
* Manages videos
|
|
*/
|
|
const Videos = 'videos';
|
|
|
|
private string $attachmentsDir = 'attachments';
|
|
|
|
private string $targetForFiles;
|
|
|
|
private string $metadataFilePath;
|
|
|
|
private MetadataManager $manager;
|
|
|
|
private FilesystemAdapter $disk;
|
|
|
|
private bool $isLoaded = false;
|
|
|
|
public function __construct(public string $kind, protected Bundle $bundle)
|
|
{
|
|
$this->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'])) {
|
|
$existingRef = $this->findAttachmentByOriginalUrl($data['url']);
|
|
|
|
if (!empty($existingRef)) {
|
|
return $existingRef;
|
|
}
|
|
|
|
// Adding from a URL which implies downloading the resource
|
|
$contents = Http::throw()->get($data['url'])->body();
|
|
|
|
$data['filename'] = basename($data['url']);
|
|
$data['original_url'] = $data['url'];
|
|
|
|
unset($data['url']);
|
|
}
|
|
|
|
$filename = $this->getFormatedFilename($data['filename']);
|
|
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
|
$fullPath = sprintf('%s/%s/original.%s', $this->targetForFiles, $reference, $extension);
|
|
$relativePath = sprintf('%s/%s/%s/original.%s', $this->attachmentsDir, $this->kind, $reference, $extension);
|
|
|
|
$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 = '/<x-attachment\s+ref="([^"]+)"\s*\/>/';
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect and return unused attachments refs
|
|
*/
|
|
public function findUnusedAttachments(string $markdown, ?string $cover = null)
|
|
{
|
|
$unused = [];
|
|
|
|
foreach (array_keys($this->manager->get('files', [])) as $ref) {
|
|
if (!empty($cover) && $cover === $ref) {
|
|
continue;
|
|
}
|
|
|
|
if (!Str::contains($markdown, $ref)) {
|
|
$unused[] = $ref;
|
|
}
|
|
}
|
|
|
|
return $unused;
|
|
}
|
|
|
|
/**
|
|
* Find an attachment from its original URL, if specified
|
|
*/
|
|
private function findAttachmentByOriginalUrl(string $url)
|
|
{
|
|
foreach ($this->manager->get('files', []) as $ref => $data) {
|
|
if (array_key_exists('original_url', $data) && $data['original_url'] === $url) {
|
|
return $ref;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
}
|
|
}
|