%PDF- %PDF-
Direktori : /home/waritko/go/src/github.com/odeke-em/drive/src/ |
Current File : //home/waritko/go/src/github.com/odeke-em/drive/src/remote.go |
// Copyright 2013 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package drive import ( "fmt" "io" "math/rand" "net/http" "net/url" "os" "strings" "time" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" "github.com/odeke-em/drive/config" "github.com/odeke-em/statos" expb "github.com/odeke-em/exponential-backoff" drive "google.golang.org/api/drive/v2" "google.golang.org/api/googleapi" ) const ( // OAuth 2.0 OOB redirect URL for authorization. RedirectURL = "urn:ietf:wg:oauth:2.0:oob" // OAuth 2.0 full Drive scope used for authorization. DriveScope = "https://www.googleapis.com/auth/drive" // OAuth 2.0 access type for offline/refresh access. AccessType = "offline" // Google Drive webpage host DriveResourceHostURL = "https://googledrive.com/host/" // Google Drive entry point DriveResourceEntryURL = "https://drive.google.com" DriveRemoteSep = "/" ) const ( OptNone = 1 << iota OptConvert OptOCR OptUpdateViewedDate OptContentAsIndexableText OptPinned OptNewRevision ) var ( ErrPathNotExists = nonExistantRemoteErr(fmt.Errorf("remote path doesn't exist")) ErrNetLookup = netLookupFailedErr(fmt.Errorf("net lookup failed")) ErrClashesDetected = clashesDetectedErr(fmt.Errorf("clashes detected. Use `%s` to override this behavior or `%s` to try fixing this", CLIOptionIgnoreNameClashes, CLIOptionFixClashesKey)) ErrClashFixingAborted = clashFixingAbortedErr(fmt.Errorf("clash fixing aborted")) ErrGoogleAPIInvalidQueryHardCoded = invalidGoogleAPIQueryErr(fmt.Errorf("GoogleAPI: Error 400: Invalid query, invalid")) errNilParent = nonExistantRemoteErr(fmt.Errorf("remote parent doesn't exist")) ) var ( UnescapedPathSep = fmt.Sprintf("%c", os.PathSeparator) EscapedPathSep = url.QueryEscape(UnescapedPathSep) ) func errCannotMkdirAll(p string) error { return mkdirFailedErr(fmt.Errorf("cannot mkdirAll: `%s`", p)) } type Remote struct { client *http.Client service *drive.Service encrypter func(io.Reader) (io.Reader, error) decrypter func(io.Reader) (io.ReadCloser, error) progressChan chan int } // NewRemoteContextFromServiceAccount returns a remote initialized // with credentials from a Google Service Account. // For more information about these accounts, see: // https://developers.google.com/identity/protocols/OAuth2ServiceAccount // https://developers.google.com/accounts/docs/application-default-credentials // // You'll also need to configure access to Google Drive. func NewRemoteContextFromServiceAccount(jwtConfig *jwt.Config) (*Remote, error) { client := jwtConfig.Client(context.Background()) return remoteFromClient(client) } func NewRemoteContext(context *config.Context) (*Remote, error) { client := newOAuthClient(context) return remoteFromClient(client) } func remoteFromClient(client *http.Client) (*Remote, error) { service, err := drive.New(client) if err != nil { return nil, err } progressChan := make(chan int) rem := &Remote{ progressChan: progressChan, service: service, client: client, } return rem, nil } func hasExportLinks(f *File) bool { if f == nil || f.IsDir { return false } return len(f.ExportLinks) >= 1 } func (r *Remote) changes(startChangeId int64) (chan *drive.Change, error) { req := r.service.Changes.List() if startChangeId >= 0 { req = req.StartChangeId(startChangeId) } changeChan := make(chan *drive.Change) go func() { pageToken := "" for { if pageToken != "" { req = req.PageToken(pageToken) } res, err := req.Do() if err != nil { break } for _, chItem := range res.Items { changeChan <- chItem } pageToken = res.NextPageToken if pageToken == "" { break } } close(changeChan) }() return changeChan, nil } func buildExpression(parentId string, typeMask int, inTrash bool) string { var exprBuilder []string exprBuilder = append(exprBuilder, fmt.Sprintf("'%s' in parents and trashed=%t", parentId, inTrash)) // Folder and NonFolder are mutually exclusive. if (typeMask & Folder) != 0 { exprBuilder = append(exprBuilder, fmt.Sprintf("mimeType = '%s'", DriveFolderMimeType)) } return strings.Join(exprBuilder, " and ") } func (r *Remote) change(changeId string) (*drive.Change, error) { return r.service.Changes.Get(changeId).Do() } func RetrieveRefreshToken(ctx context.Context, context *config.Context) (string, error) { config := newAuthConfig(context) randState := fmt.Sprintf("%s%v", time.Now(), rand.Uint32()) url := config.AuthCodeURL(randState, oauth2.AccessTypeOffline) fmt.Printf("Visit this URL to get an authorization code\n%s\n", url) code := prompt(os.Stdin, os.Stdout, "Paste the authorization code: ") token, err := config.Exchange(ctx, code) if err != nil { return "", err } return token.RefreshToken, nil } func (r *Remote) FindBackPaths(id string) (backPaths []string, err error) { f, fErr := r.FindById(id) if fErr != nil { err = fErr return } relPath := sepJoin(DriveRemoteSep, f.Name) if rootLike(relPath) { relPath = DriveRemoteSep } if len(f.Parents) < 1 { backPaths = append(backPaths, relPath) return } for _, p := range f.Parents { if p == nil { continue } if p.IsRoot { backPaths = append(backPaths, sepJoin(DriveRemoteSep, relPath)) continue } fullSubPaths, pErr := r.FindBackPaths(p.Id) if pErr != nil { continue } for _, subPath := range fullSubPaths { backPaths = append(backPaths, sepJoin(DriveRemoteSep, subPath, relPath)) } } return } func wrapInPaginationPair(f *File, err error) *paginationPair { fChan := make(chan *File) errsChan := make(chan error) go func() { defer close(fChan) fChan <- f }() go func() { defer close(errsChan) errsChan <- err }() return &paginationPair{errsChan: errsChan, filesChan: fChan} } func (r *Remote) FindByIdM(id string) *paginationPair { f, err := r.FindById(id) return wrapInPaginationPair(f, err) } func (r *Remote) FindById(id string) (*File, error) { req := r.service.Files.Get(id) f, err := req.Do() if err != nil { return nil, err } return NewRemoteFile(f), nil } func retryableChangeOp(fn func() (interface{}, error), debug bool, retryCount int) *expb.ExponentialBacker { if retryCount < 0 { retryCount = MaxFailedRetryCount } return &expb.ExponentialBacker{ Do: fn, Debug: debug, RetryCount: uint32(retryCount), StatusCheck: retryableErrorCheck, } } func (r *Remote) findByPathM(p string, trashed bool) *paginationPair { if rootLike(p) { return r.FindByIdM("root") } parts := strings.Split(p, RemoteSeparator) finder := r.findByPathRecvM if trashed { finder = r.findByPathTrashedM } return finder("root", parts[1:]) } func (r *Remote) findByPath(p string, trashed bool) (*File, error) { if rootLike(p) { return r.FindById("root") } parts := strings.Split(p, "/") finder := r.findByPathRecv if trashed { finder = r.findByPathTrashed } return finder("root", parts[1:]) } func (r *Remote) FindByPath(p string) (*File, error) { return r.findByPath(p, false) } func (r *Remote) FindByPathM(p string) *paginationPair { return r.findByPathM(p, false) } func (r *Remote) FindByPathTrashed(p string) (*File, error) { return r.findByPath(p, true) } func (r *Remote) FindByPathTrashedM(p string) *paginationPair { return r.findByPathM(p, true) } func reqDoPage(req *drive.FilesListCall, hidden bool, promptOnPagination bool) *paginationPair { return _reqDoPage(req, hidden, promptOnPagination, false) } type paginationPair struct { errsChan chan error filesChan chan *File } func _reqDoPage(req *drive.FilesListCall, hidden bool, promptOnPagination, nilOnNoMatch bool) *paginationPair { filesChan := make(chan *File) errsChan := make(chan error) throttle := time.Tick(1e8) go func() { defer func() { close(errsChan) close(filesChan) }() pageToken := "" for pageIterCount := uint64(0); ; pageIterCount++ { if pageToken != "" { req = req.PageToken(pageToken) } results, err := req.Do() if err != nil { errsChan <- err break } iterCount := uint64(0) for _, f := range results.Items { if isHidden(f.Title, hidden) { // ignore hidden files continue } iterCount += 1 filesChan <- NewRemoteFile(f) } pageToken = results.NextPageToken if pageToken == "" { if nilOnNoMatch && len(results.Items) < 1 && pageIterCount < 1 { // Item absolutely doesn't exist filesChan <- nil } break } <-throttle if iterCount < 1 { continue } if promptOnPagination && !nextPage() { filesChan <- nil break } } }() return &paginationPair{filesChan: filesChan, errsChan: errsChan} } func (r *Remote) findByParentIdRaw(parentId string, trashed, hidden bool) *paginationPair { req := r.service.Files.List() req.Q(fmt.Sprintf("%s in parents and trashed=%v", customQuote(parentId), trashed)) return reqDoPage(req, hidden, false) } func (r *Remote) FindByParentId(parentId string, hidden bool) *paginationPair { return r.findByParentIdRaw(parentId, false, hidden) } func (r *Remote) FindByParentIdTrashed(parentId string, hidden bool) *paginationPair { return r.findByParentIdRaw(parentId, true, hidden) } func (r *Remote) EmptyTrash() error { return r.service.Files.EmptyTrash().Do() } func (r *Remote) Trash(id string) error { _, err := r.service.Files.Trash(id).Do() return err } func (r *Remote) Untrash(id string) error { _, err := r.service.Files.Untrash(id).Do() return err } func (r *Remote) Delete(id string) error { return r.service.Files.Delete(id).Do() } func (r *Remote) idForEmail(email string) (string, error) { perm, err := r.service.Permissions.GetIdForEmail(email).Do() if err != nil { return "", err } return perm.Id, nil } func (r *Remote) listPermissions(id string) ([]*drive.Permission, error) { res, err := r.service.Permissions.List(id).Do() if err != nil { return nil, err } return res.Items, nil } func (r *Remote) insertPermissions(permInfo *permission) (*drive.Permission, error) { perm := &drive.Permission{ Role: permInfo.role.String(), Type: permInfo.accountType.String(), WithLink: permInfo.withLink, } if permInfo.value != "" { perm.Value = permInfo.value } req := r.service.Permissions.Insert(permInfo.fileId, perm) if permInfo.message != "" { req = req.EmailMessage(permInfo.message) } req = req.SendNotificationEmails(permInfo.notify) return req.Do() } func (r *Remote) revokePermissions(p *permission) (err error) { foundPermissionsChan, fErr := r.findPermissions(p) if fErr != nil { return fErr } successes := 0 for perm := range foundPermissionsChan { if perm == nil { continue } req := r.service.Permissions.Delete(p.fileId, perm.Id) if delErr := req.Do(); delErr != nil { err = reComposeError(err, fmt.Sprintf("err: %v fileId: %s permissionId %s", delErr, p.fileId, perm.Id)) } else { successes += 1 } } if err != nil { return err } if successes < 1 { err = noMatchesFoundErr(fmt.Errorf("no matches found!")) } return err } func stringifyPermissionForMatch(p *permission) string { // As of "Fri Nov 20 19:06:18 MST 2015", Google Drive v2 API doesn't support a PermissionsList.Q(query) // call hence the hack around will be to compare all the permissions returned to those being queried for queries := []string{} if role := p.role.String(); !unknownRole(role) { queries = append(queries, role) } if accountType := p.accountType.String(); !unknownAccountType(accountType) { queries = append(queries, accountType) } if p.value != "" { queries = append(queries, p.value) } return sepJoin("", preprocessBeforePermissionMatch(queries)...) } func preprocessBeforePermissionMatch(args []string) (preprocessed []string) { for _, arg := range args { preprocessed = append(preprocessed, strings.ToLower(strings.TrimSpace(arg))) } return preprocessed } func stringifyDrivePermissionForMatch(p *drive.Permission) string { // As of "Fri Nov 20 19:06:18 MST 2015", Google Drive v2 API doesn't support a PermissionsList.Q(query) // call hence the hack around will be to compare all the permissions returned to those being queried for params := []string{p.Role, p.Type, p.EmailAddress} repr := []string{} for _, param := range params { repr = append(repr, strings.ToLower(param)) } return sepJoin("", preprocessBeforePermissionMatch(repr)...) } func (r *Remote) findPermissions(pquery *permission) (permChan chan *drive.Permission, err error) { permChan = make(chan *drive.Permission) go func() { defer close(permChan) req := r.service.Permissions.List(pquery.fileId) results, err := req.Do() if err != nil { fmt.Println(err) return } if results == nil { return } requiredSignature := stringifyPermissionForMatch(pquery) // fmt.Println("requiredSignature", requiredSignature) for _, perm := range results.Items { if perm == nil { continue } if stringifyDrivePermissionForMatch(perm) == requiredSignature { // fmt.Println("perm", perm) permChan <- perm } } }() return } func (r *Remote) deletePermissions(id string, accountType AccountType) error { return r.service.Permissions.Delete(id, accountType.String()).Do() } func (r *Remote) Unpublish(id string) error { return r.deletePermissions(id, Anyone) } func (r *Remote) Publish(id string) (string, error) { _, err := r.insertPermissions(&permission{ fileId: id, value: "", role: Reader, accountType: Anyone, }) if err != nil { return "", err } return DriveResourceHostURL + id, nil } func urlToPath(p string, fsBound bool) string { if fsBound { return strings.Replace(p, UnescapedPathSep, EscapedPathSep, -1) } return strings.Replace(p, EscapedPathSep, UnescapedPathSep, -1) } func (r *Remote) Download(id string, exportURL string) (io.ReadCloser, error) { var url string var body io.ReadCloser var resp *http.Response var err error if len(exportURL) < 1 { resp, err = r.service.Files.Get(id).Download() } else { resp, err = r.client.Get(exportURL) } if err == nil { if resp == nil { err = illogicalStateErr(fmt.Errorf("bug on: download for url \"%s\". resp and err are both nil", url)) } else if httpOk(resp.StatusCode) { // TODO: Handle other statusCodes e.g redirects? body = resp.Body } else { err = downloadFailedErr(fmt.Errorf("download: failed for url \"%s\". StatusCode: %v", url, resp.StatusCode)) } } if r.decrypter != nil && body != nil { decR, err := r.decrypter(body) _ = body.Close() if err != nil { return nil, err } body = decR } return body, err } func (r *Remote) Touch(id string) (*File, error) { f, err := r.service.Files.Touch(id).Do() if err != nil { return nil, err } if f == nil { return nil, ErrPathNotExists } return NewRemoteFile(f), err } // SetModTime is an explicit command to just set the modification // time of a remote file. It serves the purpose of Touch but with // a custom time instead of the time on the remote server. // See Issue https://github.com/odeke-em/drive/issues/726. func (r *Remote) SetModTime(fileId string, modTime time.Time) (*File, error) { repr := &drive.File{} // Ensure that the ModifiedDate is retrieved from local repr.ModifiedDate = toUTCString(modTime) req := r.service.Files.Update(fileId, repr) // We always want it to match up with the local time req.SetModifiedDate(true) retrieved, err := req.Do() if err != nil { return nil, err } return NewRemoteFile(retrieved), nil } func toUTCString(t time.Time) string { utc := t.UTC().Round(time.Second) // Ugly but straight forward formatting as time.Parse is such a prima donna return fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.000Z", utc.Year(), utc.Month(), utc.Day(), utc.Hour(), utc.Minute(), utc.Second()) } func convert(mask int) bool { return (mask & OptConvert) != 0 } func ocr(mask int) bool { return (mask & OptOCR) != 0 } func pin(mask int) bool { return (mask & OptPinned) != 0 } func indexContent(mask int) bool { return (mask & OptContentAsIndexableText) != 0 } type upsertOpt struct { debug bool parentId string fsAbsPath string relToRootPath string src *File dest *File mask int ignoreChecksum bool mimeKey string nonStatable bool retryCount int uploadChunkSize int } func togglePropertiesInsertCall(req *drive.FilesInsertCall, mask int) *drive.FilesInsertCall { // TODO: if ocr toggled respect the quota limits if ocr is enabled. if ocr(mask) { req = req.Ocr(true) } if convert(mask) { req = req.Convert(true) } if pin(mask) { req = req.Pinned(true) } if indexContent(mask) { req = req.UseContentAsIndexableText(true) } return req } func togglePropertiesUpdateCall(req *drive.FilesUpdateCall, mask int) *drive.FilesUpdateCall { // TODO: if ocr toggled respect the quota limits if ocr is enabled. if ocr(mask) { req = req.Ocr(true) } if convert(mask) { req = req.Convert(true) } if pin(mask) { req = req.Pinned(true) } if indexContent(mask) { req = req.UseContentAsIndexableText(true) } return req } // shouldUploadBody tells whether a body of content should // be uploaded. It rejects local directories. // It returns true if the content is nonStatable e.g piped input // or if the destination on the cloud is nil // and also if there are checksum differences. // For other changes such as modTime only varying, we can // just change the modTime on the cloud as an operation of its own. func (args *upsertOpt) shouldUploadBody() bool { if args.src.IsDir { return false } if args.dest == nil || args.nonStatable { return true } mask := fileDifferences(args.src, args.dest, args.ignoreChecksum) return checksumDiffers(mask) } func (r *Remote) upsertByComparison(body io.Reader, args *upsertOpt) (f *File, mediaInserted bool, err error) { uploaded := &drive.File{ // Must ensure that the path is prepared for a URL upload Title: urlToPath(args.src.Name, false), Parents: []*drive.ParentReference{&drive.ParentReference{Id: args.parentId}}, } if args.src.IsDir { uploaded.MimeType = DriveFolderMimeType } if r.encrypter != nil && body != nil { encR, encErr := r.encrypter(body) if encErr != nil { err = encErr return } body = encR } if args.src.MimeType != "" { uploaded.MimeType = args.src.MimeType } if args.mimeKey != "" { uploaded.MimeType = guessMimeType(args.mimeKey) } // Ensure that the ModifiedDate is retrieved from local uploaded.ModifiedDate = toUTCString(args.src.ModTime) var mediaOptions []googleapi.MediaOption if args.uploadChunkSize > 0 { mediaOptions = append(mediaOptions, googleapi.ChunkSize(args.uploadChunkSize)) } if args.src.Id == "" { req := r.service.Files.Insert(uploaded) if !args.src.IsDir && body != nil { req = req.Media(body, mediaOptions...) mediaInserted = true } // Toggle the respective properties req = togglePropertiesInsertCall(req, args.mask) if uploaded, err = req.Do(); err != nil { return } f = NewRemoteFile(uploaded) return } // update the existing req := r.service.Files.Update(args.src.Id, uploaded) // We always want it to match up with the local time req.SetModifiedDate(true) if args.shouldUploadBody() { req = req.Media(body, mediaOptions...) mediaInserted = true } // Next toggle the appropriate properties req = togglePropertiesUpdateCall(req, args.mask) if uploaded, err = req.Do(); err != nil { return } f = NewRemoteFile(uploaded) return } func (r *Remote) byFileIdUpdater(fileId string, f *drive.File) (*File, error) { req := r.service.Files.Update(fileId, f) uploaded, err := req.Do() if err != nil { return nil, err } return NewRemoteFile(uploaded), nil } func (r *Remote) rename(fileId, newTitle string) (*File, error) { f := &drive.File{ Title: newTitle, } return r.byFileIdUpdater(fileId, f) } func (r *Remote) updateDescription(fileId, newDescription string) (*File, error) { f := &drive.File{ Description: newDescription, } return r.byFileIdUpdater(fileId, f) } func (r *Remote) updateStarred(fileId string, star bool) (*File, error) { f := &drive.File{ Labels: &drive.FileLabels{ Starred: star, // Since "Starred" is a non-pointer value, we'll need to // unconditionally send it with API-requests using "ForceSendFields" ForceSendFields: []string{"Starred"}, }, } return r.byFileIdUpdater(fileId, f) } func (r *Remote) removeParent(fileId, parentId string) error { return r.service.Parents.Delete(fileId, parentId).Do() } func (r *Remote) insertParent(fileId, parentId string) error { parent := &drive.ParentReference{Id: parentId} _, err := r.service.Parents.Insert(fileId, parent).Do() return err } func (r *Remote) copy(newName, parentId string, srcFile *File) (*File, error) { f := &drive.File{ Title: urlToPath(newName, false), ModifiedDate: toUTCString(srcFile.ModTime), } if parentId != "" { f.Parents = []*drive.ParentReference{&drive.ParentReference{Id: parentId}} } copied, err := r.service.Files.Copy(srcFile.Id, f).Do() if err != nil { return nil, err } return NewRemoteFile(copied), nil } func (r *Remote) UpsertByComparison(args *upsertOpt) (f *File, err error) { /* // TODO: (@odeke-em) decide: // + if to reject FIFO // + perform an assertion for fileStated.IsDir() == args.src.IsDir */ if args.src == nil { err = illogicalStateErr(fmt.Errorf("bug on: src cannot be nil")) return } var body io.Reader var cleanUp func() error if !args.src.IsDir { // In relation to issue #612, since we are not only resolving // relative to the current working directory, we should try reading // first from the source's original local fsAbsPath aka `BlobAt` // because the resolved path might be different from the original path. fsAbsPath := args.src.BlobAt if fsAbsPath == "" { fsAbsPath = args.fsAbsPath } if args.shouldUploadBody() { file, err := os.Open(fsAbsPath) if err != nil { return nil, err } // We need to make sure that we close all open handles. // See Issue https://github.com/odeke-em/drive/issues/711. cleanUp = file.Close body = file } } bd := statos.NewReader(body) go func() { commChan := bd.ProgressChan() for n := range commChan { r.progressChan <- n } }() resultLoad := make(chan *tuple) go func() { if cleanUp != nil { defer cleanUp() } emitter := func() (interface{}, error) { f, mediaInserted, err := r.upsertByComparison(bd, args) return &tuple{first: f, second: mediaInserted, last: err}, err } retrier := retryableChangeOp(emitter, args.debug, args.retryCount) res, err := expb.ExponentialBackOffSync(retrier) resultLoad <- &tuple{first: res, last: err} }() result := <-resultLoad if result == nil { return f, err } tup, tupOk := result.first.(*tuple) if tupOk { ff, fOk := tup.first.(*File) if fOk { f = ff } mediaInserted, mediaOk := tup.second.(bool) // Case in which for example just Chtime-ing if mediaOk && !mediaInserted && f != nil { chunks := chunkInt64(f.Size) for n := range chunks { r.progressChan <- n } } errV, errCastOk := tup.last.(error) if errCastOk { err = errV } } return f, err } func (r *Remote) findShared(p []string) *paginationPair { req := r.service.Files.List() expr := "sharedWithMe=true" if len(p) >= 1 { expr = fmt.Sprintf("title = '%s' and %s", p[0], expr) } req = req.Q(expr) return reqDoPage(req, false, false) } func (r *Remote) FindByPathShared(p string) *paginationPair { if p == "/" || p == "root" { return r.findShared([]string{}) } parts := strings.Split(p, "/") // TODO: use path.Split instead nonEmpty := func(strList []string) []string { var nEmpty []string for _, p := range strList { if len(p) >= 1 { nEmpty = append(nEmpty, p) } } return nEmpty }(parts) return r.findShared(nonEmpty) } func (r *Remote) FindStarred(trashed, hidden bool) *paginationPair { req := r.service.Files.List() expr := fmt.Sprintf("(starred=true) and (trashed=%v)", trashed) req.Q(expr) return reqDoPage(req, hidden, false) } func (r *Remote) FindMatches(mq *matchQuery) *paginationPair { parent, err := r.FindByPath(mq.dirPath) if err != nil || parent == nil { if parent == nil && err == nil { err = errNilParent } return wrapInPaginationPair(parent, err) } req := r.service.Files.List() parQuery := fmt.Sprintf("(%s in parents)", customQuote(parent.Id)) expr := sepJoinNonEmpty(" and ", parQuery, mq.Stringer()) req.Q(expr) return reqDoPage(req, true, false) } func (r *Remote) findChildren(parentId string, trashed bool) *paginationPair { req := r.service.Files.List() req.Q(fmt.Sprintf("%s in parents and trashed=%v", customQuote(parentId), trashed)) return reqDoPage(req, true, false) } func (r *Remote) About() (*drive.About, error) { return r.service.About.Get().Do() } func (r *Remote) findByPathRecvRawM(parentId string, p []string, trashed bool) *paginationPair { chanOChan := make(chan *paginationPair) resolvedFilesChan := make(chan *File) resolvedErrsChan := make(chan error) go func() { defer close(chanOChan) if len(p) < 1 { return } first, rest := p[0], p[1:] // find the file or directory under parentId and titled with p[0] req := r.service.Files.List() // TODO: use field selectors var expr string head := urlToPath(first, false) if trashed { expr = fmt.Sprintf("title = %s and trashed=true", customQuote(head)) } else { expr = fmt.Sprintf("%s in parents and title = %s and trashed=false", customQuote(parentId), customQuote(head)) } req.Q(expr) pager := _reqDoPage(req, true, false, true) if len(rest) < 1 { chanOChan <- pager return } resultsChan := pager.filesChan errsChan := pager.errsChan working := true for working { select { case err := <-errsChan: if err != nil { chanOChan <- wrapInPaginationPair(nil, err) } case f, stillHasContent := <-resultsChan: if !stillHasContent { working = false break } if f != nil { chanOChan <- r.findByPathRecvRawM(f.Id, rest, trashed) } else { // Ensure that we properly send // back nil even if files were not found. // See https://github.com/odeke-em/drive/issues/933. chanOChan <- wrapInPaginationPair(nil, nil) } } } }() go func() { defer func() { close(resolvedFilesChan) close(resolvedErrsChan) }() for curPagePair := range chanOChan { if curPagePair == nil { continue } errsChan := curPagePair.errsChan filesChan := curPagePair.filesChan working := true for working { select { case err := <-errsChan: if err != nil { resolvedErrsChan <- err } case f, stillHasContent := <-filesChan: if !stillHasContent { working = false break } resolvedFilesChan <- f } } } }() return &paginationPair{errsChan: resolvedErrsChan, filesChan: resolvedFilesChan} } func (r *Remote) findByPathRecvRaw(parentId string, p []string, trashed bool) (*File, error) { // find the file or directory under parentId and titled with p[0] req := r.service.Files.List() // TODO: use field selectors var expr string head := urlToPath(p[0], false) if trashed { expr = fmt.Sprintf("title = %s and trashed=true", customQuote(head)) } else { expr = fmt.Sprintf("%s in parents and title = %s and trashed=false", customQuote(parentId), customQuote(head)) } req.Q(expr) // We only need the head file since we expect only one File to be created req.MaxResults(1) files, err := req.Do() if err != nil { if err.Error() == ErrGoogleAPIInvalidQueryHardCoded.Error() { // Send the user back the query information err = invalidGoogleAPIQueryErr(fmt.Errorf("err: %v query: `%s`", err, expr)) } return nil, err } if files == nil || len(files.Items) < 1 { return nil, ErrPathNotExists } first := files.Items[0] if len(p) == 1 { return NewRemoteFile(first), nil } return r.findByPathRecvRaw(first.Id, p[1:], trashed) } func (r *Remote) findByPathRecv(parentId string, p []string) (*File, error) { return r.findByPathRecvRaw(parentId, p, false) } func (r *Remote) findByPathRecvM(parentId string, p []string) *paginationPair { return r.findByPathRecvRawM(parentId, p, false) } func (r *Remote) findByPathTrashedM(parentId string, p []string) *paginationPair { return r.findByPathRecvRawM(parentId, p, true) } func (r *Remote) findByPathTrashed(parentId string, p []string) (*File, error) { return r.findByPathRecvRaw(parentId, p, true) } func newAuthConfig(context *config.Context) *oauth2.Config { return &oauth2.Config{ ClientID: context.ClientId, ClientSecret: context.ClientSecret, RedirectURL: RedirectURL, Endpoint: google.Endpoint, Scopes: []string{DriveScope}, } } func newOAuthClient(configContext *config.Context) *http.Client { config := newAuthConfig(configContext) token := oauth2.Token{ RefreshToken: configContext.RefreshToken, Expiry: time.Now().Add(1 * time.Hour), } return config.Client(context.Background(), &token) }