diff --git a/Makefile b/Makefile index f156633..52bcdca 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,10 @@ GO_OBJECTS = \ commands/create_test.go \ commands/delete.go \ commands/delete_test.go \ + commands/export.go \ + commands/export_test.go \ commands/gc.go \ + commands/gc_test.go \ commands/import.go \ commands/import_test.go \ commands/pull.go \ diff --git a/commands/export.go b/commands/export.go new file mode 100644 index 0000000..2439949 --- /dev/null +++ b/commands/export.go @@ -0,0 +1,71 @@ +// Copyright 2025 Miklos Vajna +// +// SPDX-License-Identifier: MIT + +package commands + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +type passwordRow struct { + ID int + Machine string + Service string + User string + Password string + PasswordType PasswordType + Archived bool + Created string + Modified string +} + +func exportPasswords(db *sql.DB) ([]byte, error) { + var results []passwordRow + rows, err := db.Query("select id, machine, service, user, password, type, archived, created, modified from passwords") + if err != nil { + return nil, fmt.Errorf("db.Query(select) failed: %s", err) + } + + defer rows.Close() + for rows.Next() { + var row passwordRow + err = rows.Scan(&row.ID, &row.Machine, &row.Service, &row.User, &row.Password, &row.PasswordType, &row.Archived, &row.Created, &row.Modified) + if err != nil { + return nil, fmt.Errorf("rows.Scan() failed: %s", err) + } + + results = append(results, row) + } + + j, err := json.Marshal(results) + if err != nil { + return nil, fmt.Errorf("json.Marshal() failed: %s", err) + } + + return j, nil +} + +func newExportCommand(ctx *Context) *cobra.Command { + var cmd = &cobra.Command{ + Use: "export", + Short: "exports passwords as JSON", + RunE: func(cmd *cobra.Command, args []string) error { + j, err := exportPasswords(ctx.Database) + if err != nil { + return fmt.Errorf("readPasswords() failed: %s", err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "%s\n", j) + + ctx.NoWriteBack = true + return nil + }, + } + + return cmd +} diff --git a/commands/export_test.go b/commands/export_test.go new file mode 100644 index 0000000..2f28c80 --- /dev/null +++ b/commands/export_test.go @@ -0,0 +1,67 @@ +// Copyright 2025 Miklos Vajna +// +// SPDX-License-Identifier: MIT + +package commands + +import ( + "bytes" + "encoding/json" + "os" + "testing" +) + +func TestExport(t *testing.T) { + ctx := CreateContextForTesting(t) + _, err := ctx.Database.Exec(`insert into passwords (machine, service, user, password, type) values('mymachine', 'myservice', 'myuser', 'mypassword', 'plain');`) + if err != nil { + t.Fatalf("createPassword() = %q, want nil", err) + } + os.Args = []string{"", "export"} + inBuf := new(bytes.Buffer) + outBuf := new(bytes.Buffer) + + actualRet := Main(inBuf, outBuf) + + expectedRet := 0 + if actualRet != expectedRet { + t.Fatalf("Main() = %q, want %q", actualRet, expectedRet) + } + actualOutput := outBuf.String() + var passwords []passwordRow + err = json.NewDecoder(bytes.NewBufferString(actualOutput)).Decode(&passwords) + if err != nil { + t.Fatalf("json.Decode() = %q, want nil", err) + } + if len(passwords) != 1 { + t.Fatalf("passwords len = %q, want %q", len(passwords), 1) + } + password := passwords[0] + if password.ID != 1 { + t.Fatalf("password.ID = %q, want %q", password.ID, 1) + } + if password.Machine != "mymachine" { + t.Fatalf("password.Machine = %q, want %q", password.Machine, "mymachine") + } + if password.Service != "myservice" { + t.Fatalf("password.Service = %q, want %q", password.Service, "myservice") + } + if password.User != "myuser" { + t.Fatalf("password.User = %q, want %q", password.User, "myuser") + } + if password.Password != "mypassword" { + t.Fatalf("password.Password = %q, want %q", password.Password, "mypassword") + } + if password.PasswordType != "plain" { + t.Fatalf("password.PasswordType = %q, want %q", password.PasswordType, "plain") + } + if password.Archived != false { + t.Fatalf("password.Archived = %t, want %t", password.Archived, false) + } + if password.Created != "" { + t.Fatalf("password.Created = %q, want %q", password.Created, "") + } + if password.Modified != "" { + t.Fatalf("password.Modified = %q, want %q", password.Modified, "") + } +} diff --git a/commands/root.go b/commands/root.go index f5166a0..1d70d03 100644 --- a/commands/root.go +++ b/commands/root.go @@ -58,6 +58,7 @@ func NewRootCommand(ctx *Context) *cobra.Command { cmd.AddCommand(newPullCommand(ctx)) cmd.AddCommand(newVersionCommand(ctx)) cmd.AddCommand(newGcCommand(ctx)) + cmd.AddCommand(newExportCommand(ctx)) return cmd } @@ -77,6 +78,7 @@ func getCommands() []string { "update", "version", "gc", + "export", } } diff --git a/guide/src/news.md b/guide/src/news.md index 64bd631..67ca708 100644 --- a/guide/src/news.md +++ b/guide/src/news.md @@ -1,5 +1,9 @@ # Changelog +## main + +- new `export` command to write the password database as a JSON file + ## 25.8 - new `gc` command to rebuild the database file, repacking it into a minimal amount of disk space diff --git a/man/cpm-create.1 b/man/cpm-create.1 index ba3f6bb..0dddea6 100644 --- a/man/cpm-create.1 +++ b/man/cpm-create.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-create - creates a new password .SH SYNOPSIS -.PP \fBcpm create [flags]\fP .SH DESCRIPTION -.PP creates a new password .SH OPTIONS -.PP \fB-n\fP, \fB--dry-run\fP[=false] do everything except actually perform the database action (default: false) @@ -51,10 +47,8 @@ creates a new password .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-delete.1 b/man/cpm-delete.1 index 642706c..07de876 100644 --- a/man/cpm-delete.1 +++ b/man/cpm-delete.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-delete - deletes an existing password .SH SYNOPSIS -.PP \fBcpm delete [flags]\fP .SH DESCRIPTION -.PP deletes an existing password .SH OPTIONS -.PP \fB-n\fP, \fB--dry-run\fP[=false] do everything except actually perform the database action (default: false) @@ -31,10 +27,8 @@ deletes an existing password .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-export.1 b/man/cpm-export.1 new file mode 100644 index 0000000..7e53604 --- /dev/null +++ b/man/cpm-export.1 @@ -0,0 +1,26 @@ +.nh +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" + +.SH NAME +cpm-export - exports passwords as JSON + + +.SH SYNOPSIS +\fBcpm export [flags]\fP + + +.SH DESCRIPTION +exports passwords as JSON + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for export + + +.SH SEE ALSO +\fBcpm(1)\fP + + +.SH HISTORY +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-gc.1 b/man/cpm-gc.1 index 9b4d1c5..9772395 100644 --- a/man/cpm-gc.1 +++ b/man/cpm-gc.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-gc - rebuilds the database file, repacking it into a minimal amount of disk space .SH SYNOPSIS -.PP \fBcpm gc [flags]\fP .SH DESCRIPTION -.PP rebuilds the database file, repacking it into a minimal amount of disk space .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for gc .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-import.1 b/man/cpm-import.1 index 312aa96..2d5b64d 100644 --- a/man/cpm-import.1 +++ b/man/cpm-import.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-import - imports an old XML database .SH SYNOPSIS -.PP \fBcpm import [flags]\fP .SH DESCRIPTION -.PP imports an old XML database .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for import .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-pull.1 b/man/cpm-pull.1 index b2499a3..d04fef9 100644 --- a/man/cpm-pull.1 +++ b/man/cpm-pull.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-pull - copies a remote database to a local one .SH SYNOPSIS -.PP \fBcpm pull [flags]\fP .SH DESCRIPTION -.PP copies a remote database to a local one .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for pull .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-search.1 b/man/cpm-search.1 index 03a643d..91256ff 100644 --- a/man/cpm-search.1 +++ b/man/cpm-search.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-search - searches passwords .SH SYNOPSIS -.PP \fBcpm search [flags]\fP .SH DESCRIPTION -.PP searches passwords .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for search @@ -59,10 +55,8 @@ searches passwords .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-update.1 b/man/cpm-update.1 index f659888..9c2c10f 100644 --- a/man/cpm-update.1 +++ b/man/cpm-update.1 @@ -1,23 +1,19 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-update - updates an existing password .SH SYNOPSIS -.PP \fBcpm update [flags]\fP .SH DESCRIPTION -.PP updates an existing password .SH OPTIONS -.PP \fB-a\fP, \fB--archived\fP="" new archived value ("true" or "false"; default: keep unchanged) @@ -59,10 +55,8 @@ updates an existing password .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm-version.1 b/man/cpm-version.1 index 25331d7..2e726bb 100644 --- a/man/cpm-version.1 +++ b/man/cpm-version.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm-version - shows version information .SH SYNOPSIS -.PP \fBcpm version [flags]\fP .SH DESCRIPTION -.PP shows version information .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for version .SH SEE ALSO -.PP \fBcpm(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/cpm.1 b/man/cpm.1 index 348a389..dadc9e3 100644 --- a/man/cpm.1 +++ b/man/cpm.1 @@ -1,32 +1,26 @@ .nh -.TH "CPM" "1" "Jul 2022" "Auto generated by spf13/cobra" "" +.TH "CPM" "1" "Dec 2025" "Auto generated by spf13/cobra" "" .SH NAME -.PP cpm - turtle-cpm is a console password manager .SH SYNOPSIS -.PP \fBcpm [flags]\fP .SH DESCRIPTION -.PP turtle-cpm is a console password manager .SH OPTIONS -.PP \fB-h\fP, \fB--help\fP[=false] help for cpm .SH SEE ALSO -.PP -\fBcpm-create(1)\fP, \fBcpm-delete(1)\fP, \fBcpm-gc(1)\fP, \fBcpm-import(1)\fP, \fBcpm-pull(1)\fP, \fBcpm-search(1)\fP, \fBcpm-update(1)\fP, \fBcpm-version(1)\fP +\fBcpm-create(1)\fP, \fBcpm-delete(1)\fP, \fBcpm-export(1)\fP, \fBcpm-gc(1)\fP, \fBcpm-import(1)\fP, \fBcpm-pull(1)\fP, \fBcpm-search(1)\fP, \fBcpm-update(1)\fP, \fBcpm-version(1)\fP .SH HISTORY -.PP -22-Jul-2022 Auto generated by spf13/cobra +21-Dec-2025 Auto generated by spf13/cobra diff --git a/man/generate.go b/man/generate.go index 551764c..0aa2783 100644 --- a/man/generate.go +++ b/man/generate.go @@ -15,7 +15,7 @@ import ( func main() { var ctx commands.Context cmd := commands.NewRootCommand(&ctx) - date := time.Date(2022, 7, 22, 12, 0, 0, 0, time.UTC) + date := time.Date(2025, 12, 21, 12, 0, 0, 0, time.UTC) header := &doc.GenManHeader{ Title: "CPM", Section: "1",