mirror of
https://codeberg.org/scip/anydb.git
synced 2025-12-16 20:10:59 +01:00
336 lines
7.4 KiB
Go
336 lines
7.4 KiB
Go
/*
|
|
Copyright © 2024 Thomas von Dein
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
|
|
"github.com/spf13/cobra"
|
|
"codeberg.org/scip/anydb/app"
|
|
"codeberg.org/scip/anydb/cfg"
|
|
"codeberg.org/scip/anydb/output"
|
|
"codeberg.org/scip/anydb/rest"
|
|
)
|
|
|
|
func Export(conf *cfg.Config) *cobra.Command {
|
|
var (
|
|
attr app.DbAttr
|
|
)
|
|
|
|
var cmd = &cobra.Command{
|
|
Use: "export -o <json filename>",
|
|
Short: "Export database to json file",
|
|
Long: `Export database to json file`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// errors at this stage do not cause the usage to be shown
|
|
cmd.SilenceUsage = true
|
|
|
|
conf.Mode = "json"
|
|
|
|
entries, err := conf.DB.Getall(&attr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return output.WriteJSON(&attr, conf, entries)
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVarP(&attr.File, "output-file", "o", "", "filename or - for STDIN")
|
|
if err := cmd.MarkPersistentFlagRequired("output-file"); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cmd.Aliases = append(cmd.Aliases, "dump")
|
|
cmd.Aliases = append(cmd.Aliases, "backup")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func Import(conf *cfg.Config) *cobra.Command {
|
|
var (
|
|
attr app.DbAttr
|
|
)
|
|
|
|
var cmd = &cobra.Command{
|
|
Use: "import -i <json file>",
|
|
Short: "Import database dump",
|
|
Long: `Import database dump`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// errors at this stage do not cause the usage to be shown
|
|
cmd.SilenceUsage = true
|
|
|
|
out, err := conf.DB.Import(&attr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Print(out)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVarP(&attr.File, "import-file", "i", "", "filename or - for STDIN")
|
|
cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
|
|
if err := cmd.MarkPersistentFlagRequired("import-file"); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cmd.Aliases = append(cmd.Aliases, "restore")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func Help(conf *cfg.Config) *cobra.Command {
|
|
return nil
|
|
}
|
|
func Man(conf *cfg.Config) *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "man",
|
|
Short: "show manual page",
|
|
Long: `show manual page`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// errors at this stage do not cause the usage to be shown
|
|
cmd.SilenceUsage = true
|
|
|
|
man := exec.Command("less", "-")
|
|
|
|
var b bytes.Buffer
|
|
|
|
b.WriteString(manpage)
|
|
|
|
man.Stdout = os.Stdout
|
|
man.Stdin = &b
|
|
man.Stderr = os.Stderr
|
|
|
|
err := man.Run()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute 'less': %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func Serve(conf *cfg.Config) *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "serve [-l host:port]",
|
|
Short: "run REST API listener",
|
|
Long: `run REST API listener`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// errors at this stage do not cause the usage to be shown
|
|
cmd.SilenceUsage = true
|
|
|
|
return rest.Runserver(conf, nil)
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVarP(&conf.Listen, "listen", "l", "localhost:8787", "host:port")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func Info(conf *cfg.Config) *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "info",
|
|
Short: "info",
|
|
Long: `show info about database`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// errors at this stage do not cause the usage to be shown
|
|
cmd.SilenceUsage = true
|
|
|
|
info, err := conf.DB.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return output.Info(os.Stdout, conf, info)
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func Edit(conf *cfg.Config) *cobra.Command {
|
|
var (
|
|
attr app.DbAttr
|
|
)
|
|
|
|
var cmd = &cobra.Command{
|
|
Use: "edit <key>",
|
|
Short: "Edit a key",
|
|
Long: `Edit a key`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("no key specified")
|
|
}
|
|
|
|
// errors at this stage do not cause the usage to be shown
|
|
cmd.SilenceUsage = true
|
|
password := []byte{}
|
|
|
|
if len(args) > 0 {
|
|
attr.Key = args[0]
|
|
}
|
|
|
|
// fetch entry
|
|
entry, err := conf.DB.Get(&attr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(entry.Value) == 0 && entry.Binary {
|
|
return errors.New("key contains binary uneditable content")
|
|
}
|
|
|
|
// decrypt if needed
|
|
if entry.Encrypted {
|
|
pass, err := getPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
password = pass
|
|
|
|
clear, err := app.Decrypt(pass, []byte(entry.Value))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
entry.Value = string(clear)
|
|
entry.Encrypted = false
|
|
}
|
|
|
|
// determine editor, vi is default
|
|
editor := getEditor()
|
|
|
|
// save file to a temp file, call the editor with it, read
|
|
// it back in and compare the content with the original
|
|
// one
|
|
newcontent, err := editContent(editor, string(entry.Value))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// all is valid, fill our DB feeder
|
|
newattr := app.DbAttr{
|
|
Key: attr.Key,
|
|
Tags: attr.Tags,
|
|
Encrypted: attr.Encrypted,
|
|
Val: []byte(newcontent),
|
|
}
|
|
|
|
// encrypt if needed
|
|
if conf.Encrypt {
|
|
err = app.Encrypt(password, &attr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// done
|
|
return conf.DB.Set(&newattr)
|
|
},
|
|
}
|
|
|
|
cmd.Aliases = append(cmd.Aliases, "modify")
|
|
cmd.Aliases = append(cmd.Aliases, "mod")
|
|
cmd.Aliases = append(cmd.Aliases, "ed")
|
|
cmd.Aliases = append(cmd.Aliases, "vi")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func getEditor() string {
|
|
editor := "vi"
|
|
|
|
enveditor, present := os.LookupEnv("EDITOR")
|
|
if present {
|
|
if editor != "" {
|
|
editor = enveditor
|
|
}
|
|
}
|
|
|
|
return editor
|
|
}
|
|
|
|
// taken from github.com/tlinden/rpn/ (my own program)
|
|
func editContent(editor string, content string) (string, error) {
|
|
// create a temp file
|
|
tmp, err := os.CreateTemp("", "stack")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create templ file: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Remove(tmp.Name()); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
// put the content into a tmp file
|
|
_, err = tmp.WriteString(content)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to write value to temp file: %w", err)
|
|
}
|
|
|
|
// execute editor with our tmp file containing current stack
|
|
cmd := exec.Command(editor, tmp.Name())
|
|
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to run editor command %s: %w", editor, err)
|
|
}
|
|
|
|
// read the file back in
|
|
modified, err := os.Open(tmp.Name())
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open temp file: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := modified.Close(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
newcontent, err := io.ReadAll(modified)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read from temp file: %w", err)
|
|
}
|
|
|
|
newcontentstr := string(newcontent)
|
|
if content == newcontentstr {
|
|
return "", fmt.Errorf("content not modified, aborting")
|
|
}
|
|
|
|
return newcontentstr, nil
|
|
}
|