Загрузка файлов в firebase Storage с использованием AngularFire2

730
views

AngularFire 2 — безусловно шикарная Angular 2 библиотека для работы с Firebase, которая, к сожалению, сейчас не поддерживает передачу файлов в Firebase Storage, но все же это возможно. О том, как же все-таки реализовать загрузку файлов в Storage, если у вас Angular 2 проект и вы используете AngularFire, я расскажу далее.

Установка и настройка AngularFire

Если у вас в проекте уже используется AngularFire 2, то данный шаг можно пропустить. На момент написания статьи, последней версией была 4.0.0-rc.1, с ней мы и будем работать.

Для установки многого не надо, всего лишь:

npm install firebase angularfire2 --save

После установки firebase и angularfire2, необходимо создать файл с кредами для подключения к firebase./src/environments/environment.ts

С содержимым:

export const environment = {
  production: false,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>'
  }
};

Все необходимые креды можно получить в консоли firebase на главной странице. Остается теперь настроить @NgModule. /src/app/app.module.ts

По образу такого примера:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { AngularFireModule } from 'angularfire2';
import { environment } from '../environments/environment';

@NgModule({
  imports: [
    BrowserModule,
    AngularFireModule.initializeApp(environment.firebase)
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Делаем сервис для загрузки файла

Наш сервис должен выполнять все функции связанные с firebase Storage. А именно:

  • Загрузка файлов (вариант для бинарника и для base64)
  • Сохранение в базу ключа, имени и пути к загруженному файлу
  • Удаление файлов

Подключаем зависимости

Вся проблема в том, что AngularFire не имеет компонента, который бы позволял работать с хранилищем, поэтому нам необходимо заинжектить в наш сервис подключенный инстанс самого firebase, у которого будут все необходимые методы. А именно нас интересует FirebaseApp, который создается после вызова initializeApp.

import { Injectable, Inject } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import { FirebaseApp } from 'angularfire2';

@Injectable()
export class UploadService {
 constructor(public db: AngularFireDatabase, @Inject(FirebaseApp) public firebaseApp: any) {}
}

Это минимум компонентов, необходимых для выполнения функционала. Но в данном случае, отсутствуют тайпинги методов и объектов, которые будут использоваться в сервисе. Поэтому подключим еще сам firebase и указывая типы переменных будем ссылаться на него.

import { Injectable, Inject } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import { FirebaseApp } from 'angularfire2';
import * as firebase from 'firebase';

@Injectable()
export class UploadService {
  constructor(private db: AngularFireDatabase, @Inject(FirebaseApp) public firebaseApp: firebase.app.App) {}
}

Также еще создадим интерфейс для объекта загрузки. Назовем его Upload.

export class Upload {
  $key: string;
  file:File;
  name:string;
  url:string;
  progress:number;
  createdAt: Date = new Date();

  constructor(file:File) {
    this.file = file;
  }
}

Подключим его, а также объявим переменные класса.

import { Injectable, Inject } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import { FirebaseApp } from 'angularfire2';
import * as firebase from 'firebase';

import { Upload } from '../_models/upload';

@Injectable()
export class UploadService {
  private basePath = '/uploads';
  private uploadTask: firebase.storage.UploadTask;
  uploads: FirebaseListObservable<any>;
  cacheMetadata: any = {
      cacheControl: 'public,max-age=2592000'
    };

  constructor(private db: AngularFireDatabase, @Inject(FirebaseApp) public firebaseApp: firebase.app.App) {}
}

Теперь можно приступить к написанию методов.

Методы сервиса

Имея доступ к FirebaseApp можно использовать абсолютно все функции firebase, не ограничиваясь функционалом AngularFire. Процесс загрузки (для вывода прогрессбара) можно отслеживать с помощью observable переменной uploadTask.

Загрузка бинарного файла
//Метод загрузки файла в бинарном виде
  pushUpload(upload: Upload) {
    const storageRef = this.firebaseApp.storage().ref();
    this.uploadTask = storageRef.child(`${this.basePath}/${upload.file.name}`).put(upload.file, this.cacheMetadata);

    this.uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
      (snapshot) => {
        // загрузка в процессе
        upload.progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      },
      (error) => {
        // ошибка при загрузке
        console.warn(error);
      },
      () => {
        // загрузка успешна
        upload.url = this.uploadTask.snapshot.downloadURL;
        upload.name = upload.file.name;
        this.saveFileData(upload);
      }
    );

    return this.uploadTask;
  }

В данный метод файл будет передаваться из инпута с типом файл. Файл в сторедж будет заливаться с оригинальным именем файла.

Хочу обратить внимание, что имена у файлов в одной папке хранилища должны быть уникальными. Поэтому вместо имени файла можно использовать уникальную айдишку или добавлять таймштамп.

Для отправки файла в сервис, необходимо повесить событие на изменении инпута с типом файл, которое будет держать в памяти прикрепленный бинарник. А затем другим методом отправлять файл на загрузку.

<input type="file" (change)="detectFiles($event)" hidden>
<button (click)="uploadSingle()">Залить файл</button>
import { UploadService } from '../_services/upload.service';
import { Upload } from '../_models/upload';

export class UploadComponent {
selectedFiles: FileList;
constructor(private upSvc: UploadService) {}
  detectFiles(event) {
    this.selectedFiles = event.target.files;
  }

  uploadSingle() {
    let file = this.selectedFiles.item(0);
    this.currentUpload = new Upload(file);
    this.upSvc.pushUpload(this.currentUpload);
  }
}
Загрузка base64 файла

В случае, если вам необходимо загрузить отредактированный файл, например, обрезанную аватарку в canvas, то бинарного файла на клиенте уже не будет. Но для firebase это не проблема, он может принимать и base64.

Больших отличий при этом не возникает. Имя файла теперь нужно назначить самому, а вместо метода put() следует использовать putString().

//Метод загрузки файла в base64
  pushUploadBase(base: string, name: string) {
    const storageRef = this.firebaseApp.storage().ref();
    const upload: any = {};
    this.uploadTask = storageRef.child(`${this.basePath}/${name}`).putString(base, 'data_url', this.cacheMetadata);

    this.uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
      (snapshot) => {
        // загрузка в процессе
        upload.progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      },
      (error) => {
        // ошибка при загрузке
        console.warn(error);
      },
      () => {
        // загрузка успешна
        upload.url = this.uploadTask.snapshot.downloadURL;
        upload.name = name;
        this.saveFileData(upload);
      }
    );

    return this.uploadTask;
  }
Сохранение в базу

Вы наверно обратили внимание, что при успешной загрузке вызывается метод this.saveFileData(upload);. Он сохраняет данные о загруженном файле в базу.

// Метод записи данных о загрузке в базу firebase
  private saveFileData(upload: Upload) {
    this.db.list(`${this.basePath}/`).push(upload);
  }

Сохранив в базу информацию о загрузке можно будет либо вывести загруженные картинки в необходимом для этого месте, либо же взять данные необходимые для удаления файла.

Удаление файла

Удаление файла происходит в два этапа:

  1. Удаление из базы
  2. Удаление из хранилища

Объединим эти два этапа в один метод сервиса, который в качестве параметра функции будет ожидать объект информации о загруженном файле из базы.

//Метод удаления загруженного файла
  deleteUpload(upload: Upload) {
    this.deleteFileData(upload.$key)
      .then(() => {
        this.deleteFileStorage(upload.name);
      })
      .catch(error => console.log(error));
  }
  // Удаление данных о загрузке из базы firebase
  private deleteFileData(key: string) {
    return this.db.list(`${this.basePath}/`).remove(key);
  }

  // Удаление файла из хранилища firebase
  private deleteFileStorage(name: string) {
    const storageRef = this.firebaseApp.storage().ref();
    storageRef.child(`${this.basePath}/${name}`).delete();
  }

Еще раз напомню, что имена файлов в папке хранилища должны быть уникальными.

В завершение

Вот финальный код Angular 2 сервиса, который использует AngularFire2 для загрузки файла в firebase Storage.

import { Injectable, Inject } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';
import { FirebaseApp } from 'angularfire2';
import * as firebase from 'firebase';

import { Upload } from '../_models/upload';

@Injectable()
export class UploadService {
  private basePath = '/uploads';
  private uploadTask: firebase.storage.UploadTask;
  uploads: FirebaseListObservable<any>;
  cacheMetadata = {
    cacheControl: 'public,max-age=2592000'
  };

  constructor(private db: AngularFireDatabase, @Inject(FirebaseApp) public firebaseApp: firebase.app.App) { }

  //Метод загрузки файла в бинарном виде
  pushUpload(upload: Upload) {
    const storageRef = this.firebaseApp.storage().ref();
    this.uploadTask = storageRef.child(`${this.basePath}/${upload.file.name}`).put(upload.file, this.cacheMetadata);

    this.uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
      (snapshot) => {
        // загрузка в процессе
        upload.progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      },
      (error) => {
        // ошибка при загрузке
        console.warn(error);
      },
      () => {
        // загрузка успешна
        upload.url = this.uploadTask.snapshot.downloadURL;
        upload.name = upload.file.name;
        this.saveFileData(upload);
      }
    );

    return this.uploadTask;
  }

  //Метод загрузки файла в base64
  pushUploadBase(base: string, name: string) {
    const storageRef = this.firebaseApp.storage().ref();
    const upload: any = {};
    this.uploadTask = storageRef.child(`${this.basePath}/${name}`).putString(base, 'data_url', this.cacheMetadata);

    this.uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
      (snapshot) => {
        // загрузка в процессе
        upload.progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      },
      (error) => {
        // ошибка при загрузке
        console.warn(error);
      },
      () => {
        // загрузка успешна
        upload.url = this.uploadTask.snapshot.downloadURL;
        upload.name = name;
        this.saveFileData(upload);
      }
    );

    return this.uploadTask;
  }

  // Метод записи данных о загрузке в базу firebase
  private saveFileData(upload: Upload) {
    this.db.list(`${this.basePath}/`).push(upload);
  }

  //Метод удаления загруженного файла
  deleteUpload(upload: Upload) {
    this.deleteFileData(upload.$key)
      .then(() => {
        this.deleteFileStorage(upload.name);
      })
      .catch(error => console.log(error));
  }
  // Удаление данных о загрузке из базы firebase
  private deleteFileData(key: string) {
    return this.db.list(`${this.basePath}/`).remove(key);
  }

  // Удаление файла из хранилища firebase
  private deleteFileStorage(name: string) {
    const storageRef = this.firebaseApp.storage().ref();
    storageRef.child(`${this.basePath}/${name}`).delete();
  }
}

Подписывайтесь

Для получения уведомлений о новых публикациях — подписывайтесь на мой блог или страницы в соц. сетях: Twitter, Facebook.

Подписаться на блог по эл. почте