Files
ladill-transfer/app/Http/Controllers/Transfer/FilesController.php
T
isaaccladandCursor 19170d6bc7
Deploy Ladill Transfer / deploy (push) Successful in 29s
Add transfer search and OneDrive-style Files storage UI.
Transfers now lists all shares with filters and pagination; Files becomes a selectable cloud view with download, share, delete, move, and open-in-email actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 20:59:05 +00:00

307 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Transfer;
use App\Http\Controllers\Controller;
use App\Models\Transfer;
use App\Models\TransferFile;
use App\Services\Transfer\TransferRecipientMailService;
use App\Services\Transfer\TransferService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use RuntimeException;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ZipArchive;
class FilesController extends Controller
{
public function __construct(
private TransferService $transfers,
) {}
public function index(Request $request): View
{
$account = ladill_account();
$search = trim((string) $request->query('q', ''));
$folderId = $request->integer('folder') ?: null;
$sort = (string) $request->query('sort', 'newest');
$viewMode = (string) $request->query('view', $folderId ? 'files' : 'all');
$folderTransfer = null;
if ($folderId) {
$folderTransfer = Transfer::query()
->where('user_id', $account->id)
->where('status', '!=', Transfer::STATUS_DELETED)
->findOrFail($folderId);
}
$filesQuery = TransferFile::query()
->whereHas('transfer', fn ($q) => $q
->where('user_id', $account->id)
->where('status', '!=', Transfer::STATUS_DELETED))
->when($folderTransfer, fn ($q) => $q->where('transfer_id', $folderTransfer->id))
->when($search !== '', fn ($q) => $q->where('original_name', 'like', '%'.$search.'%'))
->with(['transfer.qrCode'])
->when($sort === 'oldest', fn ($q) => $q->oldest())
->when($sort === 'name', fn ($q) => $q->orderBy('original_name'))
->when($sort === 'size', fn ($q) => $q->orderByDesc('size_bytes'))
->when($sort === 'downloads', fn ($q) => $q->orderByDesc('downloads_total'))
->when($sort === 'newest', fn ($q) => $q->latest());
$files = $filesQuery->paginate(30)->withQueryString();
$folders = Transfer::query()
->where('user_id', $account->id)
->where('status', '!=', Transfer::STATUS_DELETED)
->whereHas('files')
->withCount('files')
->when($search !== '', fn ($q) => $q->where('title', 'like', '%'.$search.'%'))
->orderBy('title')
->get();
$moveTargets = Transfer::query()
->where('user_id', $account->id)
->accessible()
->orderBy('title')
->get(['id', 'title']);
$storageBytes = Transfer::query()
->where('user_id', $account->id)
->where('status', '!=', Transfer::STATUS_DELETED)
->sum('storage_bytes');
return view('transfer.files.index', [
'files' => $files,
'folders' => $folders,
'folderTransfer' => $folderTransfer,
'moveTargets' => $moveTargets,
'search' => $search,
'sort' => $sort,
'viewMode' => $viewMode,
'storageBytes' => (int) $storageBytes,
'storageGb' => round($storageBytes / (1024 * 1024 * 1024), 2),
'pricePerGb' => (float) config('transfer.price_per_gb_month', 0.30),
]);
}
public function download(TransferFile $file): StreamedResponse
{
$this->authorizeFile($file);
abort_unless($file->existsOnDisk(), 404);
return Storage::disk($file->disk)->response(
$file->path,
$file->original_name,
['Content-Type' => $file->mime_type],
);
}
public function bulkDownload(Request $request): StreamedResponse|RedirectResponse
{
$files = $this->authorizedFiles($request->input('files', []));
if ($files->isEmpty()) {
return back()->with('error', 'Select at least one file to download.');
}
if ($files->count() === 1) {
$file = $files->first();
abort_unless($file->existsOnDisk(), 404);
return Storage::disk($file->disk)->response(
$file->path,
$file->original_name,
['Content-Type' => $file->mime_type],
);
}
$filename = 'ladill-files-'.now()->format('Y-m-d-His').'.zip';
return response()->streamDownload(function () use ($files) {
$zip = new ZipArchive;
$tmp = tempnam(sys_get_temp_dir(), 'ladill_zip_');
if ($tmp === false || $zip->open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new RuntimeException('Could not prepare download archive.');
}
$usedNames = [];
foreach ($files as $file) {
if (! $file->existsOnDisk()) {
continue;
}
$name = $file->original_name;
if (isset($usedNames[$name])) {
$usedNames[$name]++;
$ext = pathinfo($name, PATHINFO_EXTENSION);
$base = pathinfo($name, PATHINFO_FILENAME);
$name = $base.' ('.$usedNames[$name].')'.($ext !== '' ? '.'.$ext : '');
} else {
$usedNames[$name] = 0;
}
$zip->addFromString($name, Storage::disk($file->disk)->get($file->path));
}
$zip->close();
readfile($tmp);
@unlink($tmp);
}, $filename, ['Content-Type' => 'application/zip']);
}
public function destroy(TransferFile $file): RedirectResponse
{
$this->authorizeFile($file);
$this->transfers->deleteFile($file);
return back()->with('success', 'File deleted.');
}
public function bulkDestroy(Request $request): RedirectResponse
{
$files = $this->authorizedFiles($request->input('files', []));
if ($files->isEmpty()) {
return back()->with('error', 'Select at least one file to delete.');
}
$this->transfers->deleteFiles($files->all());
return back()->with('success', $files->count().' file'.($files->count() === 1 ? '' : 's').' deleted.');
}
public function move(Request $request): RedirectResponse
{
$data = $request->validate([
'destination' => 'required|integer|exists:transfers,id',
'files' => 'required|array|min:1',
'files.*' => 'integer|exists:transfer_files,id',
]);
$destination = Transfer::query()
->where('user_id', ladill_account()->id)
->accessible()
->findOrFail($data['destination']);
$files = $this->authorizedFiles($data['files']);
$this->transfers->moveFiles($destination, $files->all());
return redirect()
->route('transfer.files.index', ['folder' => $destination->id])
->with('success', $files->count().' file'.($files->count() === 1 ? '' : 's').' moved to '.$destination->title.'.');
}
public function share(Request $request): JsonResponse
{
$files = $this->authorizedFiles($request->input('files', []));
if ($files->isEmpty()) {
return response()->json(['message' => 'Select at least one file to share.'], 422);
}
$links = $files
->map(fn (TransferFile $file) => $file->transfer->qrCode?->publicUrl())
->filter()
->unique()
->values()
->all();
return response()->json([
'links' => $links,
'message' => count($links).' share link'.(count($links) === 1 ? '' : 's').' ready to copy.',
]);
}
public function reshareEmail(Request $request): RedirectResponse
{
$data = $request->validate([
'transfer' => 'required|integer|exists:transfers,id',
'email' => 'required|email|max:255',
]);
$transfer = Transfer::query()
->where('user_id', ladill_account()->id)
->where('status', '!=', Transfer::STATUS_DELETED)
->findOrFail($data['transfer']);
$transfer->forceFill([
'recipient_email' => $data['email'],
'recipient_email_milestones' => ['created'],
'recipient_milestones_sent' => [],
])->save();
app(TransferRecipientMailService::class)->notifyRecipient($transfer->fresh(['files', 'qrCode', 'user']), 'created');
return back()->with('success', 'Share link emailed to '.$data['email'].'.');
}
public function openInEmail(Request $request): RedirectResponse
{
$files = $this->authorizedFiles($request->input('files', []));
if ($files->isEmpty()) {
return back()->with('error', 'Select at least one file to open in email.');
}
$links = $files
->map(fn (TransferFile $file) => $file->transfer->qrCode?->publicUrl())
->filter()
->unique()
->values();
$names = $files->pluck('original_name')->unique()->take(3)->implode(', ');
if ($files->count() > 3) {
$names .= ' +'.($files->count() - 3).' more';
}
$body = "Hi,\n\nI'm sharing the following files with you via Ladill Transfer:\n\n";
foreach ($links as $link) {
$body .= $link."\n";
}
$body .= "\nDownload using the link".($links->count() === 1 ? '' : 's')." above.";
$url = ladill_webmail_url('').'?'.http_build_query([
'compose' => 1,
'subject' => 'Shared files: '.$names,
'body' => $body,
]);
return redirect()->away($url);
}
/** @param array<int|string>|null $ids */
private function authorizedFiles(?array $ids): Collection
{
$ids = collect($ids ?? [])
->filter(fn ($id) => is_numeric($id))
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
if ($ids === []) {
return collect();
}
return TransferFile::query()
->whereIn('id', $ids)
->whereHas('transfer', fn ($q) => $q
->where('user_id', ladill_account()->id)
->where('status', '!=', Transfer::STATUS_DELETED))
->with(['transfer.qrCode'])
->get();
}
private function authorizeFile(TransferFile $file): void
{
abort_unless(
$file->transfer()->where('user_id', ladill_account()->id)->exists(),
403,
);
}
}