Add Files page upload flow with manual folders and duplicate handling.
Deploy Ladill Transfer / deploy (push) Successful in 44s

Users can create folders, upload files or folders, and choose replace or keep both on name conflicts instead of auto-grouping by transfer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 21:27:39 +00:00
co-authored by Cursor
parent 19170d6bc7
commit f9b16f6b61
12 changed files with 746 additions and 127 deletions
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Transfer;
use App\Http\Controllers\Controller;
use App\Models\Transfer;
use App\Models\TransferFile;
use App\Models\User;
use App\Services\Transfer\FileStorageService;
use App\Services\Transfer\TransferRecipientMailService;
use App\Services\Transfer\TransferService;
use Illuminate\Http\JsonResponse;
@@ -21,6 +23,7 @@ class FilesController extends Controller
{
public function __construct(
private TransferService $transfers,
private FileStorageService $storage,
) {}
public function index(Request $request): View
@@ -35,6 +38,7 @@ class FilesController extends Controller
if ($folderId) {
$folderTransfer = Transfer::query()
->where('user_id', $account->id)
->where('is_folder', true)
->where('status', '!=', Transfer::STATUS_DELETED)
->findOrFail($folderId);
}
@@ -42,8 +46,9 @@ class FilesController extends Controller
$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))
->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())
@@ -56,8 +61,8 @@ class FilesController extends Controller
$folders = Transfer::query()
->where('user_id', $account->id)
->where('is_folder', true)
->where('status', '!=', Transfer::STATUS_DELETED)
->whereHas('files')
->withCount('files')
->when($search !== '', fn ($q) => $q->where('title', 'like', '%'.$search.'%'))
->orderBy('title')
@@ -65,9 +70,12 @@ class FilesController extends Controller
$moveTargets = Transfer::query()
->where('user_id', $account->id)
->accessible()
->where('status', '!=', Transfer::STATUS_DELETED)
->where(function ($query) {
$query->where('is_folder', true)->orWhere('is_root_storage', true);
})
->orderBy('title')
->get(['id', 'title']);
->get(['id', 'title', 'is_root_storage']);
$storageBytes = Transfer::query()
->where('user_id', $account->id)
@@ -184,14 +192,17 @@ class FilesController extends Controller
$destination = Transfer::query()
->where('user_id', ladill_account()->id)
->accessible()
->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', ['folder' => $destination->id])
->route('transfer.files.index', $destination->is_folder ? ['folder' => $destination->id] : [])
->with('success', $files->count().' file'.($files->count() === 1 ? '' : 's').' moved to '.$destination->title.'.');
}
@@ -273,6 +284,105 @@ class FilesController extends Controller
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<int|string>|null $ids */
private function authorizedFiles(?array $ids): Collection
{