dnote/cmd/sync/sync.go

181 lines
4.2 KiB
Go

package sync
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/dnote-io/cli/core"
"github.com/dnote-io/cli/infra"
"github.com/dnote-io/cli/log"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var example = `
dnote sync`
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "sync",
Aliases: []string{"s"},
Short: "Sync dnote with the dnote server",
Example: example,
RunE: newRun(ctx),
}
return cmd
}
type responseData struct {
Actions []core.Action `json:"actions"`
Bookmark int `json:"bookmark"`
}
type syncPayload struct {
Bookmark int `json:"bookmark"`
Actions []byte `json:"actions"` // gziped
}
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
config, err := core.ReadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the config")
}
timestamp, err := core.ReadTimestamp(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the timestamp")
}
actions, err := core.ReadActionLog(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the action log")
}
if config.APIKey == "" {
fmt.Println("Login required. Please run `dnote login`")
return nil
}
payload, err := getPayload(actions, timestamp)
if err != nil {
return errors.Wrap(err, "Failed to get dnote payload")
}
log.Infof("writing changes (total %d).", len(actions))
resp, err := postActions(ctx, config.APIKey, payload)
if err != nil {
return errors.Wrap(err, "Failed to post to the server ")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "Failed to read failed response body")
}
if resp.StatusCode != http.StatusOK {
bodyStr := string(body)
fmt.Println("")
return errors.Errorf("Server error: %s", bodyStr)
}
fmt.Println(" done.")
var respData responseData
err = json.Unmarshal(body, &respData)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal payload")
}
log.Infof("resolving delta (total %d).", len(respData.Actions))
err = core.ReduceAll(ctx, respData.Actions)
if err != nil {
return errors.Wrap(err, "Failed to reduce returned actions")
}
fmt.Println(" done.")
// Update bookmark
ts, err := core.ReadTimestamp(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the timestamp")
}
ts.Bookmark = respData.Bookmark
err = core.WriteTimestamp(ctx, ts)
if err != nil {
return errors.Wrap(err, "Failed to update bookmark")
}
log.Success("success\n")
if err := core.ClearActionLog(ctx); err != nil {
return errors.Wrap(err, "Failed to clear the action log")
}
return nil
}
}
func getPayload(actions []core.Action, timestamp infra.Timestamp) (*bytes.Buffer, error) {
compressedActions, err := compressActions(actions)
if err != nil {
return &bytes.Buffer{}, errors.Wrap(err, "Failed to compress actions")
}
payload := syncPayload{
Bookmark: timestamp.Bookmark,
Actions: compressedActions,
}
b, err := json.Marshal(payload)
if err != nil {
return &bytes.Buffer{}, errors.Wrap(err, "Failed to marshal paylaod into JSON")
}
ret := bytes.NewBuffer(b)
return ret, nil
}
func compressActions(actions []core.Action) ([]byte, error) {
b, err := json.Marshal(&actions)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal actions into JSON")
}
var buf bytes.Buffer
g := gzip.NewWriter(&buf)
_, err = g.Write(b)
if err != nil {
return nil, errors.Wrap(err, "Failed to write to gzip writer")
}
if err = g.Close(); err != nil {
return nil, errors.Wrap(err, "Failed to close gzip writer")
}
return buf.Bytes(), nil
}
func postActions(ctx infra.DnoteCtx, APIKey string, payload io.Reader) (*http.Response, error) {
endpoint := fmt.Sprintf("%s/v1/sync", ctx.APIEndpoint)
req, err := http.NewRequest("POST", endpoint, payload)
if err != nil {
return &http.Response{}, errors.Wrap(err, "Failed to construct HTTP request")
}
req.Header.Set("Authorization", APIKey)
req.Header.Set("CLI-Version", core.Version)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return &http.Response{}, errors.Wrap(err, "Failed to make request")
}
return resp, nil
}