masalibの日記

システム開発、運用と猫の写真ブログです

Angular2/4 firebaseのストレージにアップ

今回も
https://github.com/codediodeio/angular-firestarter/tree/master/src/app/uploads
を参考にしています

サンプルとして既にあるのですが
ファイルアップベースにfirebase functionを動かしたりしたいので
1から作って理解したいと思いやっています

前提として

・アップロードにはauthの認証が必須にしています
 チェックはしていないので、パーミッションエラーになります

・読み込み時のアニメーションももともと入っている
 いれたい人はこのコンポーネントをいれる
 https://github.com/codediodeio/angular-firestarter/tree/master/src/app/ui/loading-spinner
 必須じゃないのでいらない人は削除する

1・クラスの作成

FirebaseoのDBに保存する形を入力する

ng g class uploads2/shared/upload

uploads2/shared/upload.tsができていると思うので下記を入力する

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

Firebaseに入った状態は以下のとおりです
f:id:masalib:20170830184337p:plain


2・サービスの作成

フォームからアップロードされたり、削除されたりする時に
動作を記述する

ng g service uploads2/shared/upload

uploads2/shared/upload.service.tsができていると思うので下記を入力する

import { Injectable } from '@angular/core';
import { Upload } from './upload';
import { AngularFireDatabase, FirebaseListObservable, FirebaseObjectObservable } from 'angularfire2/database';
import * as firebase from 'firebase';


@Injectable()
export class UploadService {

  constructor(private db: AngularFireDatabase) { }

  //firebaseのDBの保存先
  private basePath:string = '/uploads';
  uploads: FirebaseListObservable<Upload[]>;


  getUploads(query={}) {
    this.uploads = this.db.list(this.basePath, {
      query: query
    });
    return this.uploads
  }


  deleteUpload(upload: Upload) {
    //DBの部分を削除
    this.deleteFileData(upload.$key)
    .then( () => {
      //成功したらバケットにある実ファイルを削除する
      this.deleteFileStorage(upload.name)
    })
    .catch(error => console.log(error))
  }

  // ファイルをアップして、その内容をDBに書き込む処理
  // Executes the file uploading to firebase https://firebase.google.com/docs/storage/web/upload-files
  pushUpload(upload: Upload) {
    const storageRef = firebase.storage().ref();
    const uploadTask = storageRef.child(`${this.basePath}/${upload.file.name}`).put(upload.file);

    uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
      (snapshot) =>  {
        // upload in progress(アップロード中の状態)
        let snap = snapshot as firebase.storage.UploadTaskSnapshot
        upload.progress = (snap.bytesTransferred / snap.totalBytes) * 100
      },
      (error) => {
        // upload failed
        console.log(error)
      },
      () => {
        // upload success(アップロード成功時の処理)
        upload.url = uploadTask.snapshot.downloadURL	//表示する時のURL
        upload.name = upload.file.name	//ファイル名
        this.saveFileData(upload)
        return undefined
      }
    );
  }



  // Writes the file details to the realtime db
  private saveFileData(upload: Upload) {
    this.db.list(`${this.basePath}/`).push(upload);
  }

  // Writes the file details to the realtime db
  private deleteFileData(key: string) {
    return this.db.list(`${this.basePath}/`).remove(key);
  }

  // Firebase files must have unique names in their respective storage dir
  // So the name serves as a unique key
  private deleteFileStorage(name:string) {
    const storageRef = firebase.storage().ref();
    storageRef.child(`${this.basePath}/${name}`).delete()
  }


}

3・コンポーネント

ng g componentのコマンドでもつくれるのですが
src/app/app.module.tsが更新されてしまうのと
いらないSCSSができてしまうので手動で対応

# プロジェクトの直下にいる状態とする
mkdir src/app/uploads2/upload-detail
mkdir src/app/uploads2/upload-form
mkdir src/app/uploads2/uploads-list

3-1・画像リスト

# プロジェクトの直下にいる状態とする
$ vi src/app/uploads2/uploads-list/uploads-list.component.ts
import { Component, OnInit } from '@angular/core';
import { FirebaseListObservable } from 'angularfire2/database';
import { UploadService } from '../shared/upload.service';
import { Upload } from '../shared/upload';

@Component({
  selector: 'uploads-list',
  template: `現在のアップされているリスト
<div *ngFor="let upload of uploads | async">
  <upload-detail [upload]='upload'></upload-detail>
</div>

<!-- loading animations ないなら削除でOK -->
<loading-spinner *ngIf="showSpinner"></loading-spinner>

<hr>
アップする時のフォーム
<upload-form></upload-form>`
})
export class UploadsListComponent implements OnInit {

  uploads: FirebaseListObservable<Upload[]>;

  //loading animations用
  showSpinner = true;

  constructor(private upSvc: UploadService) { }

  ngOnInit() {
    //タイムスタンプが新しい順で取得:10件取得
    this.uploads = this.upSvc.getUploads({limitToLast: 10})

    //load end animations off リストが読み込んだらアニメーションをオフにする
    this.uploads.subscribe(() => this.showSpinner = false)
  }

}

3-2・画像リストの詳細

リストの下記のループで回っている部分の詳細になる

<div *ngFor="let upload of uploads | async">
  <upload-detail [upload]='upload'></upload-detail>
</div>
# プロジェクトの直下にいる状態とする
$ vi src/app/uploads2/upload-detail/upload-detail.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { UploadService } from '../shared/upload.service';
import { Upload } from '../shared/upload';

@Component({
  selector: 'upload-detail',
  template: `
<strong>{{upload.name}}</strong>
<button (click)='deleteUpload(upload)' class="button is-danger is-small">Delete</button><br>
`
})
export class UploadDetailComponent implements OnInit {

  @Input() upload: Upload;

  constructor(private upSvc: UploadService) { }

  ngOnInit() {
  }
  //削除ボタンを押された時の処理(確認画面もなく削除される・・・あとで修正)
  deleteUpload(upload) {
    this.upSvc.deleteUpload(this.upload)
  }

}

3-3・応募フォーム

$ vi src/app/uploads2/upload-form/upload-form.component.ts
import { Component, OnInit } from '@angular/core';
import { UploadService } from '../shared/upload.service';
import { Upload } from '../shared/upload';
import * as _ from "lodash";

@Component({
  selector: 'upload-form',
  templateUrl: './upload-form.component.html'
})
export class UploadFormComponent implements OnInit {

  selectedFiles: FileList;
  currentUpload: Upload;

  constructor(private upSvc: UploadService) { }

  ngOnInit() {
  }

  detectFiles(event) {
      this.selectedFiles = event.target.files;
  }

  //単一ファイルアップで仕様
  uploadSingle() {
    let file = this.selectedFiles.item(0)
    this.currentUpload = new Upload(file);
    this.upSvc.pushUpload(this.currentUpload)
  }

  //複数ファイルアップで仕様
  uploadMulti() {
    let files = this.selectedFiles
    if (_.isEmpty(files)) return;

    let filesIndex = _.range(files.length)
    _.each(filesIndex, (idx) => {
      this.currentUpload = new Upload(files[idx]);
      this.upSvc.pushUpload(this.currentUpload)}
    )
  }



}
$ vi src/app/uploads2/upload-form/upload-form.component.html
<div *ngIf="currentUpload">
    <progress class="progress is-success" min=1 max=100 value="{{ currentUpload?.progress }}"></progress>

    Progress: {{currentUpload?.name}} | {{currentUpload?.progress}}% Complete
</div>


<div class="box">
アップにはログインが必要です。ログインチェックをしていません。アップした場合は
パーミッションエラーが発生します。
  <h3>Single File Upload</h3>

    <label>
       <input type="file" class="button" (change)="detectFiles($event)">
    </label>

    <button class="button is-primary"
            [disabled]="!selectedFiles"
            (click)="uploadSingle()">

      アップを開始する
    </button>

    <hr>

    <h3>複数(Multiple) File Upload</h3>

    <label>
       <input type="file" class="button" (change)="detectFiles($event)" multiple>
    </label>


      <button class="button is-primary"
              [disabled]="!selectedFiles"
              (click)="uploadMulti()">

        アップを開始する
      </button>
</div>

4・モジュール

app.module.tsで読み込ませるために記述する

ng g module uploads2/shared/upload
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../../shared/shared.module';
import { AngularFireDatabaseModule } from 'angularfire2/database';

import { UploadService } from './upload.service';
import { UploadFormComponent } from '../upload-form/upload-form.component';
import { UploadsListComponent } from '../uploads-list/uploads-list.component';
import { UploadDetailComponent } from '../upload-detail/upload-detail.component';

import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', component: UploadsListComponent }
];

@NgModule({
  imports: [
    CommonModule,
    SharedModule,
    AngularFireDatabaseModule,
    RouterModule.forChild(routes)
  ],
  declarations: [
    UploadFormComponent,
    UploadsListComponent,
    UploadDetailComponent,
  ],
  providers: [
    UploadService
  ]
})
export class UploadModule2 { }

5・app.module.tsとrouteの設定

f:id:masalib:20170830184230p:plain
f:id:masalib:20170830184247p:plain



6・結果

https://angular-study-chat.firebaseapp.com/uploads2
f:id:masalib:20170830184300p:plain

ファイルアップができて大まかな流れを掴んだ
これをベースにサムネイル作成とか写真の情報を取得とかやりたい