Files
hishtory/client/cmd/import.go
David Dworken 2f3201ec81 Allow import-json to import custom columns (#282)
* Allow import-json to import custom columns

* Add tests
2025-01-13 13:55:59 -08:00

125 lines
3.8 KiB
Go

package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"os/user"
"time"
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
var importCmd = &cobra.Command{
Use: "import",
GroupID: GROUP_ID_MANAGEMENT,
Hidden: true,
Short: "Re-import history entries from your existing shell history",
Long: "Note that you may also pipe commands to be imported in via stdin. For example `history | hishtory import`.",
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
numImported, err := lib.ImportHistory(ctx, true, true)
lib.CheckFatalError(err)
if numImported > 0 {
fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
}
},
}
var importJsonCmd = &cobra.Command{
Use: "import-json",
GroupID: GROUP_ID_MANAGEMENT,
Short: "Import history entries formatted in JSON lines format into hiSHtory",
Long: "Data is read from stdin. For example: `cat data.txt | hishtory import-json`.\n\nExample JSON format:\n\n```\n" +
"{\"command\":\"echo foo\"}\n" +
"{\"command\":\"echo bar\", \"current_working_directory\": \"/tmp/\"}\n" +
"{\"command\":\"ls\",\"current_working_directory\":\"/tmp/\",\"local_username\":\"david\",\"hostname\":\"foo\",\"home_directory\":\"/Users/david\",\"exit_code\":0,\"start_time\":\"2024-12-30T01:14:34.656407Z\",\"end_time\":\"2024-12-30T01:14:34.657407Z\"}\n```\n",
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
numImported, err := importFromJson(ctx)
lib.CheckFatalError(err)
fmt.Printf("Imported %v history entries\n", numImported)
},
}
func importFromJson(ctx context.Context) (int, error) {
// Get the data needed for filling in any missing columns
currentUser, err := user.Current()
if err != nil {
return 0, err
}
hostname, err := os.Hostname()
if err != nil {
return 0, err
}
homedir := hctx.GetHome(ctx)
// Build the entries
lines, err := lib.ReadStdin()
if err != nil {
return 0, fmt.Errorf("failed to read stdin for import: %w", err)
}
var entries []data.HistoryEntry
importEntryId := uuid.Must(uuid.NewRandom()).String()
importTimestamp := time.Now().UTC()
for i, line := range lines {
var entry data.HistoryEntry
err := json.Unmarshal([]byte(line), &entry)
if err != nil {
return 0, fmt.Errorf("failed to parse JSON line %#v: %w", line, err)
}
if entry.Command == "" {
return 0, fmt.Errorf("cannot import history entries without a command, JSON line: %#v", line)
}
if entry.LocalUsername == "" {
entry.LocalUsername = currentUser.Username
}
if entry.Hostname == "" {
entry.Hostname = hostname
}
if entry.CurrentWorkingDirectory == "" {
entry.CurrentWorkingDirectory = "Unknown"
}
if entry.HomeDirectory == "" {
entry.HomeDirectory = homedir
}
// Set the timestamps so that they are monotonically increasing
startTime := importTimestamp.Add(time.Millisecond * time.Duration(i*2))
endTime := startTime.Add(time.Millisecond)
if entry.StartTime == *new(time.Time) {
entry.StartTime = startTime
}
if entry.EndTime == *new(time.Time) {
entry.EndTime = endTime
}
entry.DeviceId = hctx.GetConf(ctx).DeviceId
entry.EntryId = fmt.Sprintf("%s-%d", importEntryId, i)
entries = append(entries, entry)
}
// Insert the entries into the DB
db := hctx.GetDb(ctx)
err = db.CreateInBatches(entries, lib.ImportBatchSize).Error
if err != nil {
return 0, fmt.Errorf("failed to insert entries into DB: %w", err)
}
// Trigger a checkpoint so that these bulk entries are added from the WAL to the main DB
err = db.Exec("PRAGMA wal_checkpoint").Error
if err != nil {
return 0, fmt.Errorf("failed to checkpoint imported history: %w", err)
}
return len(entries), nil
}
func init() {
rootCmd.AddCommand(importCmd)
rootCmd.AddCommand(importJsonCmd)
}