昨天我們已經可以成功從LineBot上收到檔案並暫存下來,今天我們透過domainDrive.SaveContent()回傳的*os.File,把他傳入adapter並操作GoogleDrive API將檔案真的上傳上去~
首先到adapter\google\drive.go,我們建立UploadFile(folderID string, fileName string, file *os.File),folderID 是指Google Drive資料夾的ID,讓使用者能傳送檔案到自己指定的資料夾,若為空而沒設定到parents就會上傳到根目錄,fileName代表上傳上去的檔案名稱,file就是我們從domain拿到的*os.File代表檔案內容。.Create建立檔案的基本資訊,設定好目標資料夾、檔案名,接著透過.Media代入檔案內容,就沒問題了~
// internal\adapter\google\drive.go
func (d *GoogleDrive) UploadFile(folderID string, fileName string, file *os.File) error {
	defer file.Close()
	// 指定目標資料夾的 ID
	var parents []string
	if folderID != "" {
		parents = []string{folderID}
	}
	// 上傳文件
	driveFile, err := d.Service.Files.Create(&drive.File{
		Name:    fileName,
		Parents: parents,
	}).Media(file).Do()
	if err != nil {
		log.Println("Upload Error:", err)
		return err
	}
	log.Printf("Got drive.File, err: %#v, %v", driveFile, err)
	return nil
}
下一步,folderID我們現在是寫死的,但應該是要儲存起來的。我們先到adapter\dynamodb\oauth.go,改一下GoogleOAuthToken 讓他先存到dynamodb裡面,我們在GoogleOAuthToken下面新增一個Info map[string]interface{},來儲存一些接下來可能會用到的attribute。
// internal\adapter\dynamodb\oauth.go
type GoogleOAuthToken struct {
	PK           string    `dynamodbav:"PK"`
	AccessToken  string    `dynamodbav:"access_token"`
	TokenType    string    `dynamodbav:"token_type"`
	RefreshToken string    `dynamodbav:"refresh_token"`
	Expiry       time.Time `dynamodbav:"expiry"`
	Info map[string]interface{} `dynamodbav:"info"`
}
接著我們到login_service.go,讓建立GoogleOAuthToken的同時能在Info帶上upload_folder_id。
// internal\app\service\drive\login_service.go
func (dr *GoogleDriveService) Login(ctx context.Context, lineID string, authCode string) error {
	tok, err := dr.driveServiceGoogleOA.UserOAuthToken(authCode)
	if err != nil {
		return err
	}
	dToken := dynamodb.GoogleOAuthToken{
		PK:           lineID,
		AccessToken:  tok.AccessToken,
		TokenType:    tok.TokenType,
		RefreshToken: tok.RefreshToken,
		Expiry:       tok.Expiry,
		Info: map[string]interface{}{
			"upload_folder_id": ""},
	}
	err = dr.driveServiceDynamodb.AddGoogleOAuthToken(dToken)
	if err != nil {
		return err
	}
	return nil
}
接著回到adapter,改一下更新的function,我們補上對應的update.Set(),讓他能更新其他的欄位。
// internal\adapter\dynamodb\oauth.go
func (basics TableBasics) TxUpdateGoogleOAuthToken(tok GoogleOAuthToken) (*dynamodb.TransactWriteItemsOutput, error) {
	var err error
	var response *dynamodb.TransactWriteItemsOutput
	update := expression.Set(expression.Name("refresh_token"), expression.Value(tok.RefreshToken))
	update.Set(expression.Name("access_token"), expression.Value(tok.AccessToken))
	// 補上更新其他欄位
	update.Set(expression.Name("token_type"), expression.Value(tok.TokenType))
	update.Set(expression.Name("expiry"), expression.Value(tok.Expiry))
	update.Set(expression.Name("info.upload_folder_id"), expression.Value(tok.Info["upload_folder_id"]))
	expr, err := expression.NewBuilder().WithUpdate(update).Build()
	if err != nil {
		log.Printf("Couldn't build expression for update. Here's why: %v\n", err)
	} else {
		twii := &dynamodb.TransactWriteItemsInput{
			TransactItems: []types.TransactWriteItem{
				{
					Update: &types.Update{
						Key:                       tok.GetKey(),
						TableName:                 aws.String(basics.TableName),
						ExpressionAttributeNames:  expr.Names(),
						ExpressionAttributeValues: expr.Values(),
						UpdateExpression:          expr.Update(),
					},
				},
			},
		}
		response, err = basics.DynamoDbClient.TransactWriteItems(context.TODO(), twii)
		if err != nil {
			log.Printf("Couldn't trasnaciton update tok %v. Here's why: %v\n", tok.PK, err)
		}
	}
	return response, err
}
然後,我們複製之前list folder拿到的folderID,選一個來做測試。

回到drive_service.go,正常情況我們從dynamodb取出folderID,但我們還沒做選擇資料夾的功能,所以我們先在下面貼上剛剛複製的來覆寫測試。
// internal\app\service\drive\drive_service.go
func (dr *GoogleDriveService) UploadFile(ctx context.Context, lineID string, fileName string, content io.ReadCloser) error {
	dToken, err := dr.driveServiceDynamodb.GetGoogleOAuthToken(lineID)
	if err != nil {
		log.Println(err)
		return err
	}
	tok := oauth2.Token{
		AccessToken:  dToken.AccessToken,
		TokenType:    dToken.TokenType,
		RefreshToken: dToken.RefreshToken,
		Expiry:       dToken.Expiry,
	}
	d, err := dr.driveServiceGoogleOA.NewGoogleDrive(ctx, &tok)
	if err != nil {
		log.Println(err)
		return err
	}
	file, err := domainDrive.SaveContent(content)
	if err != nil {
		log.Println(err)
		return err
	}
	log.Println("START Upload File To Drive")
	folderID := dToken.Info["upload_folder_id"].(string)
	// 假設預設的儲存路徑
	folderID = "19Dxrgx_lYM68o3w_Fi4Qy5aysTX6ZAlt"
	err = d.UploadFile(folderID, fileName, file)
	if err != nil {
		log.Println("err:", err)
		return err
	}
	return nil
}
最後,跟昨天一樣用測試.txt來試試看,可以看到Linebot收到檔案後,能成功地上傳到Google Drive並且檔案有在我們指定的資料夾下,那我們今天就寫到這囉~

今天踩了一個坑,正常情形下,我們在使用d.Service.Files.Create().Media(file).Do()上傳檔案的時候,因為.Media背後其實是會帶上uploadType=media的參數,但是當要上傳的檔案大於5MB的時候,背後會變成調用"https://www.googleapis.com/upload/drive/v3/files?alt=json&prettyPrint=false&uploadType=resumable",uploadType=resumable會導致背後會走支援斷點續傳的模式,而這個自動切換也就讓我們最初NewGoogleDrive()時代入的ctx在oa.Config.Client(ctx, tok)裡面被context canceled掉。為了避免這個問題,我們把ctx改成context.Background()讓兩者不要有依賴關係,這樣就能同時讓大小檔案都能上傳了。
// internal\adapter\google\oauth.go
func (oa *GoogleOAuth) NewGoogleDrive(ctx context.Context, tok *oauth2.Token) (*GoogleDrive, error) {
	client := oa.Config.Client(context.Background(), tok)
	srv, err := drive.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		log.Printf("Unable to retrieve Drive client: %v", err)
		return nil, err
	}
	return &GoogleDrive{
		Service: srv,
	}, nil
}

