Files
cattr/app/Services/AttachmentService.php
Noor E Ilahi 7ccf44f7da first commit
2026-01-09 12:54:53 +05:30

204 lines
7.2 KiB
PHP

<?php
namespace App\Services;
use App\Contracts\AttachmentAble;
use App\Enums\AttachmentStatus;
use App\Jobs\VerifyAttachmentHash;
use App\Models\Attachment;
use App\Models\Project;
use App\Models\Task;
use App\Models\TaskComment;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\UploadedFile;
use Storage;
class AttachmentService implements \App\Contracts\AttachmentService
{
public const DISK = 'attachments';
private readonly FilesystemAdapter $storage;
public function __construct()
{
$this->storage = Storage::disk($this::DISK);
}
public function storeFile(UploadedFile $file, Attachment $attachment): string|false
{
return $file->storeAs(
$this->getPath($attachment),
['disk' => $this::DISK]
);
}
public function handleProjectChange(Task $parent): void
{
$newProjectId = $parent->getProjectId();
$parent->attachmentsRelation()->lazyById()
->each(fn(Attachment $attachment) => $this->moveAttachment($attachment, $newProjectId));
$parent->comments()->lazyById()
->each(fn(TaskComment $comment) => $comment->attachmentsRelation()->lazyById()
->each(fn(Attachment $attachment) => $this->moveAttachment($attachment, $newProjectId)));
}
public function moveAttachment(Attachment $attachment, $newProjectId): void
{
if ($attachment->status === AttachmentStatus::NOT_ATTACHED) {
return;
}
$oldPath = $this->getPath($attachment);
$oldProjectId = $attachment->project_id;
$attachment->project_id = $newProjectId;
$attachment->saveQuietly();
$newPath = $this->getPath($attachment);
$fileExists = $this->storage->exists($oldPath);
if (!$fileExists || !$this->storage->move($oldPath, $newPath)) {
$attachment->project_id = $oldProjectId;
$attachment->saveQuietly();
\Log::error("Unable to move attachment`s file", [
'attachment_id' => $attachment->id,
'attachmentable_id' => $attachment->attachmentable_id,
'attachmentable_type' => $attachment->attachmentable_type,
'attachment_project_id' => $attachment->project_id,
'old_attachment_project_id' => $oldProjectId,
'old_path' => $oldPath,
'new_path' => $newPath
]);
}
}
public function deleteAttachment(Attachment $attachment): void
{
if (($fileExists = $this->fileExists($attachment)) && $this->deleteFile($attachment)) {
$attachment->deleteQuietly();
} elseif (!$fileExists) {
$attachment->deleteQuietly();
} else {
\Log::error("Unable to delete attachment`s file", [
'attachment_id' => $attachment->id,
'attachmentable_id' => $attachment->attachmentable_id,
'attachmentable_type' => $attachment->attachmentable_type,
'attachment_project_id' => $attachment->project_id,
'path' => $this->getPath($attachment)
]);
}
}
public function deleteAttachments(array $attachmentsIds): void
{
Attachment::whereIn('id', $attachmentsIds)
->each(fn (Attachment $attachment) => $this->deleteAttachment($attachment));
}
public function handleParentDeletion(AttachmentAble $parent): void
{
$parent->attachmentsRelation()->lazyById()
->each(fn (Attachment $attachment) => $this->deleteAttachment($attachment));
}
public function handleProjectDeletion(Project $project): void
{
Attachment::whereProjectId($project->id)->delete();
if ($this->storage->directoryExists($this->getProjectPath($project)) && !$this->storage->deleteDirectory($this->getProjectPath($project))) {
\Log::error("Unable to delete project's attachments directory", [
'project_id' => $project->id,
'path' => $this->getProjectPath($project)
]);
}
}
public function fileExists(Attachment $attachment): bool
{
return $this->storage->exists($this->getPath($attachment));
}
private function deleteFile(Attachment $attachment): bool
{
return $this->storage->delete($this->getPath($attachment));
}
public function attach(AttachmentAble $parent, array $idsToAttach): void
{
$projectId = $parent->getProjectId();
Attachment::whereIn('id', $idsToAttach)
->each(function (Attachment $attachment) use ($parent, $projectId) {
if ($attachment->status !== AttachmentStatus::NOT_ATTACHED) {
return;
}
$tmpPath = $this->getPath($attachment);
if ($this->storage->exists($tmpPath)) {
$attachment->attachmentAbleRelation()->associate($parent);
$attachment->status = AttachmentStatus::PROCESSING;
$attachment->project_id = $projectId;
$attachment->saveQuietly();
$newPath = $this->getPath($attachment);
$this->storage->move($tmpPath, $newPath);
VerifyAttachmentHash::dispatch($attachment)->afterCommit();
}
});
}
public function getProjectPath(Project $project): string
{
return "projects/{$project->id}";
}
public function getPath(Attachment $attachment): string
{
return match ($attachment->status) {
AttachmentStatus::NOT_ATTACHED => "users/{$attachment->user_id}/{$attachment->id}",
AttachmentStatus::PROCESSING,
AttachmentStatus::GOOD,
AttachmentStatus::BAD => "projects/{$attachment->project_id}/{$attachment->id}",
};
}
public function getFullPath(Attachment $attachment): string
{
return $this->storage->path($this->getPath($attachment));
}
public function readStream(Attachment $attachment)
{
return $this->storage->readStream($this->getPath($attachment));
}
public function getHashAlgo(): string
{
return $this->storage->getConfig()['checksum_algo'] ?? 'sha512/256';
}
public function getHashSum(Attachment|string $attachment): false|string
{
return $this->storage->checksum($this->callIfInstance($attachment, 'getPath'), ['checksum_algo' => $this->getHashAlgo()]);
}
public function getMimeType(Attachment|string $attachment): false|string
{
return $this->storage->mimeType($this->callIfInstance($attachment, 'getPath'));
}
private function callIfInstance(Attachment|string $attachment, $action)
{
return $attachment instanceof Attachment ? $this->{$action}($attachment) : $attachment;
}
public function verifyHash(Attachment $attachment): void
{
if ($attachment->status === AttachmentStatus::PROCESSING && $hash = $this->getHashSum($attachment)) {
$attachment->hash = $hash;
$attachment->status = AttachmentStatus::GOOD;
} elseif ($attachment->status === AttachmentStatus::GOOD && $attachment->hash !== $this->getHashSum($attachment)
) {
$attachment->status = AttachmentStatus::BAD;
}
$attachment->save();
}
}