%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /home/waritko/go/src/github.com/odeke-em/drive/src/
Upload File :
Create Path :
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)
}

Zerion Mini Shell 1.0