昨天我們已經可以成功從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
}