NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Media Library for Laravel

Priame nahrávanie

Pri priamom nahrávaní posiela prehliadač súbor priamo do úložiska na URL, ktorú balík podpíše. MediaLibrary::adoptUpload($token) potom z uloženého súboru vytvorí médium: preverí ho tam, kde leží, a presunie ho v rámci úložiska, takže bajty nikdy neprejdú vašou aplikáciou, ani kvôli kopírovaniu. Predvolene je vypnuté — zapnete ho cez media.direct_uploads.enabled:

use RoundlyConsulting\MediaLibrary\Facades\MediaLibrary;

// 1. Your API signs an upload for the browser.
$upload = MediaLibrary::for($post)
    ->upload(fileName: $request->name, size: $request->size)   // declared size vs the bucket's cap
    ->declaringType($request->type)                             // optional: early refusal only
    ->withSha256($request->sha256)                              // optional: storage refuses other bytes
    ->forActor($request->user())                                // optional: only this actor may adopt
    ->usingName('Cover')->withProperty('alt', 'Sea')->onQueue('media')
    ->expiresIn(15)
    ->toMediaBucket('gallery');

return [
    'token' => $upload->token,           // shown once — the package stores only its sha256
    'method' => $upload->method->value,  // put | post | multipart
    'url' => $upload->url,
    'headers' => $upload->headers,       // send with the PUT
    'fields' => $upload->fields,         // the form fields of a POST
];

// 2. The browser uploads, then calls you back with the token.
$media = MediaLibrary::adoptUpload(
    $request->token,
    actor: $request->user(),
    expectedOwner: $post,         // the record this route serves
    expectedBucket: 'gallery',    // a ticket for another owner or bucket throws, unspent
);

MediaLibrary::upload(...)->toBucket('library') podpíše nahrávanie globálneho média a ->asDraft() ho prevezme ako draft, ktorý neskôr naviažete cez bindDraft(). Builder prijíma aj withCustomProperties(), withVisibility() a useDisk(). Všetko beží cez MediaLibraryManager, takže dependency injection funguje rovnako ($manager->for($post)->upload(...)).

DirectUploadVýznam
tokenNím súbor prevezmete. Zobrazí sa len tu — balík ukladá iba jeho sha256.
methodDirectUploadMethod::Put, Post alebo Multipart.
url, headersCieľ PUT a hlavičky, ktoré treba poslať (bez Host).
fieldsPolia formulára pri POST; pole so súborom pridajte ako posledné.
keyKam bajty dopadnú — kľúč vždy určí server.
maxSizeLimit tiketu: deklarovaná veľkosť.
sizeEnforcedBySignaturetrue, keď iné veľkosti odmietne samotné úložisko: politika POST alebo PUT, ktorý podpísal svoje sha256.
expiresAtKedy tiket a jeho URL prestanú platiť.
mediaUuiduuid, ktoré dostane prevzaté médium.
partSize, partCountLen pri multipart.

Tiket

Podpísanie zaznamená tiket v media_uploads: disk, kľúč určený serverom, vlastníka, skupinu, aktéra, limit veľkosti, viditeľnosť, zvolené voľby a uuid, ktoré médium dostane. Token tvorí id tiketu a 48 náhodných znakov; ukladá sa len jeho sha256, porovnáva sa v konštantnom čase a hlásenia výnimiek ho nikdy neobsahujú. Skôr než sa čokoľvek podpíše, balík odmietne:

  • nahrávanie pri vypnutom priamom nahrávaní a neuloženého vlastníka;
  • deklarovanú veľkosť nad limitom skupiny (maxFileSize(), inak media.max_file_size) a deklarovaný typ, ktorý skupina neprijíma, vrátane prísneho mime režimu;
  • viac ako max_pending_per_owner čakajúcich tiketov pre vlastníka (pri globálnom nahrávaní pre aktéra): TooManyPendingDirectUploads, s limitom v $limit a modelom, ktorému sa tikety započítali, v $holder;
  • disk, ktorý nevie podpísať, o čo sa žiada (DirectUploadsNotSupported).

TooManyPendingDirectUploads rozširuje DirectUploadsNotSupported, takže jeden catch stále pokryje obe; ak chcete na limit odpovedať inak — napríklad kódom 429 —, zachyťte ju ako prvú.

Kľúč si prehliadač nikdy nevyberá — súbor dopadne pod súkromný staging prefix (_uploads/). Súkromné médiá, ktorých bajty sa po kontrole nemôžu zmeniť, sa nahrávajú priamo na finálny kľúč: multipart nahrávania a PUT, ktorý na S3 podpísal svoje sha256. Verejné médiá idú vždy cez staging, takže nepreverené bajty nikdy neležia pod verejnou cestou.

Prevzatie

Naviažte token na jeho routu. Token je bearer tajomstvo: sám osebe nehovorí, ktorému záznamu slúži routa, ktorá ho preberá. Ak adoptUpload() odovzdáte expectedOwner: a expectedBucket:, tiket vydaný pre iného vlastníka (porovnáva sa morph typ a kľúč), pre žiadneho vlastníka (globálne nahrávanie alebo draft) alebo pre inú skupinu vyhodí DirectUploadTargetMismatch skôr, než sa stane čokoľvek iné. Tiket sa nenárokuje ani nezmení, takže ho správna routa môže stále prevziať, a médium už prevzatého tiketu sa nesprávnej route nevráti. $exception->mismatched je DirectUploadTargetMismatch::OWNER alebo ::BUCKET; hlásenie neuvádza vlastníka ani skupinu tiketu. completeUpload() prijíma tie isté dva argumenty a overí ich pred dokončením nahrávania. Bez nich sa prevezme tiket ktoréhokoľvek vlastníka, rovnako ako vo verzii 2.1 — ďalšia hlavná verzia plánuje vlastníka vyžadovať.

Potom prevzatie spustí AdoptDirectUploadAction:

  • Nárok — jedna podmienená aktualizácia presunie tiket zo stavu pending do adopting, takže z dvoch súbežných požiadaviek ho dostane presne jedna; druhá dostane DirectUploadIncomplete. Po prevzatí vráti opakované volanie to isté médium.
  • Veľkosť — prečíta sa z úložiska (na S3 HeadObject). Chýbajúci objekt vyhodí DirectUploadIncomplete a tiket uvoľní; objekt nad limitom sa odmietne.
  • Preverenie — typ sa rozpozná z úvodných bajtov (na S3 jedno čítanie rozsahu), nikdy sa nepreberá z uloženého Content-Type, ktorý zvolil ten, kto nahrával. Rozmery pochádzajú z tých istých bajtov vrátane EXIF orientácie a checksum je jeden streamovaný prechod s konštantnou pamäťou. Deklarované sha256 musí sedieť.
  • Pridanie — súbor vstúpi do skupiny ako pri každom pridaní: aktuálne pravidlá skupiny, rozsah dedup, singleFile(), varianty vo fronte tiketu a MediaHasBeenAdded. Objekt sa presunie v rámci úložiska na vlastnú cestu média — na S3 serverovým CopyObject, inde presunom v súborovom systéme — a nahrávanie na mieste zostane, kde je. Ak po uložení riadku čokoľvek zlyhá, riadok zmizne, objekt sa vráti späť a tiket sa dá prevziať znova, aj keď sa vráti späť vaša vlastná DB::transaction().
  • Hotovo — tiket prejde do stavu adopted a spustí sa DirectUploadHasBeenAdopted; jeho príznak deduplicated je true, keď sa zdieľal rovnaký obsah v rozsahu dedup a nahratý objekt sa po potvrdení zmazal.

Súbor, ktorý skupina odmietne — typ, veľkosť, rozmery, checksum, vlastník, ktorý už neexistuje —, sa odmietne natrvalo: jeho objekt sa po potvrdení transakcie zmaže, tiket zaznamená dôvod, spustí sa DirectUploadHasBeenRejected a znova sa vyhodí FileUnacceptableForBucket. Ďalší pokus o prevzatie vyhodí DirectUploadNotFound.

Na S3 presun nastaví ContentType na rozpoznaný typ a ContentDisposition podľa pravidla streamovacej routy, takže HTML poslané ako image/png sa uloží ako text/html, ako príloha. Médiá prevzaté na mieste si ponechajú Content-Type, ktorý poslal prehliadač, pretože ho nič neprepíše — sú vždy súkromné a každá ich dočasná URL, vrátane getTemporaryUrl(), si od pôvodu úložiska vyžiada hlavičky odpovede bez ohľadu na media.temporary_url_response_headers. Kým je priame nahrávanie zapnuté, stojí to jedno indexované vyhľadanie na inštanciu média.

PUT, POST alebo multipart

  • PUT (predvolené) — na S3 presigned PutObject (s ohľadom na presign_endpoint), inde temporaryUploadUrl() disku. Veľkosť sa overí pri prevzatí; ak je podpísané sha256, iné bajty odmietne už samotné úložisko.
  • POST (disks.<name>.method = post) — politika S3 POST s content-length-range [1, size], kľúčom a deklarovaným Content-Type: jediný podpísaný limit veľkosti. Cloudflare R2 ho nepodporuje.
  • Multipart (->multipart(?int $partSize)) — vytvorí sa pri podpísaní, časti sa podpisujú v dávkach; veľkosť sa overí pred každou dávkou aj pri dokončení.
$upload = MediaLibrary::for($video)->upload('film.mp4', $size)->multipart()->toMediaBucket('videos');
$parts = MediaLibrary::signUploadParts($upload->token, [1, 2, 3]);   // list<UploadPart>: partNumber, url, headers
// … the browser PUTs each part and keeps each response's ETag …
$media = MediaLibrary::completeUpload($upload->token, [1 => $etag1, 2 => $etag2, 3 => $etag3]);

Pred každou dávkou sa vypíšu už nahraté časti: časť väčšia ako veľkosť časti tiketu alebo časti, ktoré spolu prekročia limit, tiket odmietnu a nahrávanie prerušia. completeUpload() beží pod nárokom prevzatia, porovná časti s nahlásenými ETagmi, veľkosťou časti a limitom, nahrávanie dokončí a prevezme. Ako poistku pridajte do bucketu lifecycle pravidlo AbortIncompleteMultipartUpload.

Zrušenie, expirácia, upratovanie a dôveryhodné prevzatie

MediaLibrary::abortUpload($token);   // cancel a pending upload and remove its object
MediaLibrary::pruneUploads();        // expire stale tickets, clear leftovers; returns the count

// Adopt a trusted stored file without a ticket — never pass a path a client chose.
MediaLibrary::for($post)->adoptFromDisk('imports/2026-11/film.mp4', 's3')->toMediaBucket('videos');
  • abortUpload() prepne čakajúci tiket do stavu aborted a odstráni jeho objekt alebo nedokončené multipart nahrávanie; prevzatý, odmietnutý či zrušený tiket nechá bez zmeny, tiket práve preberaný vyhodí DirectUploadIncomplete.
  • Po expirácii sa tiket už prevziať nedá (DirectUploadExpired).
  • pruneUploads() / media:prune-uploads (naplánujte ho) expiruje neprevzaté tikety po expirácii a prune_grace, odstráni staging objekty, ktoré nedrží žiadny živý tiket, a preruší osirotené multipart nahrávania. Upratovanie a prevzatie nikdy nezískajú ten istý tiket súčasne.
  • adoptFromDisk() preverí uložený súbor na mieste a presunie ho, nikdy nie cez aplikáciu. Prevziať sa dá len cesta pod staging prefixom alebo v adoptable_prefixes, ktorú nepoužíva žiadny riadok média ani živý tiket — nikdy neodovzdávajte cestu, ktorú zvolil klient. Presun medzi dvoma diskami vyžaduje, aby oba boli S3; iné dvojice vyhodia DirectUploadsNotSupported.

Konfigurácia

// config/media.php
'direct_uploads' => [
    'enabled' => true,
    'adoptable_prefixes' => ['imports/'],
    'disks' => [
        's3' => [
            'method' => 'post',            // a signed size cap (AWS S3, MinIO; not Cloudflare R2)
            'storage_checksums' => false,  // true = trust the store's own sha256 of a signed PUT
        ],
    ],
],
Kľúč (media.direct_uploads.)PredvolenéVýznam
enabledfalseKým je vypnuté, všetko sa odmietne.
table_namemedia_uploadsTabuľka tiketov.
staging_prefix_uploads/Kam sa pred prevzatím nahráva. Držte ho súkromný: nesmie ho sprístupniť žiadne verejné ACL ani politika bucketu.
staging_disknullnull = vlastný disk média. Iný disk musí byť S3 bucket, z ktorého tie isté prihlasovacie údaje vedia kopírovať — napríklad súkromný bucket, keď médiá žijú vo verejnom; potrebné na R2, kde je verejný prístup pre celý bucket.
ttl15Minúty, počas ktorých platí tiket a jeho podpísané URL (aspoň 1); pre jedno nahrávanie expiresIn().
max_pending_per_owner20Čakajúce tikety na vlastníka, pri globálnom nahrávaní na aktéra.
sniff_bytes65536Bajty prečítané na rozpoznanie typu (aspoň 512). Pre rozmery obrázka sa prečíta najviac 512 KiB.
probe_max_bytes16 MiBNajväčší rastrový obrázok, ktorý sa počas výpočtu checksumu ponechá lokálne, aby sa mu vypočítali náhľady. Väčšie ich dostanú pri generovaní variantov.
prune_grace60Minúty po expirácii, kým media:prune-uploads tiket uprace.
adoptable_prefixes[]Prefixy, ktoré adoptFromDisk() prijme okrem staging prefixu, napr. ['imports/'].
multipart.part_size16 MiBAspoň 5 MiB (minimum S3). Cloudflare R2 vyžaduje rovnako veľké časti.
multipart.max_parts10000Najväčší počet častí jedného nahrávania.
disks.<name>.methodputpost pre podpísaný limit veľkosti (AWS S3, MinIO; nie R2).
disks.<name>.storage_checksumsfalseDôverovať vlastnému sha256 úložiska pri PUT, ktorý ho podpísal, namiesto opätovného čítania súboru (len pri algoritme sha256; najprv si úložisko otestujte).

Výnimky a eventy

  • DirectUploadsNotSupported — priame nahrávanie je vypnuté, disk nevie podpísať, o čo sa žiada (POST a multipart len S3), staging disk a disk média, ktoré nevedia kopírovať v rámci úložiska, cesta, ktorú adoptFromDisk() nesmie prevziať, priveľa častí alebo priveľa čakajúcich tiketov. Nič sa nepodpísalo.
  • DirectUploadNotFound — neznámy, poškodený, odmietnutý alebo zrušený token; token nikdy nezopakuje.
  • DirectUploadExpired — tiket expiroval pred prevzatím.
  • DirectUploadIncomplete — dočasná chyba: súbor ešte nie je v úložisku, tiket práve preberá iná požiadavka, alebo dokončenie multipart uvádza časti, ktoré úložisko nemá. Tiket zostáva použiteľný.
  • TooManyPendingDirectUploads — rozširuje DirectUploadsNotSupported: vlastník (pri globálnom nahrávaní alebo drafte aktér) už drží max_pending_per_owner čakajúcich tiketov. Verejné $limit a $holder; hlásenie neuvádza ani jedno.
  • DirectUploadActorMismatch — tiket vydaný cez forActor() prevzal iný aktér alebo žiadny.
  • DirectUploadTargetMismatch — adoptUpload() alebo completeUpload() dostali expectedOwner alebo expectedBucket, pre ktoré tiket nebol vydaný; globálny tiket ani tiket draftu očakávanému vlastníkovi nikdy nezodpovedá. Tiket zostane nedotknutý a dá sa ďalej prevziať. Verejné $mismatched (OWNER / BUCKET).
  • DirectUploadHasBeenAdopted (media, upload, deduplicated) a DirectUploadHasBeenRejected (upload, reason) sa spúšťajú popri bežných eventoch médií.

Bezpečnosť

  • Kľúče určuje server, staging prefix je súkromný a prevzatie prijme len token; tiket pevne určuje disk, kľúč, vlastníka, skupinu, príznak draftu, limit veľkosti a voliteľného aktéra.
  • Pri prevzatí aj dokončení odovzdajte expectedOwner: a expectedBucket:, aby sa token vydaný pre jeden záznam nedal dokončiť na inom.
  • Typ sa vždy rozpozná z obsahu, prípona uloženého názvu sa mu prispôsobí a platia aktuálne pravidlá skupiny — ničomu, čo deklaroval prehliadač, sa pri prevzatí neverí.
  • Prepísanie po kontrole vylučuje presun (staging) alebo nahrávanie na mieste, ktoré sa už nemôže zmeniť (dokončený multipart, podpísané sha256).
  • Lokálna routa serve od Laravelu načíta celé telo nahrávania do PHP: na vývoj stačí, pre veľké súbory v produkcii nie. Balík vlastnú prijímaciu routu nemá.

Prejavte lásku k open source

Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.

Ďalšie spôsoby podpory vrátane kryptomien

Odoslaním daru súhlasíte s našimi podmienkami prijímania darov.

Chcete to zabudovať do svojho produktu?

Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.