API REST - elaborazione di file (ad es. Immagini) - migliori pratiche


198

Stiamo sviluppando un server con API REST, che accetta e risponde con JSON. Il problema è, se è necessario caricare immagini dal client al server.

Nota: e sto anche parlando di un caso d'uso in cui l'entità (utente) può avere più file (carPhoto, licensePhoto) e anche altre proprietà (nome, e-mail ...), ma quando si crea un nuovo utente, si non inviare queste immagini, vengono aggiunte dopo il processo di registrazione.


Le soluzioni di cui sono a conoscenza, ma ognuna di esse presenta alcuni difetti

1. Utilizzare multipart / form-data anziché JSON

buono : le richieste POST e PUT sono il più RESTful possibile, possono contenere input di testo insieme al file.

contro : Non è più JSON, che è molto più facile da testare, eseguire il debug, ecc. rispetto ai dati multipart / form

2. Consentire di aggiornare file separati

La richiesta POST per la creazione di un nuovo utente non consente di aggiungere immagini (il che è ok nel nostro caso d'uso come ho detto all'inizio), il caricamento delle immagini viene effettuato dalla richiesta PUT come multipart / form-data, ad esempio / users / 4 / carPhoto

buono : tutto (tranne il caricamento del file stesso) rimane in JSON, è facile da testare ed eseguire il debug (puoi registrare richieste JSON complete senza aver paura della loro lunghezza)

contro : Non è intuitivo, non puoi POST o PUT tutte le variabili di entità contemporaneamente e anche questo indirizzo /users/4/carPhotopuò essere considerato più come una raccolta (il caso d'uso standard per l'API REST è simile a questo /users/4/shipments). Di solito non puoi (e non vuoi) OTTENERE / METTERE ogni variabile di entità, ad esempio utenti / 4 / nome. Puoi ottenere il nome con GET e modificarlo con PUT su users / 4. Se c'è qualcosa dopo l'id, di solito è un'altra raccolta, come utenti / 4 / recensioni

3. Utilizzare Base64

Invialo come JSON ma codifica i file con Base64.

buono : come la prima soluzione, è il servizio RESTful possibile.

contro : Ancora una volta, i test e il debug sono molto peggiori (il corpo può avere megabyte di dati), c'è un aumento delle dimensioni e anche dei tempi di elaborazione sia in client che in server


Vorrei davvero usare la soluzione no. 2, ma ha i suoi contro ... Chiunque può darmi una visione migliore della soluzione "qual è la migliore"?

Il mio obiettivo è avere servizi RESTful con il maggior numero possibile di standard inclusi, mentre voglio renderlo il più semplice possibile.


Si potrebbe anche trovare questo utile: stackoverflow.com/questions/4083702/...
Markon

5
So che questo argomento è vecchio ma di recente abbiamo affrontato questo problema. L'approccio migliore che abbiamo è simile al tuo numero 2. Carichiamo i file direttamente nell'API e quindi alleghiamo questi file nel modello. Con questo scenario è possibile creare immagini di caricamento prima, dopo o nella stessa pagina del modulo, non importa. Buona discussione!
Tiago Matos,

2
@TiagoMatos - sì, esattamente, l'ho descritto in una risposta che ho recentemente accettato
libik

6
Grazie per aver posto questa domanda.
Zuhayer Tahir,

1
"anche questo indirizzo / users / 4 / carPhoto può essere considerato più come una raccolta" - no, non ha l'aspetto di una raccolta e non è necessariamente da considerarsi tale. Va benissimo avere una relazione con una risorsa che non è una raccolta ma una singola risorsa.
B12Taster

Risposte:


156

OP qui (sto rispondendo a questa domanda dopo due anni, il post di Daniel Cerecedo non è stato male alla volta, ma i servizi web si stanno sviluppando molto velocemente)

Dopo tre anni di sviluppo software a tempo pieno (con particolare attenzione anche all'architettura software, alla gestione dei progetti e all'architettura dei microservizi), scelgo sicuramente il secondo modo (ma con un endpoint generale) come il migliore.

Se hai un endpoint speciale per le immagini, ti dà molto più potere sulla gestione di quelle immagini.

Abbiamo la stessa API REST (Node.js) sia per le app mobili (iOS / Android) sia per il frontend (usando React). Questo è il 2017, quindi non vuoi archiviare le immagini localmente, vuoi caricarle su un po 'di cloud storage (Google cloud, s3, cloudinary, ...), quindi vuoi una gestione generale su di esse.

Il nostro flusso tipico è che non appena si seleziona un'immagine, inizia a caricarsi sullo sfondo (di solito POST su / endpoint immagini), restituendo l'ID dopo il caricamento. Questo è davvero facile da usare, perché l'utente sceglie un'immagine e quindi in genere procede con alcuni altri campi (ad esempio indirizzo, nome, ...), quindi quando preme il pulsante "invia", l'immagine è di solito già caricata. Non aspetta e guarda lo schermo che dice "caricamento in corso ...".

Lo stesso vale per ottenere immagini. Soprattutto grazie ai telefoni cellulari e ai dati mobili limitati, non vuoi inviare immagini originali, vuoi inviare immagini ridimensionate, quindi non occupano molta larghezza di banda (e per rendere le tue app mobili più veloci, spesso non vuoi per ridimensionarlo, vuoi l'immagine che si adatta perfettamente alla tua vista). Per questo motivo, le buone app stanno usando qualcosa come cloudinary (o abbiamo il nostro server di immagini per il ridimensionamento).

Inoltre, se i dati non sono privati, si rinvia all'URL dell'app / frontend e li scarica direttamente dall'archiviazione cloud, il che comporta un enorme risparmio di larghezza di banda e tempo di elaborazione per il server. Nelle nostre app più grandi ci sono molti terabyte scaricati ogni mese, non vuoi gestirlo direttamente su ciascuno dei tuoi server API REST, che è focalizzato sull'operazione CRUD. Vuoi gestirlo in un unico posto (il nostro Imageserver, che ha la cache ecc.) O lasciare che i servizi cloud lo gestiscano.


Contro: L'unico "contro" a cui dovresti pensare è "non immagini assegnate". L'utente seleziona le immagini e continua a compilare altri campi, ma poi dice "nah" e disattiva l'app o la scheda, ma nel frattempo hai caricato correttamente l'immagine. Ciò significa che hai caricato un'immagine che non è stata assegnata da nessuna parte.

Esistono diversi modi per gestirlo. Il più semplice è "Non mi interessa", che è pertinente, se ciò non accade molto spesso o hai persino il desiderio di memorizzare ogni immagine che l'utente ti invia (per qualsiasi motivo) e non vuoi eliminazione.

Anche un altro è facile: hai CRON e cioè ogni settimana e cancelli tutte le immagini non assegnate più vecchie di una settimana.


Cosa succederà se [non appena si seleziona l'immagine, inizia il caricamento in background (di solito POST on / endpoint immagini), restituendo l'ID dopo il caricamento] quando la richiesta non è riuscita a causa della connessione a Internet? Richiederai all'utente mentre procedono con altri campi (ad es. Indirizzo, nome, ...)? Scommetto che continuerai ad aspettare fino a quando l'utente non preme il pulsante "invia" e ritenta la tua richiesta facendoli aspettare mentre guardano lo schermo dicendo "uploadiing ...".
Adromil Balais,

5
@AdromilBalais - L'API RESTful è senza stato, quindi non fa nulla (il server non tiene traccia dello stato del consumatore). Il consumatore del servizio (ovvero una pagina Web o un dispositivo mobile) è responsabile della gestione delle richieste non riuscite, pertanto il consumatore deve decidere se chiama immediatamente la stessa richiesta dopo che questa non è riuscita o cosa fare (ovvero mostrare "Caricamento immagine non riuscito - si desidera riprovare ")
libik,

2
Risposta molto istruttiva e illuminante. Grazie per aver risposto.
Zuhayer Tahir,

Questo non risolve davvero il problema iniziale. Questo dice semplicemente "usa un servizio cloud"
Martin Muzatko il

3
@MartinMuzatko: lo fa, sceglie la seconda opzione e ti dice come dovresti usarla e perché. Se vuoi dire "ma questa non è un'opzione perfetta che ti consente di inviare tutto in una richiesta e senza implicazioni" - sì, sfortunatamente non esiste una soluzione del genere.
libik,

105

Ci sono diverse decisioni da prendere :

  1. Il primo sul percorso delle risorse :

    • Modella l'immagine come una risorsa a sé stante:

      • Nidificato nell'utente (/ user /: id / image): la relazione tra l'utente e l'immagine viene stabilita in modo implicito

      • Nel percorso radice (/ image):

        • Il cliente è ritenuto responsabile per stabilire la relazione tra l'immagine e l'utente, oppure;

        • Se viene fornito un contesto di sicurezza con la richiesta POST utilizzata per creare un'immagine, il server può stabilire implicitamente una relazione tra l'utente autenticato e l'immagine.

    • Incorporare l'immagine come parte dell'utente

  2. La seconda decisione riguarda come rappresentare la risorsa immagine :

    • Come payload JSON codificato Base 64
    • Come payload multipart

Questa sarebbe la mia traccia decisionale:

  • Di solito preferisco il design piuttosto che le prestazioni, a meno che non ci sia un caso valido per questo. Rende il sistema più gestibile e può essere più facilmente compreso dagli integratori.
  • Quindi il mio primo pensiero è quello di optare per una rappresentazione Base64 della risorsa immagine perché ti consente di conservare tutto JSON. Se scegli questa opzione, puoi modellare il percorso della risorsa come preferisci.
    • Se la relazione tra utente e immagine è 1 a 1, preferirei modellare l'immagine come un attributo specialmente se entrambi i set di dati vengono aggiornati contemporaneamente. In ogni altro caso puoi scegliere liberamente di modellare l'immagine come un attributo, aggiornandola tramite PUT o PATCH o come risorsa separata.
  • Se scegli un payload multipart, mi sentirei costretto a modellare l'immagine come una risorsa propria, in modo che altre risorse, nel nostro caso, la risorsa utente, non siano influenzate dalla decisione di utilizzare una rappresentazione binaria per l'immagine.

Poi arriva la domanda: c'è qualche impatto sulle prestazioni nella scelta di base64 vs multipart? . Potremmo pensare che lo scambio di dati in formato multipart dovrebbe essere più efficiente. Ma questo articolo mostra quanto poco differiscano entrambe le rappresentazioni in termini di dimensioni.

La mia scelta Base64:

  • Decisione progettuale coerente
  • Impatto sulle prestazioni trascurabile
  • Poiché i browser comprendono gli URI dei dati (immagini codificate in base64), non è necessario trasformarli se il client è un browser
  • Non voterò se averlo come attributo o risorsa autonoma, dipende dal dominio del problema (che non conosco) e dalle tue preferenze personali.

3
Non possiamo codificare i dati usando altri protocolli di serializzazione come protobuf ecc? Fondamentalmente sto cercando di capire se ci sono altri modi più semplici per affrontare l'aumento delle dimensioni e del tempo di elaborazione fornito con la codifica base64.
Andy Dufresne,

1
Risposta molto coinvolgente. grazie per l'approccio graduale. Mi ha fatto capire molto meglio i tuoi punti.
Zuhayer Tahir,

13

La tua seconda soluzione è probabilmente la più corretta. È necessario utilizzare le specifiche HTTP e i tipi di mimetismo nel modo in cui erano previsti e caricare il file tramite multipart/form-data. Per quanto riguarda la gestione delle relazioni, utilizzerei questo processo (tenendo presente che non conosco nulla dei tuoi presupposti o della progettazione del sistema):

  1. POSTper /userscreare l'entità utente.
  2. POSTl'immagine /images, assicurandosi di riportare Locationun'intestazione in cui è possibile recuperare l'immagine secondo la specifica HTTP.
  3. PATCHper /users/carPhotoe assegnare così il ID della foto dato nella Locationtestata del passaggio 2.

1
Non ho alcun controllo diretto su "come il client utilizzerà l'API" ... Il problema è che le immagini "morte" che non sono patchate con alcune risorse ...
libik,

4
Di solito, quando si sceglie la seconda opzione, si preferisce caricare prima l'elemento multimediale e restituire l'identificatore multimediale al client, quindi il client può inviare i dati dell'entità compreso l'identificatore multimediale, questo approccio evita le entità rotte o le informazioni di mancata corrispondenza.
Kellerman Rivero,

2

Non esiste una soluzione facile. Ogni modo ha i suoi pro e contro. Ma il modo canonico è utilizzando la prima opzione: multipart/form-data. Come dice la guida alle raccomandazioni W3

Il tipo di contenuto "multipart / form-data" deve essere utilizzato per l'invio di moduli che contengono file, dati non ASCII e dati binari.

In realtà non stiamo inviando moduli, ma si applica ancora il principio implicito. L'uso di base64 come rappresentazione binaria non è corretto perché stai utilizzando lo strumento errato per raggiungere il tuo obiettivo, d'altra parte, la seconda opzione obbliga i tuoi client API a fare più lavoro per consumare il tuo servizio API. Dovresti fare il duro lavoro sul lato server per fornire un'API di facile utilizzo. La prima opzione non è facile da eseguire il debug, ma quando lo fai, probabilmente non cambia mai.

Usando multipart/form-datasei fedele alla filosofia REST / http. Puoi visualizzare una risposta a una domanda simile qui .

Un'altra opzione se si mescolano le alternative, è possibile utilizzare multipart / form-data ma invece di inviare ogni valore separatamente, è possibile inviare un valore denominato payload con il payload json al suo interno. (Ho provato questo approccio usando ASP.NET WebAPI 2 e funziona benissimo).


2
Quella guida alle raccomandazioni W3 è irrilevante qui, poiché è nel contesto delle specifiche HTML 4.
Johann,

1
Molto vero .... "dati non ASCII" richiede multipart? Nel ventunesimo secolo? In un mondo UTF-8? Naturalmente questa è una raccomandazione ridicola per oggi. Sono anche sorpreso che esistesse in HTML 4 giorni, ma a volte il mondo delle infrastrutture Internet si muove molto lentamente.
Ray Toal,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.