Initial baseline: LaraBlog core with plugin commerce surface.
Captures the current working tree after theme slots, ArticleAccess, and the payment / paid-content plugins so subsequent work has a reviewable git history.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Models\Attachment;
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Throwable;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
class AttachmentStorageService
|
||||
{
|
||||
public function uploadLocalFileToDisk(
|
||||
string $localPath,
|
||||
string $destinationPath,
|
||||
?string $disk = null,
|
||||
string $visibility = Attachment::VISIBILITY_PUBLIC,
|
||||
): Attachment {
|
||||
if (! is_file($localPath)) {
|
||||
throw new RuntimeException("Local file [{$localPath}] does not exist.");
|
||||
}
|
||||
|
||||
$disk ??= config('larablog.attachments_disk', 'attachments');
|
||||
$normalizedPath = ltrim(str_replace('\\', '/', $destinationPath), '/');
|
||||
$mime = mime_content_type($localPath) ?: null;
|
||||
$allowed = config('larablog.allowed_attachment_mimes', []);
|
||||
if (is_string($mime) && $allowed !== [] && ! in_array($mime, $allowed, true)) {
|
||||
throw new RuntimeException("MIME type [{$mime}] is not allowed for attachments.");
|
||||
}
|
||||
|
||||
$stream = fopen($localPath, 'r');
|
||||
|
||||
if ($stream === false) {
|
||||
throw new RuntimeException("Unable to read local file [{$localPath}].");
|
||||
}
|
||||
|
||||
Storage::disk($disk)->put($normalizedPath, $stream, [
|
||||
'visibility' => $visibility === Attachment::VISIBILITY_PUBLIC ? 'public' : 'private',
|
||||
]);
|
||||
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
return Attachment::query()->create([
|
||||
'disk' => $disk,
|
||||
'path' => $normalizedPath,
|
||||
'filename' => basename($normalizedPath),
|
||||
'mime' => $mime,
|
||||
'size' => filesize($localPath) ?: 0,
|
||||
'checksum' => hash_file('sha256', $localPath) ?: null,
|
||||
'visibility' => $visibility,
|
||||
'synced_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function publicUrl(Attachment $attachment): string
|
||||
{
|
||||
if ($attachment->visibility !== Attachment::VISIBILITY_PUBLIC) {
|
||||
return $this->temporaryUrl($attachment);
|
||||
}
|
||||
|
||||
$disk = Storage::disk($attachment->disk);
|
||||
|
||||
if (method_exists($disk, 'url')) {
|
||||
return $disk->url($attachment->path);
|
||||
}
|
||||
|
||||
return $this->temporaryUrl($attachment);
|
||||
}
|
||||
|
||||
public function temporaryUrl(Attachment $attachment, int $minutes = 30): string
|
||||
{
|
||||
return Storage::disk($attachment->disk)->temporaryUrl(
|
||||
$attachment->path,
|
||||
now()->addMinutes($minutes),
|
||||
[
|
||||
'ResponseContentDisposition' => 'attachment; filename="'.addslashes($attachment->filename).'"',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function resolveRedirectResponseById(int $id, ?string $ip = null): RedirectResponse|Response
|
||||
{
|
||||
$attachment = Attachment::query()->find($id);
|
||||
|
||||
if ($attachment === null) {
|
||||
abort(SymfonyResponse::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->resolveRedirectResponse($attachment, $ip);
|
||||
}
|
||||
|
||||
public function resolveRedirectResponseByLegacyPath(string $legacyPath, ?string $ip = null): RedirectResponse|Response
|
||||
{
|
||||
$raw = ltrim(str_replace('\\', '/', $legacyPath), '/');
|
||||
$normalized = $this->normalizeLegacyPath($legacyPath);
|
||||
$prefix = $this->attachmentsUrlPrefix();
|
||||
$withPrefix = ($prefix !== '' && ! Str::startsWith($raw, $prefix.'/'))
|
||||
? $prefix.'/'.$raw
|
||||
: $raw;
|
||||
|
||||
$candidates = array_values(array_unique(array_filter([$raw, $normalized, $withPrefix])));
|
||||
|
||||
$attachment = Attachment::query()
|
||||
->where(function ($query) use ($candidates) {
|
||||
$query->whereIn('legacy_filepath', $candidates)
|
||||
->orWhereIn('path', $candidates);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($attachment === null) {
|
||||
abort(SymfonyResponse::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->resolveRedirectResponse($attachment, $ip);
|
||||
}
|
||||
|
||||
public function resolveRedirectResponse(Attachment $attachment, ?string $ip = null): RedirectResponse
|
||||
{
|
||||
if ($attachment->visibility !== Attachment::VISIBILITY_PUBLIC) {
|
||||
abort(SymfonyResponse::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$this->incrementDownloads($attachment, $ip);
|
||||
|
||||
$targetUrl = $attachment->visibility === Attachment::VISIBILITY_PUBLIC
|
||||
? $this->publicUrl($attachment)
|
||||
: $this->temporaryUrl($attachment);
|
||||
|
||||
return redirect()->away($targetUrl, SymfonyResponse::HTTP_FOUND);
|
||||
}
|
||||
|
||||
public function incrementDownloads(Attachment $attachment, ?string $ip = null): void
|
||||
{
|
||||
$ip ??= request()->ip() ?? 'unknown';
|
||||
$lockKey = "attachment-download:{$attachment->id}:{$ip}";
|
||||
|
||||
$lock = Cache::lock($lockKey, 60);
|
||||
|
||||
if (! $lock->get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attachment->increment('downloads');
|
||||
}
|
||||
|
||||
protected function normalizeLegacyPath(string $legacyPath): string
|
||||
{
|
||||
$path = str_replace('\\', '/', $legacyPath);
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
$prefix = $this->attachmentsUrlPrefix();
|
||||
|
||||
if ($prefix !== '' && Str::startsWith($path, $prefix.'/')) {
|
||||
$path = Str::after($path, $prefix.'/');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function deleteFromDisk(Attachment $attachment): void
|
||||
{
|
||||
$disk = Storage::disk($attachment->disk);
|
||||
if ($attachment->path !== '' && $disk->exists($attachment->path)) {
|
||||
$disk->delete($attachment->path);
|
||||
}
|
||||
if (filled($attachment->thumb_path) && $disk->exists($attachment->thumb_path)) {
|
||||
$disk->delete($attachment->thumb_path);
|
||||
}
|
||||
}
|
||||
|
||||
protected function attachmentsUrlPrefix(): string
|
||||
{
|
||||
try {
|
||||
$fromSettings = app(GeneralSettings::class)->attachments_url_prefix;
|
||||
if (is_string($fromSettings) && $fromSettings !== '') {
|
||||
return trim($fromSettings, '/');
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// settings unavailable during early boot / migrate
|
||||
}
|
||||
|
||||
return trim((string) config('larablog.attachments_url_prefix', 'attachments'), '/');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user