• ホーム
  • 開発者ブログ
  • コラボフロー Webhook と REST API を活用して申請終了時に添付ファイルをバックアップする

開発者ブログ

コラボフロー Webhook と REST API を活用して申請終了時に添付ファイルをバックアップする

Rust で開発するんだと覚悟を決めたエンジニア Fukuda です🐣

この記事は コラボフロー Advent Calendar 2023 16日目の記事です!

添付ファイルのバックアップ

何かしらのサービスを運用していると バックアップを作成したい と思うことありますよね。

コラボフローでは申請書にファイルを添付することができる「添付ファイルパーツ」があります。

また、添付したファイルを一括で取得することができる 添付ファイル一括ダウンロード という機能があり、
ファイルのバックアップの取得のために使うことも可能です。

画面から操作することで好きなタイミングで一括ダウンロードすることができます。

でも、 自動化したいな と思ってしまうこともありますよね?

そんな要望を叶えるべく、コラボフローをカスタマイズしてみたいと思います✌️✨

添付ファイルのバックアップカスタマイズの概要

今回はコラボフロー の 経路のWebhook通知コラボフロー REST API を活用してカスタマイズしていきます。

バックアップ作成の流れ

以下の流れで添付されたファイルをバックアップします💡

  1. AWS Lambda で申請終了時の Webhook 通知を受信する
  2. 通知内容のバックアップ対象の添付ファイルのIDを取得する
  3. コラボフロー REST API で添付ファイルをダウンロードする
  4. Amazon S3 にファイルを保存する

Lambda 関数を実装する

冒頭でも述べた通り Rust で開発するんだと覚悟を決めておりますので、当然 Rust を採用しています。

なお、ソースコード一式は Github でも確認できます。

README にも記載の通り、This is not an official project, just a hobby project. です。

Cargo.toml

Cargo.toml に使用するライブラリを定義します。

主なライブラリ:

  • Lambda 関連のライブラリ
  • AWS SDK for Rust の aws-configaws-sdk-s3
  • コラボフロー REST API クライアントの collaboflow-rs
[package]
name = "collaboflow-backup-lambda"
version = "0.1.0"
edition = "2021"

[dependencies]
lambda_http = "0.8.3"
lambda_runtime = "0.8.3"
tokio = { version = "1", features = ["macros"] }
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
serde_json = "1.0.108"
serde = { version = "1.0.193", features = ["derive"] }
collaboflow-rs = "0.0.13"
config = "0.13.4"
clap = "4.4.11"
time = "0.3.30"
structopt = { version = "0.3.26", features = [] }
object = { version = "0.32.1", features = [] }
aws-config= { version = "1.0.3", features = ["behavior-version-latest"] }
aws-sdk-s3= { version = "1.5.0", features = ["rt-tokio"] }

コラボフロー REST API クライアントの collaboflow-rs は Fukuda が趣味で開発し、
昨年のコラボフロー Advent Calendar 2022 で公開した非公式のライブラリです。

(いつでも公式にしたるわ!ぐらいの勢いで開発してます🤫🔥)

collaboflow.rs

このファイルにはコラボフロー関連の処理を実装しました。

主な処理は、Webhook の通知内容から添付ファイルパーツの情報を取得する関数 get_file_info
ファイルをダウンロードする download_file です。

添付ファイルの情報は以下のような構造になっています。

{
    "contents": {
        "fidFile": {
            "label": "ファイル名",
            "link": "ダウンロード URL",
            "value": "ファイル ID",
            "type": "attachment"
        }
    }
}

そのため、ファイルダウンロードのために value であるファイル ID を取得し、
S3 に保存時に使用するために label であるファイル名を取得しています。

use collaboflow_rs::bytes::Bytes;
use collaboflow_rs::{Authorization, CollaboflowClient};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::env;

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CollaboflowWebhookResponse {
    contents: Value,
}

#[derive(Clone, Debug)]
pub struct BackupFile {
    pub id: String,
    pub name: String,
}

pub fn get_file_info(s: &str) -> Result<BackupFile, ()> {
    let resp = match serde_json::from_str::<CollaboflowWebhookResponse>(s) {
        Ok(resp) => resp,
        Err(_) => {
            return Err(());
        }
    };

    let fid =
        env::var("BACKUP_FID_KEY").unwrap_or_else(|_| panic!("{} is undefined.", "BACKUP_FID_KEY"));

    // ファイルIDを取得する
    let id = match resp.contents.get(&fid) {
        None => "".to_string(),
        Some(fid_file) => match fid_file.get("value") {
            None => "".to_string(),
            Some(value) => match value.as_str() {
                None => "".to_string(),
                Some(v) => v.to_string(),
            },
        },
    };

    // ファイル名を取得する
    let name = match resp.contents.get(&fid) {
        None => "".to_string(),
        Some(fid_file) => match fid_file.get("label") {
            None => "".to_string(),
            Some(value) => match value.as_str() {
                None => "".to_string(),
                Some(v) => v.to_string(),
            },
        },
    };

    Ok(BackupFile { id, name })
}

pub async fn download_file(id: &str) -> Result<Bytes, ()> {
    let client = collaboflow_client();

    // ファイルをダウンロード
    match client.files.get(id).await {
        Ok(resp) => Ok(resp.body),
        Err(_) => Err(()),
    }
}

pub fn collaboflow_client() -> CollaboflowClient {
    // コラボフローに接続するための情報を環境変数から取得する
    let authorization = Authorization::with_api_key(
        env::var("CF_USER_ID")
            .unwrap_or_else(|_| panic!("{} is undefined.", "CF_USER_ID"))
            .as_str(),
        env::var("CF_API_KEY")
            .unwrap_or_else(|_| panic!("{} is undefined.", "CF_API_KEY"))
            .as_str(),
    );

    // コラボフロー REST API クライアントを生成する
    CollaboflowClient::new(
        env::var("CF_BASE_URL")
            .unwrap_or_else(|_| panic!("{} is undefined.", "CF_BASE_URL"))
            .as_str(),
        authorization,
    )
}

ライブラリ内に REST API に関する処理を実装しているので、
添付ファイルのダウンロード処理の実装は client.files.get(id).await だけです。

REST API にアクセスする認証情報の生成やエンドポイントなどを意識することなく、実装を進められました。

s3.rs

このファイルには、取得した添付ファイルを Amazon S3 に PutOblect で保存する関数が定義されています。

use aws_config::meta::region::RegionProviderChain;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::{config::Region, Client};
use collaboflow_rs::bytes::Bytes;
use std::error::Error;
use tracing::info;

pub async fn put_object(bucket: &str, key: &str, data: Bytes) -> Result<(), Box<dyn Error>> {
    let region = Some("ap-northeast-1");
    let region_provider = RegionProviderChain::first_try(region.map(Region::new))
        .or_default_provider()
        .or_else(Region::new("ap-northeast-1"));
    let shared_config = aws_config::from_env().region(region_provider).load().await;
    let client = Client::new(&shared_config);

    let body = ByteStream::from(data.to_vec());
    let request = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(body)
        .send()
        .await?;

    info!("{:?}", request);

    Ok(())
}

main.rs

上記関数たちを呼び出して、最後にレスポンスを返却しています。

mod collaboflow;
mod s3;

use crate::collaboflow::{download_file, get_file_info};
use crate::s3::put_object;
use lambda_http::{run, service_fn, Body, Error, Request, Response};
use std::env;

async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    let body = event.body();
    let s = std::str::from_utf8(body).expect("invalid utf-8 sequence");

    // Serialize JSON into struct.
    // If JSON is incorrect, send back 400 with error.
    let backup_file = match get_file_info(s) {
        Ok(info) => info,
        Err(_) => {
            let resp = Response::builder()
                .status(400)
                .header("content-type", "text/html")
                .body("Error...".into())
                .map_err(Box::new)?;
            return Ok(resp);
        }
    };

    if !backup_file.id.is_empty() {
        let bucket = env::var("BACKUP_BUCKET_NAME")
            .unwrap_or_else(|_| panic!("{} is undefined.", "BACKUP_BUCKET_NAME"));

        // コラボフローからファイルを取得する
        if let Ok(data) = download_file(&backup_file.id).await {
            // ファイルIDでフォルダを作成し、その中にファイルを保存する
            let key = format!("{}/{}", &backup_file.id, &backup_file.name);
            let _ = put_object(&bucket, &key, data).await;
        }
    }

    // Return something that implements IntoResponse.
    // It will be serialized to the right response event automatically by the runtime
    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body("Success!".into())
        .map_err(Box::new)?;
    Ok(resp)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        // disable printing the name of the module in every log line.
        .with_target(false)
        // disabling time is handy because CloudWatch will add the ingestion time.
        .without_time()
        .init();

    run(service_fn(function_handler)).await
}

Cargo Lambda

Rust で Lambda 関数を実装するにあたって、Cargo Lambda を使用しました。

ビルドとデプロイは以下のコマンドで簡単に実行できます。

なお、Webhook の通知先の URL として使用するために関数 URL を有効にしてデプロイします。

cargo lambda build --release
cargo lambda deploy --enable-function-url

フォーム・経路の作成と Lambda の環境変数

コラボフローでフォームと経路を作成します。

バックアップ作成の対象である添付ファイルパーツのパーツ ID は「fidFile」としました。

また、Lambda 関数 URL を控え、経路の Webhook に設定し「申請書の経路終了時に通知する」を有効にします。

Lambda 側の設定では、Lambda の実行ロールには S3 にアクセスするため許可ポリシーを追加します。

そして、環境変数に以下を追加します。

  • BACKUP_BUCKET_NAME: バックアップを格納する S3 バケット名
  • BACKUP_FID_KEY: 添付ファイルパーツのパーツ ID 「fidFile
  • CF_API_KEY: コラボフロー API キー
  • CF_BASE_URL: コラボフロー API URL (https://cloud.collaboflow.com/{instance}/api/index.cfm)
  • CF_USER_ID: コラボフローの管理者ユーザー ID

申請して承認まで進める

動作確認のため、申請して承認まで進めます。

「お見積もり.pdf」がバックアップ対象となる添付ファイルです。

承認後、AWS コンソールから S3 バケットを確認してファイルが保存されていることを確認します。

見事ファイルが格納されていました!

添付ファイルのバックアップができました🙌

collaboflow-rs をアップデート

今回のカスタマイズにあたって、開発している OSS ライブラリである collaboflow-rs のアップデートもできました。

昨年のコラボフロー Advent Calendar では添付ファイル関連の REST API には非対応だったんです笑

このバージョンアップで、主要な API への対応が完了しました!

やはり、自分でライブラリを開発することで得られる知見は大きいですね。

人に見られるコード を書くということは自分の成長にも繋がると感じています。

まとめ

コラボフロー の経路のWebhook通知とコラボフロー REST APを活用して、
添付ファイルのバックアップ自動化をやってみました。

好みの技術スタックである Rust と Cargo Lambda を使用することができて、
満足のいく お勉強駆動開発 ができました。

やはり、Rust はええで

Rust.Tokyo 2024 の登壇を目標 に、もっともっと Rust を書いていきたいと思います🔥


コラボフロー Advent Calendar 2023 はまだまだ続きます!

それでは、また🐥

まずは無料で
体験
してみませんか?

30日間の無料お試し
30日間の無料お試し

とりあえず自分で試してみたい方は、
無料お試しをお申し込みください

オンラインデモ
オンラインデモ

操作手順や設定の流れを、
30分程度のお時間で簡単にご説明致します

お試しハンズオン
お試しハンズオン

コラボフローを実際に体験していただける
オンラインセミナーを開催しています