Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Http/Controllers/App/AssetController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public function storeChunked(StoreChunkedAssetRequest $request, ChunkedAssetRece
(int) $request->validated('range_start'),
(int) $request->validated('range_end'),
(int) $request->validated('total_size'),
(string) $request->validated('upload_id'),
)->toResponse();
}

Expand Down
8 changes: 3 additions & 5 deletions app/Http/Requests/App/Asset/StoreChunkedAssetRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protected function prepareForValidation(): void
'range_end' => $parsed[1] ?? null,
'total_size' => $parsed[2] ?? null,
'file_name' => strtolower(rawurldecode((string) $this->header('X-File-Name', 'upload'))),
'upload_id' => $this->header('X-Upload-Id'),
]);
}

Expand All @@ -47,6 +48,7 @@ public function rules(): array
'range_end' => ['required', 'integer', 'gte:range_start'],
'total_size' => ['required', 'integer', 'min:1', 'max:'.MediaType::Video->maxSizeInBytes()],
'file_name' => ['required', 'string', 'ends_with:'.implode(',', $allowedSuffixes)],
'upload_id' => ['required', 'string', 'uuid'],
];
}

Expand All @@ -56,11 +58,7 @@ public function rules(): array
public function messages(): array
{
return [
'range_start.required' => 'Invalid Content-Range header',
'range_end.required' => 'Invalid Content-Range header',
'total_size.required' => 'Invalid Content-Range header',
'total_size.max' => 'File size exceeds the maximum allowed ('.MediaType::Video->maxSizeInMb().' MB).',
'file_name.ends_with' => 'File type not supported.',
'total_size.max' => __('assets.upload.file_too_large', ['max' => MediaType::Video->maxSizeInMb()]),
];
}
}
8 changes: 0 additions & 8 deletions app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,4 @@ public function rules(): array
'logo_url' => ['nullable', 'url', 'max:1024'],
];
}

public function messages(): array
{
return [
'name.required' => 'O nome do workspace é obrigatório.',
'name.max' => 'O nome do workspace deve ter no máximo 255 caracteres.',
];
}
}
8 changes: 0 additions & 8 deletions app/Http/Requests/App/Workspace/UpdateWorkspaceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,4 @@ public function rules(): array
'logo_url' => ['nullable', 'url', 'max:1024'],
];
}

public function messages(): array
{
return [
'name.required' => 'The workspace name is required.',
'name.max' => 'The workspace name must be at most 255 characters.',
];
}
}
3 changes: 2 additions & 1 deletion app/Services/Media/ChunkedAssetReceiver.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ public function receive(
int $rangeStart,
int $rangeEnd,
int $totalSize,
string $attemptId,
): ChunkReceipt {
$identifier = md5($user->id.$fileName.$totalSize);
$identifier = md5("{$user->id}{$fileName}{$totalSize}{$attemptId}");

return $this->cloud->shouldUseMultipart($fileName)
? $this->receiveViaMultipart($workspace, $identifier, $fileName, $chunk, $rangeStart, $rangeEnd, $totalSize)
Expand Down
2 changes: 2 additions & 0 deletions lang/ar/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG، PNG، GIF، WebP، MP4، PDF',
'uploading' => 'جارٍ الرفع...',
'failed' => 'تعذر رفع :file. يرجى المحاولة مرة أخرى.',
'file_too_large' => 'حجم الملف يتجاوز الحد الأقصى المسموح به (:max ميجابايت).',
'cancelled' => 'تم إلغاء الرفع.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/de/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Wird hochgeladen...',
'failed' => ':file konnte nicht hochgeladen werden. Bitte versuche es erneut.',
'file_too_large' => 'Die Dateigröße überschreitet das zulässige Maximum (:max MB).',
'cancelled' => 'Upload abgebrochen.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/el/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Μεταφόρτωση...',
'failed' => 'Δεν ήταν δυνατή η μεταφόρτωση του :file. Παρακαλούμε δοκιμάστε ξανά.',
'file_too_large' => 'Το μέγεθος του αρχείου υπερβαίνει το μέγιστο επιτρεπόμενο (:max MB).',
'cancelled' => 'Η μεταφόρτωση ακυρώθηκε.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/en/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Uploading...',
'failed' => 'Could not upload :file. Please try again.',
'file_too_large' => 'File size exceeds the maximum allowed (:max MB).',
'cancelled' => 'Upload cancelled.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/es/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Subiendo...',
'failed' => 'No se pudo subir :file. Inténtalo de nuevo.',
'file_too_large' => 'El tamaño del archivo supera el máximo permitido (:max MB).',
'cancelled' => 'Subida cancelada.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/fr/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Import en cours...',
'failed' => 'Impossible d\'importer :file. Veuillez réessayer.',
'file_too_large' => 'La taille du fichier dépasse le maximum autorisé (:max Mo).',
'cancelled' => 'Import annulé.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/it/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Caricamento in corso...',
'failed' => 'Impossibile caricare :file. Riprova.',
'file_too_large' => 'La dimensione del file supera il massimo consentito (:max MB).',
'cancelled' => 'Caricamento annullato.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/ja/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG、PNG、GIF、WebP、MP4、PDF',
'uploading' => 'アップロード中...',
'failed' => ':file をアップロードできませんでした。もう一度お試しください。',
'file_too_large' => 'ファイルサイズが許容される最大値(:max MB)を超えています。',
'cancelled' => 'アップロードをキャンセルしました。',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/ko/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => '업로드 중...',
'failed' => ':file을(를) 업로드할 수 없습니다. 다시 시도해 주세요.',
'file_too_large' => '파일 크기가 허용된 최대값(:max MB)을 초과했습니다.',
'cancelled' => '업로드가 취소되었습니다.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/nl/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Uploaden...',
'failed' => ':file kon niet worden geüpload. Probeer het opnieuw.',
'file_too_large' => 'Bestandsgrootte overschrijdt het toegestane maximum (:max MB).',
'cancelled' => 'Upload geannuleerd.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/pl/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Przesyłanie...',
'failed' => 'Nie udało się przesłać :file. Spróbuj ponownie.',
'file_too_large' => 'Rozmiar pliku przekracza dozwolone maksimum (:max MB).',
'cancelled' => 'Przesyłanie anulowane.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/pt-BR/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Enviando...',
'failed' => 'Não foi possível enviar :file. Tente novamente.',
'file_too_large' => 'O tamanho do arquivo excede o máximo permitido (:max MB).',
'cancelled' => 'Envio cancelado.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/ru/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Загрузка...',
'failed' => 'Не удалось загрузить :file. Попробуйте ещё раз.',
'file_too_large' => 'Размер файла превышает максимально допустимый (:max МБ).',
'cancelled' => 'Загрузка отменена.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/tr/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Yükleniyor...',
'failed' => ':file yüklenemedi. Lütfen tekrar deneyin.',
'file_too_large' => 'Dosya boyutu izin verilen maksimumu aşıyor (:max MB).',
'cancelled' => 'Yükleme iptal edildi.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/uk/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF',
'uploading' => 'Завантаження...',
'failed' => 'Не вдалося завантажити :file. Спробуйте ще раз.',
'file_too_large' => 'Розмір файлу перевищує максимально допустимий (:max МБ).',
'cancelled' => 'Завантаження скасовано.',
],

'empty' => [
Expand Down
2 changes: 2 additions & 0 deletions lang/zh/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'formats' => 'JPEG、PNG、GIF、WebP、MP4、PDF',
'uploading' => '上传中…',
'failed' => '无法上传 :file,请重试。',
'file_too_large' => '文件大小超过允许的最大值(:max MB)。',
'cancelled' => '上传已取消。',
],

'empty' => [
Expand Down
16 changes: 14 additions & 2 deletions resources/js/components/assets/GalleryBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const uploadsSentinel = useTemplateRef<HTMLDivElement>('uploadsSentinel');
const isDragging = ref(false);
const uploading = ref(false);
let uploadsObserver: IntersectionObserver | null = null;
let uploadAbortController: AbortController | null = null;

const fetchUploads = async (page: number, term: string) => {
const response = await fetch(
Expand Down Expand Up @@ -240,15 +241,22 @@ const handleDrop = (event: DragEvent) => {
};

const uploadFiles = async (files: File[]) => {
if (uploading.value) return;
uploading.value = true;
uploadAbortController = new AbortController();
for (const file of files) {
try {
await uploadChunked({
file,
url: assetsStoreChunked.url(),
collection: 'assets',
signal: uploadAbortController.signal,
});
} catch {
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
toast.info(trans('assets.upload.cancelled'));
break;
}
toast.error(trans('assets.upload.failed', { file: file.name }));
}
}
Expand Down Expand Up @@ -596,6 +604,7 @@ onUnmounted(() => {
uploadsObserver?.disconnect();
unsplashObserver?.disconnect();
giphyObserver?.disconnect();
uploadAbortController?.abort();
});
</script>

Expand All @@ -612,7 +621,10 @@ onUnmounted(() => {
<TabsContent value="uploads" class="mt-6">
<div
class="relative mb-4 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed p-8 text-center transition-colors"
:class="isDragging ? 'border-foreground bg-violet-100' : 'border-foreground/25 bg-card hover:bg-foreground/5'"
:class="[
isDragging ? 'border-foreground bg-violet-100' : 'border-foreground/25 bg-card hover:bg-foreground/5',
uploading ? 'pointer-events-none' : '',
]"
@click="triggerFileInput"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
Expand Down
6 changes: 6 additions & 0 deletions resources/js/utils/chunkedUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface ChunkedUploadOptions {
modelId?: string;
collection?: string;
chunkSize?: number;
signal?: AbortSignal;
onProgress?: (progress: number) => void;
onComplete?: (response: any) => void;
onError?: (error: any) => void;
Expand All @@ -30,6 +31,7 @@ export const uploadChunked = async (options: ChunkedUploadOptions): Promise<Chun
modelId,
collection = 'default',
chunkSize = DEFAULT_CHUNK_SIZE,
signal,
onProgress,
onComplete,
onError,
Expand All @@ -38,6 +40,7 @@ export const uploadChunked = async (options: ChunkedUploadOptions): Promise<Chun
const csrfToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content ?? '';
const totalSize = file.size;
const totalChunks = Math.ceil(totalSize / chunkSize);
const uploadId = crypto.randomUUID();
let uploadedBytes = 0;

try {
Expand All @@ -50,6 +53,7 @@ export const uploadChunked = async (options: ChunkedUploadOptions): Promise<Chun
'Content-Type': 'application/octet-stream',
'Content-Range': `bytes ${start}-${end - 1}/${totalSize}`,
'X-File-Name': encodeURIComponent(file.name),
'X-Upload-Id': uploadId,
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
Expand All @@ -63,6 +67,7 @@ export const uploadChunked = async (options: ChunkedUploadOptions): Promise<Chun
method: 'POST',
headers,
body: chunk,
signal,
});

if (!response.ok) throw new Error(`Upload chunk failed: ${response.statusText}`);
Expand All @@ -81,6 +86,7 @@ export const uploadChunked = async (options: ChunkedUploadOptions): Promise<Chun

throw new Error('Upload did not complete');
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') throw error;
onError?.(error);
throw error;
}
Expand Down
8 changes: 8 additions & 0 deletions tests/Feature/AssetControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

beforeEach(function () {
Storage::fake();
Expand Down Expand Up @@ -214,6 +215,7 @@
[
'HTTP_CONTENT_RANGE' => 'bytes 0-'.($size - 1).'/'.$size,
'HTTP_X_FILE_NAME' => 'test.png',
'HTTP_X_UPLOAD_ID' => Str::uuid()->toString(),
'HTTP_ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/octet-stream',
],
Expand All @@ -238,6 +240,7 @@
[
'HTTP_CONTENT_RANGE' => 'bytes 0-'.($size - 1).'/'.$size,
'HTTP_X_FILE_NAME' => 'deck.pdf',
'HTTP_X_UPLOAD_ID' => Str::uuid()->toString(),
'HTTP_ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/octet-stream',
],
Expand All @@ -257,6 +260,7 @@
[
'HTTP_CONTENT_RANGE' => 'bytes 0-499/1000',
'HTTP_X_FILE_NAME' => 'test-video.mp4',
'HTTP_X_UPLOAD_ID' => Str::uuid()->toString(),
'HTTP_ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/octet-stream',
],
Expand All @@ -276,13 +280,15 @@
[
'HTTP_CONTENT_RANGE' => 'bytes 0-99/100',
'HTTP_X_FILE_NAME' => 'malware.exe',
'HTTP_X_UPLOAD_ID' => Str::uuid()->toString(),
'HTTP_ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/octet-stream',
],
str_repeat('x', 100),
);

$response->assertUnprocessable();
$response->assertJsonValidationErrors('file_name');
});

test('chunked upload rejects invalid Content-Range header', function () {
Expand All @@ -293,6 +299,7 @@
[
'HTTP_CONTENT_RANGE' => 'invalid',
'HTTP_X_FILE_NAME' => 'test.jpg',
'HTTP_X_UPLOAD_ID' => Str::uuid()->toString(),
'HTTP_ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/octet-stream',
],
Expand All @@ -302,6 +309,7 @@
// FormRequest validation surfaces parse failures as 422
// (range_start / range_end / total_size all required).
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['range_start', 'range_end', 'total_size']);
});

test('chunked upload rejects unauthenticated', function () {
Expand Down
Loading