備あれば憂いなし〜FirebaseでQiita記事の自動バックアップ環境を整備する

Qiitaにかれこれ3年以上前から投稿を続け累計記事数も100を超えました。

そんな中、最近ふと**「Qiitaのサービス停止またはデータ消失で俺の投稿記事すべて消えるのでは？」**と不安になりました。
そう。Qiitaへの投稿記事は厳密には自分のものではなく、当たり前ですがQiitaのプラットフォームに依存しています。

それで良いのか？いや、良くない。
ということでFirebase cloud functionsとFirestoreを使って自分のQiita記事を定期的にバックアップする環境を作りました💪
その紹介です。

**※ 注 Qiita記事がいきなり消えることはありないというのは重々承知です。**

# TL;DR
以下GithubリポジトリのREADME通りにFirebaseを立てれば消滅に備えられます。

https://github.com/kawamataryo/qiita-backup-with-firebase


# 手順
以下のような流れでバックアップを行います。

1. [Qiita API v2](https://www.google.com/search?q=qiita+api&oq=qiita+api&aqs=chrome.0.69i59j69i64j69i60l3.1480j0j1&sourceid=chrome&ie=UTF-8)から自分の投稿をすべて取得
2. Firestoreに記事データを保存
3. 1,2をFirebase cloud functionsを使って定期実行する

最終的にFirestoreにバックアップされた際のデータ構造は以下のようになります。

```bash
qiita-backups
  └── 2020-01-07 00:00 # (バックアップ日時のドキュメント）
       └── items # （記事ごとのサブコレクション）
             └── fdsadfsafeweafe # （記事データのドキュメント）
```

![Jan-20-2020 05-58-32 (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/87c460c6-f8e3-5d6c-bd5c-5ac2d43208a3.gif)

今回はQiitaで記事が消滅するというありえないシナリオに備えたものですが、「Firebase Cloud Functionsで定期的にAPIを叩いて情報取得、データをFirestoreに保存する」という一連の流れは他にも汎用的に使えるのではと思っています。

# Qiita記事一覧の取得
まず、自分の投稿記事一覧の取得です。
`functions.config().qiita.key`でQiitaのアクセスキーをセットした上で、axiosで[Qiita API v2](https://www.google.com/search?q=qiita+api&oq=qiita+api&aqs=chrome.0.69i59j69i64j69i60l3.1480j0j1&sourceid=chrome&ie=UTF-8)の各APIを叩いています。
Firebase cloud functionsから外部APIを叩くため、FirebaseプロジェクトのプランはBlazeプラン（従量課金）にする必要があるので注意してください。

また、記事の取得APIにはページングの概念があるので、自分のユーザー情報から投稿記事数を取得して、
ページ枚にループで記事取得APIを叩いて、自分の投稿記事全件を取得しています。


```functions/src/lib/client.ts
import axios, { AxiosResponse } from "axios";
import * as functions from "firebase-functions";
import { Item, User } from "../@types/qiita-type";

axios.defaults.baseURL = "https://qiita.com/api/v2";
axios.defaults.headers.common["Authorization"] = `Bearer ${
  functions.config().qiita.key
}`

const MAX_PER_PAGE = 100;

export const fetchItems = async (
  page: number,
  perPage: number
): Promise<AxiosResponse<Item[]>> => {
  return await axios.get<Item[]>(
    `/authenticated_user/items?page=${page}&per_page=${perPage}`
  );
};

export const fetchCurrentUser = async (): Promise<AxiosResponse<User>> => {
  return await axios.get<User>("/authenticated_user");
};

export const fetchCurrentUserAllItems = async (): Promise<Item[]> => {
  // Get Qiita items count from current user data
  const currentUserData = await fetchCurrentUser();
  const itemsCount = currentUserData.data.items_count;

  // Fetch all items;
  let items: Item[] = [];
  await Promise.all(
    [...Array(Math.ceil(itemsCount / MAX_PER_PAGE)).keys()].map(async i => {
      const res = await fetchItems(i + 1, MAX_PER_PAGE);
      items = [...items, ...res.data];
    })
  );
  return items;
};
```



# Firestoreへの保存
続いて取得した記事データをFirestoreに保存する部分です。
こちらは`qiita-backups"というコレクションのバックアップ日時ごとのドキュメントのサブコレクションに投稿記事を一件ずつ保存しています。

```functions/src/lib/firestore.ts
import { Item } from "../@types/qiita-type";
import * as admin from "firebase-admin";
import * as dayjs from "dayjs";

admin.initializeApp();

export const addItemsToFirestore = async (items: Item[]): Promise<void> => {
  // Backup Qiita items to firestore;
  const itemsRef = admin
    .firestore()
    .collection("qiita-backups")
    .doc(dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"))
    .collection("items");

  await Promise.all(
    items.map(async item => {
      await itemsRef.add(item);
    })
  );
};
```

# Firebase functionsとcloud scheduleで定期実行
最後に指定日時で定期実行の部分です。
`functions.pubsub.schedule`でCloud Scheduleを使った指定周期での実行関数を定義しています。
`BACKUP_SCHEDULE = "0 9 * * 1"`の通り、毎週月曜の9時にこの関数が走ります。
~~FirebaseプロジェクトでCloud Scheduleを使うためには、ロケーションを`us-central1`にする必要があるので注意です。
（GAEを内部的に使うためぽい。詳細は[ドキュメント](https://firebase.google.com/docs/functions/schedule-functions?hl=ja)に）~~
（そんなこともないようです）


```functions/src/index.ts
import * as functions from "firebase-functions";
import { fetchCurrentUserAllItems } from "./lib/client";
import { addItemsToFirestore } from "./lib/firestore";

const BACKUP_SCHEDULE = "0 9 * * 1";

export const scheduledBackupQiitaItems = functions.pubsub
  .schedule(BACKUP_SCHEDULE)
  .onRun(async _context => {
    try {
      const items = await fetchCurrentUserAllItems();
      await addItemsToFirestore(items);
      console.log(`Successfully saved ${items.length} items.`);
    } catch (e) {
      console.log(`A error has occurred.${e}`);
    }
  });
```

# 終わりに
以上、「FirebaseでQiita記事の自動バックアップ環境を整備する」でした。
Firebaseのリソースを使えば、このようなものも低コストで実装できるので良いですね。
この記事がどなたかの参考になれば幸いです。

※ 厳密にバックアップを取るのなら、記事中に含まれる画像のFirebase Storageへの保存もやったほうが良いかもしれない。