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('is_folder', true) ->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 ($inner) => $inner->whereKey($folderTransfer->id), fn ($inner) => $inner->where('is_folder', false))) ->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('is_folder', true) ->where('status', '!=', Transfer::STATUS_DELETED) ->withCount('files') ->when($search !== '', fn ($q) => $q->where('title', 'like', '%'.$search.'%')) ->orderBy('title') ->get(); $moveTargets = Transfer::query() ->where('user_id', $account->id) ->where('status', '!=', Transfer::STATUS_DELETED) ->where(function ($query) { $query->where('is_folder', true)->orWhere('is_root_storage', true); }) ->orderBy('title') ->get(['id', 'title', 'is_root_storage']); $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) ->where('status', '!=', Transfer::STATUS_DELETED) ->where(function ($query) { $query->where('is_folder', true)->orWhere('is_root_storage', true); }) ->findOrFail($data['destination']); $files = $this->authorizedFiles($data['files']); $this->transfers->moveFiles($destination, $files->all()); return redirect() ->route('transfer.files.index', $destination->is_folder ? ['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); } public function storeFolder(Request $request): RedirectResponse { $data = $request->validate([ 'name' => 'required|string|max:120', 'folder' => 'nullable|integer|exists:transfers,id', ]); $account = ladill_account(); $parent = null; if (! empty($data['folder'])) { $parent = Transfer::query() ->where('user_id', $account->id) ->where('is_folder', true) ->findOrFail($data['folder']); } try { $folder = $this->storage->createFolder($account, $data['name'], $parent); } catch (RuntimeException $e) { return back()->with('error', $e->getMessage()); } return redirect() ->route('transfer.files.index', ['folder' => $folder->id]) ->with('success', 'Folder created.'); } public function checkUpload(Request $request): JsonResponse { $data = $request->validate([ 'filenames' => 'required|array|min:1', 'filenames.*' => 'required|string|max:255', 'folder' => 'nullable|integer|exists:transfers,id', ]); $account = ladill_account(); $folder = $this->resolveUploadFolder($account, $data['folder'] ?? null); $conflicts = $this->storage->findConflicts( $account, $folder, $data['filenames'], ); return response()->json(['conflicts' => $conflicts]); } public function storeUpload(Request $request): RedirectResponse|JsonResponse { $data = $request->validate([ 'files' => 'required|array|min:1', 'files.*' => 'required|file', 'folder' => 'nullable|integer|exists:transfers,id', 'resolutions' => 'nullable|array', 'resolutions.*' => 'in:replace,keep_both', ]); $account = ladill_account(); $folder = $this->resolveUploadFolder($account, $data['folder'] ?? null); $files = array_values($request->file('files', []) ?? []); try { $uploaded = $this->storage->uploadFiles( $account, $folder, $files, $data['resolutions'] ?? [], ); } catch (RuntimeException $e) { if ($request->expectsJson()) { return response()->json(['message' => $e->getMessage()], 422); } return back()->with('error', $e->getMessage()); } $count = count($uploaded); $message = $count.' file'.($count === 1 ? '' : 's').' uploaded.'; if ($request->expectsJson()) { return response()->json(['message' => $message, 'count' => $count]); } return back()->with('success', $message); } private function resolveUploadFolder(User $user, ?int $folderId): ?Transfer { if ($folderId === null) { return null; } return Transfer::query() ->where('user_id', $user->id) ->where('is_folder', true) ->where('status', '!=', Transfer::STATUS_DELETED) ->findOrFail($folderId); } /** @param array|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, ); } }