How to Upload multiple files to Azure Blob storage from Angular 8

How to upload multiple files to blob storage in a browser with a Shared Access Signature (SAS) token generated from your back-end.

We’ll use Angular 8 and the @azure/storage-blob library to upload the files.

The final code is on Github which also contains examples on listing containers, blob items and deleting and downloading blob items.

We’ll go over

  • Creating a component to select and upload files
  • Creating a service to manage view state for the uploads
  • Securing the upload to Blob Storage with a SAS token
  • Creating a service to wrap the uploadBrowserData method in the @azure/storage-blob library to upload a file to Blob Storage
  • Creating a component to display upload progress

Create File Upload Component

The InputFileComponent component allows the user to select one or more files to upload

We’ll cover

  1. Selecting one or more files to upload

  2. Calling the method on the view state service to start the upload

import { Component, ElementRef, ViewChild } from '@angular/core';
import { BlobUploadsViewStateService } from '../services/blob-uploads-view-state.service';

@Component({
  selector: 'app-input-file',
  template: `
    <input
      style="display: none"
      type="file"
      #fileInput
      multiple="multiple"
      (change)="onSelected($event.target.files)"
    />
    <button (click)="showFileDialog()">Click here to Upload File</button>
  `
})
export class InputFileComponent {
  @ViewChild('fileInput', { static: false }) fileInput: ElementRef<
    HTMLInputElement
  >;

  constructor(private blobState: BlobUploadsViewStateService) {}

  onSelected(files: FileList): void {
    this.fileInput.nativeElement.value === '';
    this.blobState.uploadItems(files);
  }

  showFileDialog(): void {
    this.fileInput.nativeElement.click();
  }
}

What’s happening?

We’ve hidden the input and the file dialogue is opened with the button. The uploadItems method is called on the BlobUploadsViewStateService when the user selects files. We could add some validation here, but to me, it makes more sense to validate in the view state service.

Create uploads view state service

The BlobUploadsViewStateService service manages the shared view state for the components. I’m quite familiar with NGRX and redux patterns so I’ve created a service into which you can supply items to trigger actions and have observables listening for changes. It also means we can keep the business logic out of the components and the components will generally have a single responsibility.

We’ll cover

  1. Listing to an observable to trigger uploads

  2. Getting a SAS token

  3. Uploading a file to blob storage

  4. Keeping a list of all upload progress in the view state

import { Injectable } from '@angular/core';
import { from, OperatorFunction, Subject } from 'rxjs';
import { map, mergeMap, startWith, switchMap } from 'rxjs/operators';
import { BlobContainerRequest, BlobItemUpload } from '../types/azure-storage';
import { BlobSharedViewStateService } from './blob-shared-view-state.service';
import { BlobStorageService } from './blob-storage.service';

@Injectable({
  providedIn: 'root'
})
export class BlobUploadsViewStateService {
  private uploadQueueInner$ = new Subject<FileList>();

  uploadedItems$ = this.uploadQueue$.pipe(
    mergeMap(file => this.uploadFile(file)),
    this.blobState.scanEntries()
  );

  get uploadQueue$() {
    return this.uploadQueueInner$
      .asObservable()
      .pipe(mergeMap(files => from(files)));
  }

  constructor(
    private blobStorage: BlobStorageService,
    private blobState: BlobSharedViewStateService
  ) {}

  uploadItems(files: FileList): void {
    this.uploadQueueInner$.next(files);
  }

  private uploadFile = (file: File) =>
    this.blobState.getStorageOptionsWithContainer().pipe(
      switchMap(options =>
        this.blobStorage
          .uploadToBlobStorage(file, {
            ...options,
            filename: file.name + new Date().getTime()
          })
          .pipe(
            this.mapUploadResponse(file, options),
            this.blobState.finaliseBlobChange(options.containerName)
          )
      )
    );

  private mapUploadResponse = (
    file: File,
    options: BlobContainerRequest
  ): OperatorFunction<number, BlobItemUpload> => source =>
    source.pipe(
      map(progress => ({
        filename: file.name,
        containerName: options.containerName,
        progress: parseInt(((progress / file.size) * 100).toString(), 10)
      })),
      startWith({
        filename: file.name,
        containerName: options.containerName,
        progress: 0
      })
    );
}

What’s happening?

The public uploadItems method accepts a list of files and calls the next method on the uploadQueueInner$ subject. The uploadQueue$ getter is listening to the subject as an observable and will emit each file in the files list as a separate item. This is one place the files could be validated.

The uploadedItems$ property is listening to the uploadQueue$ and will call the uploadFile method in the service for each file item emitted. Something will need to subscribe to the uploadedItems$ for uploads to start and display upload progress (we’ll cover this below in the upload progress component)

The uploadFile method gets the latest SAS token and calls the uploadToBlobStorage on the Blob Storage Wrapper Service with the token and file details (we’ll cover this below).

The uploadToBlobStorage method returns an observable that emits the loadedBtyes each time it changes and can be used to track upload progress.

We then map the upload response to a percentage in the mapUploadResponse method and include extra detail. The method also has the startWith operator so any subscribers will be notified as soon as the upload starts rather than waiting for progress to be emitted.

We then call the finalise method when the upload is complete to refresh the items in the blob container.

The mapped response from the uploadFile method is then piped into a custom scan operator function to reduce the emitted values into an array. This allows us to store all uploads in progress and add further uploads when uploads are already in progress.

Secure the upload with a SAS token

The service above calls a method to get a SAS before every upload from a service. The method in this code example returns a hard codes SAS token I generated in the Azure portal, but in the real world, you would call an API to generate and return the SAS token. Here is a basic example in C# using the Azure.Storage.Blobs (v12.0.0) package to generate an Account SAS which can be used for many operations. You can also create SAS tokens for specific blob items or containers when you want/need to be more granular.

var key = Environment.GetEnvironmentVariable("AZURE_ACCOUNT_KEY");
var sharedKeyCredentials = new StorageSharedKeyCredential('<accountName>', key);
var sasBuilder = new AccountSasBuilder()
{
  StartsOn = DateTimeOffset.UtcNow,
  ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5),
  Services = AccountSasServices.Blobs,
  ResourceTypes = AccountSasResourceTypes.All,
  Protocol = SasProtocol.Https
};
sasBuilder.SetPermissions(AccountSasPermissions.All);

var sasToken = sasBuilder.ToSasQueryParameters(sharedKeyCredentials).ToString();

Wrap the uploadBrowserData method in the @azure/storage-blob library

We wrap the @azure/storage-blob library to return observables rather than promises as it works well when emitting progress events.

We’ll cover

  1. Creating an injection token which gets the blobServiceClient from the @azure/storage-blob library to make our service testable
  2. Calling the uploadBrowserData method and returning an observable of the loadedBytes

Here is a snippet from the full service

private uploadFile(blockBlobClient: BlockBlobClient, file: File) {
    return new Observable<number>(observer => {
      blockBlobClient
        .uploadBrowserData(file, {
          onProgress: this.onProgress(observer),
          blobHTTPHeaders: {
            blobContentType: file.type
          }
        })
        .then(
          this.onUploadComplete(observer, file),
          this.onUploadError(observer)
        );
    }).pipe(distinctUntilChanged());
  }

  private onUploadError(observer: Subscriber<number>) {
    return (error: any) => observer.error(error);
  }

  private onUploadComplete(observer: Subscriber<number>, file: File) {
    return () => {
      observer.next(file.size);
      observer.complete();
    };
  }

What’s happening?

The uploadToBlobStorage method in the wrapper service accepts the file to be uploaded and an object with the SAS token. It calls a method which will use the injected token service to return the blobServiceClient which was created using a connection string.

We then pass the BlockBlobClient created using the blobServiceClient and the file into the uploadFile method which wraps the uploadBrowserData method. We listen to the onProgress events and emit the value each time the progress changes. We then emit the file size and complete the observable when the upload completes.

Create a component to display upload progress

In this code example, the view state services manage the shared data and the components display the data. This good practice allows us to separate the file input and upload progress components and keeps our components small and have fewer responsibilities (hopefully just one).

import { Component } from '@angular/core';
import { BlobUploadsViewStateService } from '../services/blob-uploads-view-state.service';

@Component({
  selector: 'app-items-uploaded',
  template: `
    <h3>Uploads</h3>
    <div *ngFor="let upload of uploads$ | async">
      <pre>{{ upload | json }}</pre>
    </div>
  `
})
export class ItemsUploadedComponent {
  uploads$ = this.blobState.uploadedItems$;
  constructor(private blobState: BlobUploadsViewStateService) {}
}

What’s happening?

The uploads$ property is assigned the value from the uploadedItems$ property on the BlobUploadsViewStateService service we referenced above.

We then subscribe to the observable using the async pipe and display each item in the array.

Conclusion

This is one way the upload to Azure blob storage could be implemented and I’ve attempted to demonstrate a basic architecture for this as well as just showing example code.

You can also see a working example of the solution here. Apologies for the lack of styling.

Thank you for reading !

#Angular #Angular8 #JavaScript #Azure #Web Development

How to Upload multiple files to Azure Blob storage from Angular 8
3 Likes210.45 GEEK