Desenvolupem la teva web presencial SPA DES DE 300€. Si, es una bogeria. Web SPA DES DE 300€ — Si, es una bogeria. Parlem →

Com pujar fitxers directament a S3 i R2 sense que passin pel teu servidor

Com pujar fitxers directament a S3 i R2 sense que passin pel teu servidor

Rebre un fitxer al servidor i copiar-lo al bucket és la manera per defecte de fer-ho, i per a un avatar està perfectament bé. Per a un vídeo de dos gigues, aquest mateix camí t'obliga a barallar-te amb la memòria, els temps d'espera i mitja configuració de PHP. Aquí veuràs com signar la pujada perquè el byte vagi del navegador al bucket i els tres detalls que trenquen la implementació quan la fas per primera vegada. Els exemples van en Laravel, una de les nostres especialitats, però la signatura és un mecanisme del mateix S3 i s'aplica igual des de qualsevol llenguatge.

El fitxer que passa per PHP et costa memòria, temps i límits

La manera per defecte de rebre un fitxer és que el navegador l'enviï al teu servidor, que el teu servidor el guardi en disc temporal i que el teu codi el copiï al bucket.

Per a un avatar de 200 kilobytes és perfecte i no hi ha motiu per complicar-ho. Per a un vídeo de dos gigues el mateix camí t'obliga a barallar-te amb quatre coses alhora:

upload_max_filesize i post_max_size a PHP, que cal pujar al contenidor i al servidor web. El temps màxim de petició, perquè una pujada lenta des d'una connexió dolenta manté un procés PHP ocupat durant minuts. La memòria, segons com es gestioni el flux. I l'amplada de banda, perquè el byte viatja dues vegades: del navegador a la teva màquina i de la teva màquina al bucket.

I hi ha un cost que no es veu en cap mètrica: cada pujada gran t'ocupa un treballador PHP que no està servint pàgines.

La signatura la dona el teu servidor, els bytes van directes

L'alternativa fa anys que és al SDK d'S3 i continua infrautilitzada. El servidor no rep el fitxer, rep la petició de permís.

El flux té tres passos i només el primer i el tercer toquen la teva aplicació:

Un. El navegador demana una autorització de pujada indicant el tipus de fitxer i la mida. El teu servidor signa una URL temporal contra el bucket i la retorna.

Dos. El navegador fa un PUT amb el binari directament contra aquesta URL. La teva aplicació no se n'assabenta i no consumeix res.

Tres. El navegador avisa la teva aplicació que ha acabat, amb la clau de l'objecte. La teva aplicació registra el fitxer a la base de dades i llança el que hagi de fer després.

El resultat és que el fitxer no passa mai per PHP i els límits de configuració deixen d'importar. I la signatura caduca, així que l'autorització no és reutilitzable.

El fitxer arriba abans que el model, i aquí hi ha el problema de debò

Aquí és on gairebé totes les implementacions casolanes s'encallen, i és un problema d'ordre.

Imagina un formulari de creació. L'usuari adjunta el document abans d'haver guardat el registre, així que en el moment de signar la pujada encara no existeix l'identificador al qual pertanyerà aquell fitxer. No pots construir la ruta definitiva perquè no la coneixes.

La solució que funciona és de dues fases. Si en signar ja es coneixen el model i la col·lecció, la clau es construeix directament al seu lloc. Si no, el fitxer aterra en una carpeta temporal, i en confirmar es mou a la seva ruta canònica.

I aquest moviment és la part que cal fer bé: no es descarrega i es torna a pujar, es copia dins del bucket i s'esborra l'origen.

$client->copyObject([
    'Bucket'     => $bucket,
    'CopySource' => "{$bucket}/" . rawurlencode($fromKey),
    'Key'        => $toKey,
]);

$client->deleteObject(['Bucket' => $bucket, 'Key' => $fromKey]);

Dues crides a l'API i zero bytes travessant el teu servidor. Si en lloc d'això descarregues i puges, has tornat exactament al problema que volies evitar, només que ara passa després.

El detall del prefix que trenca les pujades signades

Hi ha una mena d'error que només apareix quan dues capes fan el mateix sense saber l'una de l'altra, i les rutes d'S3 són terreny adobat. El símptoma és un fitxer que es puja sense errors i després no és on hauria de ser.

Si el teu disc té configurada una arrel, del tipus dev/ o produccio/, Laravel l'aplica sola en totes les lectures i escriptures. Però quan signes un PUT que executarà el navegador, aquesta capa no hi intervé: el navegador parla directament amb el bucket.

Si signes amb la clau relativa, l'objecte es puja sense el prefix, i a partir d'aquí cap lectura des de la teva aplicació no el troba. El fitxer hi és, ocupa espai i factura, però per al teu codi no existeix.

La solució és aplicar el prefix a mà en signar i guardar a la base de dades la clau relativa, perquè la resta de l'aplicació continuï funcionant com sempre:

$root    = trim((string) ($cfg['root'] ?? ''), '/');
$fullKey = $root !== '' ? "{$root}/{$key}" : $key;
Andie recomana

Signa sempre amb ContentType i ContentLength inclosos. No és cosmètic: formen part del que se signa, així que el navegador no pot pujar un tipus diferent ni un fitxer més gran de l'autoritzat. Si signes sense ells, has lliurat una autorització de pujada sense límit de mida, i aquesta és la mena de detall que apareix a la factura abans que als registres.

L'endpoint que signa és el que decideix qui puja on

Una signatura és un permís d'escriptura amb data de caducitat. L'endpoint que l'emet no està tornant una URL: està decidint, a cada crida, què pot escriure qui i en quin lloc del teu bucket.

Quan exposes un endpoint que signa pujades, estàs delegant escriptura al teu bucket. Qualsevol que el pugui cridar obté permís d'escriptura sobre la clau que demani. Així que aquest endpoint ha de comprovar tres coses i cap no ve de sèrie:

Que el disc estigui en una llista blanca. Si tens cinc buckets separats per tipus de contingut, un usuari no hauria de poder demanar signatura contra qualsevol d'ells només perquè està autenticat.

Que l'usuari sigui propietari del model al qual diu que adjunta. Si la ruta canònica es construeix a partir del tipus i l'identificador que envia el client, sense comprovar propietat, un usuari pot escriure dins de la carpeta d'un altre.

I un límit de peticions. Signar és barat, però permet iniciar pujades, i les pujades iniciades i abandonades continuen ocupant espai.

Cap llibreria no pot resoldre això per tu, perquè la propietat d'un model és lògica del teu domini. El que sí que t'ha de donar és el punt on enganxar-ho.

Fitxers grans: multipart, i el llindar és només una pista

Per sobre de certa mida una sola petició deixa de ser bona idea: si falla al noranta per cent, es perd tot.

Aquí entra la pujada per parts, que són quatre operacions: iniciar la sessió, demanar URLs signades per a cada tros, completar, i avortar si l'usuari se'n penedeix. El navegador puja els trossos en paral·lel i pot reintentar només el que falli.

Un detall d'arquitectura que convé entendre: el llindar a partir del qual es fa servir multipart el decideix el navegador, perquè és qui coneix la mida real del fitxer abans de pujar-lo. El servidor no l'imposa. És lògic, i alhora significa que un client pot iniciar multipart per a un fitxer petit, així que el límit de peticions també ha de cobrir aquests endpoints.

I hi ha una conseqüència operativa que no es veu fins que arriba la factura: una sessió multipart abandonada deixa els seus trossos vius al bucket. No són un objecte visible, però es cobren. Si actives multipart, necessites una tasca programada que avorti les sessions caducades.

Les variants són fitxers, no camps

Una miniatura i una versió mitjana són el mateix tipus de cosa que l'original: un fitxer en un bucket amb la seva mida, el seu mime i la seva ruta. Desar-les com a columnes del registre les converteix en una altra cosa, i aquesta decisió es paga quan apareix la quarta variant.

Quan puges una imatge i generes una miniatura i una versió mitjana, hi ha dues maneres de guardar-les. Com a columnes del mateix registre, del tipus thumb_path i medium_path, o com a registres propis que apunten a l'original.

La segona és més feina al principi i resol sola tot el que ve després: cada variant té la seva pròpia mida, el seu propi tipus, la seva pròpia URL signada i el seu propi control d'accés, i afegir una variant nova no és una migració. Esborrar l'original esborra les seves variants en cascada perquè la relació és real.

Amb columnes, cada format nou és una columna nova, i el dia que vols una marca d'aigua diferent per variant no tens on posar-la.

Què hi ha al mercat i en què es diferencia

Les tres opcions raonables, amb el seu compromís real.

Les pujades de Livewire són la via més curta i per a la majoria de casos són la resposta correcta. Un wire:model sobre un camp de fitxer i llestos. El compromís és exactament el d'aquest article: el binari travessa PHP i aterra en un disc temporal abans d'arribar a la seva destinació. Per a documents i fotos va perfecte; per a vídeo, no.

spatie/laravel-medialibrary és la referència de l'ecosistema per associar fitxers a models, amb col·leccions, conversions d'imatge i una comunitat enorme al darrere. Si la teva necessitat és "adjuntar imatges a un model i generar miniatures", és una elecció sòlida i molt provada. La diferència d'enfocament que convé valorar és si la pujada directa al bucket és un camí de primera classe o un afegit, perquè això condiciona la mida de fitxer que pots acceptar sense tocar la configuració del servidor.

I Storage::putFile() a pèl no té res de dolent. Si puges quatre fitxers petits al mes, qualsevol abstracció al damunt és pes mort.

El principi que a nosaltres ens va guiar és que les credencials dels discos continuen vivint a config/filesystems.php. Una capa de gestió de fitxers que et demana configurar un altre cop les claus del teu bucket ha duplicat configuració, i les configuracions duplicades es desincronitzen.

I si no treballes amb Laravel

Els criteris són del disseny i valen en qualsevol llenguatge:

  • Signa al servidor, puja des del client. És l'única manera que la mida del fitxer deixi de ser un problema d'infraestructura.
  • Inclou tipus i mida a la signatura. Una autorització sense límits és una autorització perillosa.
  • Resol l'ordre amb una carpeta temporal i un moviment dins del bucket. Mai descarregant i tornant a pujar.
  • Tracta l'endpoint de signatura com el que és, un punt d'escriptura delegada, amb llista blanca de destinacions i comprovació de propietat.
  • I neteja el que quedi abandonat. Els fitxers temporals i les sessions multipart caducades facturen igual que els bons.

On és el codi

El paquet és a Packagist, el codi a GitHub i la documentació del paquet recull la configuració completa, que és extensa: col·leccions, polítiques d'accés, variants, previsualitzacions de PDF i vídeo, marques d'aigua i extracció de text.

És en producció a Crowd Legal, amb cinc buckets separats per tipus de contingut, i a Abodara.

Si el teu producte accepta fitxers d'usuaris i ja has hagut de tocar els límits de PHP perquè hi càpiguen, això és el que cal canviar. Ho treballem a aplicacions web i SaaS a mida, i si ens expliques què pugen els teus usuaris et diem quin muntatge et va bé.

Escrit per
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desenvolupador full-stack amb més de 15 anys d'experiència en Laravel, React, Node.js i arquitectures cloud. Ajudo empreses a Andorra a construir la seva presència digital.

Partner de diseño · ionospace.
Necessites ajuda? ×
Andie by AndorraDev
Assistent IA + equip humà
Assistent IA d'AndorraDev
Andie
Hola! Soc Andie, l'assistent IA d'AndorraDev. En què et puc ajudar? Si necessites parlar amb Edu, només demana-ho.
17:29