Search code examples
angularfirebaserxjsfirebase-storageangularfire2

How to upload array of files using AngularFire2 and Firebase Storage?


I was struggling with finding an answer to this question. Most examples, including AngularFire2 docs and a great tutorial from angularfirebase.com only focuses on uploading one file at the time. I want to build a photo gallery cms, so that option would be inefficient, to say the least...

To my original question, I got one answer from Leon Radley and thanks for that, it helped rethink the approach.

Nevertheless, the solution was not optimal for me. I decided to answer my own question so it might help somebody, or even better, somebody will suggest a better solution.

I will be updating my answer, as I come up with more functionality. I am simply rewriting solution provided by angularfirebase.com to make use of an array of files.

Note that there is Custom Drop Event and FileList Directive used, refer to the source for more details

Original Solution:

file-upload.component.ts

import { Component, OnInit } from '@angular/core';
import { AngularFireStorage, AngularFireUploadTask } from 'angularfire2/storage';
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'file-upload',
  templateUrl: './file-upload.component.html',
  styleUrls: ['./file-upload.component.scss']
})
export class FileUploadComponent {

  // Main task 
  task: AngularFireUploadTask;

  // Progress monitoring
  percentage: Observable<number>;

  snapshot: Observable<any>;

  // Download URL
  downloadURL: Observable<string>;

  // State for dropzone CSS toggling
  isHovering: boolean;

  constructor(private storage: AngularFireStorage, private db: AngularFirestore) { }


  toggleHover(event: boolean) {
    this.isHovering = event;
  }


  startUpload(event: FileList) {
    // The File object
    const file = event.item(0)

    // Client-side validation example
    if (file.type.split('/')[0] !== 'image') { 
      console.error('unsupported file type :( ')
      return;
    }

    // The storage path
    const path = `test/${new Date().getTime()}_${file.name}`;

    // Totally optional metadata
    const customMetadata = { app: 'My AngularFire-powered PWA!' };

    // The main task
    this.task = this.storage.upload(path, file, { customMetadata })

    // Progress monitoring
    this.percentage = this.task.percentageChanges();
    this.snapshot   = this.task.snapshotChanges()

    // The file's download URL
    this.downloadURL = this.task.downloadURL(); 
  }

  // Determines if the upload task is active
  isActive(snapshot) {
    return snapshot.state === 'running' && snapshot.bytesTransferred < snapshot.totalBytes
  }

}

file-upload.component.html

<div class="dropzone" 
     dropZone
     (hovered)="toggleHover($event)"
     (dropped)="startUpload($event)"
     [class.hovering]="isHovering">



     <h3>AngularFire Drop Zone</h3>

     <div class="file">
        <label class="file-label">


        <input class="file-input" type="file" (change)="startUpload($event.target.files)">


          <span class="file-cta">
            <span class="file-icon">
              <i class="fa fa-upload"></i>
            </span>
            <span class="file-label">
              or choose a file…
            </span>
          </span>
        </label>
      </div>
</div>

<div *ngIf="percentage | async as pct">

  <progress class="progress is-info" 
            [value]="pct" 
            max="100">        
  </progress>

  {{ pct | number }}%

</div>


<div *ngIf="snapshot | async as snap">
  {{ snap.bytesTransferred | fileSize }} of {{ snap.totalBytes | fileSize }} 

  <div *ngIf="downloadURL | async as url">
    <h3>Results!</h3>
    <img [src]="url"><br>
    <a [href]="url" target="_blank" rel="noopener">Download Me!</a>
  </div> 

  <button (click)="task.pause()" class="button is-warning" [disabled]="!isActive(snap)">Pause</button>
  <button (click)="task.cancel()" class="button is-danger" [disabled]="!isActive(snap)">Cancel</button>
  <button (click)="task.resume()" class="button is-info"   [disabled]="!(snap?.state === 'paused')">Resume</button>

</div>

Solution

  • So far I managed to replicate the following functionality:

    • upload
    • snapshots (bytesTransferred / totalBytes)
    • progresses

    file-upload.component.ts

    @Component({
      selector: 'app-file-upload',
      templateUrl: './file-upload.component.html',
      styleUrls: ['./file-upload.component.scss'],
    })
    export class FileUploadComponent {
      tasks$: Observable<any>
      progresses$: any
      snapshots$: any
      isHovering: boolean
    
      constructor(
        private storage: AngularFireStorage,
        private db: AngularFirestore,
      ) {}
    
      toggleHover(event: boolean) {
        this.isHovering = event
      }
    
      startUpload(event: HTMLInputEvent) {
        this.tasks$ = from([Array.from(event.target.files)]).pipe(
          map(files =>
            files.map(file => {
              const path = `test/${new Date().getTime()}_${file.name}`
              const customMetadata = { app: 'My AngularFire-powered PWA!' }
    
              return this.storage.upload(path, file, { customMetadata })
            }),
          ),
        )
    
        this.snapshots$ = this.tasks$.pipe(
          map(files =>
            files.map(file =>
              file.snapshotChanges(),
            ),
    
          )
        )
    
        this.progresses$ = this.tasks$.pipe(
          map(files =>
            files.map(file =>
              file.percentageChanges()
            ),
          )
        )
      }
    }
    

    file-upload.component.html

    <div class="dropzone"
         appDropZone
         (hovered)="toggleHover($event)"
         (dropped)="startUpload($event)"
         [class.hovering]="isHovering">
    
    
      <h3>AngularFire Drop Zone</h3>
    
      <div class="file">
        <label class="file-label">
    
    
          <input class="file-input" type="file" (change)="startUpload($event)"
                 multiple>
    
    
          <span class="file-cta">
                <span class="file-icon">
                  <i class="fa fa-upload"></i>
                </span>
                <span class="file-label">
                  or choose a file…
                </span>
              </span>
        </label>
      </div>
    </div>
    
    
    <div>
      <ul>
        <li *ngFor="let progress of progresses$ | async">
          <progress
            class="progress is-info"
            [value]="progress | async"
            max="100">
          </progress>
          {{progress | async | number }}%
        </li>
      </ul>
    
    </div>
    
    <div>
      <ul>
        <li *ngFor="let snap of snapshots$ | async">
          <p *ngIf="snap | async">
            {{ (snap | async)?.bytesTransferred | fileSize }} of
            {{ (snap | async)?.totalBytes | fileSize }}
          </p>
        </li>
      </ul>
    </div>