You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
7.9 KiB
292 lines
7.9 KiB
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/andrew-d/go-termutil"
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
"google.golang.org/api/drive/v3"
|
|
"google.golang.org/api/googleapi"
|
|
"google.golang.org/api/option"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type TokenCache struct {
|
|
CredentialPath string
|
|
CredentialScopes []string
|
|
ConfigPath string
|
|
TokenPath string
|
|
configLoaded bool
|
|
CredentialData *google.Credentials
|
|
ConfigData oauth2.Config
|
|
}
|
|
|
|
func (c *TokenCache) Credentials() (*google.Credentials, error) {
|
|
if c.CredentialData == nil {
|
|
f, err := os.Open(c.CredentialPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not open credentials: %v", err)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
data, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not read credentials: %v", err)
|
|
}
|
|
c.CredentialData, err = google.CredentialsFromJSON(context.Background(), data, c.CredentialScopes...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not decode credentials: %v", err)
|
|
}
|
|
}
|
|
return c.CredentialData, nil
|
|
}
|
|
|
|
func (c *TokenCache) Config() (*oauth2.Config, error) {
|
|
if !c.configLoaded {
|
|
f, err := os.Open(c.ConfigPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not open config: %v", err)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
err = json.NewDecoder(f).Decode(&c.ConfigData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not decode config: %v", err)
|
|
}
|
|
c.configLoaded = true
|
|
}
|
|
return &c.ConfigData, nil
|
|
}
|
|
|
|
func (c *TokenCache) CredentialToken() (*oauth2.Token, error) {
|
|
log.Printf("Getting token from credentials file %s", c.CredentialPath)
|
|
creds, err := c.Credentials()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return creds.TokenSource.Token()
|
|
}
|
|
|
|
func (c *TokenCache) Token() (*oauth2.Token, error) {
|
|
if c.CredentialPath != "" {
|
|
return c.CredentialToken()
|
|
} else {
|
|
return c.StandardToken()
|
|
}
|
|
}
|
|
|
|
// Retrieves a token and saves the token.
|
|
func (c *TokenCache) StandardToken() (*oauth2.Token, error) {
|
|
// The file token.json stores the user's access and refresh tokens, and is
|
|
// created automatically when the authorization flow completes for the first
|
|
// time.
|
|
log.Printf("Getting token from secret file %s with cache in %s", c.ConfigPath, c.TokenPath)
|
|
tok, err := c.CachedToken()
|
|
if err == nil && tok.Valid() {
|
|
return tok, nil
|
|
}
|
|
if err != nil {
|
|
log.Printf("Could not get token from cache: %v", err)
|
|
} else if !tok.Valid() {
|
|
log.Print("Cached token is invalid, refreshing...")
|
|
tok, err = c.RefreshToken(tok)
|
|
if err != nil {
|
|
log.Printf("Cached token failed to refresh: %v", err)
|
|
}
|
|
}
|
|
if err != nil {
|
|
if !termutil.Isatty(os.Stdin.Fd()) {
|
|
return nil, errors.New("cached token is invalid, and can't get token from web without stdin being a terminal")
|
|
}
|
|
log.Print("Could not use cached token. Getting a new token from web...")
|
|
tok, err = c.NewToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
err = c.SaveToken(tok)
|
|
if err != nil {
|
|
log.Printf("Failed caching token: %v", err)
|
|
}
|
|
return tok, nil
|
|
}
|
|
|
|
// Request a token from the web, then returns the retrieved token.
|
|
func (c *TokenCache) NewToken() (*oauth2.Token, error) {
|
|
config, err := c.Config()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed loading config: %v", err)
|
|
}
|
|
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)
|
|
|
|
var authCode string
|
|
if _, err := fmt.Scan(&authCode); err != nil {
|
|
return nil, fmt.Errorf("unable to read authorization code: %v", err)
|
|
}
|
|
|
|
tok, err := config.Exchange(context.TODO(), authCode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to retrieve token from web: %v", err)
|
|
}
|
|
return tok, nil
|
|
}
|
|
|
|
func (c *TokenCache) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
|
config, err := c.Config()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed loading config: %v", err)
|
|
}
|
|
|
|
ts := config.TokenSource(context.Background(), token)
|
|
newToken, err := ts.Token()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if token.AccessToken == newToken.AccessToken {
|
|
return nil, fmt.Errorf("refreshed token is the same as the original token")
|
|
}
|
|
return newToken, nil
|
|
}
|
|
|
|
// Retrieves a token from a local file.
|
|
func (c *TokenCache) CachedToken() (*oauth2.Token, error) {
|
|
f, err := os.Open(c.TokenPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
tok := &oauth2.Token{}
|
|
err = json.NewDecoder(f).Decode(tok)
|
|
return tok, err
|
|
}
|
|
|
|
// Saves a token to a file path.
|
|
func (c *TokenCache) SaveToken(token *oauth2.Token) error {
|
|
f, err := os.OpenFile(c.TokenPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write to cache for OAuth2 token: %v", err)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
err = json.NewEncoder(f).Encode(token)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write OAuth2 token to cache: %v", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func main() {
|
|
tokenCacheSource := TokenCache{
|
|
CredentialScopes: []string{drive.DriveScope},
|
|
ConfigData: oauth2.Config{
|
|
Endpoint: google.Endpoint,
|
|
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
|
|
Scopes: []string{drive.DriveFileScope},
|
|
},
|
|
}
|
|
flag.StringVar(
|
|
&tokenCacheSource.CredentialPath,
|
|
"credentials",
|
|
"",
|
|
"If set, overrides secrets.json and token.json. The path where the credentials file should be read from.")
|
|
flag.StringVar(
|
|
&tokenCacheSource.TokenPath,
|
|
"token",
|
|
"./token.json",
|
|
"The path where the cached token should be stored and read from.")
|
|
flag.StringVar(
|
|
&tokenCacheSource.ConfigPath,
|
|
"secrets",
|
|
"./secrets.json",
|
|
"The path to the json file containing the client ID and secret.")
|
|
fileId := flag.String(
|
|
"file-id",
|
|
"",
|
|
"The ID of the file that should be overwritten. If not provided, a new file will be uploaded.")
|
|
stdin := flag.String(
|
|
"use-stdin-as",
|
|
"",
|
|
"Accepts input from stdin instead of a file, and uses the provided value as the file's name.")
|
|
checkToken := flag.Bool(
|
|
"check-token",
|
|
false,
|
|
"True to exit after getting the token.")
|
|
contentType := flag.String(
|
|
"content-type",
|
|
"",
|
|
"The content type to upload as. If omitted, no explicit content type is set.")
|
|
flag.Parse()
|
|
var dataFile *os.File
|
|
var dataFilename string
|
|
switch flag.NArg() {
|
|
case 0:
|
|
if *stdin == "" && !*checkToken {
|
|
flag.Usage()
|
|
log.Fatal("The data file path must be given, or --use-stdin-as or --check-token set.")
|
|
}
|
|
dataFile = os.Stdin
|
|
dataFilename = *stdin
|
|
case 1:
|
|
if *stdin != "" || *checkToken {
|
|
flag.Usage()
|
|
log.Fatal("The data file path is not used if --use-stdin-as or --check-token is set.")
|
|
}
|
|
dataPath := flag.Arg(0)
|
|
dataFilename = filepath.Base(dataPath)
|
|
var err error
|
|
dataFile, err = os.Open(dataPath)
|
|
if err != nil {
|
|
log.Fatalf("The data file %s could not be opened: %v", dataPath, err)
|
|
}
|
|
defer func() { _ = dataFile.Close() }()
|
|
default:
|
|
flag.Usage()
|
|
log.Fatal("The only argument accepted is the data file path, if any.")
|
|
}
|
|
|
|
token, err := tokenCacheSource.Token()
|
|
if err != nil {
|
|
log.Fatalf("Failed getting token: %v", err)
|
|
}
|
|
if *checkToken {
|
|
fmt.Println(token.Expiry)
|
|
return
|
|
}
|
|
ts := oauth2.ReuseTokenSource(token, &tokenCacheSource)
|
|
|
|
srv, err := drive.NewService(
|
|
context.Background(),
|
|
option.WithTokenSource(ts))
|
|
if err != nil {
|
|
log.Fatalf("Unable to initialize Drive service: %v", err)
|
|
}
|
|
|
|
metadata := drive.File{
|
|
Name: dataFilename,
|
|
}
|
|
|
|
var mediaOptions []googleapi.MediaOption
|
|
if *contentType != "" {
|
|
mediaOptions = append(mediaOptions, googleapi.ContentType(*contentType))
|
|
}
|
|
|
|
if *fileId != "" {
|
|
fileOut, err := srv.Files.Update(*fileId, &metadata).Media(dataFile, mediaOptions...).Do()
|
|
if err != nil {
|
|
log.Fatalf("Failed updating file %s: %v", *fileId, err)
|
|
}
|
|
fmt.Println(fileOut.Id)
|
|
} else {
|
|
fileOut, err := srv.Files.Create(&metadata).Media(dataFile, mediaOptions...).Do()
|
|
if err != nil {
|
|
log.Fatalf("Failed uploading file: %v", err)
|
|
}
|
|
fmt.Println(fileOut.Id)
|
|
}
|
|
}
|
|
|