Home Download Docs Code Community
     1	/*
     2	Copyright 2013 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	// Package service translates blobserver.Storage methods
    18	// into Google Drive API methods.
    19	package service // import "perkeep.org/pkg/blobserver/google/drive/service"
    20	
    21	import (
    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	
    33	const (
    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.
    40	type 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.
    50	func 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.
    65	func (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.
    81	func (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.
   101	func (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	
   120	var errNoDownload = errors.New("file can not be downloaded directly (conversion needed?)")
   121	
   122	// Fetch retrieves the metadata and contents of a file.
   123	func (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.
   156	func (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.
   165	func (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	}
Website layout inspired by memcached.
Content by the authors.