@jagijagijag1の技術ブログ

つくったもの

  • 定期実行でDropboxの特定フォルダ下ファイルをS3へ転送する機能
  • 具体的には,CloudWatch Eventで定期起動し下記を実行するLambda関数を開発
    1. Dropbox APIを叩いて特定フォルダ配下の全ファイルを取得
    2. 当該ファイルをダウンロードし,S3の特定バケットに保存
    3. バケット保存が成功した場合,Dropbox側のオリジナルファイルを削除
経緯

  • しかしながら,これだけのためにZapierに課金するのも辛いので,イベント駆動を妥協して定期実行でDropbox→S3転送機能を実現してみた
  • これにより,Zapierを経由せず,iPhoneからPixelaをシームレスに結合

環境

  • MacOS Mojave
  • Go 1.11.5
  • Serverless Framework 1.38.0

ライブラリ選択

前準備

  • 新規Dropboxアプリを登録
  • 作成後,SettingsタブのOAuth 2 - Generated access tokenで”Generate”すると,(テスト向け)トークンを取得可能

開発詳細

先につまづきメモ

  • Dropbox APIでファイル一覧を取得した際の返り値が[]IsMetadataで,ファイル(FileMetadata)として操作できず
    • Goの型アサーションでFileMetadataとして処理が可能に
    • 詳細はこちら
  • Dropboxから取得したファイルコンテンツをS3にわたすため,下記変換手順を踏む必要あり
    • 正直なところGoのio周りやDropboxのDwonload/AWSのReadSeekCloserの仕様を理解していないので,とりあえず[]byteらしきところを引っこ抜いて渡したらうまくいった状態
// download from dropbox  
dlArg := files.NewDownloadArg(fpath)  
res, content, err := dbx.Download(dlArg)  

// extract file content as []byte  
blob, _ := ioutil.ReadAll(content)  
// []byte -> Reader  
f := bytes.NewReader(blob)  
// Reader -> aws.ReadSeekCloser  
rs := aws.ReadSeekCloser(f)  
// S3 PutObject requires aws.ReadSeekCloser as its input body  
input := &s3.PutObjectInput{  
    Body:   rs,  
    Bucket: aws.String(bucketName),  
    Key:    aws.String(fileName),  
}

開発の初期設定

  • 作ったものはGitHub
  • $GOHOME/src配下の任意フォルダで作業
$ sls create -t aws-go-dep -p <project-name>
  • 生成されたhelloworldの2つの関数は不要なので削除
  • dropbox2s3ディレクトリとmain.goを作成
  • Makefile修正
.PHONY: build clean deploy  

build:  
    dep ensure -v  
    env GOOS=linux go build -ldflags="-s -w" -o bin/dropbox2s3 dropbox2s3/main.go  

clean:  
    rm -rf ./bin ./vendor Gopkg.lock  

deploy: clean build  
    sls deploy --verbose
  • serverless.yml修正
service: dropbox2s3-periodic  

frameworkVersion: ">=1.28.0 <2.0.0"  

provider:  
  name: aws  
  runtime: go1.x  
  region: ap-northeast-1  

  iamRoleStatements:  
    - Effect: "Allow"  
      Action:  
        - "s3:ListBucket"  
      Resource: "*"  
    - Effect: "Allow"  
      Action:  
        - "s3:PutObject"  
      Resource: "*"  

package:  
 exclude:  
   - ./**  
 include:  
   - ./bin/**  

functions:  
  dropbox2s3:  
    handler: bin/dropbox2s3  
    events:  
      - schedule: cron(0 16 * * ? *)  
    environment:  
      TZ: Asia/Tokyo  
      DROPBOX_TOKEN: <your-api-token>  
      IMG_FOLDER_PATH: <target-project-id>   
      BUCKET_NAME: <your-bucket>  
    timeout: 60

main関数修正

  • 冒頭の処理をナイーブに実装
package main  

import (  
    "bytes"  
    "context"  
    "fmt"  
    "io"  
    "io/ioutil"  
    "os"  

    "github.com/aws/aws-sdk-go/aws/session"  

    "github.com/aws/aws-sdk-go/aws"  
    "github.com/aws/aws-sdk-go/service/s3"  

    "github.com/aws/aws-lambda-go/lambda"  
    "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox"  
    "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files"  
)  

// Handler is our lambda handler invoked by the `lambda.Start` function call  
func Handler(ctx context.Context) error {  
    // extract env var  
    dropboxToken := os.Getenv("DROPBOX_TOKEN")  
    imgFolderPath := os.Getenv("IMG_FOLDER_PATH")  
    bucketName := os.Getenv("BUCKET_NAME")  

    // tansport image from dropbox to s3  
    transport(dropboxToken, imgFolderPath, bucketName)  

    return nil  
}  

func transport(dropboxToken, imgFolderPath, bucketName string) {  
    // dropbox setting  
    config := dropbox.Config{  
        Token: dropboxToken,  
    }  
    dbx := files.New(config)  

    // get info under the imgFolderPath folder  
    arg := files.NewListFolderArg(imgFolderPath)  
    resp, err := dbx.ListFolder(arg)  
    if err != nil {  
        fmt.Println("err in accesing dropbox folder")  
        fmt.Println(err)  
        return  
    }  

    // for each file/folder  
    for _, e := range resp.Entries {  
        // use type annotation to cast e (IsMetadata) to file (FileMetadata)  
        f, ok := e.(*files.FileMetadata)  
        if ok {  
            // if e is file, download content  
            fmt.Println("find file ", f.Name)  
            fpath := f.Metadata.PathLower  
            dlArg := files.NewDownloadArg(fpath)  
            res, content, err := dbx.Download(dlArg)  
            if err != nil {  
                fmt.Println("err in downloading file")  
                fmt.Println(err)  
                return  
            }  

            // and then put it to S3  
            err = putToS3(bucketName, res.Metadata.Name, content)  
            if err == nil {  
                // if successed, remove transported file on dropbox  
                deleteFromDropbox(dbx, fpath)  
            }  
        }  
    }  
}  

func putToS3(bucketName, fileName string, content io.ReadCloser) error {  
    // create s3 client  
    sess := session.Must(session.NewSession(&aws.Config{  
        Region: aws.String("ap-northeast-1"),  
    }))  
    svc := s3.New(sess)  

    // create put-object input  
    blob, _ := ioutil.ReadAll(content)  
    f := bytes.NewReader(blob)  
    rs := aws.ReadSeekCloser(f)  
    input := &s3.PutObjectInput{  
        Body:   rs,  
        Bucket: aws.String(bucketName),  
        Key:    aws.String(fileName),  
    }  

    // put contet to s3  
    _, err := svc.PutObject(input)  
    if err != nil {  
        fmt.Println("err in putting object")  
        fmt.Println(err)  
        return err  
    }  

    fmt.Println("put object ", fileName, " to ", bucketName)  
    return nil  
}  

func deleteFromDropbox(dbx files.Client, filepath string) {  
    delArg := files.NewDeleteArg(filepath)  
    _, err := dbx.Delete(delArg)  
    if err != nil {  
        fmt.Println("err in downloading file")  
        fmt.Println(err)  
        return  
    }  
}  

func main() {  
    lambda.Start(Handler)  
}

デプロイ

  • 以下でデプロイ
    • make deploy = make + sls deploy
$ cd <project-name>  
$ make deploy

この記事へのコメント

まだコメントはありません