Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.
Merged
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
47 changes: 27 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,35 @@ This depends on the official [Google Photos Library API](https://developers.goog

## Getting Started

You can install this from brew tap or [releases](https://github.com/int128/gpup/releases).

```sh
brew tap int128/gpup
brew install gpup
```

Setup your API access by the following steps:

1. Open https://console.cloud.google.com/apis/library/photoslibrary.googleapis.com/
1. Enable Photos Library API.
1. Open https://console.cloud.google.com/apis/credentials
1. Create an OAuth client ID where the application type is other.
1. Set the following environment variables in your shell:
1. Run `gpup` and follow the instruction as follows.

```
export GOOGLE_CLIENT_ID=
export GOOGLE_CLIENT_SECRET=
```
% gpup
2018/09/13 15:38:13 Skip reading ~/.gpupconfig: Could not open ~/.gpupconfig: open /user/.gpupconfig: no such file or directory
2018/09/13 15:38:13 Setup your API access by the following steps:

You can install this from brew tap or [releases](https://github.com/int128/gpup/releases).
1. Open https://console.cloud.google.com/apis/library/photoslibrary.googleapis.com/
1. Enable Photos Library API.
1. Open https://console.cloud.google.com/apis/credentials
1. Create an OAuth client ID where the application type is other.

```sh
brew tap int128/gpup
brew install gpup
Enter your OAuth client ID (e.g. xxx.apps.googleusercontent.com): YOUR_CLIENT_ID.apps.googleusercontent.com
Enter your OAuth client secret: YOUR_CLIENT_SECRET
2018/09/13 15:38:22 Saved credentials to ~/.gpupconfig
2018/09/13 15:38:22 Error: Nothing to upload
```

To upload files in a folder to your Google Photos library:
Expand All @@ -37,6 +48,7 @@ $ gpup my-photos/
2018/06/14 10:28:40 Open http://localhost:8000 for authorization
2018/06/14 10:28:43 GET /
2018/06/14 10:28:49 GET /?state=...&code=...
2018/06/14 10:28:49 Saved token to ~/.gpupconfig
2018/06/14 10:28:49 Queued 2 file(s)
2018/06/14 10:28:49 Uploading travel.jpg
2018/06/14 10:28:49 Uploading lunch.jpg
Expand All @@ -58,21 +70,16 @@ gpup -n "My Album" my-photos/
Usage:
gpup [OPTIONS] FILE or DIRECTORY...

Setup:
1. Open https://console.cloud.google.com/apis/library/photoslibrary.googleapis.com/
2. Enable Photos Library API.
3. Open https://console.cloud.google.com/apis/credentials
4. Create an OAuth client ID where the application type is other.
5. Export GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET variables or set the options.

Application Options:
-n, --new-album=TITLE Create an album and add files into it
--google-client-id= Google API client ID [$GOOGLE_CLIENT_ID]
--google-client-secret= Google API client secret [$GOOGLE_CLIENT_SECRET]
--debug Enable request and response logging [$DEBUG]
--gpupconfig= Path to the config file (default: ~/.gpupconfig) [$GPUPCONFIG]
-n, --new-album=TITLE Create an album and add files into it
--debug Enable request and response logging [$DEBUG]
--google-client-id= Google API client ID [$GOOGLE_CLIENT_ID]
--google-client-secret= Google API client secret [$GOOGLE_CLIENT_SECRET]
--google-token= Google API token [$GOOGLE_TOKEN]

Help Options:
-h, --help Show this help message
-h, --help Show this help message
```


Expand Down
146 changes: 69 additions & 77 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,102 +4,94 @@ import (
"context"
"fmt"
"log"
"strings"

"github.com/int128/gpup/authz"

"github.com/int128/gpup/debug"
"github.com/int128/gpup/photos"
flags "github.com/jessevdk/go-flags"
"golang.org/x/oauth2"
)

// CLI has the command options.
// CLI represents input for the command.
type CLI struct {
NewAlbum string `short:"n" long:"new-album" value-name:"TITLE" description:"Create an album and add files into it"`
ClientID string `long:"google-client-id" env:"GOOGLE_CLIENT_ID" required:"1" description:"Google API client ID"`
ClientSecret string `long:"google-client-secret" env:"GOOGLE_CLIENT_SECRET" required:"1" description:"Google API client secret"`
Debug bool `long:"debug" env:"DEBUG" description:"Enable request and response logging"`
paths []string
ConfigName string `long:"gpupconfig" env:"GPUPCONFIG" default:"~/.gpupconfig" description:"Path to the config file"`
NewAlbum string `short:"n" long:"new-album" value-name:"TITLE" description:"Create an album and add files into it"`
Debug bool `long:"debug" env:"DEBUG" description:"Enable request and response logging"`

externalConfig // default to values in the config

Paths []string
}

// Parse command line and returns a CLI.
func Parse(osArgs []string, version string) (*CLI, error) {
var o CLI
parser := flags.NewParser(&o, flags.HelpFlag)
parser.Usage = "[OPTIONS] FILE or DIRECTORY..."
parser.LongDescription = fmt.Sprintf(`
Version %s
type externalConfig struct {
ClientID string `yaml:"client-id" long:"google-client-id" env:"GOOGLE_CLIENT_ID" description:"Google API client ID"`
ClientSecret string `yaml:"client-secret" long:"google-client-secret" env:"GOOGLE_CLIENT_SECRET" description:"Google API client secret"`
EncodedToken EncodedToken `yaml:"token" long:"google-token" env:"GOOGLE_TOKEN" description:"Google API token"`
}

Setup:
1. Open https://console.cloud.google.com/apis/library/photoslibrary.googleapis.com/
2. Enable Photos Library API.
3. Open https://console.cloud.google.com/apis/credentials
4. Create an OAuth client ID where the application type is other.
5. Export GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET variables or set the options.`,
version)
args, err := parser.ParseArgs(osArgs[1:])
if err != nil {
// New creates a new CLI object.
//
// This does the followings:
// - Determine path to the config
// - Read the config
// - Parse the arguments
// - Validate
//
// If the config is invalid, it will be ignored.
func New(osArgs []string, version string) (*CLI, error) {
var c CLI
parser := flags.NewParser(&c, flags.HelpFlag)
parser.Usage = "[OPTIONS] FILE or DIRECTORY..."
parser.LongDescription = fmt.Sprintf("Version %s", version)
if _, err := parser.ParseArgs(osArgs[1:]); err != nil {
return nil, err
}
if len(args) == 0 {
return nil, fmt.Errorf("Too few argument")
if err := readConfig(c.ConfigName, &c.externalConfig); err != nil {
log.Printf("Skip reading %s: %s", c.ConfigName, err)
}
o.paths = args
return &o, nil
var err error
c.Paths, err = parser.ParseArgs(osArgs[1:])
if err != nil {
return nil, err
}
return &c, nil
}

// Run runs the command.
func (c *CLI) Run() error {
files, err := findFiles(c.paths)
if err != nil {
return err
}
if len(files) == 0 {
return fmt.Errorf("File not found in %s", strings.Join(c.paths, ", "))
func (c *CLI) Run(ctx context.Context) error {
if c.ClientID == "" || c.ClientSecret == "" {
if err := c.initialSetup(ctx); err != nil {
return err
}
}
log.Printf("The following %d files will be uploaded:", len(files))
for i, file := range files {
fmt.Printf("%3d: %s\n", i+1, file)
switch {
case len(c.Paths) == 0:
return fmt.Errorf("Nothing to upload")
case c.NewAlbum != "":
return c.createAlbum(ctx)
default:
return c.addToLibrary(ctx)
}
}

ctx := context.Background()
oauth2Config := oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Endpoint: photos.Endpoint,
Scopes: photos.Scopes,
RedirectURL: "http://localhost:8000",
}
flow := authz.AuthCodeFlow{
Config: &oauth2Config,
ServerPort: 8000,
}
token, err := flow.GetToken(ctx)
if err != nil {
return err
}
client := oauth2Config.Client(ctx, token)
if err != nil {
return err
}
if c.Debug {
client = debug.NewClient(client)
}
func (c *CLI) initialSetup(ctx context.Context) error {
log.Printf(`Setup your API access by the following steps:

service, err := photos.New(client)
if err != nil {
return err
1. Open https://console.cloud.google.com/apis/library/photoslibrary.googleapis.com/
1. Enable Photos Library API.
1. Open https://console.cloud.google.com/apis/credentials
1. Create an OAuth client ID where the application type is other.

`)
fmt.Printf("Enter your OAuth client ID (e.g. xxx.apps.googleusercontent.com): ")
fmt.Scanln(&c.externalConfig.ClientID)
if c.externalConfig.ClientID == "" {
return fmt.Errorf("OAuth client ID must not be empty")
}
if c.NewAlbum != "" {
_, err := service.CreateAlbum(ctx, c.NewAlbum, files)
if err != nil {
return err
}
} else {
if err := service.AddToLibrary(ctx, files); err != nil {
return err
}
fmt.Printf("Enter your OAuth client secret: ")
fmt.Scanln(&c.externalConfig.ClientSecret)
if c.externalConfig.ClientSecret == "" {
return fmt.Errorf("OAuth client ID must not be empty")
}
if err := writeConfig(c.ConfigName, &c.externalConfig); err != nil {
return fmt.Errorf("Could not save credentials to %s: %s", c.ConfigName, err)
}
log.Printf("Saved credentials to %s", c.ConfigName)
return nil
}
74 changes: 74 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cli

import (
"encoding/base64"
"encoding/json"
"fmt"
"os"

homedir "github.com/mitchellh/go-homedir"
"golang.org/x/oauth2"
yaml "gopkg.in/yaml.v2"
)

func readConfig(name string, c *externalConfig) error {
p, err := homedir.Expand(name)
if err != nil {
return fmt.Errorf("Could not expand %s: %s", name, err)
}
f, err := os.Open(p)
if err != nil {
return fmt.Errorf("Could not open %s: %s", name, err)
}
defer f.Close()
d := yaml.NewDecoder(f)
if err := d.Decode(&c); err != nil {
return fmt.Errorf("Could not read YAML: %s", err)
}
return nil
}

func writeConfig(name string, c *externalConfig) error {
p, err := homedir.Expand(name)
if err != nil {
return fmt.Errorf("Could not expand %s: %s", name, err)
}
f, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("Could not open %s: %s", name, err)
}
defer f.Close()
e := yaml.NewEncoder(f)
if err := e.Encode(c); err != nil {
return fmt.Errorf("Could not write to YAML: %s", err)
}
return nil
}

// EncodedToken is a base64 encoded json of token.
type EncodedToken string

// Decode returns the token object.
func (t EncodedToken) Decode() (*oauth2.Token, error) {
if t == "" {
return nil, nil
}
b, err := base64.StdEncoding.DecodeString(string(t))
if err != nil {
return nil, fmt.Errorf("Invalid base64: %s", err)
}
var token oauth2.Token
if err := json.Unmarshal(b, &token); err != nil {
return nil, fmt.Errorf("Invalid json: %s", err)
}
return &token, nil
}

// EncodeToken returns an EncodedToken.
func EncodeToken(token *oauth2.Token) (EncodedToken, error) {
b, err := json.Marshal(token)
if err != nil {
return "", fmt.Errorf("Could not encode: %s", err)
}
return EncodedToken(base64.StdEncoding.EncodeToString(b)), nil
}
55 changes: 55 additions & 0 deletions cli/oauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cli

import (
"context"
"fmt"
"log"
"net/http"

"github.com/int128/gpup/authz"
"github.com/int128/gpup/debug"
"github.com/int128/gpup/photos"
"golang.org/x/oauth2"
)

func (c *CLI) newClient(ctx context.Context) (*http.Client, error) {
token, err := c.EncodedToken.Decode()
if err != nil {
return nil, fmt.Errorf("Invalid config: %s", err)
}
oauth2Config := oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Endpoint: photos.Endpoint,
Scopes: photos.Scopes,
RedirectURL: "http://localhost:8000",
}
if token == nil {
flow := authz.AuthCodeFlow{
Config: &oauth2Config,
ServerPort: 8000,
}
token, err = flow.GetToken(ctx)
if err != nil {
return nil, err
}
c.EncodedToken, err = EncodeToken(token)
if err != nil {
return nil, err
}
if err := writeConfig(c.ConfigName, &c.externalConfig); err != nil {
return nil, fmt.Errorf("Could not write token to %s: %s", c.ConfigName, err)
}
log.Printf("Saved token to %s", c.ConfigName)
} else {
log.Printf("Using token in %s", c.ConfigName)
}
client := oauth2Config.Client(ctx, token)
if err != nil {
return nil, err
}
if c.Debug {
client = debug.NewClient(client)
}
return client, nil
}
Loading