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'), '/'); } }