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:
ak
2026-08-12 01:15:38 +08:00
commit 263b98b218
337 changed files with 31393 additions and 0 deletions
@@ -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'), '/');
}
}