diff --git a/README.md b/README.md index 396c822..9092264 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ If you don't need the ability to sync your shell history, you can install hiSHto curl https://hishtory.dev/install.py | HISHTORY_OFFLINE=true python3 - ``` -This disables syncing and it is not possible to re-enable syncing after doing this. +This disables syncing completely so that the client will not rely on the hiSHtory backend at all. You can also change the syncing status via `hishtory syncing enable` or `hishtory syncing disable`. diff --git a/client/client_test.go b/client/client_test.go index 1958991..a169b93 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -3127,4 +3127,66 @@ func TestForceInit(t *testing.T) { require.NotContains(t, tester.RunInteractiveShell(t, `hishtory export`), "echo foobar") } +func TestChangeSyncingStatus(t *testing.T) { + markTestForSharding(t, 14) + defer testutils.BackupAndRestore(t)() + tester := zshTester{} + + // Install it offline and record a command or two + userSecret := installWithOnlineStatus(t, tester, Offline) + assertOnlineStatus(t, Offline) + tester.RunInteractiveShell(t, `echo "device1_whileOffline_1"`) + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory status -v | grep -v User | grep -v Device | grep -v Secret`), + "TestChangeSyncingStatus-Offline", + ) + + // Go online + out := tester.RunInteractiveShell(t, `hishtory syncing enable`) + require.Equal(t, "Enabled syncing successfully\n", out) + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory status -v | grep -v User | grep -v Device | grep -v Secret`), + "TestChangeSyncingStatus-Online", + ) + + // Back up that device and set up another device to confirm syncing is working + restoreDev1 := testutils.BackupAndRestoreWithId(t, "dev1") + installHishtory(t, tester, userSecret) + out = tester.RunInteractiveShell(t, `hishtory export`) + require.Contains(t, out, "device1_whileOffline_1") + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory status -v | grep -v User | grep -v Device | grep -v Secret`), + "TestChangeSyncingStatus-Online", + ) + + // Go back to the first device, disable syncing, and then record a command + restoreDev2 := testutils.BackupAndRestoreWithId(t, "dev2") + restoreDev1() + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory status -v | grep -v User | grep -v Device | grep -v Secret`), + "TestChangeSyncingStatus-Online", + ) + out = tester.RunInteractiveShell(t, `hishtory syncing disable`) + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory status -v | grep -v User | grep -v Device | grep -v Secret`), + "TestChangeSyncingStatus-Offline", + ) + require.Equal(t, "Disabled syncing successfully\n", out) + tester.RunInteractiveShell(t, `echo "device1_whileOffline_2"`) + out = tester.RunInteractiveShell(t, `hishtory export`) + require.Contains(t, out, "device1_whileOffline_1") + require.Contains(t, out, "device1_whileOffline_2") + + // Then go back to the second device which won't see that command + testutils.BackupAndRestoreWithId(t, "dev1") + restoreDev2() + out = tester.RunInteractiveShell(t, `hishtory export`) + require.Contains(t, out, "device1_whileOffline_1") + require.NotContains(t, out, "device1_whileOffline_2") + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory status -v | grep -v User | grep -v Device | grep -v Secret`), + "TestChangeSyncingStatus-Online", + ) +} + // TODO: somehow test/confirm that hishtory works even if only bash/only zsh is installed diff --git a/client/cmd/install.go b/client/cmd/install.go index 999f0ec..acce593 100644 --- a/client/cmd/install.go +++ b/client/cmd/install.go @@ -599,12 +599,15 @@ func setup(userSecret string, isOffline bool) error { if config.IsOffline { return nil } - ctx := hctx.MakeContext() + return registerAndBootstrapDevice(hctx.MakeContext(), &config, db, userSecret) +} + +func registerAndBootstrapDevice(ctx context.Context, config *hctx.ClientConfig, db *gorm.DB, userSecret string) error { registerPath := "/api/v1/register?user_id=" + data.UserId(userSecret) + "&device_id=" + config.DeviceId if isIntegrationTestDevice() { registerPath += "&is_integration_test_device=true" } - _, err = lib.ApiGet(ctx, registerPath) + _, err := lib.ApiGet(ctx, registerPath) if err != nil { return fmt.Errorf("failed to register device with backend: %w", err) } diff --git a/client/cmd/syncing.go b/client/cmd/syncing.go new file mode 100644 index 0000000..5c45a61 --- /dev/null +++ b/client/cmd/syncing.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/ddworken/hishtory/client/data" + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var syncingCmd = &cobra.Command{ + Use: "syncing", + Short: "Configure syncing to enable or disable syncing with the hishtory backend", + ValidArgs: []string{"disable", "enable"}, + Args: cobra.MatchAll(cobra.OnlyValidArgs, cobra.ExactArgs(1)), + Run: func(cmd *cobra.Command, args []string) { + syncingStatus := false + if args[0] == "disable" { + syncingStatus = false + } else if args[0] == "enable" { + syncingStatus = true + } else { + lib.CheckFatalError(fmt.Errorf("unexpected syncing argument %q", args[0])) + } + + ctx := hctx.MakeContext() + conf := hctx.GetConf(ctx) + if syncingStatus { + if conf.IsOffline { + lib.CheckFatalError(switchToOnline(ctx)) + fmt.Println("Enabled syncing successfully") + } else { + lib.CheckFatalError(fmt.Errorf("device is already online")) + } + } else { + if conf.IsOffline { + lib.CheckFatalError(fmt.Errorf("device is already offline")) + } else { + lib.CheckFatalError(switchToOffline(ctx)) + fmt.Println("Disabled syncing successfully") + } + } + }, +} + +func switchToOnline(ctx context.Context) error { + config := hctx.GetConf(ctx) + config.IsOffline = false + err := hctx.SetConfig(config) + if err != nil { + return fmt.Errorf("failed to switch device to online due to error while setting config: %w", err) + } + err = registerAndBootstrapDevice(ctx, config, hctx.GetDb(ctx), config.UserSecret) + if err != nil { + return fmt.Errorf("failed to register device with backend: %w", err) + } + err = lib.Reupload(ctx) + if err != nil { + return fmt.Errorf("failed to switch device to online due to error while uploading history entries: %w", err) + } + return nil +} + +func switchToOffline(ctx context.Context) error { + config := hctx.GetConf(ctx) + config.IsOffline = true + err := hctx.SetConfig(config) + if err != nil { + return fmt.Errorf("failed to switch device to offline due to error while setting config: %w", err) + } + _, err = lib.ApiPost(ctx, "/api/v1/uninstall?user_id="+data.UserId(hctx.GetConf(ctx).UserSecret)+"&device_id="+hctx.GetConf(ctx).DeviceId, "application/json", []byte{}) + if err != nil { + return fmt.Errorf("failed to switch device to offline due to error while deleting sync state: %w", err) + } + return nil +} + +func init() { + rootCmd.AddCommand(syncingCmd) +} diff --git a/client/testdata/TestChangeSyncingStatus-Offline b/client/testdata/TestChangeSyncingStatus-Offline new file mode 100644 index 0000000..7aa5b5b --- /dev/null +++ b/client/testdata/TestChangeSyncingStatus-Offline @@ -0,0 +1,4 @@ +hiSHtory: v0.Unknown +Enabled: true +Sync Mode: Disabled +Commit Hash: Unknown diff --git a/client/testdata/TestChangeSyncingStatus-Online b/client/testdata/TestChangeSyncingStatus-Online new file mode 100644 index 0000000..dd68c6a --- /dev/null +++ b/client/testdata/TestChangeSyncingStatus-Online @@ -0,0 +1,6 @@ +hiSHtory: v0.Unknown +Enabled: true +Sync Mode: Enabled +Sync Server: http://localhost:8080 +Sync Status: Synced +Commit Hash: Unknown