my icon

jagijagijag1 tech note

jagijagijag1

つくったもの

  • 定期実行でDropboxの特定フォルダ下ファイルをS3へ転送する機能
  • 具体的には,CloudWatch Eventで定期起動し下記を実行するLambda関数を開発
    1. Dropbox APIを叩いて特定フォルダ配下の全ファイルを取得
    2. 当該ファイルをダウンロードし,S3の特定バケットに保存
    3. バケット保存が成功した場合,Dropbox側のオリジナルファイルを削除
経緯
  • しかしながら,これだけのためにZapierに課金するのも辛いので,イベント駆動を妥協して定期実行でDropbox→S3転送機能を実現してみた
  • これにより,Zapierを経由せず,iPhoneからPixelaをシームレスに結合

undefined.jpg

環境

  • 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

Say Something

Comments

Recent Posts

Categories

Blog PVs

Powered by Pixela