From fcc3fb9142b8fbf4baebc25f83293a7d4e18f1b5 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Tue, 6 May 2025 23:41:16 +0100
Subject: [PATCH] Initial folders module

---
 accounts/accounts.go                | 15 +++----
 folders/folders.go                  | 63 +++++++++++++++++++++++++++++
 folders/sql/1_create_table.down.sql |  1 +
 folders/sql/1_create_table.up.sql   | 18 +++++++++
 go.mod                              |  5 +++
 go.sum                              | 13 ++++++
 repl/command.go                     |  4 ++
 repl/folders.go                     | 18 +++++++++
 8 files changed, 127 insertions(+), 10 deletions(-)
 create mode 100644 folders/folders.go
 create mode 100644 folders/sql/1_create_table.down.sql
 create mode 100644 folders/sql/1_create_table.up.sql

diff --git a/accounts/accounts.go b/accounts/accounts.go
index def5a4b..e69a617 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -9,6 +9,7 @@ import (
 	"path"
 	"strings"
 
+	"git.lyda.ie/kevin/bulletin/folders"
 	"github.com/adrg/xdg"
 	_ "modernc.org/sqlite" // Loads sqlite driver.
 )
@@ -19,7 +20,7 @@ type UserData struct {
 	Account        string
 	FullName       string
 	pref           *sql.DB
-	bull           *sql.DB
+	Folders        *folders.Store
 	CurrentFolder  string
 	CurrentMessage int
 }
@@ -59,16 +60,10 @@ func Open(acc string) error {
 	}
 	// TODO: run prefs migration - move this to a storage module.
 
-	bulldir := path.Join(xdg.DataHome, "BULLETIN")
-	err = os.MkdirAll(bulldir, 0700)
+	User.Folders, err = folders.Open()
 	if err != nil {
-		return errors.New("bulletin directory problem")
-	}
-	User.bull, err = sql.Open("sqlite", path.Join(bulldir, "bboard.db"))
-	if err != nil {
-		return errors.New("bulletin database problem")
+		return err
 	}
-	// TODO: run prefs migration - move this to a storage module.
 
 	return nil
 }
@@ -76,7 +71,7 @@ func Open(acc string) error {
 // Close closes the resources open for the account.
 func (u *UserData) Close() {
 	u.pref.Close()
-	u.bull.Close()
+	u.Folders.Close()
 }
 
 // IsAdmin returns true if the user is an admin
diff --git a/folders/folders.go b/folders/folders.go
new file mode 100644
index 0000000..9f4c8fa
--- /dev/null
+++ b/folders/folders.go
@@ -0,0 +1,63 @@
+// Package folders are all the routines and sql for managing folders.
+package folders
+
+import (
+	"database/sql"
+	"embed"
+	"errors"
+	"log"
+	"os"
+	"path"
+
+	"github.com/adrg/xdg"
+	"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 sql/*.sql
+var fs embed.FS
+
+// Store is the store for folders.
+type Store struct {
+	db *sql.DB
+}
+
+// Open opens the folders database.
+func Open() (*Store, error) {
+	fdir := path.Join(xdg.DataHome, "BULLETIN")
+	err := os.MkdirAll(fdir, 0700)
+	if err != nil {
+		return nil, errors.New("bulletin directory problem")
+	}
+	fdb := path.Join(fdir, "bboard.db")
+
+	sqldir, err := iofs.New(fs, "sql")
+	if err != nil {
+		return nil, err
+	}
+	m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb)
+	if err != nil {
+		log.Fatal(err)
+	}
+	err = m.Up()
+	if err != nil {
+		return nil, err
+	}
+	m.Close()
+
+	store := &Store{}
+	store.db, err = sql.Open("sqlite", fdb)
+	if err != nil {
+		return nil, errors.New("bulletin database problem")
+	}
+	return store, nil
+}
+
+// Close closes the db backing the store.
+func (fstore *Store) Close() {
+	fstore.db.Close()
+}
diff --git a/folders/sql/1_create_table.down.sql b/folders/sql/1_create_table.down.sql
new file mode 100644
index 0000000..df3837b
--- /dev/null
+++ b/folders/sql/1_create_table.down.sql
@@ -0,0 +1 @@
+DROP TABLE folders;
diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql
new file mode 100644
index 0000000..232fd99
--- /dev/null
+++ b/folders/sql/1_create_table.up.sql
@@ -0,0 +1,18 @@
+CREATE TABLE folders (
+  name        VARCHAR(25)  NOT NULL UNIQUE,
+  always      INT          DEFAULT 0,
+  brief       INT          DEFAULT 0,
+  description VARCHAR(53),
+  co_owners   TEXT,
+  notify      INT          DEFAULT 0,
+  owner       TEXT,
+  readnew     INT          DEFAULT 0,
+  shownew     INT          DEFAULT 0,
+  system      INT          DEFAULT 0,
+  expire      INT          DEFAULT 14,
+  visibility  TEXT         DEFAULT "public"
+);
+
+INSERT INTO folders (name, description, system, shownew, owner)
+       VALUES ("GENERAL", "Default general bulletin folder.", 1, 1, "SYSTEM");
+
diff --git a/go.mod b/go.mod
index cba97c6..4f17e6a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,17 +5,22 @@ go 1.24.2
 require (
 	github.com/adrg/xdg v0.5.3
 	github.com/chzyer/readline v1.5.1
+	github.com/golang-migrate/migrate/v4 v4.18.3
 	github.com/urfave/cli/v3 v3.3.2
 	modernc.org/sqlite v1.37.0
 )
 
 require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/golang-migrate/migrate v3.5.4+incompatible // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	go.uber.org/atomic v1.11.0 // indirect
 	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	modernc.org/libc v1.65.0 // indirect
diff --git a/go.sum b/go.sum
index a9657c7..95baed5 100644
--- a/go.sum
+++ b/go.sum
@@ -4,20 +4,33 @@ github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwys
 github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
+github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
+github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
+github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o=
 github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
diff --git a/repl/command.go b/repl/command.go
index a963b64..378d46a 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -311,6 +311,7 @@ folder is stored in a file name created with the folder name).
 NOTE:  Creation  of folders may be a restricted command if the installer
 has  elected  to  install  it  as  such.   This  is  done  by  modifying
 BULLCOM.CLD.`,
+		Action:  ActionCreate,
 		MinArgs: 1,
 		MaxArgs: 1,
 		Flags: dclish.Flags{
@@ -849,6 +850,7 @@ owner of the folder or a user with privileges can use this command.
   Format:
  
     MODIFY`,
+		Action: ActionModify,
 		Flags: dclish.Flags{
 			"/DESCRIPTION": {
 				Description: `Specifies a new description for the folder.  You will be prompted for
@@ -1295,6 +1297,8 @@ BULLCP process running (invoked by BULLETIN/STARTUP command.)
 After selecting a folder, the user will notified of the number of unread
 messages,  and  the  message  pointer will be placed at the first unread
 message.`,
+		Action:  ActionSelect,
+		MaxArgs: 1,
 		Flags: dclish.Flags{
 			"/MARKED": {
 				Description: `Selects  only messages that have been marked (indicated by an asterisk).
diff --git a/repl/folders.go b/repl/folders.go
index 3e0db73..7611b81 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -27,3 +27,21 @@ func ActionIndex(cmd *dclish.Command) error {
 	fmt.Printf("TODO: implement INDEX:\n%s\n\n", cmd.Description)
 	return nil
 }
+
+// ActionCreate handles the `CREATE` command.  This creates a folder.
+func ActionCreate(cmd *dclish.Command) error {
+	fmt.Printf("TODO: implement CREATE:\n%s\n\n", cmd.Description)
+	return nil
+}
+
+// ActionSelect handles the `SELECT` command.  This selects a folder.
+func ActionSelect(cmd *dclish.Command) error {
+	fmt.Printf("TODO: implement SELECT:\n%s\n\n", cmd.Description)
+	return nil
+}
+
+// ActionModify handles the `MODIFY` command.  This modifies a folder.
+func ActionModify(cmd *dclish.Command) error {
+	fmt.Printf("TODO: implement MODIFY:\n%s\n\n", cmd.Description)
+	return nil
+}
-- 
GitLab