Support any writefreely instance Resolves T586 T594 T595 T635pull/45/head
@@ -24,24 +24,29 @@ COMMANDS: | |||||
posts List all of your posts | posts List all of your posts | ||||
claim Claim local unsynced posts | claim Claim local unsynced posts | ||||
blogs List blogs | blogs List blogs | ||||
claim Claim local unsynced posts | |||||
auth Authenticate with Write.as | auth Authenticate with Write.as | ||||
logout Log out of Write.as | logout Log out of Write.as | ||||
help, h Shows a list of commands or help for one command | help, h Shows a list of commands or help for one command | ||||
GLOBAL OPTIONS: | GLOBAL OPTIONS: | ||||
-c value, -b value Optional blog to post to | |||||
--tor, -t Perform action on Tor hidden service | |||||
--tor-port value Use a different port to connect to Tor (default: 9150) | |||||
--code Specifies this post is code | |||||
--md Returns post URL with Markdown enabled | |||||
--verbose, -v Make the operation more talkative | |||||
--font value Sets post font to given value (default: "mono") | |||||
--lang value Sets post language to given ISO 639-1 language code | |||||
--user-agent value Sets the User-Agent for API requests | |||||
--help, -h show help | |||||
--version, -V print the version | |||||
-c value, -b value Optional blog to post to | |||||
--tor, -t Perform action on Tor hidden service | |||||
--tor-port value Use a different port to connect to Tor (default: 9150) | |||||
--code Specifies this post is code | |||||
--md Returns post URL with Markdown enabled | |||||
--verbose, -v Make the operation more talkative | |||||
--font value Sets post font to given value (default: "mono") | |||||
--lang value Sets post language to given ISO 639-1 language code | |||||
--user-agent value Sets the User-Agent for API requests | |||||
--host value, -H value Operate against a custom hostname | |||||
--user value, -u value Use authenticated user, other than default | |||||
--help, -h show help | |||||
--version, -V print the version | |||||
``` | ``` | ||||
> Note: the host and user flags are only available in `writefreely`. | |||||
#### Share something | #### Share something | ||||
By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard: | By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard: | ||||
@@ -86,7 +91,7 @@ dev My Dev Log | |||||
#### List posts | #### List posts | ||||
This lists all posts you've published from your device | |||||
This lists all anonymous posts you've published. If authenticated, it will include posts on your account as well as any local / unclaimed posts. | |||||
Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. | Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. | ||||
@@ -4,8 +4,6 @@ writeas-cli | |||||
Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. | Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. | ||||
**NOTE: the `master` branch is currently unstable while we prepare the v2.0 release! You should install via official release channel, or build from the `v1.2` tag.** | |||||
## Features | ## Features | ||||
* Publish anonymously to Write.as | * Publish anonymously to Write.as | ||||
@@ -26,10 +24,10 @@ The easiest way to get the CLI is to download a pre-built executable for your OS | |||||
Get the latest version for your operating system as a standalone executable. | Get the latest version for your operating system as a standalone executable. | ||||
**Windows**<br /> | **Windows**<br /> | ||||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_386.zip) executable and put it somewhere in your `%PATH%`. | |||||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. | |||||
**macOS**<br /> | **macOS**<br /> | ||||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||||
**Debian-based Linux**<br /> | **Debian-based Linux**<br /> | ||||
```bash | ```bash | ||||
@@ -39,7 +37,7 @@ sudo apt-get update && sudo apt-get install writeas-cli | |||||
``` | ``` | ||||
**Linux (other)**<br /> | **Linux (other)**<br /> | ||||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||||
### Go get it | ### Go get it | ||||
```bash | ```bash | ||||
@@ -81,19 +79,23 @@ COMMANDS: | |||||
help, h Shows a list of commands or help for one command | help, h Shows a list of commands or help for one command | ||||
GLOBAL OPTIONS: | GLOBAL OPTIONS: | ||||
-c value, -b value Optional blog to post to | |||||
--tor, -t Perform action on Tor hidden service | |||||
--tor-port value Use a different port to connect to Tor (default: 9150) | |||||
--code Specifies this post is code | |||||
--md Returns post URL with Markdown enabled | |||||
--verbose, -v Make the operation more talkative | |||||
--font value Sets post font to given value (default: "mono") | |||||
--lang value Sets post language to given ISO 639-1 language code | |||||
--user-agent value Sets the User-Agent for API requests | |||||
--help, -h show help | |||||
--version, -V print the version | |||||
-c value, -b value Optional blog to post to | |||||
--tor, -t Perform action on Tor hidden service | |||||
--tor-port value Use a different port to connect to Tor (default: 9150) | |||||
--code Specifies this post is code | |||||
--md Returns post URL with Markdown enabled | |||||
--verbose, -v Make the operation more talkative | |||||
--font value Sets post font to given value (default: "mono") | |||||
--lang value Sets post language to given ISO 639-1 language code | |||||
--user-agent value Sets the User-Agent for API requests | |||||
--host value, -H value Operate against a custom hostname | |||||
--user value, -u value Use authenticated user, other than default | |||||
--help, -h show help | |||||
--version, -V print the version | |||||
``` | ``` | ||||
> Note: the host and user flags are only available in `wf` the community edition | |||||
## Contributing to the CLI | ## Contributing to the CLI | ||||
For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). | For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). | ||||
@@ -2,44 +2,70 @@ package api | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"path/filepath" | |||||
"strings" | |||||
"github.com/atotto/clipboard" | "github.com/atotto/clipboard" | ||||
writeas "github.com/writeas/go-writeas/v2" | writeas "github.com/writeas/go-writeas/v2" | ||||
"github.com/writeas/web-core/posts" | "github.com/writeas/web-core/posts" | ||||
"github.com/writeas/writeas-cli/config" | "github.com/writeas/writeas-cli/config" | ||||
"github.com/writeas/writeas-cli/fileutils" | |||||
"github.com/writeas/writeas-cli/executable" | |||||
"github.com/writeas/writeas-cli/log" | "github.com/writeas/writeas-cli/log" | ||||
cli "gopkg.in/urfave/cli.v1" | cli "gopkg.in/urfave/cli.v1" | ||||
) | ) | ||||
func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { | |||||
func HostURL(c *cli.Context) string { | |||||
host := c.GlobalString("host") | |||||
if host == "" { | |||||
return "" | |||||
} | |||||
insecure := c.Bool("insecure") | |||||
if parts := strings.Split(host, "://"); len(parts) > 1 { | |||||
host = parts[1] | |||||
} | |||||
scheme := "https://" | |||||
if insecure { | |||||
scheme = "http://" | |||||
} | |||||
return scheme + host | |||||
} | |||||
func newClient(c *cli.Context) (*writeas.Client, error) { | |||||
var client *writeas.Client | var client *writeas.Client | ||||
if config.IsTor(c) { | |||||
client = writeas.NewTorClient(config.TorPort(c)) | |||||
} else { | |||||
if config.IsDev() { | |||||
client = writeas.NewDevClient() | |||||
var clientConfig writeas.Config | |||||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
return nil, fmt.Errorf("Failed to load configuration file: %v", err) | |||||
} | |||||
if host := HostURL(c); host != "" { | |||||
clientConfig.URL = host + "/api" | |||||
} else if cfg.Default.Host != "" && cfg.Default.User != "" { | |||||
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { | |||||
clientConfig.URL = cfg.Default.Host + "/api" | |||||
} else { | } else { | ||||
client = writeas.NewClient() | |||||
clientConfig.URL = "https://" + cfg.Default.Host + "/api" | |||||
} | } | ||||
} else if config.IsDev() { | |||||
clientConfig.URL = config.DevBaseURL + "/api" | |||||
} else if c.App.Name == "writeas" { | |||||
clientConfig.URL = config.WriteasBaseURL + "/api" | |||||
} else { | |||||
return nil, fmt.Errorf("Must supply a host. Example: %s --host example.com %s", executable.Name(), c.Command.Name) | |||||
} | } | ||||
client.UserAgent = config.UserAgent(c) | |||||
// TODO: load user into var shared across the app | |||||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if u != nil { | |||||
client.SetToken(u.AccessToken) | |||||
} else if authRequired { | |||||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth <username>") | |||||
if config.IsTor(c) { | |||||
clientConfig.URL = config.TorURL(c) | |||||
clientConfig.TorPort = config.TorPort(c) | |||||
} | } | ||||
client = writeas.NewClientWith(clientConfig) | |||||
client.UserAgent = config.UserAgent(c) | |||||
return client, nil | return client, nil | ||||
} | } | ||||
// DoFetch retrieves the Write.as post with the given friendlyID, | // DoFetch retrieves the Write.as post with the given friendlyID, | ||||
// optionally via the Tor hidden service. | // optionally via the Tor hidden service. | ||||
func DoFetch(c *cli.Context, friendlyID string) error { | func DoFetch(c *cli.Context, friendlyID string) error { | ||||
cl, err := newClient(c, false) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
@@ -59,11 +85,18 @@ func DoFetch(c *cli.Context, friendlyID string) error { | |||||
// DoFetchPosts retrieves all remote posts for the | // DoFetchPosts retrieves all remote posts for the | ||||
// authenticated user | // authenticated user | ||||
func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { | func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { | ||||
cl, err := newClient(c, true) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return nil, fmt.Errorf("%v", err) | return nil, fmt.Errorf("%v", err) | ||||
} | } | ||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||||
} | |||||
posts, err := cl.GetUserPosts() | posts, err := cl.GetUserPosts() | ||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
@@ -75,11 +108,18 @@ func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { | |||||
// DoPost creates a Write.as post, returning an error if it was | // DoPost creates a Write.as post, returning an error if it was | ||||
// unsuccessful. | // unsuccessful. | ||||
func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) { | func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) { | ||||
cl, err := newClient(c, false) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return nil, fmt.Errorf("%v", err) | return nil, fmt.Errorf("%v", err) | ||||
} | } | ||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||||
} | |||||
pp := &writeas.PostParams{ | pp := &writeas.PostParams{ | ||||
Font: config.GetFont(code, font), | Font: config.GetFont(code, font), | ||||
Collection: config.Collection(c), | Collection: config.Collection(c), | ||||
@@ -93,14 +133,22 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ | |||||
return nil, fmt.Errorf("Unable to post: %v", err) | return nil, fmt.Errorf("Unable to post: %v", err) | ||||
} | } | ||||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
return nil, fmt.Errorf("Couldn't check for config file: %v", err) | |||||
} | |||||
var url string | var url string | ||||
if p.Collection != nil { | if p.Collection != nil { | ||||
url = p.Collection.URL + p.Slug | url = p.Collection.URL + p.Slug | ||||
} else { | } else { | ||||
if config.IsTor(c) { | |||||
url = config.TorBaseURL | |||||
if host := HostURL(c); host != "" { | |||||
url = host | |||||
} else if cfg.Default.Host != "" { | |||||
url = cfg.Default.Host | |||||
} else if config.IsDev() { | } else if config.IsDev() { | ||||
url = config.DevBaseURL | url = config.DevBaseURL | ||||
} else if config.IsTor(c) { | |||||
url = config.TorBaseURL | |||||
} else { | } else { | ||||
url = config.WriteasBaseURL | url = config.WriteasBaseURL | ||||
} | } | ||||
@@ -119,7 +167,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ | |||||
// Copy URL to clipboard | // Copy URL to clipboard | ||||
err = clipboard.WriteAll(string(url)) | err = clipboard.WriteAll(string(url)) | ||||
if err != nil { | if err != nil { | ||||
log.Errorln("writeas: Didn't copy to clipboard: %s", err) | |||||
log.Errorln(executable.Name()+": Didn't copy to clipboard: %s", err) | |||||
} else { | } else { | ||||
log.Info(c, "Copied to clipboard.") | log.Info(c, "Copied to clipboard.") | ||||
} | } | ||||
@@ -133,7 +181,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ | |||||
// DoFetchCollections retrieves a list of the currently logged in users | // DoFetchCollections retrieves a list of the currently logged in users | ||||
// collections. | // collections. | ||||
func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | ||||
cl, err := newClient(c, true) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
if config.Debug() { | if config.Debug() { | ||||
log.ErrorlnQuit("could not create client: %v", err) | log.ErrorlnQuit("could not create client: %v", err) | ||||
@@ -141,6 +189,13 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | |||||
return nil, fmt.Errorf("Couldn't create new client") | return nil, fmt.Errorf("Couldn't create new client") | ||||
} | } | ||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||||
} | |||||
colls, err := cl.GetUserCollections() | colls, err := cl.GetUserCollections() | ||||
if err != nil { | if err != nil { | ||||
if config.Debug() { | if config.Debug() { | ||||
@@ -165,11 +220,20 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | |||||
// DoUpdate updates the given post on Write.as. | // DoUpdate updates the given post on Write.as. | ||||
func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error { | func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error { | ||||
cl, err := newClient(c, false) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return fmt.Errorf("%v", err) | return fmt.Errorf("%v", err) | ||||
} | } | ||||
if token == "" { | |||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return fmt.Errorf("You must either provide and edit token or log in to delete a post.") | |||||
} | |||||
} | |||||
params := writeas.PostParams{} | params := writeas.PostParams{} | ||||
params.Title, params.Content = posts.ExtractTitle(string(post)) | params.Title, params.Content = posts.ExtractTitle(string(post)) | ||||
if lang := config.Language(c, false); lang != "" { | if lang := config.Language(c, false); lang != "" { | ||||
@@ -191,11 +255,20 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code | |||||
// DoDelete deletes the given post on Write.as, and removes any local references | // DoDelete deletes the given post on Write.as, and removes any local references | ||||
func DoDelete(c *cli.Context, friendlyID, token string) error { | func DoDelete(c *cli.Context, friendlyID, token string) error { | ||||
cl, err := newClient(c, false) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return fmt.Errorf("%v", err) | return fmt.Errorf("%v", err) | ||||
} | } | ||||
if token == "" { | |||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return fmt.Errorf("You must either provide and edit token or log in to delete a post.") | |||||
} | |||||
} | |||||
err = cl.DeletePost(friendlyID, token) | err = cl.DeletePost(friendlyID, token) | ||||
if err != nil { | if err != nil { | ||||
if config.Debug() { | if config.Debug() { | ||||
@@ -204,13 +277,13 @@ func DoDelete(c *cli.Context, friendlyID, token string) error { | |||||
return fmt.Errorf("Post doesn't exist, or bad edit token given.") | return fmt.Errorf("Post doesn't exist, or bad edit token given.") | ||||
} | } | ||||
RemovePost(c.App.ExtraInfo()["configDir"], friendlyID) | |||||
RemovePost(c, friendlyID) | |||||
return nil | return nil | ||||
} | } | ||||
func DoLogIn(c *cli.Context, username, password string) error { | func DoLogIn(c *cli.Context, username, password string) error { | ||||
cl, err := newClient(c, false) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return fmt.Errorf("%v", err) | return fmt.Errorf("%v", err) | ||||
} | } | ||||
@@ -223,7 +296,7 @@ func DoLogIn(c *cli.Context, username, password string) error { | |||||
return err | return err | ||||
} | } | ||||
err = config.SaveUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]), u) | |||||
err = config.SaveUser(c, u) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
@@ -232,11 +305,18 @@ func DoLogIn(c *cli.Context, username, password string) error { | |||||
} | } | ||||
func DoLogOut(c *cli.Context) error { | func DoLogOut(c *cli.Context) error { | ||||
cl, err := newClient(c, true) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return fmt.Errorf("%v", err) | return fmt.Errorf("%v", err) | ||||
} | } | ||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else if c.App.Name == "writeas" { | |||||
return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||||
} | |||||
err = cl.LogOut() | err = cl.LogOut() | ||||
if err != nil { | if err != nil { | ||||
if config.Debug() { | if config.Debug() { | ||||
@@ -245,11 +325,6 @@ func DoLogOut(c *cli.Context) error { | |||||
return err | return err | ||||
} | } | ||||
// Delete local user data | |||||
err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), config.UserFile)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
// delete local user file | |||||
return config.DeleteUser(c) | |||||
} | } |
@@ -12,6 +12,7 @@ import ( | |||||
writeas "github.com/writeas/go-writeas/v2" | writeas "github.com/writeas/go-writeas/v2" | ||||
"github.com/writeas/writeas-cli/config" | "github.com/writeas/writeas-cli/config" | ||||
"github.com/writeas/writeas-cli/executable" | |||||
"github.com/writeas/writeas-cli/fileutils" | "github.com/writeas/writeas-cli/fileutils" | ||||
"github.com/writeas/writeas-cli/log" | "github.com/writeas/writeas-cli/log" | ||||
cli "gopkg.in/urfave/cli.v1" | cli "gopkg.in/urfave/cli.v1" | ||||
@@ -42,7 +43,11 @@ type RemotePost struct { | |||||
} | } | ||||
func AddPost(c *cli.Context, id, token string) error { | func AddPost(c *cli.Context, id, token string) error { | ||||
f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) | |||||
hostDir, err := config.HostDirectory(c) | |||||
if err != nil { | |||||
return fmt.Errorf("Error checking for host directory: %v", err) | |||||
} | |||||
f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) | |||||
if err != nil { | if err != nil { | ||||
return fmt.Errorf("Error creating local posts list: %s", err) | return fmt.Errorf("Error creating local posts list: %s", err) | ||||
} | } | ||||
@@ -60,10 +65,18 @@ func AddPost(c *cli.Context, id, token string) error { | |||||
// ClaimPost adds a local post to the authenticated user's account and deletes | // ClaimPost adds a local post to the authenticated user's account and deletes | ||||
// the local reference | // the local reference | ||||
func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) { | func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) { | ||||
cl, err := newClient(c, true) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||||
} | |||||
postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts)) | postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts)) | ||||
for i, post := range *localPosts { | for i, post := range *localPosts { | ||||
postsToClaim[i] = writeas.OwnedPostParams{ | postsToClaim[i] = writeas.OwnedPostParams{ | ||||
@@ -76,7 +89,8 @@ func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, | |||||
} | } | ||||
func TokenFromID(c *cli.Context, id string) string { | func TokenFromID(c *cli.Context, id string) string { | ||||
post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id) | |||||
hostDir, _ := config.HostDirectory(c) | |||||
post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), id) | |||||
if post == "" { | if post == "" { | ||||
return "" | return "" | ||||
} | } | ||||
@@ -89,12 +103,15 @@ func TokenFromID(c *cli.Context, id string) string { | |||||
return parts[1] | return parts[1] | ||||
} | } | ||||
func RemovePost(path, id string) { | |||||
fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id) | |||||
func RemovePost(c *cli.Context, id string) { | |||||
hostDir, _ := config.HostDirectory(c) | |||||
fullPath := filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile) | |||||
fileutils.RemoveLine(fullPath, id) | |||||
} | } | ||||
func GetPosts(c *cli.Context) *[]Post { | func GetPosts(c *cli.Context) *[]Post { | ||||
lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile)) | |||||
hostDir, _ := config.HostDirectory(c) | |||||
lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile)) | |||||
posts := []Post{} | posts := []Post{} | ||||
@@ -8,6 +8,7 @@ import ( | |||||
"path/filepath" | "path/filepath" | ||||
"github.com/writeas/writeas-cli/config" | "github.com/writeas/writeas-cli/config" | ||||
"github.com/writeas/writeas-cli/executable" | |||||
"github.com/writeas/writeas-cli/fileutils" | "github.com/writeas/writeas-cli/fileutils" | ||||
"github.com/writeas/writeas-cli/log" | "github.com/writeas/writeas-cli/log" | ||||
cli "gopkg.in/urfave/cli.v1" | cli "gopkg.in/urfave/cli.v1" | ||||
@@ -25,14 +26,21 @@ func CmdPull(c *cli.Context) error { | |||||
} | } | ||||
// Create posts directory if needed | // Create posts directory if needed | ||||
if cfg.Posts.Directory == "" { | if cfg.Posts.Directory == "" { | ||||
syncSetUp(c.App.ExtraInfo()["configDir"], cfg) | |||||
syncSetUp(c, cfg) | |||||
} | } | ||||
cl, err := newClient(c, true) | |||||
cl, err := newClient(c) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | |||||
cl.SetToken(u.AccessToken) | |||||
} else { | |||||
return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||||
} | |||||
// Fetch posts | // Fetch posts | ||||
posts, err := cl.GetUserPosts() | posts, err := cl.GetUserPosts() | ||||
if err != nil { | if err != nil { | ||||
@@ -79,10 +87,10 @@ func CmdPull(c *cli.Context) error { | |||||
return nil | return nil | ||||
} | } | ||||
func syncSetUp(path string, cfg *config.UserConfig) error { | |||||
func syncSetUp(c *cli.Context, cfg *config.Config) error { | |||||
// Get user information and fail early (before we make the user do | // Get user information and fail early (before we make the user do | ||||
// anything), if we're going to | // anything), if we're going to | ||||
u, err := config.LoadUser(config.UserDataDir(path)) | |||||
u, err := config.LoadUser(c) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
@@ -118,7 +126,7 @@ func syncSetUp(path string, cfg *config.UserConfig) error { | |||||
// Save preference | // Save preference | ||||
cfg.Posts.Directory = dir | cfg.Posts.Directory = dir | ||||
err = config.SaveConfig(config.UserDataDir(path), cfg) | |||||
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) | |||||
if err != nil { | if err != nil { | ||||
if config.Debug() { | if config.Debug() { | ||||
log.Errorln("Unable to save config: %s", err) | log.Errorln("Unable to save config: %s", err) | ||||
@@ -0,0 +1 @@ | |||||
wf |
@@ -0,0 +1,198 @@ | |||||
package main | |||||
import ( | |||||
"fmt" | |||||
"os" | |||||
"path/filepath" | |||||
"strings" | |||||
"github.com/writeas/writeas-cli/api" | |||||
"github.com/writeas/writeas-cli/commands" | |||||
"github.com/writeas/writeas-cli/config" | |||||
"github.com/writeas/writeas-cli/executable" | |||||
"github.com/writeas/writeas-cli/log" | |||||
cli "gopkg.in/urfave/cli.v1" | |||||
) | |||||
func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { | |||||
return func(c *cli.Context) error { | |||||
// check for logged in users when host is provided without user | |||||
if c.GlobalIsSet("host") && !c.GlobalIsSet("user") { | |||||
// multiple users should display a list | |||||
if num, users, err := usersLoggedIn(c); num > 1 && err == nil { | |||||
return cli.NewExitError(fmt.Sprintf("Multiple logged in users, please use '-u' or '-user' to specify one of:\n%s", strings.Join(users, ", ")), 1) | |||||
} else if num == 1 && err == nil { | |||||
// single user found for host should be set as user flag so LoadUser can | |||||
// succeed, and notify the client | |||||
if err := c.GlobalSet("user", users[0]); err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to set user flag for only logged in user at host %s: %v", users[0], err), 1) | |||||
} | |||||
log.Info(c, "Host specified without user flag, using logged in user: %s\n", users[0]) | |||||
} else if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) | |||||
} | |||||
} else if !c.GlobalIsSet("host") && !c.GlobalIsSet("user") { | |||||
// check for global configured pair host/user | |||||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to load config from file: %v", err), 1) | |||||
// set flags if found | |||||
} | |||||
// set flags if both were found in config | |||||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||||
err = c.GlobalSet("host", cfg.Default.Host) | |||||
if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to set host from global config: %v", err), 1) | |||||
} | |||||
err = c.GlobalSet("user", cfg.Default.User) | |||||
if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to set user from global config: %v", err), 1) | |||||
} | |||||
} else { | |||||
num, err := totalUsersLoggedIn(c) | |||||
if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) | |||||
} else if num > 0 { | |||||
return cli.NewExitError("You are authenticated, but have no default user/host set. Supply -user and -host flags.", 1) | |||||
} | |||||
} | |||||
} | |||||
u, err := config.LoadUser(c) | |||||
if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Couldn't load user: %v", err), 1) | |||||
} | |||||
if u == nil { | |||||
return cli.NewExitError("You must be authenticated to "+action+".\nLog in first with: "+executable.Name()+" auth <username>", 1) | |||||
} | |||||
return f(c) | |||||
} | |||||
} | |||||
// usersLoggedIn checks for logged in users for the set host flag | |||||
// it returns the number of users and a slice of usernames | |||||
func usersLoggedIn(c *cli.Context) (int, []string, error) { | |||||
path, err := config.UserHostDir(c) | |||||
if err != nil { | |||||
return 0, nil, err | |||||
} | |||||
dir, err := os.Open(path) | |||||
if err != nil { | |||||
return 0, nil, err | |||||
} | |||||
contents, err := dir.Readdir(0) | |||||
if err != nil { | |||||
return 0, nil, err | |||||
} | |||||
var names []string | |||||
for _, file := range contents { | |||||
if file.IsDir() { | |||||
// stat user.json | |||||
if _, err := os.Stat(filepath.Join(path, file.Name(), "user.json")); err == nil { | |||||
names = append(names, file.Name()) | |||||
} | |||||
} | |||||
} | |||||
return len(names), names, nil | |||||
} | |||||
// totalUsersLoggedIn checks for logged in users for any host | |||||
// it returns the number of users and an error if any | |||||
func totalUsersLoggedIn(c *cli.Context) (int, error) { | |||||
path := config.UserDataDir(c.App.ExtraInfo()["configDir"]) | |||||
dir, err := os.Open(path) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
contents, err := dir.Readdir(0) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
count := 0 | |||||
for _, file := range contents { | |||||
if file.IsDir() { | |||||
subDir, err := os.Open(filepath.Join(path, file.Name())) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
subContents, err := subDir.Readdir(0) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
for _, subFile := range subContents { | |||||
if subFile.IsDir() { | |||||
if _, err := os.Stat(filepath.Join(path, file.Name(), subFile.Name(), "user.json")); err == nil { | |||||
count++ | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
return count, nil | |||||
} | |||||
func cmdAuth(c *cli.Context) error { | |||||
err := commands.CmdAuth(c) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// Get the username from the command, just like commands.CmdAuth does | |||||
username := c.Args().Get(0) | |||||
// Update config if this is user's first auth | |||||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
log.Errorln("Not saving config. Unable to load config: %s", err) | |||||
return err | |||||
} | |||||
if cfg.Default.Host == "" && cfg.Default.User == "" { | |||||
// This is user's first auth, so save defaults | |||||
cfg.Default.Host = api.HostURL(c) | |||||
cfg.Default.User = username | |||||
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) | |||||
if err != nil { | |||||
log.Errorln("Not saving config. Unable to save config: %s", err) | |||||
return err | |||||
} | |||||
fmt.Printf("Set %s on %s as default account.\n", username, c.GlobalString("host")) | |||||
} | |||||
return nil | |||||
} | |||||
func cmdLogOut(c *cli.Context) error { | |||||
err := commands.CmdLogOut(c) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// Remove this from config if it's the default account | |||||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
log.Errorln("Not updating config. Unable to load: %s", err) | |||||
return err | |||||
} | |||||
username, err := config.CurrentUser(c) | |||||
if err != nil { | |||||
log.Errorln("Not updating config. Unable to load current user: %s", err) | |||||
return err | |||||
} | |||||
reqHost := api.HostURL(c) | |||||
if reqHost == "" { | |||||
// No --host given, so we're using the default host | |||||
reqHost = cfg.Default.Host | |||||
} | |||||
if cfg.Default.Host == reqHost && cfg.Default.User == username { | |||||
// We're logging out of default username + host, so remove from config file | |||||
cfg.Default.Host = "" | |||||
cfg.Default.User = "" | |||||
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) | |||||
if err != nil { | |||||
log.Errorln("Not updating config. Unable to save config: %s", err) | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} |
@@ -0,0 +1,5 @@ | |||||
// +build !windows | |||||
package main | |||||
const configDir = ".writefreely" |
@@ -0,0 +1,5 @@ | |||||
// +build windows | |||||
package main | |||||
const configDir = "WriteFreely" |
@@ -0,0 +1,16 @@ | |||||
package main | |||||
import ( | |||||
"gopkg.in/urfave/cli.v1" | |||||
) | |||||
var flags = []cli.Flag{ | |||||
cli.StringFlag{ | |||||
Name: "host, H", | |||||
Usage: "Operate against a custom hostname", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "user, u", | |||||
Usage: "Use authenticated user, other than default", | |||||
}, | |||||
} |
@@ -0,0 +1,265 @@ | |||||
package main | |||||
import ( | |||||
"os" | |||||
"github.com/writeas/writeas-cli/commands" | |||||
"github.com/writeas/writeas-cli/config" | |||||
cli "gopkg.in/urfave/cli.v1" | |||||
) | |||||
func main() { | |||||
appInfo := map[string]string{ | |||||
"configDir": configDir, | |||||
"version": "1.0", | |||||
} | |||||
config.DirMustExist(config.UserDataDir(appInfo["configDir"])) | |||||
cli.VersionFlag = cli.BoolFlag{ | |||||
Name: "version, V", | |||||
Usage: "print the version", | |||||
} | |||||
// Run the app | |||||
app := cli.NewApp() | |||||
app.Name = "wf" | |||||
app.Version = appInfo["version"] | |||||
app.Usage = "Publish to any WriteFreely instance from the command-line." | |||||
// TODO: who is the author? the contributors? link to GH? | |||||
app.Authors = []cli.Author{ | |||||
{ | |||||
Name: "Write.as", | |||||
Email: "hello@write.as", | |||||
}, | |||||
} | |||||
app.ExtraInfo = func() map[string]string { | |||||
return appInfo | |||||
} | |||||
app.Action = requireAuth(commands.CmdPost, "publish") | |||||
app.Flags = append(config.PostFlags, flags...) | |||||
app.Commands = []cli.Command{ | |||||
{ | |||||
Name: "post", | |||||
Usage: "Alias for default action: create post from stdin", | |||||
Action: requireAuth(commands.CmdPost, "publish"), | |||||
Flags: config.PostFlags, | |||||
Description: `Create a new post on WriteFreely from stdin. | |||||
Use the --code flag to indicate that the post should use syntax | |||||
highlighting. Or use the --font [value] argument to set the post's | |||||
appearance, where [value] is mono, monospace (default), wrap (monospace | |||||
font with word wrapping), serif, or sans.`, | |||||
}, | |||||
{ | |||||
Name: "new", | |||||
Usage: "Compose a new post from the command-line and publish", | |||||
Description: `An alternative to piping data to the program. | |||||
On Windows, this will use 'copy con' to start reading what you input from the | |||||
prompt. Press F6 or Ctrl-Z then Enter to end input. | |||||
On *nix, this will use the best available text editor, starting with the | |||||
value set to the WRITEAS_EDITOR or EDITOR environment variable, or vim, or | |||||
finally nano. | |||||
Use the --code flag to indicate that the post should use syntax | |||||
highlighting. Or use the --font [value] argument to set the post's | |||||
appearance, where [value] is mono, monospace (default), wrap (monospace | |||||
font with word wrapping), serif, or sans. | |||||
If posting fails for any reason, 'wf' will show you the temporary file | |||||
location and how to pipe it to 'wf' to retry.`, | |||||
Action: requireAuth(commands.CmdNew, "publish"), | |||||
Flags: config.PostFlags, | |||||
}, | |||||
{ | |||||
Name: "publish", | |||||
Usage: "Publish a file", | |||||
Action: requireAuth(commands.CmdPublish, "publish"), | |||||
Flags: config.PostFlags, | |||||
}, | |||||
{ | |||||
Name: "delete", | |||||
Usage: "Delete a post", | |||||
Action: requireAuth(commands.CmdDelete, "delete a post"), | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Delete via Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Make the operation more talkative", | |||||
}, | |||||
}, | |||||
}, | |||||
{ | |||||
Name: "update", | |||||
Usage: "Update (overwrite) a post", | |||||
Action: requireAuth(commands.CmdUpdate, "update a post"), | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Update via Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "code", | |||||
Usage: "Specifies this post is code", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "font", | |||||
Usage: "Sets post font to given value", | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Make the operation more talkative", | |||||
}, | |||||
}, | |||||
}, | |||||
{ | |||||
Name: "get", | |||||
Usage: "Read a raw post", | |||||
Action: commands.CmdGet, | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Get from Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Make the operation more talkative", | |||||
}, | |||||
}, | |||||
}, | |||||
{ | |||||
Name: "posts", | |||||
Usage: "List all of your posts", | |||||
Description: "This will list only local posts.", | |||||
Action: requireAuth(commands.CmdListPosts, "view posts"), | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "id", | |||||
Usage: "Show list with post IDs (default)", | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "md", | |||||
Usage: "Use with --url to return URLs with Markdown enabled", | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "url", | |||||
Usage: "Show list with URLs", | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Show verbose post listing, including Edit Tokens", | |||||
}, | |||||
}, | |||||
}, { | |||||
Name: "blogs", | |||||
Usage: "List blogs", | |||||
Action: requireAuth(commands.CmdCollections, "view blogs"), | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Authenticate via Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "url", | |||||
Usage: "Show list with URLs", | |||||
}, | |||||
}, | |||||
}, { | |||||
Name: "claim", | |||||
Usage: "Claim local unsynced posts", | |||||
Action: requireAuth(commands.CmdClaim, "claim unsynced posts"), | |||||
Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: wf posts.", | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Authenticate via Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Make the operation more talkative", | |||||
}, | |||||
}, | |||||
}, { | |||||
Name: "auth", | |||||
Usage: "Authenticate with a WriteFreely instance", | |||||
Action: cmdAuth, | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Authenticate via Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Make the operation more talkative", | |||||
}, | |||||
}, | |||||
}, | |||||
{ | |||||
Name: "logout", | |||||
Usage: "Log out of a WriteFreely instance", | |||||
Action: requireAuth(cmdLogOut, "logout"), | |||||
Flags: []cli.Flag{ | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | |||||
Usage: "Authenticate via Tor hidden service", | |||||
}, | |||||
cli.IntFlag{ | |||||
Name: "tor-port", | |||||
Usage: "Use a different port to connect to Tor", | |||||
Value: 9150, | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "verbose, v", | |||||
Usage: "Make the operation more talkative", | |||||
}, | |||||
}, | |||||
}, | |||||
} | |||||
cli.CommandHelpTemplate = `NAME: | |||||
{{.Name}} - {{.Usage}} | |||||
USAGE: | |||||
wf {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} | |||||
DESCRIPTION: | |||||
{{.Description}}{{end}}{{if .Flags}} | |||||
OPTIONS: | |||||
{{range .Flags}}{{.}} | |||||
{{end}}{{ end }} | |||||
` | |||||
app.Run(os.Args) | |||||
} |
@@ -2,6 +2,4 @@ | |||||
package main | package main | ||||
var appInfo = map[string]string{ | |||||
"configDir": ".writeas", | |||||
} | |||||
const configDir = ".writeas" |
@@ -2,6 +2,4 @@ | |||||
package main | package main | ||||
var appInfo = map[string]string{ | |||||
"configDir": "Write.as", | |||||
} | |||||
const configDir = "Write.as" |
@@ -0,0 +1,13 @@ | |||||
package main | |||||
import ( | |||||
"gopkg.in/urfave/cli.v1" | |||||
) | |||||
var flags = []cli.Flag{ | |||||
cli.StringFlag{ | |||||
Name: "user, u", | |||||
Hidden: true, | |||||
Value: "user", | |||||
}, | |||||
} |
@@ -5,12 +5,15 @@ import ( | |||||
"github.com/writeas/writeas-cli/commands" | "github.com/writeas/writeas-cli/commands" | ||||
"github.com/writeas/writeas-cli/config" | "github.com/writeas/writeas-cli/config" | ||||
"github.com/writeas/writeas-cli/log" | |||||
cli "gopkg.in/urfave/cli.v1" | cli "gopkg.in/urfave/cli.v1" | ||||
) | ) | ||||
func main() { | func main() { | ||||
initialize(appInfo["configDir"]) | |||||
appInfo := map[string]string{ | |||||
"configDir": configDir, | |||||
"version": "2.0", | |||||
} | |||||
config.DirMustExist(config.UserDataDir(appInfo["configDir"])) | |||||
cli.VersionFlag = cli.BoolFlag{ | cli.VersionFlag = cli.BoolFlag{ | ||||
Name: "version, V", | Name: "version, V", | ||||
Usage: "print the version", | Usage: "print the version", | ||||
@@ -19,7 +22,7 @@ func main() { | |||||
// Run the app | // Run the app | ||||
app := cli.NewApp() | app := cli.NewApp() | ||||
app.Name = "writeas" | app.Name = "writeas" | ||||
app.Version = config.Version | |||||
app.Version = appInfo["version"] | |||||
app.Usage = "Publish text quickly" | app.Usage = "Publish text quickly" | ||||
app.Authors = []cli.Author{ | app.Authors = []cli.Author{ | ||||
{ | { | ||||
@@ -31,7 +34,7 @@ func main() { | |||||
return appInfo | return appInfo | ||||
} | } | ||||
app.Action = commands.CmdPost | app.Action = commands.CmdPost | ||||
app.Flags = config.PostFlags | |||||
app.Flags = append(config.PostFlags, flags...) | |||||
app.Commands = []cli.Command{ | app.Commands = []cli.Command{ | ||||
{ | { | ||||
Name: "post", | Name: "post", | ||||
@@ -274,18 +277,3 @@ OPTIONS: | |||||
` | ` | ||||
app.Run(os.Args) | app.Run(os.Args) | ||||
} | } | ||||
func initialize(dataDirName string) { | |||||
// Ensure we have a data directory to use | |||||
if !config.DataDirExists(dataDirName) { | |||||
err := config.CreateDataDir(dataDirName) | |||||
if err != nil { | |||||
if config.Debug() { | |||||
panic(err) | |||||
} else { | |||||
log.Errorln("Error creating data directory: %s", err) | |||||
return | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -4,11 +4,13 @@ import ( | |||||
"fmt" | "fmt" | ||||
"io/ioutil" | "io/ioutil" | ||||
"os" | "os" | ||||
"strings" | |||||
"text/tabwriter" | "text/tabwriter" | ||||
"github.com/howeyc/gopass" | "github.com/howeyc/gopass" | ||||
"github.com/writeas/writeas-cli/api" | "github.com/writeas/writeas-cli/api" | ||||
"github.com/writeas/writeas-cli/config" | "github.com/writeas/writeas-cli/config" | ||||
"github.com/writeas/writeas-cli/executable" | |||||
"github.com/writeas/writeas-cli/log" | "github.com/writeas/writeas-cli/log" | ||||
cli "gopkg.in/urfave/cli.v1" | cli "gopkg.in/urfave/cli.v1" | ||||
) | ) | ||||
@@ -67,7 +69,7 @@ func CmdNew(c *cli.Context) error { | |||||
func CmdPublish(c *cli.Context) error { | func CmdPublish(c *cli.Context) error { | ||||
filename := c.Args().Get(0) | filename := c.Args().Get(0) | ||||
if filename == "" { | if filename == "" { | ||||
return cli.NewExitError("usage: writeas publish <filename>", 1) | |||||
return cli.NewExitError("usage: "+executable.Name()+" publish <filename>", 1) | |||||
} | } | ||||
content, err := ioutil.ReadFile(filename) | content, err := ioutil.ReadFile(filename) | ||||
if err != nil { | if err != nil { | ||||
@@ -92,16 +94,16 @@ func CmdDelete(c *cli.Context) error { | |||||
friendlyID := c.Args().Get(0) | friendlyID := c.Args().Get(0) | ||||
token := c.Args().Get(1) | token := c.Args().Get(1) | ||||
if friendlyID == "" { | if friendlyID == "" { | ||||
return cli.NewExitError("usage: writeas delete <postId> [<token>]", 1) | |||||
return cli.NewExitError("usage: "+executable.Name()+" delete <postId> [<token>]", 1) | |||||
} | } | ||||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
u, _ := config.LoadUser(c) | |||||
if token == "" { | if token == "" { | ||||
// Search for the token locally | // Search for the token locally | ||||
token = api.TokenFromID(c, friendlyID) | token = api.TokenFromID(c, friendlyID) | ||||
if token == "" && u == nil { | if token == "" && u == nil { | ||||
log.Errorln("Couldn't find an edit token locally. Did you create this post here?") | log.Errorln("Couldn't find an edit token locally. Did you create this post here?") | ||||
log.ErrorlnQuit("If you have an edit token, use: writeas delete %s <token>", friendlyID) | |||||
log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" delete %s <token>", friendlyID) | |||||
} | } | ||||
} | } | ||||
@@ -124,16 +126,16 @@ func CmdUpdate(c *cli.Context) error { | |||||
friendlyID := c.Args().Get(0) | friendlyID := c.Args().Get(0) | ||||
token := c.Args().Get(1) | token := c.Args().Get(1) | ||||
if friendlyID == "" { | if friendlyID == "" { | ||||
return cli.NewExitError("usage: writeas update <postId> [<token>]", 1) | |||||
return cli.NewExitError("usage: "+executable.Name()+" update <postId> [<token>]", 1) | |||||
} | } | ||||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
u, _ := config.LoadUser(c) | |||||
if token == "" { | if token == "" { | ||||
// Search for the token locally | // Search for the token locally | ||||
token = api.TokenFromID(c, friendlyID) | token = api.TokenFromID(c, friendlyID) | ||||
if token == "" && u == nil { | if token == "" && u == nil { | ||||
log.Errorln("Couldn't find an edit token locally. Did you create this post here?") | log.Errorln("Couldn't find an edit token locally. Did you create this post here?") | ||||
log.ErrorlnQuit("If you have an edit token, use: writeas update %s <token>", friendlyID) | |||||
log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" update %s <token>", friendlyID) | |||||
} | } | ||||
} | } | ||||
@@ -155,7 +157,7 @@ func CmdUpdate(c *cli.Context) error { | |||||
func CmdGet(c *cli.Context) error { | func CmdGet(c *cli.Context) error { | ||||
friendlyID := c.Args().Get(0) | friendlyID := c.Args().Get(0) | ||||
if friendlyID == "" { | if friendlyID == "" { | ||||
return cli.NewExitError("usage: writeas get <postId>", 1) | |||||
return cli.NewExitError("usage: "+executable.Name()+" get <postId>", 1) | |||||
} | } | ||||
if config.IsTor(c) { | if config.IsTor(c) { | ||||
@@ -175,7 +177,7 @@ func CmdAdd(c *cli.Context) error { | |||||
friendlyID := c.Args().Get(0) | friendlyID := c.Args().Get(0) | ||||
token := c.Args().Get(1) | token := c.Args().Get(1) | ||||
if friendlyID == "" || token == "" { | if friendlyID == "" || token == "" { | ||||
return cli.NewExitError("usage: writeas add <postId> <token>", 1) | |||||
return cli.NewExitError("usage: "+executable.Name()+" add <postId> <token>", 1) | |||||
} | } | ||||
err := api.AddPost(c, friendlyID, token) | err := api.AddPost(c, friendlyID, token) | ||||
@@ -192,7 +194,7 @@ func CmdListPosts(c *cli.Context) error { | |||||
posts := api.GetPosts(c) | posts := api.GetPosts(c) | ||||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
u, _ := config.LoadUser(c) | |||||
if u != nil { | if u != nil { | ||||
if config.IsTor(c) { | if config.IsTor(c) { | ||||
log.Info(c, "Getting posts via hidden service...") | log.Info(c, "Getting posts via hidden service...") | ||||
@@ -205,7 +207,11 @@ func CmdListPosts(c *cli.Context) error { | |||||
} | } | ||||
if len(remotePosts) > 0 { | if len(remotePosts) > 0 { | ||||
fmt.Println("Anonymous Posts") | |||||
if c.App.Name == "wf" { | |||||
fmt.Println("Draft Posts") | |||||
} else { | |||||
fmt.Println("Anonymous Posts") | |||||
} | |||||
if details { | if details { | ||||
identifier := "URL" | identifier := "URL" | ||||
if ids || !urls { | if ids || !urls { | ||||
@@ -261,9 +267,28 @@ func CmdListPosts(c *cli.Context) error { | |||||
} | } | ||||
func getPostURL(c *cli.Context, slug string) string { | func getPostURL(c *cli.Context, slug string) string { | ||||
base := config.WriteasBaseURL | |||||
if config.IsDev() { | |||||
base = config.DevBaseURL | |||||
var base string | |||||
if c.App.Name == "writeas" { | |||||
if config.IsDev() { | |||||
base = config.DevBaseURL | |||||
} else { | |||||
base = config.WriteasBaseURL | |||||
} | |||||
} else { | |||||
if host := api.HostURL(c); host != "" { | |||||
base = host | |||||
} else { | |||||
// TODO handle error, or load config globally, see T601 | |||||
// https://phabricator.write.as/T601 | |||||
cfg, _ := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||||
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { | |||||
base = cfg.Default.Host | |||||
} else { | |||||
base = "https://" + cfg.Default.Host | |||||
} | |||||
} | |||||
} | |||||
} | } | ||||
ext := "" | ext := "" | ||||
// Output URL in requested format | // Output URL in requested format | ||||
@@ -274,12 +299,12 @@ func getPostURL(c *cli.Context, slug string) string { | |||||
} | } | ||||
func CmdCollections(c *cli.Context) error { | func CmdCollections(c *cli.Context) error { | ||||
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
u, err := config.LoadUser(c) | |||||
if err != nil { | if err != nil { | ||||
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | ||||
} | } | ||||
if u == nil { | if u == nil { | ||||
return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth <username>", 1) | |||||
return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: "+executable.Name()+" auth <username>", 1) | |||||
} | } | ||||
if config.IsTor(c) { | if config.IsTor(c) { | ||||
log.Info(c, "Getting blogs via hidden service...") | log.Info(c, "Getting blogs via hidden service...") | ||||
@@ -309,12 +334,12 @@ func CmdCollections(c *cli.Context) error { | |||||
} | } | ||||
func CmdClaim(c *cli.Context) error { | func CmdClaim(c *cli.Context) error { | ||||
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
u, err := config.LoadUser(c) | |||||
if err != nil { | if err != nil { | ||||
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | ||||
} | } | ||||
if u == nil { | if u == nil { | ||||
return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: writeas auth <username>", 1) | |||||
return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: "+executable.Name()+" auth <username>", 1) | |||||
} | } | ||||
localPosts := api.GetPosts(c) | localPosts := api.GetPosts(c) | ||||
@@ -348,7 +373,7 @@ func CmdClaim(c *cli.Context) error { | |||||
log.Info(c, "%sOK", status) | log.Info(c, "%sOK", status) | ||||
okCount++ | okCount++ | ||||
// only delete local if successful | // only delete local if successful | ||||
api.RemovePost(c.App.ExtraInfo()["configDir"], id) | |||||
api.RemovePost(c, id) | |||||
} | } | ||||
} | } | ||||
log.Info(c, "%d claimed, %d failed", okCount, errCount) | log.Info(c, "%d claimed, %d failed", okCount, errCount) | ||||
@@ -356,19 +381,31 @@ func CmdClaim(c *cli.Context) error { | |||||
} | } | ||||
func CmdAuth(c *cli.Context) error { | func CmdAuth(c *cli.Context) error { | ||||
username := c.Args().Get(0) | |||||
if username == "" && c.GlobalIsSet("user") { | |||||
username = c.GlobalString("user") | |||||
} | |||||
// Check configuration | // Check configuration | ||||
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
u, err := config.LoadUser(c) | |||||
if err != nil { | if err != nil { | ||||
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | ||||
} | } | ||||
if u != nil && u.AccessToken != "" { | |||||
return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: writeas logout", 1) | |||||
if u != nil && u.AccessToken != "" && username == u.User.Username { | |||||
return cli.NewExitError("You're already authenticated as "+u.User.Username, 1) | |||||
} | } | ||||
// Validate arguments and get password | // Validate arguments and get password | ||||
username := c.Args().Get(0) | |||||
if username == "" { | if username == "" { | ||||
return cli.NewExitError("usage: writeas auth <username>", 1) | |||||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
return cli.NewExitError(fmt.Sprintf("Failed to load config: %v", err), 1) | |||||
} | |||||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||||
username = cfg.Default.User | |||||
fmt.Printf("No user provided, using default user %s for host %s...\n", cfg.Default.User, cfg.Default.Host) | |||||
} else { | |||||
return cli.NewExitError("usage: "+executable.Name()+" auth <username>", 1) | |||||
} | |||||
} | } | ||||
fmt.Print("Password: ") | fmt.Print("Password: ") | ||||
@@ -8,32 +8,43 @@ import ( | |||||
) | ) | ||||
const ( | const ( | ||||
UserConfigFile = "config.ini" | |||||
// ConfigFile is the full filename for application configuration files | |||||
ConfigFile = "config.ini" | |||||
) | ) | ||||
type ( | type ( | ||||
// APIConfig is not currently used | |||||
APIConfig struct { | APIConfig struct { | ||||
} | } | ||||
// PostsConfig stores the directory for the user post cache | |||||
PostsConfig struct { | PostsConfig struct { | ||||
Directory string `ini:"directory"` | Directory string `ini:"directory"` | ||||
} | } | ||||
UserConfig struct { | |||||
API APIConfig `ini:"api"` | |||||
Posts PostsConfig `ini:"posts"` | |||||
// DefaultConfig stores the default host and user to authenticate with | |||||
DefaultConfig struct { | |||||
Host string `ini:"host"` | |||||
User string `ini:"user"` | |||||
} | |||||
// Config represents the entire base configuration | |||||
Config struct { | |||||
API APIConfig `ini:"api"` | |||||
Default DefaultConfig `ini:"default"` | |||||
Posts PostsConfig `ini:"posts"` | |||||
} | } | ||||
) | ) | ||||
func LoadConfig(dataDir string) (*UserConfig, error) { | |||||
func LoadConfig(dataDir string) (*Config, error) { | |||||
// TODO: load config to var shared across app | // TODO: load config to var shared across app | ||||
cfg, err := ini.LooseLoad(filepath.Join(dataDir, UserConfigFile)) | |||||
cfg, err := ini.LooseLoad(filepath.Join(dataDir, ConfigFile)) | |||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
// Parse INI file | // Parse INI file | ||||
uc := &UserConfig{} | |||||
uc := &Config{} | |||||
err = cfg.MapTo(uc) | err = cfg.MapTo(uc) | ||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
@@ -41,14 +52,14 @@ func LoadConfig(dataDir string) (*UserConfig, error) { | |||||
return uc, nil | return uc, nil | ||||
} | } | ||||
func SaveConfig(dataDir string, uc *UserConfig) error { | |||||
func SaveConfig(dataDir string, uc *Config) error { | |||||
cfg := ini.Empty() | cfg := ini.Empty() | ||||
err := ini.ReflectFrom(cfg, uc) | err := ini.ReflectFrom(cfg, uc) | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
return cfg.SaveTo(filepath.Join(dataDir, UserConfigFile)) | |||||
return cfg.SaveTo(filepath.Join(dataDir, ConfigFile)) | |||||
} | } | ||||
var editors = []string{"WRITEAS_EDITOR", "EDITOR"} | var editors = []string{"WRITEAS_EDITOR", "EDITOR"} | ||||
@@ -5,16 +5,36 @@ import ( | |||||
"path/filepath" | "path/filepath" | ||||
"github.com/writeas/writeas-cli/fileutils" | "github.com/writeas/writeas-cli/fileutils" | ||||
"github.com/writeas/writeas-cli/log" | |||||
) | ) | ||||
// UserDataDir returns a platform specific directory under the user's home | |||||
// directory | |||||
func UserDataDir(dataDirName string) string { | func UserDataDir(dataDirName string) string { | ||||
return filepath.Join(parentDataDir(), dataDirName) | return filepath.Join(parentDataDir(), dataDirName) | ||||
} | } | ||||
func DataDirExists(dataDirName string) bool { | |||||
return fileutils.Exists(UserDataDir(dataDirName)) | |||||
func dataDirExists(dataDirName string) bool { | |||||
return fileutils.Exists(dataDirName) | |||||
} | } | ||||
func CreateDataDir(dataDirName string) error { | |||||
return os.Mkdir(UserDataDir(dataDirName), 0700) | |||||
func createDataDir(dataDirName string) error { | |||||
return os.Mkdir(dataDirName, 0700) | |||||
} | |||||
// DirMustExist checks for a directory, creates it if not found and either | |||||
// panics or logs and error depending on the status of Debug | |||||
func DirMustExist(dataDirName string) { | |||||
// Ensure we have a data directory to use | |||||
if !dataDirExists(dataDirName) { | |||||
err := createDataDir(dataDirName) | |||||
if err != nil { | |||||
if Debug() { | |||||
panic(err) | |||||
} else { | |||||
log.Errorln("Error creating data directory: %s", err) | |||||
return | |||||
} | |||||
} | |||||
} | |||||
} | } |
@@ -7,6 +7,7 @@ import ( | |||||
"os/exec" | "os/exec" | ||||
homedir "github.com/mitchellh/go-homedir" | homedir "github.com/mitchellh/go-homedir" | ||||
"github.com/writeas/writeas-cli/executable" | |||||
) | ) | ||||
const ( | const ( | ||||
@@ -39,5 +40,5 @@ func EditPostCmd(fname string) *exec.Cmd { | |||||
} | } | ||||
func MessageRetryCompose(fname string) string { | func MessageRetryCompose(fname string) string { | ||||
return fmt.Sprintf("To retry this post, run:\n cat %s | writeas", fname) | |||||
return fmt.Sprintf("To retry this post, run:\n cat %s | %s", fname, executable.Name()) | |||||
} | } |
@@ -6,6 +6,8 @@ import ( | |||||
"fmt" | "fmt" | ||||
"os" | "os" | ||||
"os/exec" | "os/exec" | ||||
"github.com/writeas/writeas-cli/executable" | |||||
) | ) | ||||
const ( | const ( | ||||
@@ -22,5 +24,5 @@ func EditPostCmd(fname string) *exec.Cmd { | |||||
} | } | ||||
func MessageRetryCompose(fname string) string { | func MessageRetryCompose(fname string) string { | ||||
return fmt.Sprintf("To retry this post, run:\n type %s | writeas.exe", fname) | |||||
return fmt.Sprintf("To retry this post, run:\n type %s | %s", fname, executable.Name()) | |||||
} | } |
@@ -12,6 +12,10 @@ var PostFlags = []cli.Flag{ | |||||
Value: "", | Value: "", | ||||
}, | }, | ||||
cli.BoolFlag{ | cli.BoolFlag{ | ||||
Name: "insecure", | |||||
Usage: "Send request insecurely.", | |||||
}, | |||||
cli.BoolFlag{ | |||||
Name: "tor, t", | Name: "tor, t", | ||||
Usage: "Perform action on Tor hidden service", | Usage: "Perform action on Tor hidden service", | ||||
}, | }, | ||||
@@ -1,6 +1,8 @@ | |||||
package config | package config | ||||
import ( | import ( | ||||
"strings" | |||||
"github.com/cloudfoundry/jibber_jabber" | "github.com/cloudfoundry/jibber_jabber" | ||||
"github.com/writeas/writeas-cli/log" | "github.com/writeas/writeas-cli/log" | ||||
cli "gopkg.in/urfave/cli.v1" | cli "gopkg.in/urfave/cli.v1" | ||||
@@ -8,8 +10,8 @@ import ( | |||||
// Application constants. | // Application constants. | ||||
const ( | const ( | ||||
Version = "2.0" | |||||
defaultUserAgent = "writeas-cli v" + Version | |||||
writeasUserAgent = "writeas-cli v" | |||||
wfUserAgent = "wf-cli v" | |||||
// Defaults for posts on Write.as. | // Defaults for posts on Write.as. | ||||
DefaultFont = PostFontMono | DefaultFont = PostFontMono | ||||
WriteasBaseURL = "https://write.as" | WriteasBaseURL = "https://write.as" | ||||
@@ -19,11 +21,16 @@ const ( | |||||
) | ) | ||||
func UserAgent(c *cli.Context) string { | func UserAgent(c *cli.Context) string { | ||||
client := wfUserAgent | |||||
if c.App.Name == "writeas" { | |||||
client = writeasUserAgent | |||||
} | |||||
ua := c.String("user-agent") | ua := c.String("user-agent") | ||||
if ua == "" { | if ua == "" { | ||||
return defaultUserAgent | |||||
return client + c.App.ExtraInfo()["version"] | |||||
} | } | ||||
return ua + " (" + defaultUserAgent + ")" | |||||
return ua + " (" + client + c.App.ExtraInfo()["version"] + ")" | |||||
} | } | ||||
func IsTor(c *cli.Context) bool { | func IsTor(c *cli.Context) bool { | ||||
@@ -37,6 +44,18 @@ func TorPort(c *cli.Context) int { | |||||
return torPort | return torPort | ||||
} | } | ||||
func TorURL(c *cli.Context) string { | |||||
flagHost := c.String("host") | |||||
if flagHost != "" && strings.HasSuffix(flagHost, "onion") { | |||||
return flagHost | |||||
} | |||||
cfg, _ := LoadConfig(c.App.ExtraInfo()["configDir"]) | |||||
if cfg != nil && cfg.Default.Host != "" && strings.HasSuffix(cfg.Default.Host, "onion") { | |||||
return cfg.Default.Host | |||||
} | |||||
return TorBaseURL | |||||
} | |||||
func Language(c *cli.Context, auto bool) string { | func Language(c *cli.Context, auto bool) string { | ||||
if l := c.String("lang"); l != "" { | if l := c.String("lang"); l != "" { | ||||
return l | return l | ||||
@@ -62,3 +81,28 @@ func Collection(c *cli.Context) string { | |||||
} | } | ||||
return "" | return "" | ||||
} | } | ||||
// HostDirectory returns the sub directory string for the host. Order of | |||||
// precedence is a host flag if any, then the configured default, if any | |||||
func HostDirectory(c *cli.Context) (string, error) { | |||||
cfg, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
// flag takes precedence over defaults | |||||
if hostFlag := c.GlobalString("host"); hostFlag != "" { | |||||
if parts := strings.Split(hostFlag, "://"); len(parts) > 1 { | |||||
return parts[1], nil | |||||
} | |||||
return hostFlag, nil | |||||
} | |||||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||||
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { | |||||
return parts[1], nil | |||||
} | |||||
return cfg.Default.Host, nil | |||||
} | |||||
return "", nil | |||||
} |
@@ -7,12 +7,23 @@ import ( | |||||
writeas "github.com/writeas/go-writeas/v2" | writeas "github.com/writeas/go-writeas/v2" | ||||
"github.com/writeas/writeas-cli/fileutils" | "github.com/writeas/writeas-cli/fileutils" | ||||
"gopkg.in/urfave/cli.v1" | |||||
) | ) | ||||
const UserFile = "user.json" | |||||
func LoadUser(dataDir string) (*writeas.AuthUser, error) { | |||||
fname := filepath.Join(dataDir, UserFile) | |||||
func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { | |||||
dir, err := UserHostDir(c) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
DirMustExist(dir) | |||||
username, err := CurrentUser(c) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if username == "user" { | |||||
username = "" | |||||
} | |||||
fname := filepath.Join(dir, username, "user.json") | |||||
userJSON, err := ioutil.ReadFile(fname) | userJSON, err := ioutil.ReadFile(fname) | ||||
if err != nil { | if err != nil { | ||||
if !fileutils.Exists(fname) { | if !fileutils.Exists(fname) { | ||||
@@ -31,17 +42,130 @@ func LoadUser(dataDir string) (*writeas.AuthUser, error) { | |||||
return u, nil | return u, nil | ||||
} | } | ||||
func SaveUser(dataDir string, u *writeas.AuthUser) error { | |||||
func DeleteUser(c *cli.Context) error { | |||||
dir, err := UserHostDir(c) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
username, err := CurrentUser(c) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if username == "user" { | |||||
username = "" | |||||
} | |||||
// Delete user data | |||||
err = fileutils.DeleteFile(filepath.Join(dir, username, "user.json")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// Do additional cleanup in wf-cli | |||||
if c.App.Name == "wf" { | |||||
// Delete user dir if it's empty | |||||
userEmpty, err := fileutils.IsEmpty(filepath.Join(dir, username)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !userEmpty { | |||||
return nil | |||||
} | |||||
err = fileutils.DeleteFile(filepath.Join(dir, username)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// Delete host dir if it's empty | |||||
hostEmpty, err := fileutils.IsEmpty(dir) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !hostEmpty { | |||||
return nil | |||||
} | |||||
err = fileutils.DeleteFile(dir) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func SaveUser(c *cli.Context, u *writeas.AuthUser) error { | |||||
// Marshal struct into pretty-printed JSON | // Marshal struct into pretty-printed JSON | ||||
userJSON, err := json.MarshalIndent(u, "", " ") | userJSON, err := json.MarshalIndent(u, "", " ") | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
dir, err := UserHostDir(c) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// Save file | // Save file | ||||
err = ioutil.WriteFile(filepath.Join(dataDir, UserFile), userJSON, 0600) | |||||
username, err := CurrentUser(c) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if username != "user" { | |||||
dir = filepath.Join(dir, u.User.Username) | |||||
} | |||||
DirMustExist(dir) | |||||
err = ioutil.WriteFile(filepath.Join(dir, "user.json"), userJSON, 0600) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
return nil | return nil | ||||
} | } | ||||
// UserHostDir returns the path to the user data directory with the host based | |||||
// subpath if the host flag is set | |||||
func UserHostDir(c *cli.Context) (string, error) { | |||||
dataDir := UserDataDir(c.App.ExtraInfo()["configDir"]) | |||||
hostDir, err := HostDirectory(c) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
return filepath.Join(dataDir, hostDir), nil | |||||
} | |||||
// CurrentUser returns the username of the user taking action in the current | |||||
// cli.Context. | |||||
func CurrentUser(c *cli.Context) (string, error) { | |||||
if c.App.Name == "writeas" { | |||||
return "user", nil | |||||
} | |||||
// Use user flag value | |||||
if c.GlobalString("user") != "" { | |||||
return c.GlobalString("user"), nil | |||||
} | |||||
// Load host-level config, if host flag is set | |||||
hostDir, err := UserHostDir(c) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
cfg, err := LoadConfig(hostDir) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
if cfg.Default.User == "" { | |||||
// Load app-level config | |||||
globalCFG, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
// only use global defaults when both are set and no host flag | |||||
if globalCFG.Default.User != "" && | |||||
globalCFG.Default.Host != "" && | |||||
!c.GlobalIsSet("host") { | |||||
cfg = globalCFG | |||||
} | |||||
} | |||||
return cfg.Default.User, nil | |||||
} |
@@ -0,0 +1,13 @@ | |||||
// Package executable holds utility functions that assist both CLI executables, | |||||
// writeas and wf. | |||||
package executable | |||||
import ( | |||||
"os" | |||||
"path" | |||||
) | |||||
func Name() string { | |||||
n := os.Args[0] | |||||
return path.Base(n) | |||||
} |
@@ -3,6 +3,7 @@ package fileutils | |||||
import ( | import ( | ||||
"bufio" | "bufio" | ||||
"fmt" | "fmt" | ||||
"io" | |||||
"os" | "os" | ||||
"strings" | "strings" | ||||
) | ) | ||||
@@ -109,3 +110,18 @@ func FindLine(p, startsWith string) string { | |||||
func DeleteFile(p string) error { | func DeleteFile(p string) error { | ||||
return os.Remove(p) | return os.Remove(p) | ||||
} | } | ||||
// IsEmpty returns whether or not the given directory is empty | |||||
func IsEmpty(d string) (bool, error) { | |||||
f, err := os.Open(d) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
defer f.Close() | |||||
_, err = f.Readdirnames(1) | |||||
if err == io.EOF { | |||||
return true, nil | |||||
} | |||||
return false, err | |||||
} |
@@ -14,7 +14,7 @@ require ( | |||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect | ||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect | ||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect | ||||
github.com/writeas/go-writeas/v2 v2.0.0 | |||||
github.com/writeas/go-writeas/v2 v2.0.2 | |||||
github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect | github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect | ||||
github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 | github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 | ||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect | ||||
@@ -33,8 +33,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE | |||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= | ||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= | ||||
github.com/writeas/go-writeas/v2 v2.0.0 h1:KjDI5bQSAIH0IzkKW3uGoY98I1A4DrBsSqBklgyOvHw= | |||||
github.com/writeas/go-writeas/v2 v2.0.0/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= | |||||
github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk= | |||||
github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= | |||||
github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI= | github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI= | ||||
github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8= | github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8= | ||||
github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= | github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= | ||||
@@ -10,7 +10,7 @@ import ( | |||||
// Info logs general diagnostic messages, shown only when the -v or --verbose | // Info logs general diagnostic messages, shown only when the -v or --verbose | ||||
// flag is provided. | // flag is provided. | ||||
func Info(c *cli.Context, s string, p ...interface{}) { | func Info(c *cli.Context, s string, p ...interface{}) { | ||||
if c.Bool("v") || c.Bool("verbose") { | |||||
if c.Bool("v") || c.Bool("verbose") || c.GlobalBool("v") || c.GlobalBool("verbose") { | |||||
fmt.Fprintf(os.Stderr, s+"\n", p...) | fmt.Fprintf(os.Stderr, s+"\n", p...) | ||||
} | } | ||||
} | } | ||||