1/* 2Copyright 2013 The Perkeep Authors 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17// Package service translates blobserver.Storage methods 18// into Google Drive API methods. 19package service // import "perkeep.org/pkg/blobserver/google/drive/service" 20 21import ( 22 "context" 23 "errors" 24 "fmt" 25 "io" 26 "math" 27 "net/http" 28 "os" 29 30 client "google.golang.org/api/drive/v2" 31) 32 33const ( 34 MimeTypeDriveFolder = "application/vnd.google-apps.folder" 35 MimeTypeCamliBlob = "application/vnd.camlistore.blob" 36) 37 38// DriveService wraps Google Drive API to implement utility methods to 39// be performed on the root Drive destination folder. 40type DriveService struct { 41 client *http.Client 42 apiservice *client.Service 43 parentID string 44} 45 46// New initiates a new DriveService. parentID is the ID of the directory 47// that will be used as the current directory in methods on the returned 48// DriveService (such as Get). If empty, it defaults to the root of the 49// drive. 50func New(oauthClient *http.Client, parentID string) (*DriveService, error) { 51 apiservice, err := client.New(oauthClient) 52 if err != nil { 53 return nil, err 54 } 55 if parentID == "" { 56 // because "root" is known as a special alias for the root directory in drive. 57 parentID = "root" 58 } 59 service := &DriveService{client: oauthClient, apiservice: apiservice, parentID: parentID} 60 return service, err 61} 62 63// Get retrieves a file with its title equal to the provided title and a child of 64// the parentID as given to New. If not found, os.ErrNotExist is returned. 65func (s *DriveService) Get(ctx context.Context, title string) (*client.File, error) { 66 // TODO: use field selectors 67 query := fmt.Sprintf("'%s' in parents and title = '%s'", s.parentID, title) 68 req := s.apiservice.Files.List().Context(ctx).Q(query) 69 files, err := req.Do() 70 if err != nil { 71 return nil, err 72 } 73 if len(files.Items) < 1 { 74 return nil, os.ErrNotExist 75 } 76 return files.Items[0], nil 77} 78 79// List returns a list of files. When limit is greater than zero a paginated list is returned 80// using the next response as a pageToken in subsequent calls. 81func (s *DriveService) List(pageToken string, limit int) (files []*client.File, next string, err error) { 82 req := s.apiservice.Files.List() 83 req.Q(fmt.Sprintf("'%s' in parents and mimeType != '%s'", s.parentID, MimeTypeDriveFolder)) 84 85 if pageToken != "" { 86 req.PageToken(pageToken) 87 } 88 89 if limit > 0 { 90 req.MaxResults(int64(limit)) 91 } 92 93 result, err := req.Do() 94 if err != nil { 95 return 96 } 97 return result.Items, result.NextPageToken, err 98} 99 100// Upsert inserts a file, or updates if such a file exists. 101func (s *DriveService) Upsert(ctx context.Context, title string, data io.Reader) (file *client.File, err error) { 102 if file, err = s.Get(ctx, title); err != nil { 103 if !os.IsNotExist(err) { 104 return 105 } 106 } 107 if file == nil { 108 file = &client.File{Title: title} 109 file.Parents = []*client.ParentReference{ 110 {Id: s.parentID}, 111 } 112 file.MimeType = MimeTypeCamliBlob 113 return s.apiservice.Files.Insert(file).Media(data).Context(ctx).Do() 114 } 115 116 // TODO: handle large blobs 117 return s.apiservice.Files.Update(file.Id, file).Media(data).Context(ctx).Do() 118} 119 120var errNoDownload = errors.New("file can not be downloaded directly (conversion needed?)") 121 122// Fetch retrieves the metadata and contents of a file. 123func (s *DriveService) Fetch(ctx context.Context, title string) (body io.ReadCloser, size uint32, err error) { 124 file, err := s.Get(ctx, title) 125 if err != nil { 126 return 127 } 128 // TODO: maybe in the case of no download link, remove the file. 129 // The file should have malformed or converted to a Docs file 130 // unwantedly. 131 // TODO(mpl): I do not think the above comment is accurate. It 132 // looks like at least one case we do not get a DownloadUrl is when 133 // the UI would make you pick a conversion format first (spreadsheet, 134 // doc, etc). -> we should see if the API offers the possibility to do 135 // that conversion. and we could pass the type(s) we want (pdf, xls, doc...) 136 // as arguments (in an options struct) to Fetch. 137 if file.DownloadUrl == "" { 138 err = errNoDownload 139 return 140 } 141 142 req, _ := http.NewRequest("GET", file.DownloadUrl, nil) 143 req.WithContext(ctx) 144 var resp *http.Response 145 if resp, err = s.client.Transport.RoundTrip(req); err != nil { 146 return 147 } 148 if file.FileSize > math.MaxUint32 || file.FileSize < 0 { 149 err = errors.New("file too big") 150 } 151 return resp.Body, uint32(file.FileSize), err 152} 153 154// Stat retrieves file metadata and returns 155// file size. Returns error if file is not found. 156func (s *DriveService) Stat(ctx context.Context, title string) (int64, error) { 157 file, err := s.Get(ctx, title) 158 if err != nil || file == nil { 159 return 0, err 160 } 161 return file.FileSize, err 162} 163 164// Trash trashes the file with the given title. 165func (s *DriveService) Trash(ctx context.Context, title string) error { 166 file, err := s.Get(ctx, title) 167 if err != nil { 168 if os.IsNotExist(err) { 169 return nil 170 } 171 return err 172 } 173 _, err = s.apiservice.Files.Trash(file.Id).Context(ctx).Do() 174 return err 175} 176