diff --git a/ask/ask.go b/ask/ask.go index 1626a1ce62bc4ca6efef5e9a61ebc1522d45412d..e8d6a9cdea473f36ff30647f64632eefdfc4f3bc 100644 --- a/ask/ask.go +++ b/ask/ask.go @@ -4,7 +4,20 @@ getting a line of text, getting a choice from a liat and other things. */ package ask -import "github.com/chzyer/readline" +import ( + "fmt" + "os" + + "github.com/chzyer/readline" +) + +// CheckErr prints an error message and exits. +func CheckErr(err error) { + if err != nil { + fmt.Printf("ERROR: %s\n", err) + os.Exit(1) + } +} // GetLine gets a line. func GetLine(prompt string) (string, error) { diff --git a/batch/batch.go b/batch/batch.go index 1763f04332f0f200c59a31127521b6e01f1cef0f..3bba087fdaca6fa146d0a8984e1f71d21ad3d0d2 100644 --- a/batch/batch.go +++ b/batch/batch.go @@ -2,11 +2,19 @@ package batch import ( + "context" _ "embed" + "errors" "fmt" "os" + "path" "strings" "text/template" + + "git.lyda.ie/kevin/bulletin/ask" + "git.lyda.ie/kevin/bulletin/key" + "git.lyda.ie/kevin/bulletin/storage" + "github.com/adrg/xdg" ) //go:embed crontab @@ -26,6 +34,44 @@ func Expire() int { // Install is an interactive command used to install the crontab. func Install() int { + // Check if install has run before. + touchfile := path.Join(xdg.Home, ".bulletin-installed") + if _, err := os.Stat(touchfile); err == nil { + ask.CheckErr(errors.New("~/.bulletin-installed exists; BULLETIN install has run")) + } else { + if !errors.Is(err, os.ErrNotExist) { + fmt.Println("ERROR: Unknown error checking in ~/.bulletin-installed exists.") + fmt.Println("ERROR: Can't see if BULLETIN has been installed.") + ask.CheckErr(err) + } + } + + // Connect to the database. + store, err := storage.Open() + ask.CheckErr(err) + q := storage.New(store.DB) + + // Create the initial users. + login, err := ask.GetLine("Enter login of initial user: ") + ask.CheckErr(err) + name, err := ask.GetLine("Enter name of initial user: ") + ask.CheckErr(err) + sshkey, err := ask.GetLine("Enter ssh public key of initial user: ") + ask.CheckErr(err) + + // Seed data. + ctx := context.TODO() + ask.CheckErr(q.SeedUserSystem(ctx)) + ask.CheckErr(q.SeedFolderGeneral(ctx)) + ask.CheckErr(q.SeedGeneralOwner(ctx)) + _, err = q.AddUser(ctx, storage.AddUserParams{ + Login: login, + Name: name, + }) + ask.CheckErr(q.SeedGeneralOwner(ctx)) + key.Add(login, sshkey) + + // Install crontab. bulletin, err := os.Executable() if err != nil { panic(err) // TODO: cleanup error handling. @@ -34,9 +80,16 @@ func Install() int { template.Must(template.New("crontab").Parse(crontabTemplate)). Execute(crontab, map[string]string{"Bulletin": bulletin}) fmt.Printf("Add this to crontab:\n\n%s\n", crontab.String()) + err = installCrontab(crontab.String()) + if err != nil { + panic(err) // TODO: cleanup error handling. + } + + // Mark that install has happened. + err = touch(touchfile) + if err != nil { + panic(err) // TODO: cleanup error handling. + } - fmt.Println("TODO: Finish installing bulletin.") - // TODO: Offer to add ssh key for system user. - // TODO: Offer to add ssh key for other user(s). return 0 } diff --git a/batch/file.go b/batch/file.go new file mode 100644 index 0000000000000000000000000000000000000000..bdec4659819b95c0cb6c890372f696c10b394bf5 --- /dev/null +++ b/batch/file.go @@ -0,0 +1,55 @@ +package batch + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func touch(name string) error { + f, err := os.OpenFile(name, os.O_RDONLY|os.O_CREATE, 0666) + if err != nil { + return err + } + f.Close() + return nil +} + +func installCrontab(crontab string) error { + // Make a temp file. + f, err := os.CreateTemp("", "crontab") + if err != nil { + return err + } + defer func() { + f.Close() + os.Remove(f.Name()) + }() + + // Put the crontab in the temp file. + n, err := f.WriteString(crontab) + if err != nil { + return err + } + if n != len(crontab) { + return fmt.Errorf("Failed to write crontab fully %d of %d chars written", + n, len(crontab)) + } + + // Have the crontab command read in the file. + cmd := exec.Command("crontab", f.Name()) + cmd.Stdin = strings.NewReader("") + var stdout strings.Builder + cmd.Stdout = &stdout + var stderr strings.Builder + cmd.Stderr = &stdout + err = cmd.Run() + if err != nil { + fmt.Printf("ERROR: stdout: [%s]\n", stdout.String()) + fmt.Printf("ERROR: stderr: [%s]\n", stderr.String()) + return err + } + + return nil +} diff --git a/key/key.go b/key/key.go new file mode 100644 index 0000000000000000000000000000000000000000..cb4ade461f27fb0faa1d1159c60166a427d1461a --- /dev/null +++ b/key/key.go @@ -0,0 +1,40 @@ +// Package key manages the authorized keys file. +package key + +import ( + "fmt" + "os" + "path" + + "github.com/adrg/xdg" +) + +var keytemplate = `command="%s -u %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s\n` + +// Add adds an ssh key to the `authorized_keys` file. +func Add(login, public string) error { + bulletin, err := os.Executable() + if err != nil { + return err + } + + keyline := fmt.Sprintf(keytemplate, bulletin, login, public) + + keyfile := path.Join(xdg.Home, ".ssh", "authorized_keys") + + f, err := os.OpenFile(keyfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + defer f.Close() + n, err := f.WriteString(keyline) + if err != nil { + return err + } + if n != len(keyline) { + return fmt.Errorf("Failed to write authorized_key fully %d of %d chars written", + n, len(keyline)) + } + + return nil +} diff --git a/storage/connection.go b/storage/connection.go index fcf7c97d9683dab16b58b603a8c7c922e5fa611b..ad10c3fccafb0ff54f209e3f1f75d5debdbde84b 100644 --- a/storage/connection.go +++ b/storage/connection.go @@ -1,17 +1,51 @@ package storage import ( + "embed" "errors" - - "github.com/jmoiron/sqlx" + "os" + "path" // Included to connect to sqlite. _ "github.com/golang-migrate/migrate/v4/database/sqlite" _ "modernc.org/sqlite" + + "github.com/adrg/xdg" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/jmoiron/sqlx" ) +//go:embed migrations/*.sql +var migrationsFS embed.FS + // Open opens the bulletin database. -func Open(dbfile string) (*sqlx.DB, error) { +func Open() (*sqlx.DB, error) { + // Determine path names and create components. + bulldir := path.Join(xdg.DataHome, "BULLETIN") + err := os.MkdirAll(bulldir, 0700) + if err != nil { + return nil, errors.New("bulletin directory problem") + } + dbfile := path.Join(bulldir, "bulletin.db") + + // Run db migrations if needed. + migrations, err := iofs.New(migrationsFS, "migrations") + if err != nil { + return nil, err + } + m, err := migrate.NewWithSourceInstance("iofs", migrations, + "sqlite://"+dbfile+"?_pragma=foreign_keys(1)") + if err != nil { + return nil, err + } + defer m.Close() + err = m.Up() + if err != nil && err != migrate.ErrNoChange { + return nil, err + } + + // Connect to the db. db, err := sqlx.Connect("sqlite", "file://"+dbfile+"?_pragma=foreign_keys(1)") if err != nil { return nil, errors.New("bulletin database problem") diff --git a/storage/migrate.go b/storage/migrate.go deleted file mode 100644 index ab8f218ba5dd0b774610829d50eb55a5faa7d8dd..0000000000000000000000000000000000000000 --- a/storage/migrate.go +++ /dev/null @@ -1,35 +0,0 @@ -package storage - -import ( - "embed" - - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/source/iofs" - - // Included to connect to sqlite. - _ "github.com/golang-migrate/migrate/v4/database/sqlite" - _ "modernc.org/sqlite" -) - -//go:embed migrations/*.sql -var migrationsFS embed.FS - -// Migrate creates and updates the database. -func Migrate(dbfile string) error { - // Run db migrations if needed. - migrations, err := iofs.New(migrationsFS, "migrations") - if err != nil { - return err - } - m, err := migrate.NewWithSourceInstance("iofs", migrations, - "sqlite://"+dbfile+"?_pragma=foreign_keys(1)") - if err != nil { - return err - } - defer m.Close() - err = m.Up() - if err != nil && err != migrate.ErrNoChange { - return err - } - return nil -} diff --git a/this/this.go b/this/this.go index 9154f1593a4d66fab99e30407eaab8e2adec8efc..55ce0aabff924ade98b3f2d93670d3091bf56bdb 100644 --- a/this/this.go +++ b/this/this.go @@ -13,22 +13,16 @@ import ( "context" "errors" "fmt" - "os" - "path" "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/storage" "git.lyda.ie/kevin/bulletin/users" - "github.com/adrg/xdg" "github.com/jmoiron/sqlx" ) // User is the user for this session. var User storage.User -// DBFile is the path for the storage. -var DBFile string - // Store is the store for this session. var Store *sqlx.DB @@ -49,16 +43,8 @@ func StartThis(login, name string) error { return err } - // Run migrations. - bulldir := path.Join(xdg.DataHome, "BULLETIN") - err = os.MkdirAll(bulldir, 0700) - if err != nil { - return errors.New("bulletin directory problem") - } - DBFile := path.Join(bulldir, "bulletin.db") - storage.Migrate(DBFile) - - Store, err = storage.Open(DBFile) + // Connect to the DB. + Store, err = storage.Open() if err != nil { return err }