Initial Ladill Transfer app — file sharing with QR links.
Scaffolded from the Ladill mini/events extraction pattern: multi-file transfers, retention controls, public landing pages, analytics, and Gitea deploy workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QrCode;
|
||||
use App\Models\Transfer;
|
||||
use App\Models\TransferFile;
|
||||
use App\Services\Qr\QrScanRecorder;
|
||||
use App\Services\Transfer\TransferService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class TransferPublicController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private QrScanRecorder $scanRecorder,
|
||||
private TransferService $transfers,
|
||||
) {}
|
||||
|
||||
public function landing(Request $request, string $shortCode): RedirectResponse|View
|
||||
{
|
||||
$qrCode = QrCode::query()->where('short_code', $shortCode)->firstOrFail();
|
||||
$this->scanRecorder->record($qrCode, $request);
|
||||
|
||||
if (! $qrCode->is_active || ! $qrCode->isTransferType()) {
|
||||
return view('public.qr.inactive', ['qrCode' => $qrCode]);
|
||||
}
|
||||
|
||||
$transfer = $this->resolveTransfer($qrCode);
|
||||
abort_unless($transfer && $transfer->isActive(), 404);
|
||||
|
||||
if ($transfer->isPasswordProtected() && ! $request->session()->get("transfer_unlocked.{$transfer->id}")) {
|
||||
return view('public.transfer.password', [
|
||||
'qrCode' => $qrCode,
|
||||
'transfer' => $transfer,
|
||||
]);
|
||||
}
|
||||
|
||||
return view('public.transfer.landing', [
|
||||
'qrCode' => $qrCode,
|
||||
'transfer' => $transfer->load('files'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function unlock(Request $request, string $shortCode): RedirectResponse|View
|
||||
{
|
||||
$qrCode = QrCode::query()->where('short_code', $shortCode)->firstOrFail();
|
||||
$transfer = $this->resolveTransfer($qrCode);
|
||||
abort_unless($transfer, 404);
|
||||
|
||||
$data = $request->validate(['password' => 'required|string|max:64']);
|
||||
|
||||
if (! $this->transfers->verifyPassword($transfer, $data['password'])) {
|
||||
return back()->with('error', 'Incorrect password.');
|
||||
}
|
||||
|
||||
$request->session()->put("transfer_unlocked.{$transfer->id}", true);
|
||||
|
||||
return redirect()->route('qr.public.transfer', $shortCode);
|
||||
}
|
||||
|
||||
public function download(Request $request, string $shortCode, TransferFile $file): StreamedResponse
|
||||
{
|
||||
$qrCode = QrCode::query()->where('short_code', $shortCode)->firstOrFail();
|
||||
$transfer = $this->resolveTransfer($qrCode);
|
||||
abort_unless($transfer && $transfer->isActive() && $file->transfer_id === $transfer->id, 404);
|
||||
|
||||
if ($transfer->isPasswordProtected() && ! $request->session()->get("transfer_unlocked.{$transfer->id}")) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
abort_unless($file->existsOnDisk(), 404);
|
||||
|
||||
$this->transfers->recordDownload($transfer, $file, $request);
|
||||
|
||||
return Storage::disk($file->disk)->response(
|
||||
$file->path,
|
||||
$file->original_name,
|
||||
['Content-Type' => $file->mime_type],
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveTransfer(QrCode $qrCode): ?Transfer
|
||||
{
|
||||
$transferId = $qrCode->content()['transfer_id'] ?? null;
|
||||
|
||||
if ($transferId) {
|
||||
return Transfer::query()->with('files')->find($transferId);
|
||||
}
|
||||
|
||||
return Transfer::query()->where('qr_code_id', $qrCode->id)->with('files')->first();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user