| | package google_drive |
| |
|
| | import ( |
| | "context" |
| | "crypto/x509" |
| | "encoding/pem" |
| | "fmt" |
| | "net/http" |
| | "os" |
| | "regexp" |
| | "strconv" |
| | "time" |
| |
|
| | "github.com/alist-org/alist/v3/pkg/http_range" |
| |
|
| | "github.com/alist-org/alist/v3/drivers/base" |
| | "github.com/alist-org/alist/v3/internal/model" |
| | "github.com/alist-org/alist/v3/pkg/utils" |
| | "github.com/go-resty/resty/v2" |
| | "github.com/golang-jwt/jwt/v4" |
| | log "github.com/sirupsen/logrus" |
| | ) |
| |
|
| | |
| |
|
| | type googleDriveServiceAccount struct { |
| | |
| | |
| | |
| | PrivateKey string `json:"private_key"` |
| | ClientEMail string `json:"client_email"` |
| | |
| | |
| | TokenURI string `json:"token_uri"` |
| | |
| | |
| | } |
| |
|
| | func (d *GoogleDrive) refreshToken() error { |
| | |
| | gdsaFile, gdsaFileErr := os.Stat(d.RefreshToken) |
| | if gdsaFileErr == nil { |
| | gdsaFileThis := d.RefreshToken |
| | if gdsaFile.IsDir() { |
| | if len(d.ServiceAccountFileList) <= 0 { |
| | gdsaReadDir, gdsaDirErr := os.ReadDir(d.RefreshToken) |
| | if gdsaDirErr != nil { |
| | log.Error("read dir fail") |
| | return gdsaDirErr |
| | } |
| | var gdsaFileList []string |
| | for _, fi := range gdsaReadDir { |
| | if !fi.IsDir() { |
| | match, _ := regexp.MatchString("^.*\\.json$", fi.Name()) |
| | if !match { |
| | continue |
| | } |
| | gdsaDirText := d.RefreshToken |
| | if d.RefreshToken[len(d.RefreshToken)-1:] != "/" { |
| | gdsaDirText = d.RefreshToken + "/" |
| | } |
| | gdsaFileList = append(gdsaFileList, gdsaDirText+fi.Name()) |
| | } |
| | } |
| | d.ServiceAccountFileList = gdsaFileList |
| | gdsaFileThis = d.ServiceAccountFileList[d.ServiceAccountFile] |
| | d.ServiceAccountFile++ |
| | } else { |
| | if d.ServiceAccountFile < len(d.ServiceAccountFileList) { |
| | d.ServiceAccountFile++ |
| | } else { |
| | d.ServiceAccountFile = 0 |
| | } |
| | gdsaFileThis = d.ServiceAccountFileList[d.ServiceAccountFile] |
| | } |
| | } |
| |
|
| | gdsaFileThisContent, err := os.ReadFile(gdsaFileThis) |
| | if err != nil { |
| | return err |
| | } |
| |
|
| | |
| | var jsonData googleDriveServiceAccount |
| | err = utils.Json.Unmarshal(gdsaFileThisContent, &jsonData) |
| | if err != nil { |
| | return err |
| | } |
| |
|
| | gdsaScope := "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.metadata https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/drive.scripts" |
| |
|
| | timeNow := time.Now() |
| | var timeStart int64 = timeNow.Unix() |
| | var timeEnd int64 = timeNow.Add(time.Minute * 60).Unix() |
| |
|
| | |
| | privateKeyPem, _ := pem.Decode([]byte(jsonData.PrivateKey)) |
| | privateKey, _ := x509.ParsePKCS8PrivateKey(privateKeyPem.Bytes) |
| |
|
| | jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, |
| | jwt.MapClaims{ |
| | "iss": jsonData.ClientEMail, |
| | "scope": gdsaScope, |
| | "aud": jsonData.TokenURI, |
| | "exp": timeEnd, |
| | "iat": timeStart, |
| | }) |
| | assertion, err := jwtToken.SignedString(privateKey) |
| | if err != nil { |
| | return err |
| | } |
| |
|
| | var resp base.TokenResp |
| | var e TokenError |
| | res, err := base.RestyClient.R().SetResult(&resp).SetError(&e). |
| | SetFormData(map[string]string{ |
| | "assertion": assertion, |
| | "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", |
| | }).Post(jsonData.TokenURI) |
| | if err != nil { |
| | return err |
| | } |
| | log.Debug(res.String()) |
| | if e.Error != "" { |
| | return fmt.Errorf(e.Error) |
| | } |
| | d.AccessToken = resp.AccessToken |
| | return nil |
| | } |
| | if gdsaFileErr != nil && os.IsExist(gdsaFileErr) { |
| | return gdsaFileErr |
| | } |
| | url := "https://www.googleapis.com/oauth2/v4/token" |
| | var resp base.TokenResp |
| | var e TokenError |
| | res, err := base.RestyClient.R().SetResult(&resp).SetError(&e). |
| | SetFormData(map[string]string{ |
| | "client_id": d.ClientID, |
| | "client_secret": d.ClientSecret, |
| | "refresh_token": d.RefreshToken, |
| | "grant_type": "refresh_token", |
| | }).Post(url) |
| | if err != nil { |
| | return err |
| | } |
| | log.Debug(res.String()) |
| | if e.Error != "" { |
| | return fmt.Errorf(e.Error) |
| | } |
| | d.AccessToken = resp.AccessToken |
| | return nil |
| | } |
| |
|
| | func (d *GoogleDrive) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { |
| | req := base.RestyClient.R() |
| | req.SetHeader("Authorization", "Bearer "+d.AccessToken) |
| | req.SetQueryParam("includeItemsFromAllDrives", "true") |
| | req.SetQueryParam("supportsAllDrives", "true") |
| | if callback != nil { |
| | callback(req) |
| | } |
| | if resp != nil { |
| | req.SetResult(resp) |
| | } |
| | var e Error |
| | req.SetError(&e) |
| | res, err := req.Execute(method, url) |
| | if err != nil { |
| | return nil, err |
| | } |
| | if e.Error.Code != 0 { |
| | if e.Error.Code == 401 { |
| | err = d.refreshToken() |
| | if err != nil { |
| | return nil, err |
| | } |
| | return d.request(url, method, callback, resp) |
| | } |
| | return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors) |
| | } |
| | return res.Body(), nil |
| | } |
| |
|
| | func (d *GoogleDrive) getFiles(id string) ([]File, error) { |
| | pageToken := "first" |
| | res := make([]File, 0) |
| | for pageToken != "" { |
| | if pageToken == "first" { |
| | pageToken = "" |
| | } |
| | var resp Files |
| | orderBy := "folder,name,modifiedTime desc" |
| | if d.OrderBy != "" { |
| | orderBy = d.OrderBy + " " + d.OrderDirection |
| | } |
| | query := map[string]string{ |
| | "orderBy": orderBy, |
| | "fields": "files(id,name,mimeType,size,modifiedTime,createdTime,thumbnailLink,shortcutDetails,md5Checksum,sha1Checksum,sha256Checksum),nextPageToken", |
| | "pageSize": "1000", |
| | "q": fmt.Sprintf("'%s' in parents and trashed = false", id), |
| | |
| | |
| | "pageToken": pageToken, |
| | } |
| | _, err := d.request("https://www.googleapis.com/drive/v3/files", http.MethodGet, func(req *resty.Request) { |
| | req.SetQueryParams(query) |
| | }, &resp) |
| | if err != nil { |
| | return nil, err |
| | } |
| | pageToken = resp.NextPageToken |
| | res = append(res, resp.Files...) |
| | } |
| | return res, nil |
| | } |
| |
|
| | func (d *GoogleDrive) chunkUpload(ctx context.Context, stream model.FileStreamer, url string) error { |
| | var defaultChunkSize = d.ChunkSize * 1024 * 1024 |
| | var offset int64 = 0 |
| | for offset < stream.GetSize() { |
| | if utils.IsCanceled(ctx) { |
| | return ctx.Err() |
| | } |
| | chunkSize := stream.GetSize() - offset |
| | if chunkSize > defaultChunkSize { |
| | chunkSize = defaultChunkSize |
| | } |
| | reader, err := stream.RangeRead(http_range.Range{Start: offset, Length: chunkSize}) |
| | if err != nil { |
| | return err |
| | } |
| | _, err = d.request(url, http.MethodPut, func(req *resty.Request) { |
| | req.SetHeaders(map[string]string{ |
| | "Content-Length": strconv.FormatInt(chunkSize, 10), |
| | "Content-Range": fmt.Sprintf("bytes %d-%d/%d", offset, offset+chunkSize-1, stream.GetSize()), |
| | }).SetBody(reader).SetContext(ctx) |
| | }, nil) |
| | if err != nil { |
| | return err |
| | } |
| | offset += chunkSize |
| | } |
| | return nil |
| | } |
| |
|