Skip to content

GDrive: Fix fieldNotWritable, Add SupportSharedDrive, Add service_account method #512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ proxy-path | path prefix when service is run behind a proxy | | PROXY_PATH |
proxy-port | port of the proxy when the service is run behind a proxy | | PROXY_PORT |
email-contact | email contact for the front end | | EMAIL_CONTACT |
ga-key | google analytics key for the front end | | GA_KEY |
provider | which storage provider to use | (s3, storj, gdrive or local) |
provider | which storage provider to use | (s3, storj, gdrive or local) | PROVIDER |
uservoice-key | user voice key for the front end | | USERVOICE_KEY |
aws-access-key | aws access key | | AWS_ACCESS_KEY |
aws-secret-key | aws access key | | AWS_SECRET_KEY |
Expand All @@ -107,9 +107,10 @@ storj-bucket | Bucket to use within the project | | STORJ_BUCKET |
basedir | path storage for local/gdrive provider | | BASEDIR |
gdrive-client-json-filepath | path to oauth client json config for gdrive provider | | GDRIVE_CLIENT_JSON_FILEPATH |
gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider| | GDRIVE_LOCAL_CONFIG_PATH |
gdrive-auth-type | which auth type to use for gdrive provider | (oauth or service_account) | GDRIVE_AUTH_TYPE |
gdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB) | | GDRIVE_CHUNK_SIZE |
lets-encrypt-hosts | hosts to use for lets encrypt certificates (comma seperated) | | HOSTS |
log | path to log file| | LOG |
log | path to log file | | LOG |
cors-domains | comma separated list of domains for CORS, setting it enable CORS | | CORS_DOMAINS |
clamav-host | host for clamav feature | | CLAMAV_HOST |
perform-clamav-prescan | prescan every upload through clamav feature (clamav-host must be a local clamd unix socket) | | PERFORM_CLAMAV_PRESCAN |
Expand Down
17 changes: 12 additions & 5 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package cmd

import (
"fmt"
"github.com/dutchcoders/transfer.sh/server/storage"
"log"
"os"
"strings"

"github.com/dutchcoders/transfer.sh/server"
"github.com/dutchcoders/transfer.sh/server/storage"
"github.com/fatih/color"
"github.com/urfave/cli"
"google.golang.org/api/googleapi"
Expand Down Expand Up @@ -169,6 +169,12 @@ var globalFlags = []cli.Flag{
Value: "",
EnvVar: "GDRIVE_CLIENT_JSON_FILEPATH",
},
cli.StringFlag{
Name: "gdrive-auth-type",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this way you pass gdrive-client-json-filepath when it's a gdrive-service-account-filepath indeed

also it's a breaking change, I cannot accept it, sorry :(

you can add gdrive-service-account-filepath, check that only this or gdrive-client-json-filepath is set (and at least one of them), and set the authType value to pass to the factory: https://github.com/dutchcoders/transfer.sh/pull/512/files#diff-8e494a434a8037b6c0b888e25b2baae7618fe65e792d4a155dadd096e9350667R488

please, remember to change the name of the param in the factory to something generic, like gdriveCredentialsFilepath

Usage: "oauth2|service_account",
Value: "",
EnvVar: "GDRIVE_AUTH_TYPE",
},
cli.StringFlag{
Name: "gdrive-local-config-path",
Usage: "",
Expand Down Expand Up @@ -471,14 +477,15 @@ func New() *Cmd {
}
case "gdrive":
chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024
localConfigPath := c.String("gdrive-local-config-path")

if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" {
panic("client-json-filepath not set.")
} else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" {
panic("local-config-path not set.")
panic("gdrive-client-json-filepath not set.")
} else if basedir := c.String("basedir"); basedir == "" {
panic("basedir not set.")
} else if store, err := storage.NewGDriveStorage(clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil {
} else if authType := c.String("gdrive-auth-type"); authType == "" {
panic("gdrive-auth-type not set.")
} else if store, err := storage.NewGDriveStorage(clientJSONFilepath, localConfigPath, basedir, authType, chunkSize, logger); err != nil {
panic(err)
} else {
options = append(options, server.UseStorage(store))
Expand Down
104 changes: 53 additions & 51 deletions server/storage/gdrive.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,17 @@ import (
type GDrive struct {
service *drive.Service
rootID string
basedir string
localConfigPath string
authType string
chunkSize int
logger *log.Logger
}

const gDriveRootConfigFile = "root_id.conf"
const gDriveTokenJSONFile = "token.json"
const gDriveDirectoryMimeType = "application/vnd.google-apps.folder"

// NewGDriveStorage is the factory for GDrive
func NewGDriveStorage(clientJSONFilepath string, localConfigPath string, basedir string, chunkSize int, logger *log.Logger) (*GDrive, error) {
func NewGDriveStorage(clientJSONFilepath string, localConfigPath string, basedir string, authType string, chunkSize int, logger *log.Logger) (*GDrive, error) {

ctx := context.TODO()

Expand All @@ -45,65 +44,66 @@ func NewGDriveStorage(clientJSONFilepath string, localConfigPath string, basedir
}

// If modifying these scopes, delete your previously saved client_secret.json.
config, err := google.ConfigFromJSON(b, drive.DriveScope, drive.DriveMetadataScope)
if err != nil {
return nil, err
}
var httpClient *http.Client

switch authType {
case "service_account": // Using Service Account credentials
logger.Println("GDrive: using Service Account credentials")
config, err := google.JWTConfigFromJSON(b, drive.DriveScope, drive.DriveMetadataScope)
if err != nil {
return nil, err
}
httpClient = config.Client(ctx)

httpClient := getGDriveClient(ctx, config, localConfigPath, logger)
case "oauth2": // Using OAuth2 credentials
if localConfigPath == "" {
return nil, fmt.Errorf("gdrive-local-config-path not set")
}

logger.Println("GDrive: using OAuth2 credentials")
config, err := google.ConfigFromJSON(b, drive.DriveScope, drive.DriveMetadataScope)
if err != nil {
return nil, err
}
httpClient = getGDriveClientFromToken(ctx, config, localConfigPath, logger)

default:
return nil, fmt.Errorf("invalid gdrive-auth-type: %s", authType)
}

srv, err := drive.NewService(ctx, option.WithHTTPClient(httpClient))
if err != nil {
return nil, err
}

storage := &GDrive{service: srv, basedir: basedir, rootID: "", localConfigPath: localConfigPath, chunkSize: chunkSize, logger: logger}
err = storage.setupRoot()
storage := &GDrive{service: srv, rootID: basedir, localConfigPath: localConfigPath, authType: authType, chunkSize: chunkSize, logger: logger}
err = storage.checkRoot()
if err != nil {
return nil, err
}

return storage, nil
}

func (s *GDrive) setupRoot() error {
rootFileConfig := filepath.Join(s.localConfigPath, gDriveRootConfigFile)

rootID, err := ioutil.ReadFile(rootFileConfig)
if err != nil && !os.IsNotExist(err) {
return err
}

if string(rootID) != "" {
s.rootID = string(rootID)
return nil
}

dir := &drive.File{
Name: s.basedir,
MimeType: gDriveDirectoryMimeType,
}

di, err := s.service.Files.Create(dir).Fields("id").Do()
if err != nil {
return err
}

s.rootID = di.Id
err = ioutil.WriteFile(rootFileConfig, []byte(s.rootID), os.FileMode(0600))
if err != nil {
return err
func (s *GDrive) checkRoot() error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a breaking change.

basedir is the name of the folder to be created in the gdrive, rootID is the id of the folder once created and that we save. the two carry two different information.

if we replce rootID value with basedir value:

  1. we don't create the folder
  2. current installations have to change the value that they pass to basedir, to use instead rootID

I guess you meant this change to be the way to select a shared drive, is it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

I changed basedir to insert folder id, to select rootID.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot accept a breaking change, sorry
you have to find another way to provide the sharedfolder to use and the basedir as it was

if s.rootID == "root" {
switch s.authType {
case "service_account":
return fmt.Errorf("GDrive: Folder \"root\" is not available when using Service Account credentials")
case "oauth2":
s.logger.Println("GDrive: Warning: Folder \"root\" is not recommended.")
}
}

return nil
_, err := s.service.Files.Get(s.rootID).SupportsAllDrives(true).Do()
return err
}

func (s *GDrive) hasChecksum(f *drive.File) bool {
return f.Md5Checksum != ""
}

func (s *GDrive) list(nextPageToken string, q string) (*drive.FileList, error) {
return s.service.Files.List().Fields("nextPageToken, files(id, name, mimeType)").Q(q).PageToken(nextPageToken).Do()
return s.service.Files.List().Fields("nextPageToken, files(id, name, mimeType)").Q(q).PageToken(nextPageToken).SupportsAllDrives(true).IncludeItemsFromAllDrives(true).Do()
}

func (s *GDrive) findID(filename string, token string) (string, error) {
Expand Down Expand Up @@ -184,7 +184,7 @@ func (s *GDrive) Head(ctx context.Context, token string, filename string) (conte
}

var fi *drive.File
if fi, err = s.service.Files.Get(fileID).Context(ctx).Fields("size").Do(); err != nil {
if fi, err = s.service.Files.Get(fileID).Context(ctx).Fields("size").SupportsAllDrives(true).Do(); err != nil {
return
}

Expand All @@ -202,7 +202,7 @@ func (s *GDrive) Get(ctx context.Context, token string, filename string) (reader
}

var fi *drive.File
fi, err = s.service.Files.Get(fileID).Fields("size", "md5Checksum").Do()
fi, err = s.service.Files.Get(fileID).Fields("size", "md5Checksum").SupportsAllDrives(true).Do()
if err != nil {
return
}
Expand All @@ -214,7 +214,7 @@ func (s *GDrive) Get(ctx context.Context, token string, filename string) (reader
contentLength = uint64(fi.Size)

var res *http.Response
res, err = s.service.Files.Get(fileID).Context(ctx).Download()
res, err = s.service.Files.Get(fileID).Context(ctx).SupportsAllDrives(true).Download()
if err != nil {
return
}
Expand All @@ -227,15 +227,15 @@ func (s *GDrive) Get(ctx context.Context, token string, filename string) (reader
// Delete removes a file from storage
func (s *GDrive) Delete(ctx context.Context, token string, filename string) (err error) {
metadata, _ := s.findID(fmt.Sprintf("%s.metadata", filename), token)
_ = s.service.Files.Delete(metadata).Do()
_ = s.service.Files.Delete(metadata).SupportsAllDrives(true).Do()

var fileID string
fileID, err = s.findID(filename, token)
if err != nil {
return
}

err = s.service.Files.Delete(fileID).Context(ctx).Do()
err = s.service.Files.Delete(fileID).Context(ctx).SupportsAllDrives(true).Do()
return
}

Expand All @@ -252,7 +252,7 @@ func (s *GDrive) Purge(ctx context.Context, days time.Duration) (err error) {

for 0 < len(l.Files) {
for _, fi := range l.Files {
err = s.service.Files.Delete(fi.Id).Context(ctx).Do()
err = s.service.Files.Delete(fi.Id).Context(ctx).SupportsAllDrives(true).Do()
if err != nil {
return
}
Expand Down Expand Up @@ -296,10 +296,9 @@ func (s *GDrive) Put(ctx context.Context, token string, filename string, reader
Name: token,
Parents: []string{s.rootID},
MimeType: gDriveDirectoryMimeType,
Size: int64(contentLength),
}

di, err := s.service.Files.Create(dir).Fields("id").Do()
di, err := s.service.Files.Create(dir).Fields("id").SupportsAllDrives(true).Do()
if err != nil {
return err
}
Expand All @@ -314,7 +313,7 @@ func (s *GDrive) Put(ctx context.Context, token string, filename string, reader
MimeType: contentType,
}

_, err = s.service.Files.Create(dst).Context(ctx).Media(reader, googleapi.ChunkSize(s.chunkSize)).Do()
_, err = s.service.Files.Create(dst).Context(ctx).Media(reader, googleapi.ChunkSize(s.chunkSize)).SupportsAllDrives(true).Do()

if err != nil {
return err
Expand All @@ -324,7 +323,7 @@ func (s *GDrive) Put(ctx context.Context, token string, filename string, reader
}

// Retrieve a token, saves the token, then returns the generated client.
func getGDriveClient(ctx context.Context, config *oauth2.Config, localConfigPath string, logger *log.Logger) *http.Client {
func getGDriveClientFromToken(ctx context.Context, config *oauth2.Config, localConfigPath string, logger *log.Logger) *http.Client {
tokenFile := filepath.Join(localConfigPath, gDriveTokenJSONFile)
tok, err := gDriveTokenFromFile(tokenFile)
if err != nil {
Expand All @@ -337,10 +336,13 @@ func getGDriveClient(ctx context.Context, config *oauth2.Config, localConfigPath

// Request a token from the web, then returns the retrieved token.
func getGDriveTokenFromWeb(ctx context.Context, config *oauth2.Config, logger *log.Logger) *oauth2.Token {
config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)

fmt.Printf("Go to the following link in your browser then type the "+
"authorization code: \n%v\n", authURL)
"authorization code.\n%v\n", authURL)

fmt.Printf("Authorization code: ")
var authCode string
if _, err := fmt.Scan(&authCode); err != nil {
logger.Fatalf("Unable to read authorization code %v", err)
Expand Down