From 868d1c5d1fd34db92fc582a45c136e0ea5837469 Mon Sep 17 00:00:00 2001 From: Cheer Xiao Date: Wed, 1 Oct 2014 00:57:38 +0200 Subject: [PATCH] Add store package. --- Makefile | 2 +- store/data-dir.go | 24 +++++++++++++++++++++ store/dir.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ store/dir_test.go | 26 +++++++++++++++++++++++ store/sql.go | 11 ++++++++++ store/store.go | 49 ++++++++++++++++++++++++++++++++++++++++++ store/store_test.go | 31 +++++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 store/data-dir.go create mode 100644 store/dir.go create mode 100644 store/dir_test.go create mode 100644 store/sql.go create mode 100644 store/store.go create mode 100644 store/store_test.go diff --git a/Makefile b/Makefile index e5dc3ba3..c71c3f45 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ EXE := elvish -PKGS := edit eval parse util sys +PKGS := edit eval parse util sys store PKG_PATHS := $(addprefix ./,$(PKGS)) # go tools want an explicit ./ PKG_COVERS := $(addprefix cover/,$(PKGS)) diff --git a/store/data-dir.go b/store/data-dir.go new file mode 100644 index 00000000..f20bf130 --- /dev/null +++ b/store/data-dir.go @@ -0,0 +1,24 @@ +package store + +import ( + "errors" + "os" + "strings" +) + +var ( + errEmptyHOME = errors.New("Environment variable HOME is empty") +) + +// ensureDataDir ensures Elvish's data directory exists, creating it if +// necessary. It returns the path to the data directory (never with a +// trailing slash) and possible error. +func ensureDataDir() (string, error) { + home := os.Getenv("HOME") + if home == "" { + return "", errEmptyHOME + } + home = strings.TrimRight(home, "/") + ddir := home + "/.elvish" + return ddir, os.MkdirAll(ddir, 0700) +} diff --git a/store/dir.go b/store/dir.go new file mode 100644 index 00000000..f24e8dc1 --- /dev/null +++ b/store/dir.go @@ -0,0 +1,52 @@ +package store + +import ( + "database/sql" + + "github.com/coopernurse/gorp" +) + +type Dir struct { + Path string + Score float64 +} + +const ( + InitScore = 10 + ScoreIncrement = 10 +) + +func init() { + tableAdders = append(tableAdders, func(dm *gorp.DbMap) { + t := dm.AddTable(Dir{}).SetKeys(false, "Path") + t.ColMap("Path").SetUnique(true) + }) +} + +func (s *Store) AddDir(d string) error { + tx, err := s.dm.Begin() + if err != nil { + return err + } + defer tx.Commit() + + dir := Dir{} + err = tx.SelectOne(&dir, "select * from Dir where Path=?", d) + if err == sql.ErrNoRows { + dir = Dir{Path: d, Score: InitScore} + return tx.Insert(&dir) + } else { + dir.Score += ScoreIncrement + _, err = tx.Update(&dir) + return err + } +} + +func (s *Store) FindDirs(p string) ([]Dir, error) { + var dirs []Dir + _, err := s.dm.Select( + &dirs, + "select * from Dir where Path glob ? order by score desc", + "*"+EscapeGlob(p)+"*") + return dirs, err +} diff --git a/store/dir_test.go b/store/dir_test.go new file mode 100644 index 00000000..fef0b222 --- /dev/null +++ b/store/dir_test.go @@ -0,0 +1,26 @@ +package store + +import ( + "reflect" + "testing" +) + +var ( + dirsToAdd = []string{"/usr", "/usr/bin", "/usr"} + wantedDirs = []Dir{Dir{"/usr", 20}, Dir{"/usr/bin", 10}} +) + +func TestDir(t *testing.T) { + for _, path := range dirsToAdd { + err := tStore.AddDir(path) + if err != nil { + t.Errorf("tStore.AddDir(%q) => %v, want ", path, err) + } + } + + dirs, err := tStore.FindDirs("usr") + if err != nil || !reflect.DeepEqual(dirs, wantedDirs) { + t.Errorf(`tStore.FindDirs("usr") => (%v, %v), want (%v, )`, + dirs, err, wantedDirs) + } +} diff --git a/store/sql.go b/store/sql.go new file mode 100644 index 00000000..88bedb42 --- /dev/null +++ b/store/sql.go @@ -0,0 +1,11 @@ +package store + +import "strings" + +var globEscaper = strings.NewReplacer("\\", "\\\\", "?", "\\?", "*", "\\*") + +// EscapeGlob escapes s to be suitable as an argument to SQLite's GLOB +// operator. +func EscapeGlob(s string) string { + return globEscaper.Replace(s) +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 00000000..7e09d79d --- /dev/null +++ b/store/store.go @@ -0,0 +1,49 @@ +package store + +import ( + "database/sql" + "net/url" + + "github.com/coopernurse/gorp" + _ "github.com/mattn/go-sqlite3" +) + +type Store struct { + dm *gorp.DbMap +} + +var tableAdders []func(*gorp.DbMap) + +// DefaultDB returns the default database for storage. +func DefaultDB() (*sql.DB, error) { + ddir, err := ensureDataDir() + if err != nil { + return nil, err + } + uri := "file:" + url.QueryEscape(ddir+"/db") + + "?mode=rwc&cache=shared&vfs=unix-dotfile" + return sql.Open("sqlite3", uri) +} + +// NewStore creates a new Store with the default database. +func NewStore() (*Store, error) { + db, err := DefaultDB() + if err != nil { + return nil, err + } + return NewStoreDB(db) +} + +// NewStoreDB creates a new Store with a custom database. The database must be +// a SQLite database. +func NewStoreDB(db *sql.DB) (*Store, error) { + dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + for _, ta := range tableAdders { + ta(dbmap) + } + err := dbmap.CreateTablesIfNotExists() + if err != nil { + return nil, err + } + return &Store{dbmap}, nil +} diff --git a/store/store_test.go b/store/store_test.go new file mode 100644 index 00000000..d688e433 --- /dev/null +++ b/store/store_test.go @@ -0,0 +1,31 @@ +package store + +// This file also sets up the test fixture. + +import ( + "database/sql" + "fmt" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +var tStore *Store + +func init() { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(fmt.Sprintf("Failed to create in-memory SQLite3 DB: %v", err)) + } + tStore, err = NewStoreDB(db) + if err != nil { + panic(fmt.Sprintf("Failed to create Store instance: %v", err)) + } +} + +func TestNewStore(t *testing.T) { + _, err := NewStore() + if err != nil { + t.Errorf("NewStore() -> (*, %v), want (*, )", err) + } +}