mirror of
https://github.com/ddworken/hishtory.git
synced 2024-11-28 19:23:53 +01:00
init versions pre-split
This commit is contained in:
commit
6fbad3a194
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
client
|
||||
server
|
||||
!client/
|
||||
!server/
|
207
client/client.go
Normal file
207
client/client.go
Normal file
@ -0,0 +1,207 @@
|
||||
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
|
||||
}
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module github.com/ddworken/hishtory
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/rodaine/table v1.0.1 // indirect
|
||||
gorm.io/driver/sqlite v1.2.6 // indirect
|
||||
gorm.io/gorm v1.22.4 // indirect
|
||||
)
|
36
go.sum
Normal file
36
go.sum
Normal file
@ -0,0 +1,36 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI=
|
||||
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
|
||||
github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
|
||||
gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
|
||||
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM=
|
||||
gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk=
|
129
server/server.go
Normal file
129
server/server.go
Normal file
@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ddworken/hishtory/shared"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
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) {
|
||||
fmt.Println("Received search query: " + query)
|
||||
tokens, err := tokenize(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to tokenize query: %v")
|
||||
}
|
||||
tx := db.Debug().Where("user_secret = ?", userSecret)
|
||||
for _, token := range tokens {
|
||||
if strings.Contains(token, ":") {
|
||||
splitToken := strings.SplitN(token, ":", 2)
|
||||
field := splitToken[0]
|
||||
val := splitToken[1]
|
||||
// tx = tx.Where()
|
||||
panic("TODO(ddworken): Use " + field + val)
|
||||
} else {
|
||||
wildcardedToken := "%" + token + "%"
|
||||
tx = tx.Where("(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken)
|
||||
}
|
||||
}
|
||||
tx = tx.Order("end_time DESC")
|
||||
if limit > 0 {
|
||||
tx = tx.Limit(limit)
|
||||
}
|
||||
var historyEntries []*shared.HistoryEntry
|
||||
result := tx.Find(&historyEntries)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("DB query error: %v", result.Error)
|
||||
}
|
||||
return historyEntries, nil
|
||||
}
|
||||
|
||||
func tokenize(query string) ([]string, error) {
|
||||
return strings.Split(query, " "), nil
|
||||
}
|
||||
|
||||
func apiSubmitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var entry shared.HistoryEntry
|
||||
err := decoder.Decode(&entry)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = persist(entry)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func apiSearchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userSecret := r.URL.Query().Get("user_secret")
|
||||
if userSecret == "" {
|
||||
panic("cannot search without specifying a user secret")
|
||||
}
|
||||
query := r.URL.Query().Get("query")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
limit = 0
|
||||
}
|
||||
db, err := openDB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
entries, err := search(db, userSecret, query, limit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
entry.UserSecret = ""
|
||||
}
|
||||
resp, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w.Write(resp)
|
||||
}
|
||||
|
||||
func main() {
|
||||
_, err := openDB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("Listening on localhost:8080")
|
||||
http.HandleFunc("/api/v1/submit", apiSubmitHandler)
|
||||
http.HandleFunc("/api/v1/search", apiSearchHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
14
shared/data.go
Normal file
14
shared/data.go
Normal file
@ -0,0 +1,14 @@
|
||||
package shared
|
||||
|
||||
import "time"
|
||||
|
||||
type HistoryEntry struct {
|
||||
UserSecret string `json:"user_secret"`
|
||||
LocalUsername string `json:"local_username"`
|
||||
Hostname string `json:"hostname"`
|
||||
Command string `json:"command"`
|
||||
CurrentWorkingDirectory string `json:"current_working_directory"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
}
|
Loading…
Reference in New Issue
Block a user