mirror of
https://github.com/ddworken/hishtory.git
synced 2025-06-26 23:12:09 +02:00
split into local client and remote client, and add tests
This commit is contained in:
parent
6fbad3a194
commit
a523504c40
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Installation:
|
||||||
|
|
||||||
|
```
|
||||||
|
export PROMPT_COMMAND='~/.hishtory-client upload $? "`history 1`"'
|
||||||
|
```
|
207
client/client.go
207
client/client.go
@ -1,207 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rodaine/table"
|
|
||||||
|
|
||||||
"github.com/ddworken/hishtory/shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "upload":
|
|
||||||
upload()
|
|
||||||
case "query":
|
|
||||||
query()
|
|
||||||
case "init":
|
|
||||||
setup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() {
|
|
||||||
userSecret := uuid.Must(uuid.NewRandom()).String()
|
|
||||||
if len(os.Args) > 2 && os.Args[2] != "" {
|
|
||||||
userSecret = os.Args[2]
|
|
||||||
}
|
|
||||||
fmt.Println("Setting secret hishtory key to " + string(userSecret))
|
|
||||||
|
|
||||||
homedir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = os.WriteFile(path.Join(homedir, ".hishtory.secret"), []byte(userSecret), 0400)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUserSecret() (string, error) {
|
|
||||||
homedir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read secret hishtory key: %v", err)
|
|
||||||
}
|
|
||||||
secret, err := os.ReadFile(path.Join(homedir, ".hishtory.secret"))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read secret hishtory key: %v", err)
|
|
||||||
}
|
|
||||||
return string(secret), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServerHostname() string {
|
|
||||||
if server := os.Getenv("HISHTORY_SERVER"); server != "" {
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
return "http://localhost:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
func query() {
|
|
||||||
userSecret, err := getUserSecret()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", getServerHostname()+"/api/v1/search", nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
q := req.URL.Query()
|
|
||||||
q.Add("query", strings.Join(os.Args[2:], " "))
|
|
||||||
q.Add("user_secret", userSecret)
|
|
||||||
q.Add("limit", "25")
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
resp_body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if resp.Status != "200 OK" {
|
|
||||||
panic("search API returned invalid result. status=" + resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []*shared.HistoryEntry
|
|
||||||
err = json.Unmarshal(resp_body, &data)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
display(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func display(results []*shared.HistoryEntry) {
|
|
||||||
headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
|
|
||||||
tbl := table.New("Hostname", "CWD", "Timestamp", "Exit Code", "Command")
|
|
||||||
tbl.WithHeaderFormatter(headerFmt)
|
|
||||||
|
|
||||||
for _, result := range results {
|
|
||||||
tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
tbl.Print()
|
|
||||||
}
|
|
||||||
|
|
||||||
func upload() {
|
|
||||||
var entry shared.HistoryEntry
|
|
||||||
|
|
||||||
// exitCode
|
|
||||||
exitCode, err := strconv.Atoi(os.Args[2])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
entry.ExitCode = exitCode
|
|
||||||
|
|
||||||
// user
|
|
||||||
user, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
entry.LocalUsername = user.Username
|
|
||||||
|
|
||||||
// cwd
|
|
||||||
cwd, err := getCwd()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
entry.CurrentWorkingDirectory = cwd
|
|
||||||
|
|
||||||
// TODO(ddworken): start time
|
|
||||||
|
|
||||||
// end time
|
|
||||||
entry.EndTime = time.Now()
|
|
||||||
|
|
||||||
// command
|
|
||||||
cmd, err := getLastCommand(os.Args[3])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
entry.Command = cmd
|
|
||||||
|
|
||||||
// hostname
|
|
||||||
hostname, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
entry.Hostname = hostname
|
|
||||||
|
|
||||||
// user secret
|
|
||||||
userSecret, err := getUserSecret()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
entry.UserSecret = userSecret
|
|
||||||
|
|
||||||
err = send(entry)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCwd() (string, error) {
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get cwd for last command: %v", err)
|
|
||||||
}
|
|
||||||
homedir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get user's home directory: %v", err)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(cwd, homedir) {
|
|
||||||
return strings.Replace(cwd, homedir, "~", 1), nil
|
|
||||||
}
|
|
||||||
return cwd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLastCommand(history string) (string, error) {
|
|
||||||
return strings.TrimSpace(strings.SplitN(strings.TrimSpace(history), " ", 2)[1]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func send(entry shared.HistoryEntry) error {
|
|
||||||
jsonValue, err := json.Marshal(entry)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal HistoryEntry as json: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = http.Post(getServerHostname()+"/api/v1/submit", "application/json", bytes.NewBuffer(jsonValue))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send HistoryEntry to api: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
46
clients/local/client.go
Normal file
46
clients/local/client.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ddworken/hishtory/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "saveHistoryEntry":
|
||||||
|
saveHistoryEntry()
|
||||||
|
case "query":
|
||||||
|
query()
|
||||||
|
case "init":
|
||||||
|
err := shared.Setup(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerHostname() string {
|
||||||
|
if server := os.Getenv("HISHTORY_SERVER"); server != "" {
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
return "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
func query() {
|
||||||
|
// TODO(ddworken)
|
||||||
|
var data []*shared.HistoryEntry
|
||||||
|
shared.DisplayResults(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveHistoryEntry() {
|
||||||
|
entry, err := shared.BuildHistoryEntry(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = shared.Persist(*entry)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
98
clients/remote/client.go
Normal file
98
clients/remote/client.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ddworken/hishtory/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "saveHistoryEntry":
|
||||||
|
saveHistoryEntry()
|
||||||
|
case "query":
|
||||||
|
query()
|
||||||
|
case "init":
|
||||||
|
err := shared.Setup(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerHostname() string {
|
||||||
|
if server := os.Getenv("HISHTORY_SERVER"); server != "" {
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
return "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
func query() {
|
||||||
|
userSecret, err := shared.GetUserSecret()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", getServerHostname()+"/api/v1/search", nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("query", strings.Join(os.Args[2:], " "))
|
||||||
|
q.Add("user_secret", userSecret)
|
||||||
|
q.Add("limit", "25")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
resp_body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if resp.Status != "200 OK" {
|
||||||
|
panic("search API returned invalid result. status=" + resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*shared.HistoryEntry
|
||||||
|
err = json.Unmarshal(resp_body, &data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
shared.DisplayResults(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveHistoryEntry() {
|
||||||
|
entry, err := shared.BuildHistoryEntry(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = send(*entry)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(entry shared.HistoryEntry) error {
|
||||||
|
jsonValue, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal HistoryEntry as json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = http.Post(getServerHostname()+"/api/v1/submit", "application/json", bytes.NewBuffer(jsonValue))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send HistoryEntry to api: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -6,6 +6,6 @@ require (
|
|||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/rodaine/table v1.0.1 // indirect
|
github.com/rodaine/table v1.0.1 // indirect
|
||||||
gorm.io/driver/sqlite v1.2.6 // indirect
|
gorm.io/driver/sqlite v1.2.6
|
||||||
gorm.io/gorm v1.22.4 // indirect
|
gorm.io/gorm v1.22.4
|
||||||
)
|
)
|
||||||
|
@ -5,46 +5,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ddworken/hishtory/shared"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/ddworken/hishtory/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
func openDB() (*gorm.DB, error) {
|
|
||||||
homedir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get user's home directory: %v", err)
|
|
||||||
}
|
|
||||||
db, err := gorm.Open(sqlite.Open(path.Join(homedir, ".hishtory.db")), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
panic("failed to connect database")
|
|
||||||
}
|
|
||||||
db.AutoMigrate(&shared.HistoryEntry{})
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func persist(entry shared.HistoryEntry) error {
|
|
||||||
log.Printf("Saving %#v to the DB\n", entry)
|
|
||||||
db, err := openDB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conn, err := db.DB()
|
|
||||||
defer conn.Close()
|
|
||||||
db.Create(&entry).Commit()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func search(db *gorm.DB, userSecret, query string, limit int) ([]*shared.HistoryEntry, error) {
|
func search(db *gorm.DB, userSecret, query string, limit int) ([]*shared.HistoryEntry, error) {
|
||||||
fmt.Println("Received search query: " + query)
|
fmt.Println("Received search query: " + query)
|
||||||
tokens, err := tokenize(query)
|
tokens, err := tokenize(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to tokenize query: %v")
|
return nil, fmt.Errorf("failed to tokenize query: %v", err)
|
||||||
}
|
}
|
||||||
tx := db.Debug().Where("user_secret = ?", userSecret)
|
tx := db.Debug().Where("user_secret = ?", userSecret)
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
@ -72,6 +45,9 @@ func search(db *gorm.DB, userSecret, query string, limit int) ([]*shared.History
|
|||||||
}
|
}
|
||||||
|
|
||||||
func tokenize(query string) ([]string, error) {
|
func tokenize(query string) ([]string, error) {
|
||||||
|
if query == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
return strings.Split(query, " "), nil
|
return strings.Split(query, " "), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +58,7 @@ func apiSubmitHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = persist(entry)
|
err = shared.Persist(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -99,7 +75,7 @@ func apiSearchHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
limit = 0
|
limit = 0
|
||||||
}
|
}
|
||||||
db, err := openDB()
|
db, err := shared.OpenDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -118,7 +94,7 @@ func apiSearchHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_, err := openDB()
|
_, err := shared.OpenDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
58
server/server_test.go
Normal file
58
server/server_test.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ddworken/hishtory/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubmitThenQuery(t *testing.T) {
|
||||||
|
// Set up
|
||||||
|
defer shared.BackupAndRestore(t)
|
||||||
|
shared.Check(t, shared.Setup([]string{}))
|
||||||
|
|
||||||
|
// Submit an entry
|
||||||
|
entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "})
|
||||||
|
shared.Check(t, err)
|
||||||
|
reqBody, err := json.Marshal(entry)
|
||||||
|
shared.Check(t, err)
|
||||||
|
submitReq := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody))
|
||||||
|
apiSubmitHandler(nil, submitReq)
|
||||||
|
// Also submit one for another user
|
||||||
|
otherEntry := *entry
|
||||||
|
otherEntry.UserSecret = "aaaaaaaaa"
|
||||||
|
otherEntry.Command = "other"
|
||||||
|
reqBody, err = json.Marshal(otherEntry)
|
||||||
|
shared.Check(t, err)
|
||||||
|
submitReq = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody))
|
||||||
|
apiSubmitHandler(nil, submitReq)
|
||||||
|
|
||||||
|
// Retrieve the entry
|
||||||
|
secret, err := shared.GetUserSecret()
|
||||||
|
shared.Check(t, err)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
searchReq := httptest.NewRequest(http.MethodGet, "/?user_secret="+secret, nil)
|
||||||
|
apiSearchHandler(w, searchReq)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
shared.Check(t, err)
|
||||||
|
var retrievedEntries []*shared.HistoryEntry
|
||||||
|
shared.Check(t, json.Unmarshal(data, &retrievedEntries))
|
||||||
|
dbEntry := retrievedEntries[0]
|
||||||
|
if len(retrievedEntries) != 1 {
|
||||||
|
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
||||||
|
}
|
||||||
|
if dbEntry.UserSecret != "" {
|
||||||
|
t.Fatalf("Response contains a user secret: %#v", *dbEntry)
|
||||||
|
}
|
||||||
|
entry.UserSecret = ""
|
||||||
|
if !shared.EntryEquals(*dbEntry, *entry) {
|
||||||
|
t.Fatalf("DB data is different than input! \ndb =%#v\ninput=%#v", *dbEntry, *entry)
|
||||||
|
}
|
||||||
|
}
|
133
shared/client.go
Normal file
133
shared/client.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rodaine/table"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SECRET_PATH = ".hishtory.secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCwd() (string, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get cwd for last command: %v", err)
|
||||||
|
}
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user's home directory: %v", err)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(cwd, homedir) {
|
||||||
|
return strings.Replace(cwd, homedir, "~", 1), nil
|
||||||
|
}
|
||||||
|
return cwd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildHistoryEntry(args []string) (*HistoryEntry, error) {
|
||||||
|
var entry HistoryEntry
|
||||||
|
|
||||||
|
// exitCode
|
||||||
|
exitCode, err := strconv.Atoi(args[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||||
|
}
|
||||||
|
entry.ExitCode = exitCode
|
||||||
|
|
||||||
|
// user
|
||||||
|
user, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||||
|
}
|
||||||
|
entry.LocalUsername = user.Username
|
||||||
|
|
||||||
|
// cwd
|
||||||
|
cwd, err := getCwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||||
|
}
|
||||||
|
entry.CurrentWorkingDirectory = cwd
|
||||||
|
|
||||||
|
// TODO(ddworken): start time
|
||||||
|
|
||||||
|
// end time
|
||||||
|
entry.EndTime = time.Now()
|
||||||
|
|
||||||
|
// command
|
||||||
|
cmd, err := getLastCommand(args[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||||
|
}
|
||||||
|
entry.Command = cmd
|
||||||
|
|
||||||
|
// hostname
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||||
|
}
|
||||||
|
entry.Hostname = hostname
|
||||||
|
|
||||||
|
// user secret
|
||||||
|
userSecret, err := GetUserSecret()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||||
|
}
|
||||||
|
entry.UserSecret = userSecret
|
||||||
|
|
||||||
|
return &entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLastCommand(history string) (string, error) {
|
||||||
|
return strings.TrimSpace(strings.SplitN(strings.TrimSpace(history), " ", 2)[1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserSecret() (string, error) {
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read secret hishtory key: %v", err)
|
||||||
|
}
|
||||||
|
secret, err := os.ReadFile(path.Join(homedir, SECRET_PATH))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read secret hishtory key: %v", err)
|
||||||
|
}
|
||||||
|
return string(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup(args []string) error {
|
||||||
|
userSecret := uuid.Must(uuid.NewRandom()).String()
|
||||||
|
if len(args) > 2 && args[2] != "" {
|
||||||
|
userSecret = args[2]
|
||||||
|
}
|
||||||
|
fmt.Println("Setting secret hishtory key to " + string(userSecret))
|
||||||
|
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve homedir: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path.Join(homedir, SECRET_PATH), []byte(userSecret), 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write hishtory secret: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisplayResults(results []*HistoryEntry) {
|
||||||
|
headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
|
||||||
|
tbl := table.New("Hostname", "CWD", "Timestamp", "Exit Code", "Command")
|
||||||
|
tbl.WithHeaderFormatter(headerFmt)
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
tbl.Print()
|
||||||
|
}
|
66
shared/client_test.go
Normal file
66
shared/client_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetup(t *testing.T) {
|
||||||
|
defer BackupAndRestore(t)
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
Check(t, err)
|
||||||
|
if _, err := os.Stat(path.Join(homedir, SECRET_PATH)); err == nil {
|
||||||
|
t.Fatalf("hishtory secret file already exists!")
|
||||||
|
}
|
||||||
|
Check(t, Setup([]string{}))
|
||||||
|
if _, err := os.Stat(path.Join(homedir, SECRET_PATH)); err != nil {
|
||||||
|
t.Fatalf("hishtory secret file does not exist after Setup()!")
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path.Join(homedir, SECRET_PATH))
|
||||||
|
Check(t, err)
|
||||||
|
if len(data) < 10 {
|
||||||
|
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildHistoryEntry(t *testing.T) {
|
||||||
|
defer BackupAndRestore(t)
|
||||||
|
Check(t, Setup([]string{}))
|
||||||
|
entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "})
|
||||||
|
Check(t, err)
|
||||||
|
if entry.UserSecret == "" || len(entry.UserSecret) < 10 || strings.TrimSpace(entry.UserSecret) != entry.UserSecret {
|
||||||
|
t.Fatalf("history entry has unexpected user secret: %v", entry.UserSecret)
|
||||||
|
}
|
||||||
|
if entry.ExitCode != 120 {
|
||||||
|
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||||
|
}
|
||||||
|
if entry.LocalUsername != "david" {
|
||||||
|
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||||
|
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||||
|
}
|
||||||
|
if entry.Command != "ls /" {
|
||||||
|
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserSecret(t *testing.T) {
|
||||||
|
defer BackupAndRestore(t)
|
||||||
|
Check(t, Setup([]string{}))
|
||||||
|
secret1, err := GetUserSecret()
|
||||||
|
Check(t, err)
|
||||||
|
if len(secret1) < 10 || strings.Contains(secret1, " ") || strings.Contains(secret1, "\n") {
|
||||||
|
t.Fatalf("unexpected secret: %v", secret1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Check(t, Setup([]string{}))
|
||||||
|
secret2, err := GetUserSecret()
|
||||||
|
Check(t, err)
|
||||||
|
|
||||||
|
if secret1 == secret2 {
|
||||||
|
t.Fatalf("GetUserSecret() returned the same values for different setups! val=%v", secret1)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,15 @@
|
|||||||
package shared
|
package shared
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type HistoryEntry struct {
|
type HistoryEntry struct {
|
||||||
UserSecret string `json:"user_secret"`
|
UserSecret string `json:"user_secret"`
|
||||||
@ -12,3 +21,32 @@ type HistoryEntry struct {
|
|||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `json:"start_time"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime time.Time `json:"end_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DB_PATH = ".hishtory.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Persist(entry HistoryEntry) error {
|
||||||
|
log.Printf("Saving %#v to the DB\n", entry)
|
||||||
|
db, err := OpenDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn, err := db.DB()
|
||||||
|
defer conn.Close()
|
||||||
|
db.Create(&entry).Commit()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenDB() (*gorm.DB, error) {
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user's home directory: %v", err)
|
||||||
|
}
|
||||||
|
db, err := gorm.Open(sqlite.Open(path.Join(homedir, DB_PATH)), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to connect database")
|
||||||
|
}
|
||||||
|
db.AutoMigrate(&HistoryEntry{})
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
26
shared/data_test.go
Normal file
26
shared/data_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPersist(t *testing.T) {
|
||||||
|
defer BackupAndRestore(t)
|
||||||
|
Check(t, Setup([]string{}))
|
||||||
|
entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "})
|
||||||
|
Check(t, err)
|
||||||
|
Check(t, Persist(*entry))
|
||||||
|
|
||||||
|
db, err := OpenDB()
|
||||||
|
Check(t, err)
|
||||||
|
var historyEntries []*HistoryEntry
|
||||||
|
result := db.Find(&historyEntries)
|
||||||
|
Check(t, result.Error)
|
||||||
|
if len(historyEntries) != 1 {
|
||||||
|
t.Fatalf("DB has %d entries, expected 1!", len(historyEntries))
|
||||||
|
}
|
||||||
|
dbEntry := historyEntries[0]
|
||||||
|
if !EntryEquals(*entry, *dbEntry) {
|
||||||
|
t.Fatalf("DB data is different than input! \ndb =%#v \ninput=%#v", *dbEntry, *entry)
|
||||||
|
}
|
||||||
|
}
|
40
shared/testutils.go
Normal file
40
shared/testutils.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Check(t *testing.T, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BackupAndRestore(t *testing.T) func() {
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to retrieve homedir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Rename(path.Join(homedir, DB_PATH), path.Join(homedir, DB_PATH+".bak"))
|
||||||
|
os.Rename(path.Join(homedir, SECRET_PATH), path.Join(homedir, SECRET_PATH+".bak"))
|
||||||
|
return func() {
|
||||||
|
Check(t, os.Rename(path.Join(homedir, DB_PATH+".bak"), path.Join(homedir, DB_PATH)))
|
||||||
|
Check(t, os.Rename(path.Join(homedir, SECRET_PATH+".bak"), path.Join(homedir, SECRET_PATH)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryEquals(entry1, entry2 HistoryEntry) bool {
|
||||||
|
return entry1.UserSecret == entry2.UserSecret &&
|
||||||
|
entry1.LocalUsername == entry2.LocalUsername &&
|
||||||
|
entry1.Hostname == entry2.Hostname &&
|
||||||
|
entry1.Command == entry2.Command &&
|
||||||
|
entry1.CurrentWorkingDirectory == entry2.CurrentWorkingDirectory &&
|
||||||
|
entry1.ExitCode == entry2.ExitCode &&
|
||||||
|
entry1.StartTime.Format(time.RFC3339) == entry2.StartTime.Format(time.RFC3339) &&
|
||||||
|
entry1.EndTime.Format(time.RFC3339) == entry2.EndTime.Format(time.RFC3339)
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user