Files
ladill-transfer/app/Http/Controllers/Transfer/TransferController.php
T
isaaccladandCursor 4b616a7f04
Deploy Ladill Transfer / deploy (push) Successful in 47s
Simplify recipient emails and add live upload progress on create.
Recipients always get the download link immediately when an email is set.
Large files upload in the background with per-file and overall progress bars.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:25:43 +00:00

193 lines
6.4 KiB
PHP

<?php
namespace App\Http\Controllers\Transfer;
use App\Http\Controllers\Controller;
use App\Models\Transfer;
use App\Services\Qr\QrImageGeneratorService;
use App\Services\Qr\QrPdfExporter;
use App\Services\Transfer\TransferService;
use App\Services\Upload\ChunkedUploadService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class TransferController extends Controller
{
public function __construct(
private TransferService $transfers,
private ChunkedUploadService $chunkedUploads,
private QrImageGeneratorService $imageGenerator,
private QrPdfExporter $pdfExporter,
) {}
public function index(Request $request): View
{
$account = ladill_account();
$transfers = Transfer::query()
->where('user_id', $account->id)
->accessible()
->with(['qrCode', 'files'])
->latest()
->paginate(20);
return view('transfer.transfers.index', compact('transfers'));
}
public function create(): View
{
return view('transfer.transfers.create', [
'maxFiles' => (int) config('transfer.max_files_per_transfer', 20),
'pricePerGb' => (float) config('transfer.price_per_gb_month', 0.30),
'gracePeriodDays' => (int) config('transfer.grace_period_days', 15),
]);
}
public function store(Request $request): RedirectResponse
{
$account = ladill_account();
$maxBytes = (int) config('transfer.max_file_bytes', 0);
$fileRules = ['nullable', 'file'];
if ($maxBytes > 0) {
$fileRules[] = 'max:'.(int) ceil($maxBytes / 1024);
}
$data = $request->validate([
'title' => 'required|string|max:120',
'message' => 'nullable|string|max:2000',
'recipient_email' => 'nullable|email|max:255',
'password' => 'nullable|string|min:4|max:64',
'files' => 'nullable|array',
'files.*' => $fileRules,
'upload_ids' => 'nullable|array',
'upload_ids.*' => 'required|string|uuid',
]);
$ownerKey = 'user:'.$account->id;
$files = array_values($request->file('files', []) ?? []);
$uploadIds = array_values($data['upload_ids'] ?? []);
if ($files === [] && $uploadIds === []) {
return back()->withInput()->with('error', 'Add at least one file to share.');
}
foreach ($uploadIds as $uploadId) {
try {
$files[] = $this->chunkedUploads->toUploadedFile($uploadId, $ownerKey);
} catch (RuntimeException $e) {
return back()->withInput()->with('error', $e->getMessage());
}
}
try {
$transfer = $this->transfers->create($account, $data, $files);
} catch (RuntimeException $e) {
return back()->withInput()->with('error', $e->getMessage());
} finally {
foreach ($uploadIds as $uploadId) {
$this->chunkedUploads->cleanup($uploadId);
}
}
$success = 'Transfer created. Share the link or QR code with recipients.';
if ($transfer->recipient_email && $transfer->recipientMilestoneSent('created')) {
$success .= ' A download link was emailed to '.$transfer->recipient_email.'.';
}
return redirect()
->route('transfer.transfers.show', $transfer)
->with('success', $success);
}
public function show(Transfer $transfer): View
{
$this->authorizeTransfer($transfer);
$transfer->load(['files', 'qrCode']);
$previewDataUri = $transfer->qrCode
? $this->imageGenerator->previewDataUri($transfer->qrCode)
: null;
return view('transfer.transfers.show', [
'transfer' => $transfer,
'previewDataUri' => $previewDataUri,
]);
}
public function destroy(Transfer $transfer): RedirectResponse
{
$this->authorizeTransfer($transfer);
$this->transfers->delete($transfer);
return redirect()
->route('transfer.transfers.index')
->with('success', 'Transfer deleted.');
}
public function preview(Transfer $transfer): Response
{
$this->authorizeTransfer($transfer);
$qrCode = $transfer->qrCode;
abort_unless($qrCode, 404);
$qrCode = $this->imageGenerator->ensureValidImages($qrCode);
$bytes = $this->imageGenerator->normalizeStoredPng($qrCode->png_path);
if ($bytes === null) {
$bytes = $this->imageGenerator->renderPng($qrCode->encodedPayload(), $qrCode->style());
$this->imageGenerator->generateAndStore($qrCode);
}
return response($bytes, 200, [
'Content-Type' => 'image/png',
'Cache-Control' => 'private, max-age=3600',
]);
}
public function download(Transfer $transfer, string $format): StreamedResponse
{
$this->authorizeTransfer($transfer);
$qrCode = $transfer->qrCode;
abort_unless($qrCode, 404);
$qrCode = $this->imageGenerator->ensureValidImages($qrCode);
$filename = Str::slug($qrCode->label).'-qr.'.($format === 'svg' ? 'svg' : 'png');
if ($format === 'png') {
$bytes = $this->imageGenerator->normalizeStoredPng($qrCode->png_path);
if ($bytes === null) {
$bytes = $this->imageGenerator->renderPng($qrCode->encodedPayload(), $qrCode->style());
$this->imageGenerator->generateAndStore($qrCode);
}
return response()->streamDownload(fn () => print($bytes), $filename, [
'Content-Type' => 'image/png',
]);
}
if ($format === 'svg') {
abort_unless($qrCode->svg_path, 404);
return response()->streamDownload(
fn () => print(file_get_contents(storage_path('app/private/qr/'.$qrCode->svg_path))),
$filename,
['Content-Type' => 'image/svg+xml'],
);
}
return $this->pdfExporter->download($qrCode);
}
private function authorizeTransfer(Transfer $transfer): void
{
abort_unless($transfer->user_id === ladill_account()->id, 403);
}
}