2022-09-21 07:28:40 +02:00
package hctx
2022-09-21 07:03:15 +02:00
import (
"context"
"encoding/json"
2022-09-21 08:30:57 +02:00
"errors"
2022-09-21 07:03:15 +02:00
"fmt"
"os"
"path"
"sync"
"time"
"github.com/ddworken/hishtory/client/data"
2022-12-12 04:15:29 +01:00
"github.com/google/uuid"
2022-11-10 01:14:44 +01:00
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
2022-09-21 07:03:15 +02:00
"gorm.io/gorm"
"gorm.io/gorm/logger"
2022-09-22 04:49:24 +02:00
// Needed to use sqlite without CGO
"github.com/glebarez/sqlite"
2022-09-21 07:03:15 +02:00
)
var (
2022-11-10 01:14:44 +01:00
hishtoryLogger * logrus . Logger
2022-09-21 07:03:15 +02:00
getLoggerOnce sync . Once
)
2022-11-10 01:14:44 +01:00
func GetLogger ( ) * logrus . Logger {
2022-09-21 07:03:15 +02:00
getLoggerOnce . Do ( func ( ) {
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
panic ( fmt . Errorf ( "failed to get user's home directory: %w" , err ) )
2022-09-21 07:03:15 +02:00
}
2022-09-21 08:30:57 +02:00
err = MakeHishtoryDir ( )
if err != nil {
panic ( err )
}
2022-11-10 01:14:44 +01:00
lumberjackLogger := & lumberjack . Logger {
2022-12-17 07:22:57 +01:00
Filename : path . Join ( homedir , data . GetHishtoryPath ( ) , "hishtory.log" ) ,
2022-11-10 01:14:44 +01:00
MaxSize : 1 , // MB
MaxBackups : 10 ,
MaxAge : 30 , // days
2022-09-21 07:03:15 +02:00
}
2022-11-10 01:14:44 +01:00
logFormatter := new ( logrus . TextFormatter )
logFormatter . TimestampFormat = time . RFC3339
logFormatter . FullTimestamp = true
hishtoryLogger = logrus . New ( )
hishtoryLogger . SetFormatter ( logFormatter )
hishtoryLogger . SetLevel ( logrus . InfoLevel )
hishtoryLogger . SetOutput ( lumberjackLogger )
2022-09-21 07:03:15 +02:00
} )
return hishtoryLogger
}
2022-09-21 08:30:57 +02:00
func MakeHishtoryDir ( ) error {
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to get user's home directory: %w" , err )
2022-09-21 08:30:57 +02:00
}
2022-12-17 07:22:57 +01:00
err = os . MkdirAll ( path . Join ( homedir , data . GetHishtoryPath ( ) ) , 0 o744 )
2022-09-21 08:30:57 +02:00
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to create ~/%s dir: %w" , data . GetHishtoryPath ( ) , err )
2022-09-21 08:30:57 +02:00
}
return nil
}
2022-09-21 07:03:15 +02:00
func OpenLocalSqliteDb ( ) ( * gorm . DB , error ) {
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
return nil , fmt . Errorf ( "failed to get user's home directory: %w" , err )
2022-09-21 07:03:15 +02:00
}
2022-09-21 08:30:57 +02:00
err = MakeHishtoryDir ( )
2022-09-21 07:03:15 +02:00
if err != nil {
2022-09-21 08:30:57 +02:00
return nil , err
2022-09-21 07:03:15 +02:00
}
newLogger := logger . New (
2022-11-17 05:28:25 +01:00
GetLogger ( ) . WithField ( "fromSQL" , true ) ,
2022-09-21 07:03:15 +02:00
logger . Config {
SlowThreshold : 100 * time . Millisecond ,
LogLevel : logger . Warn ,
IgnoreRecordNotFoundError : false ,
Colorful : false ,
} ,
)
2022-12-17 07:22:57 +01:00
dbFilePath := path . Join ( homedir , data . GetHishtoryPath ( ) , data . DB_PATH )
2022-11-16 08:20:19 +01:00
dsn := fmt . Sprintf ( "file:%s?mode=rwc&_journal_mode=WAL" , dbFilePath )
2022-11-12 15:39:37 +01:00
db , err := gorm . Open ( sqlite . Open ( dsn ) , & gorm . Config { SkipDefaultTransaction : true , Logger : newLogger } )
2022-09-21 07:03:15 +02:00
if err != nil {
2023-09-05 21:08:55 +02:00
return nil , fmt . Errorf ( "failed to connect to the DB: %w" , err )
2022-09-21 07:03:15 +02:00
}
tx , err := db . DB ( )
if err != nil {
return nil , err
}
err = tx . Ping ( )
if err != nil {
return nil , err
}
db . AutoMigrate ( & data . HistoryEntry { } )
db . Exec ( "PRAGMA journal_mode = WAL" )
2023-02-19 07:26:18 +01:00
db . Exec ( "CREATE INDEX IF NOT EXISTS end_time_index ON history_entries(end_time)" )
2022-09-21 07:03:15 +02:00
return db , nil
}
type hishtoryContextKey string
2023-09-05 21:54:47 +02:00
const (
configCtxKey hishtoryContextKey = "config"
dbCtxKey hishtoryContextKey = "db"
homedirCtxKey hishtoryContextKey = "homedir"
)
2023-09-05 21:45:17 +02:00
func MakeContext ( ) context . Context {
2022-09-21 07:03:15 +02:00
ctx := context . Background ( )
config , err := GetConfig ( )
if err != nil {
2023-09-05 21:08:55 +02:00
panic ( fmt . Errorf ( "failed to retrieve config: %w" , err ) )
2022-09-21 07:03:15 +02:00
}
2023-09-05 21:54:47 +02:00
ctx = context . WithValue ( ctx , configCtxKey , config )
2022-09-21 07:03:15 +02:00
db , err := OpenLocalSqliteDb ( )
if err != nil {
2023-09-05 21:08:55 +02:00
panic ( fmt . Errorf ( "failed to open local DB: %w" , err ) )
2022-09-21 07:03:15 +02:00
}
2023-09-05 21:54:47 +02:00
ctx = context . WithValue ( ctx , dbCtxKey , db )
2022-09-22 05:19:11 +02:00
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
panic ( fmt . Errorf ( "failed to get homedir: %w" , err ) )
2022-09-22 05:19:11 +02:00
}
2023-09-05 21:54:47 +02:00
ctx = context . WithValue ( ctx , homedirCtxKey , homedir )
2023-09-05 21:45:17 +02:00
return ctx
2022-09-21 07:03:15 +02:00
}
2023-09-05 21:45:17 +02:00
func GetConf ( ctx context . Context ) ClientConfig {
2023-09-05 21:54:47 +02:00
v := ctx . Value ( configCtxKey )
2022-09-21 07:28:40 +02:00
if v != nil {
return v . ( ClientConfig )
}
2022-09-21 08:30:57 +02:00
panic ( fmt . Errorf ( "failed to find config in ctx" ) )
2022-09-21 07:28:40 +02:00
}
2023-09-05 21:45:17 +02:00
func GetDb ( ctx context . Context ) * gorm . DB {
2023-09-05 21:54:47 +02:00
v := ctx . Value ( dbCtxKey )
2022-09-21 07:03:15 +02:00
if v != nil {
return v . ( * gorm . DB )
}
2022-09-21 08:30:57 +02:00
panic ( fmt . Errorf ( "failed to find db in ctx" ) )
2022-09-21 07:03:15 +02:00
}
2023-09-05 21:45:17 +02:00
func GetHome ( ctx context . Context ) string {
2023-09-05 21:54:47 +02:00
v := ctx . Value ( homedirCtxKey )
2022-09-22 05:19:11 +02:00
if v != nil {
return v . ( string )
}
panic ( fmt . Errorf ( "failed to find homedir in ctx" ) )
}
2022-09-21 07:03:15 +02:00
type ClientConfig struct {
// The user secret that is used to derive encryption keys for syncing history entries
UserSecret string ` json:"user_secret" `
// Whether hishtory recording is enabled
IsEnabled bool ` json:"is_enabled" `
// A device ID used to track which history entry came from which device for remote syncing
DeviceId string ` json:"device_id" `
// Used for skipping history entries prefixed with a space in bash
LastSavedHistoryLine string ` json:"last_saved_history_line" `
// Used for uploading history entries that we failed to upload due to a missing network connection
HaveMissedUploads bool ` json:"have_missed_uploads" `
MissedUploadTimestamp int64 ` json:"missed_upload_timestamp" `
// Used for avoiding double imports of .bash_history
HaveCompletedInitialImport bool ` json:"have_completed_initial_import" `
2022-10-16 18:29:14 +02:00
// Whether control-r bindings are enabled
ControlRSearchEnabled bool ` json:"enable_control_r_search" `
2022-10-24 05:54:46 +02:00
// The set of columns that the user wants to be displayed
DisplayedColumns [ ] string ` json:"displayed_columns" `
2022-10-24 06:42:22 +02:00
// Custom columns
CustomColumns [ ] CustomColumnDefinition ` json:"custom_columns" `
2022-11-03 21:16:45 +01:00
// Whether this is an offline instance of hishtory with no syncing
IsOffline bool ` json:"is_offline" `
2022-11-04 04:36:36 +01:00
// Whether duplicate commands should be displayed
FilterDuplicateCommands bool ` json:"filter_duplicate_commands" `
2022-11-12 02:17:54 +01:00
// A format string for the timestamp
TimestampFormat string ` json:"timestamp_format" `
2023-08-27 23:24:59 +02:00
// Beta mode, enables unspecified additional beta features
// Currently: This enables pre-saving of history entries to better handle long-running commands
BetaMode bool ` json:"beta_mode" `
2022-10-24 06:42:22 +02:00
}
type CustomColumnDefinition struct {
ColumnName string ` json:"column_name" `
ColumnCommand string ` json:"column_command" `
2022-09-21 07:03:15 +02:00
}
2022-10-24 01:51:39 +02:00
func GetConfigContents ( ) ( [ ] byte , error ) {
2022-09-21 07:03:15 +02:00
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
return nil , fmt . Errorf ( "failed to retrieve homedir: %w" , err )
2022-09-21 07:03:15 +02:00
}
2022-12-17 07:22:57 +01:00
dat , err := os . ReadFile ( path . Join ( homedir , data . GetHishtoryPath ( ) , data . CONFIG_PATH ) )
2022-09-21 07:03:15 +02:00
if err != nil {
2022-12-17 07:22:57 +01:00
files , err := os . ReadDir ( path . Join ( homedir , data . GetHishtoryPath ( ) ) )
2022-09-21 07:03:15 +02:00
if err != nil {
2023-09-05 21:08:55 +02:00
return nil , fmt . Errorf ( "failed to read config file (and failed to list too): %w" , err )
2022-09-21 07:03:15 +02:00
}
filenames := ""
for _ , file := range files {
filenames += file . Name ( )
filenames += ", "
}
2023-09-05 21:08:55 +02:00
return nil , fmt . Errorf ( "failed to read config file (files in HISHTORY_PATH: %s): %w" , filenames , err )
2022-10-24 01:51:39 +02:00
}
2022-10-28 06:53:47 +02:00
return dat , nil
2022-10-24 01:51:39 +02:00
}
func GetConfig ( ) ( ClientConfig , error ) {
data , err := GetConfigContents ( )
if err != nil {
return ClientConfig { } , err
2022-09-21 07:03:15 +02:00
}
var config ClientConfig
err = json . Unmarshal ( data , & config )
if err != nil {
2023-09-05 21:08:55 +02:00
return ClientConfig { } , fmt . Errorf ( "failed to parse config file: %w" , err )
2022-09-21 07:03:15 +02:00
}
2022-10-24 05:54:46 +02:00
if config . DisplayedColumns == nil || len ( config . DisplayedColumns ) == 0 {
config . DisplayedColumns = [ ] string { "Hostname" , "CWD" , "Timestamp" , "Runtime" , "Exit Code" , "Command" }
}
2022-11-12 02:17:54 +01:00
if config . TimestampFormat == "" {
config . TimestampFormat = "Jan 2 2006 15:04:05 MST"
}
2022-09-21 07:03:15 +02:00
return config , nil
}
func SetConfig ( config ClientConfig ) error {
2023-09-13 02:51:55 +02:00
// TODO: Currently there is a consistency bug here where we update fields in the ClientConfig, and we write that to disk, but we never re-store it in hctx
2022-09-21 07:03:15 +02:00
serializedConfig , err := json . Marshal ( config )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to serialize config: %w" , err )
2022-09-21 07:03:15 +02:00
}
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to retrieve homedir: %w" , err )
2022-09-21 07:03:15 +02:00
}
2022-09-21 08:30:57 +02:00
err = MakeHishtoryDir ( )
2022-09-21 07:03:15 +02:00
if err != nil {
2022-09-21 08:30:57 +02:00
return err
2022-09-21 07:03:15 +02:00
}
2022-12-17 07:22:57 +01:00
configPath := path . Join ( homedir , data . GetHishtoryPath ( ) , data . CONFIG_PATH )
2022-12-12 04:15:29 +01:00
stagedConfigPath := configPath + ".tmp-" + uuid . Must ( uuid . NewRandom ( ) ) . String ( )
2022-10-24 04:29:29 +02:00
err = os . WriteFile ( stagedConfigPath , serializedConfig , 0 o644 )
2022-09-21 07:03:15 +02:00
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to write config: %w" , err )
2022-09-21 07:03:15 +02:00
}
2022-10-24 04:29:29 +02:00
err = os . Rename ( stagedConfigPath , configPath )
2022-10-15 01:42:47 +02:00
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to replace config file with the updated version: %w" , err )
2022-10-15 01:42:47 +02:00
}
2022-09-21 07:03:15 +02:00
return nil
}
2022-09-21 08:30:57 +02:00
func InitConfig ( ) error {
homedir , err := os . UserHomeDir ( )
if err != nil {
return err
}
2022-12-17 07:22:57 +01:00
_ , err = os . Stat ( path . Join ( homedir , data . GetHishtoryPath ( ) , data . CONFIG_PATH ) )
2022-09-21 08:30:57 +02:00
if errors . Is ( err , os . ErrNotExist ) {
return SetConfig ( ClientConfig { } )
}
return err
}