Add chunked uploads and raise storage rate to GHS 0.30/GB/month.
Deploy Ladill Transfer / deploy (push) Successful in 39s

Large files upload in 5 MB chunks (hosting file manager pattern) for the
transfer UI and Ladill Mail S2S API, with no app-level file size cap when
TRANSFER_MAX_FILE_BYTES=0.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 12:07:27 +00:00
co-authored by Cursor
parent fb131cd7fa
commit 65b634bb0b
23 changed files with 797 additions and 27 deletions
@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Transfer;
use App\Http\Controllers\Controller;
use App\Services\Upload\ChunkedUploadService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
class ChunkedUploadController extends Controller
{
public function __construct(
private ChunkedUploadService $uploads,
) {}
public function init(Request $request): JsonResponse
{
$account = ladill_account();
$validated = $request->validate([
'filename' => 'required|string|max:255',
'filesize' => 'required|integer|min:1',
]);
$uploadId = $this->uploads->init($this->ownerKey($account->id), [
'filename' => $validated['filename'],
'filesize' => (int) $validated['filesize'],
]);
return response()->json([
'success' => true,
'upload_id' => $uploadId,
]);
}
public function chunk(Request $request): JsonResponse
{
$account = ladill_account();
$ownerKey = $this->ownerKey($account->id);
$validated = $request->validate([
'upload_id' => 'required|string|uuid',
'chunk_index' => 'required|integer|min:0',
'chunk_size' => 'required|integer|min:1',
'chunk' => 'required|file|max:10240',
]);
$chunk = $request->file('chunk');
if ($chunk === null) {
return response()->json(['success' => false, 'error' => 'Chunk missing.'], 422);
}
try {
$received = $this->uploads->receiveChunk(
$validated['upload_id'],
$ownerKey,
(int) $validated['chunk_index'],
$chunk,
(int) $validated['chunk_size'],
);
} catch (RuntimeException $e) {
return response()->json(['success' => false, 'error' => $e->getMessage()], 400);
}
return response()->json([
'success' => true,
'chunk_index' => (int) $validated['chunk_index'],
'chunks_received' => $received,
]);
}
public function finalize(Request $request): JsonResponse
{
$account = ladill_account();
$ownerKey = $this->ownerKey($account->id);
$validated = $request->validate([
'upload_id' => 'required|string|uuid',
'total_chunks' => 'required|integer|min:1',
]);
try {
$this->uploads->finalize(
$validated['upload_id'],
$ownerKey,
(int) $validated['total_chunks'],
);
} catch (RuntimeException $e) {
return response()->json(['success' => false, 'error' => $e->getMessage()], 400);
}
return response()->json([
'success' => true,
'upload_id' => $validated['upload_id'],
]);
}
private function ownerKey(int $userId): string
{
return 'user:'.$userId;
}
}