Loading ask/ask.go +14 −1 Original line number Diff line number Diff line Loading @@ -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) { Loading batch/batch.go +56 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 } batch/file.go 0 → 100644 +55 −0 Original line number Diff line number Diff line 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 } key/key.go 0 → 100644 +40 −0 Original line number Diff line number Diff line // 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 } storage/connection.go +37 −3 Original line number Diff line number Diff line 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") Loading Loading
ask/ask.go +14 −1 Original line number Diff line number Diff line Loading @@ -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) { Loading
batch/batch.go +56 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 }
batch/file.go 0 → 100644 +55 −0 Original line number Diff line number Diff line 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 }
key/key.go 0 → 100644 +40 −0 Original line number Diff line number Diff line // 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 }
storage/connection.go +37 −3 Original line number Diff line number Diff line 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") Loading