diff --git a/README.md b/README.md index 50099a2e..faa89c59 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 | diff --git a/cmd/cmd.go b/cmd/cmd.go index 180ec8dd..320a040e 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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" @@ -169,6 +169,12 @@ var globalFlags = []cli.Flag{ Value: "", EnvVar: "GDRIVE_CLIENT_JSON_FILEPATH", }, + cli.StringFlag{ + Name: "gdrive-auth-type", + Usage: "oauth2|service_account", + Value: "", + EnvVar: "GDRIVE_AUTH_TYPE", + }, cli.StringFlag{ Name: "gdrive-local-config-path", Usage: "", @@ -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)) diff --git a/server/storage/gdrive.go b/server/storage/gdrive.go index 61e9f48c..e72ed9e1 100644 --- a/server/storage/gdrive.go +++ b/server/storage/gdrive.go @@ -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() @@ -45,20 +44,40 @@ 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 } @@ -66,36 +85,17 @@ func NewGDriveStorage(clientJSONFilepath string, localConfigPath string, basedir 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 { + 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 { @@ -103,7 +103,7 @@ func (s *GDrive) hasChecksum(f *drive.File) bool { } 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) { @@ -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 } @@ -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 } @@ -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 } @@ -227,7 +227,7 @@ 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) @@ -235,7 +235,7 @@ func (s *GDrive) Delete(ctx context.Context, token string, filename string) (err return } - err = s.service.Files.Delete(fileID).Context(ctx).Do() + err = s.service.Files.Delete(fileID).Context(ctx).SupportsAllDrives(true).Do() return } @@ -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 } @@ -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 } @@ -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 @@ -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 { @@ -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)