AngularでGoogle Drive APIを使ってみる

Google Driveを使ったWebアプリを作成しようと思ったので、Drive APIを使うまでの手順を残しておきます。

# 環境

```shell
$ ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 10.1.7
Node: 12.16.3
OS: darwin x64

Angular: 10.1.6
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1001.7
@angular-devkit/build-angular   0.1001.7
@angular-devkit/core            10.1.7
@angular-devkit/schematics      10.1.7
@angular/cli                    10.1.7
@schematics/angular             10.1.7
@schematics/update              0.1001.7
rxjs                            6.6.3
typescript                      4.0.5
    

```



# Google Drive APIの準備

[Browser Quickstart](https://developers.google.com/drive/api/v3/quickstart/js)を参考にDrive APIの準備をします

※Node.js向けのページがありますが、Angularだと色んなパッケージが足りないというエラーになってめんどくさいので、シンプルなJS用手順でやります


### 前提

- GCP（Google Cloud Platformへの登録が必要です）

### 1. GCPにてDriveAPIを有効化

- `Enable Drive API`ボタンを押下
- プロジェクト名を入力するダイアログが表示されるため、プロジェクト名を入力しNEXT押下
    - GCP上に指定の名前でプロジェクトが作成されます
- しばらくすると作成が完了した旨が表示され、クライアントIDやシークレットが表示されるためコピーしておきます
    - credentials.jsonのダウンロードができるので、ダウンロードしておきます


### 2. API Keyの発行

- `Create API Key`ボタンを押下します
- 上記で作成したプロジェクト名を指定します
- しばらくするとAPI Keyが発行されるので、コピーします

### 3. APIの有効化

- [Google API Console](https://console.developers.google.com/)にアクセスします
- 「Create API Key」で作成されたプロジェクトを選択します
- 「APIとサービスの有効化」を押下

![スクリーンショット 2021-01-17 10.46.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/c14f44ff-d727-a056-b785-1958fd70f57e.png)

- Drive APIを選択します

![スクリーンショット 2021-01-17 10.47.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/93cf1789-3389-c5c1-265b-742701115428.png)

- 「有効にする」を押下します

![スクリーンショット 2021-01-17 10.48.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/759cd67e-9b54-beb3-3bd8-e52cb18fff4f.png)


#### ※API Keyの制限

Angularの場合、環境変数はenvironment.tsにベタ書きすることになるため、プライベートリポジトリでも念のためAPIKeyは見られても問題ないようにしておきます。

- Google API Consoleの「認証情報」メニューからAPI Keyの編集を行います
- 今回はHTTPリファラで制限します
    - 今回はGitHub Pagesで公開するため、GitHub PagesのURLを指定します
        - 面倒な場合は`https://{username}.github.io`と`https://{username}.github.io/*`を登録しておくと良いと思います
    - これで、指定のURL意外でAPI Keyを使われても大丈夫です

![スクリーンショット 2021-01-17 10.39.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/f82825d5-739a-2a50-1ddd-839a3c3fd72b.png)

参考：https://cloud.google.com/docs/authentication/api-keys?hl=ja#api_key_restrictions



これでDrive APIの準備は完了です


# Angular側の対応


### 環境変数の定義

環境変数に`クライアントID`と`API Key`をセットします
<font color="red">※公開リポジトリの場合、注意が必要です</font>

```src/environments/environment.ts
export const environment = {
  production: false,
  googleDriveAPI: {
    clientId: {クライアントIDを指定},
    apiKey: {API Keyを指定},
  }
};
```

※prodにも同様の定義を追加します
 本番用にAPI KeyとOAuthClientIDを別途発行した方が良いです

### GoogleAPI用のサービスを作成

```src/shared/google-api.service.ts
import { EventEmitter, Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';

@Injectable({ providedIn: 'root' })
export class GoogleApiService {
  readonly DISCOVERY_DOCS = [
    'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
  ];
  readonly SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';

  /** Google API JS読み込み完了時 */
  readonly onLoad = new EventEmitter<boolean>();
  /** 認証状態変更検知用 */
  readonly onChangeStatus = new EventEmitter<boolean>();

  get gapi() {
    return (window as any).gapi;
  }
  get instAuth2() {
    return this.gapi.auth2.getAuthInstance();
  }

  public init() {
    this.loadGoogleApiScript();
  }

  /**
   * sign in
   *
   * 完了はonChangeStatusをsubscribeすること
   */
  public signIn() {
    this.instAuth2.signIn();
  }

  /**
   * sign out
   *
   * 完了はonChangeStatusをsubscribeすること
   */
  public signOut() {
    this.instAuth2.signOut();
  }

  /**
   * GoogleAPIのJSファイル読み込み
   * ※これやらないとグローバル変数のgapiが参照できない
   */
  private loadGoogleApiScript() {
    const elScript = document.createElement('script');
    elScript.async = true;
    elScript.defer = true;
    elScript.src = 'https://apis.google.com/js/api.js';

    // jsファイル読み込み完了時（フラグを落とすことで画面表示を開始する）
    elScript.onload = this.handleClientLoad();

    document.head.appendChild(elScript);
  }

  private handleClientLoad() {
    const self = this;
    return (event) => self.gapi.load('client:auth2', self.handleInitClient());
  }

  private handleInitClient() {
    const self = this;
    return () =>
      self.gapi.client
        .init({
          apiKey: environment.googleDriveAPI.apiKey,
          clientId: environment.googleDriveAPI.clientId,
          discoveryDocs: self.DISCOVERY_DOCS,
          scope: self.SCOPES,
        })
        .then(
          () => {
            // JS読み込み完了を通知
            self.onLoad.emit(true);

            const isSignedIn = self.instAuth2.isSignedIn;
            // Listen for sign-in state changes.
            isSignedIn.listen((isSignIn: boolean) => {
              self.onChangeStatus.emit(isSignIn);
            });

            // 最初のSignIn状態を通知
            self.onChangeStatus.next(isSignedIn.get());
          },
          (error) => {
            console.error(error);
            // on error
            self.onLoad.emit(false);
          }
        );
  }
}
```

### 認証が通らないと画面が出ないように制御

- Google APIのJSが読み込まれるまで画面が出ないように制御
- 認証済みであれば`sign out`ボタン、未認証であれば`sign in`ボタンを表示する

```src/app/app.component.ts
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { GoogleApiService } from './shared/google-api.service';

@Component({
  selector: 'app-root',
  template: `
    <div *ngIf="!isLoading">
      <div>
        <button *ngIf="isSignedIn" (click)="onClickSignOut()">sign out</button>
        <button *ngIf="!isSignedIn" (click)="onClickSignIn()">sign in</button>
      </div>
      <router-outlet *ngIf="isSignedIn"></router-outlet>
      <div></div>
    </div>
  `,
})
export class AppComponent implements OnInit {
  /**
   * 読み込み中フラグ
   * trueの間は画面表示させない
   */
  isLoading = true;
  isSignedIn = false;

  constructor(
    private googleapi: GoogleApiService,
    private cdRef: ChangeDetectorRef
  ) {}

  async ngOnInit() {
    this.googleapi.init();

    this.googleapi.onChangeStatus.subscribe((isSignIn: boolean) => {
      this.isSignedIn = isSignIn;
      // 画面更新
      this.cdRef.detectChanges();
    });

    this.googleapi.onLoad.subscribe((result) => {
      this.isLoading = false;
      // 画面更新
      this.cdRef.detectChanges();
    });
  }

  onClickSignIn() {
    this.googleapi.signIn();
  }
  onClickSignOut() {
    this.googleapi.signOut();
  }
}
```



### ローカル起動時のポートを変更

localhost:4200だとOAuthClientのロードでエラーになるためポートを8000に変更します

```package.json
"scripts": {
  "start": "ng serve --port 8000"
}
```


# 起動してみる

- `npm start`でサーバーを起動し、http://localhost:8000 にアクセスします
- `sign in`ボタンを押下すると以下のようなGoogleのログインダイアログが表示されます

![スクリーンショット 2021-01-17 11.52.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/2ccca592-52dd-7d56-babc-fa20eace09f0.png)




# Drive APIを使う

Drive API用のサービスを作成します

```src/app/shared/google-drive-api.service.ts
import { Injectable } from '@angular/core';
import { GoogleApiService } from './google-api.service';

@Injectable()
export class GoogleDriveApiService {
  constructor(private googleapi: GoogleApiService) {}

  get drive() {
    return this.googleapi.gapi.client.drive;
  }

  async getFiles(): Promise<{ id: string; name: string }[]> {
    try {
      const result = await this.drive.files.list({
        pageSize: 10,
        fields: 'nextPageToken, files(id, name)',
      });
      console.log(result);
      if (result.status === 200) {
        return result.result.files;
      } else {
        throw new Error();
      }
    } catch (err) {
      console.error(err);
      return [];
    }
  }
}
```

これでファイル一覧が取得できます