Amazon Web Services offre un ottimo e conosciutissimo servizio per lo storage di file chiamato S3 (Simple Storage Service) che in Innoteam apprezziamo particolarmente per la sua semplicità, robustezza e per il suo pricing particolarmente conveniente.
In questo articolo vedremo come interfacciare una Web App con il servizio S3 di Amazon per effettuare upload e download di file in maniera sicura ed efficiente.
Il punto di forza dell’approccio descritto è che upload e download vengono effettuati direttamente verso il bucket S3 senza necessità di dover appoggiare i file su server intermediari.
Supponiamo di dover sviluppare un’app gestionale che ci permetta di caricare e scaricare documenti.
Bucket S3
Per questo tipo di applicazioni non possiamo utilizzare un bucket pubblico e accessibile da chiunque ma, anzi, vogliamo creare un bucket privato a cui possa accedere solo e soltanto il nostro applicativo.
Creiamo quindi un bucket privato dal pannello di amministrazione di Amazon S3 come indicato nella guida ufficiale di AWS.
Pre-Signed URL
Come possiamo gestire upload e download di file verso un bucket privato?
Utilizziamo una feature di Amazon S3 chiamata Pre-Signed URL.
Una Pre-Signed URL è una particolare URL che è stata generata e “firmata” dall’SDK di Amazon e che ci permette di accedere ad un ben definito oggetto di un bucket S3 in lettura oppure in scrittura in base al tipo di Pre-Signed URL.
Chi è in possesso di una di queste URL è quindi in grado fare il download oppure l’upload di uno specifico file, effettuando verso questa URL rispettivamente una chiamata di tipo HTTP GET per il download e HTTP PUT per l’upload.
Una Pre-Signed URL ha una validità temporale prefissata, terminata la quale non è più utilizzabile.
Per poter utilizzare le Pre-Signed URL è necessario modificare le configurazioni CORS del bucket S3 che abbiamo creato, in modo che vengano accettate chiamate HTTP di tipo GET e PUT verso il bucket.
E’ possibile modificare la configurazione CORS del bucket dal pannello di amministrazione di Amazon S3:
Cliccare sul nome del bucket -> Permissions -> CORS configuration
<?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>PUT</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <AllowedHeader>*</AllowedHeader> </CORSRule> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <AllowedHeader>Authorization</AllowedHeader> </CORSRule> </CORSConfiguration>
Architettura
In questo articolo faremo riferimento ad un’architettura che prevede un’applicazione Angular ed un’API REST JSON con un DB relazionale sottostante.
Modello Dati
Nella nostra applicazione terremo traccia di tutti i file presenti sul bucket S3, creando un record nel nostro DB associato ad ogni file.
Avremo quindi una tabella `attachments` strutturata in questo modo:
Campo | Descrizione |
---|---|
filename: string | Nome del file (key nel bucket) |
uploaded: boolean | Flag che indica se l’upload del file sul bucket è terminato con successo |
E avremo i seguenti endpoint di una classica API REST:
Endpoint | Descrizione |
---|---|
POST /attachments | Creazione di un nuovo record di tipo Attachment |
PUT /attachments/:id | Modifica di un record esistente di tipo Attachment |
GET /attachments/:id | Recupero di un record esistente di tipo Attachment |
Download di un file
Quando il frontend Angular effettua una richiesta GET all’endpoint `/attachments/:id` per il recupero di un record di tipo Attachment, la nostra API, nel caso il record abbia flag `uploaded` impostato a `true`, provvederà a generare una Pre-Signed URL di lettura e ad aggiungerla alle informazioni prelevate da DB.
Generazione di una Pre-Signed URL di lettura in PHP:
// Creating a presigned URL $cmd = $s3Client->getCommand('GetObject', [ 'Bucket' => 'my-bucket', 'Key' => 'testKey' ]); $request = $s3Client->createPresignedRequest($cmd, '+20 minutes'); // Get the actual presigned-url $presignedUrl = (string) $request->getUri();
Body della risposta:
{ filename: "notes.txt", uploaded: true, presignedUrl: "https://..." }
Upload di un file
L’upload di un file verrà invece effettuato in tre passaggi.
1. Creazione di un record Attachment in DB e conseguente recupero della Pre-Signed URL di scrittura
Il frontend Angular effettua una richiesta POST all’endpoint `/attachments` per la creazione di un nuovo record di tipo Attachment.
Body della richiesta:
{ filename: "notes.txt" }
L’API REST crea un record nel DB, imposta automaticamente il flag `uploaded` di questo record a `false` e utilizza l’SDK di AWS per generare una Pre-Signed URL di scrittura.
La Pre-Signed URL viene restituita ad Angular.
Generazione di una Pre-Signed URL di scrittura in PHP:
// Creating a presigned URL $cmd = $s3Client->getCommand('PutObject', [ 'Bucket' => 'my-bucket', 'Key' => 'testKey' ]); $request = $s3Client->createPresignedRequest($cmd, '+20 minutes'); // Get the actual presigned-url $presignedUrl = (string) $request->getUri();
Body della risposta:
{ id: 44, filename: "notes.txt", presignedUrl: "https://...", uploaded: false }
2. Upload vero e proprio direttamente verso il bucket di Amazon S3
Angular farà semplicemente una richiesta HTTP PUT verso la Pre-Signed URL ricevuta dalle API con il contenuto del file.
Upload di file utilizzando Pre-Signed URL in service Angular:
@Injectable() export class AttachmentService { ... constructor(private http: HttpClient) {} ... upload(file: File, presignedUrl: string): Promise<any> { const options = { headers: { 'Content-Type': file.type }, responseType: 'text' }; return this.http .put(presignedUrl, file, options) .toPromise(); } ... }
3. Aggiornamento del record precedentemente creato con flag `uploaded` impostato a `true`
Una volta terminato l’upload verso il bucket S3, Angular chiamerà le API per aggiornare il record Attachment nel DB.
Body della richiesta:
{ uploaded: true }
I 3 step possono essere riassunti dal seguente diagramma:
Conclusioni
Uno dei più grandi vantaggi di questo approccio è sicuramente il fatto di poter effettuare upload e download comunicando direttamente con il bucket S3 senza dover passare dal server che ospita le API, sgravandolo dal lavoro di intermediario verso S3 e non occupandone inutilmente banda e risorse.
Ci teniamo inoltre a sottolineare che per gli esempi è stato utilizzato codice PHP e Angular ma l’approccio descritto è del tutto generale e può essere applicato con qualsiasi combinazione di tecnologie Backend/Frontend.