Merging in refactor branch
14 files changed, 386 insertions(+), 309 deletions(-)

M cmd/clear.go
M cmd/db.go
M cmd/delete.go
M cmd/files.go
M cmd/merge.go
M cmd/root.go
M cmd/search.go
M cmd/stats.go
M cmd/tag.go
M cmd/tags.go
A => lib/db.go
A => lib/flags.go
M cmd/helpers.go => lib/helpers.go
M cmd/types.go => lib/types.go
M cmd/clear.go +9 -8
@@ 4,6 4,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 38,15 39,15 @@ func init() {
 // ClearFiles remove all tags for given file
 func ClearFiles(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}
 	defer qm.DB.Close()
 
-	file := NewFile(qm, args[0])
+	file := lib.NewFile(qm, args[0])
 	if err := file.Load(qm, false); err != nil {
-		if _, ok := err.(NotFoundError); ok {
+		if _, ok := err.(lib.NotFoundError); ok {
 			fmt.Println(err.Error())
 			return nil
 		}

          
@@ 56,27 57,27 @@ func ClearFiles(cmd *cobra.Command, args
 	if err = file.Clear(qm); err != nil {
 		return err
 	}
-	fmt.Printf("Successfully cleared all tags for %s.\n", RelativePath(file.FullPath()))
+	fmt.Printf("Successfully cleared all tags for %s.\n", lib.RelativePath(file.FullPath()))
 	return nil
 }
 
 // ClearTags remove all tag assignments for given tag
 func ClearTags(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}
 	defer qm.DB.Close()
 
-	if _, err := ValidateTagName(args[0]); err != nil {
+	if _, err := lib.ValidateTagName(args[0]); err != nil {
 		fmt.Println(err)
 		return nil
 	}
 
-	tag := Tag{Name: args[0]}
+	tag := lib.Tag{Name: args[0]}
 	if err := tag.Fetch(qm); err != nil {
-		if _, ok := err.(NotFoundError); ok {
+		if _, ok := err.(lib.NotFoundError); ok {
 			fmt.Println(err.Error())
 			return nil
 		}

          
M cmd/db.go +12 -225
@@ 3,15 3,12 @@ package cmd
 import (
 	"fmt"
 	"os"
-	"path/filepath"
 
-	"github.com/jmoiron/sqlx"
 	_ "github.com/mattn/go-sqlite3" // sqlite3
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
-const schemaVersion int = 1
-
 func init() {
 
 	var dbCmd = &cobra.Command{

          
@@ 42,180 39,15 @@ func init() {
 	rootCmd.AddCommand(dbCmd)
 }
 
-// WriteSchemaVersion write schema version to db
-func WriteSchemaVersion(db *sqlx.DB, version int) error {
-	tx, err := db.Beginx()
-	if err != nil {
-		return err
-	}
-	defer tx.Rollback() // Ignored after Commit()
-
-	q := "UPDATE schema_version SET version = ?"
-	res, err := tx.Exec(q, version)
-	if err != nil {
-		return fmt.Errorf("Error with version query: %v", err)
-	}
-	cnt, err := res.RowsAffected()
-	if err != nil {
-		return err
-	}
-	if cnt != 1 {
-		// Not yet set, write the version
-		q = "INSERT INTO schema_version VALUES (?)"
-		_, err := tx.Exec(q, version)
-		if err != nil {
-			return fmt.Errorf("Writing schema version failed: %v", err)
-		}
-	}
-
-	// Commit the transaction
-	if err := tx.Commit(); err != nil {
-		return err
-	}
-	return nil
-}
-
-// TxExec Run the query
-func TxExec(tx *sqlx.Tx, query string, args ...interface{}) error {
-	_, err := tx.Exec(query, args...)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-// CreateTables create db tables
-func CreateTables(db *sqlx.DB) error {
-	tx, err := db.Beginx()
-	if err != nil {
-		return err
-	}
-	defer tx.Rollback() // Ignored after Commit()
-
-	// Schema version table
-	sql := `
-		CREATE TABLE IF NOT EXISTS schema_version (
-		    version INTEGER NOT NULL PRIMARY KEY
-		)`
-
-	if err = TxExec(tx, sql); err != nil {
-		return err
-	}
-
-	// Main file table
-	sql = `
-		CREATE TABLE IF NOT EXISTS file (
-		    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-		    directory TEXT NOT NULL,
-		    name TEXT NOT NULL,
-		    fingerprint TEXT NOT NULL,
-		    content_type TEXT NOT NULL,
-		    mod_time DATETIME NOT NULL,
-		    size INTEGER NOT NULL,
-		    is_dir BOOLEAN NOT NULL,
-			is_index BOOLEAN NOT NULL,
-		    CONSTRAINT con_file_path UNIQUE (directory, name)
-		)`
-
-	if err = TxExec(tx, sql); err != nil {
-		return err
-	}
-
-	sql = `
-		CREATE VIRTUAL TABLE file_index USING fts5(
-			file_id UNINDEXED,
-			filename,
-			desc,
-			contents
-		)`
-
-	if err = TxExec(tx, sql); err != nil {
-		return err
-	}
-
-	sql = `
-		CREATE TABLE IF NOT EXISTS tag (
-		    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-		    name TEXT NOT NULL,
-		    CONSTRAINT con_tag_name UNIQUE (name)
-		)`
-
-	if err = TxExec(tx, sql); err != nil {
-		return err
-	}
-
-	sql = "CREATE INDEX IF NOT EXISTS idx_tag_name ON tag(name)"
-	if err = TxExec(tx, sql); err != nil {
-		return err
-	}
-
-	// Tag mapping table
-	sql = `
-		CREATE TABLE IF NOT EXISTS tag_map (
-		    file_id INTEGER NOT NULL,
-		    tag_id INTEGER NOT NULL,
-		    PRIMARY KEY (file_id, tag_id),
-		    FOREIGN KEY (file_id) REFERENCES file(id),
-		    FOREIGN KEY (tag_id) REFERENCES tag(id)
-		    CONSTRAINT con_file_tag UNIQUE (file_id, tag_id)
-		)`
-	if err = TxExec(tx, sql); err != nil {
-		return err
-	}
-
-	// Commit the transaction
-	if err := tx.Commit(); err != nil {
-		return err
-	}
-	return nil
-}
-
-// CreateDatabase creates the db file
-func CreateDatabase(dbfile string) error {
-	fmt.Printf("Creating database %v...\n", dbfile)
-
-	dir := filepath.Dir(dbfile)
-
-	_, err := os.Stat(dir)
-	if err != nil {
-		if os.IsNotExist(err) {
-			// Create directories
-			fmt.Printf("Base directory %v doesn't exist. Creating...\n", dir)
-			if err = os.MkdirAll(dir, 0755); err != nil {
-				return err
-			}
-		} else {
-			return err
-		}
-	}
-
-	file, err := os.Create(dbfile)
-	if err != nil {
-		return err
-	}
-	file.Close()
-
-	sqldb, _ := sqlx.Open("sqlite3", dbfile)
-	defer sqldb.Close()
-
-	if err = CreateTables(sqldb); err != nil {
-		return err
-	}
-	if err = WriteSchemaVersion(sqldb, schemaVersion); err != nil {
-		return err
-	}
-	return nil
-}
-
 // DatabaseInit initializes the db file
 func DatabaseInit(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
 	force, _ := cmd.Flags().GetBool("force")
-	fpath := GetFullFilePath(dbFile)
+	fpath := lib.GetFullFilePath(dbFile)
 	_, err := os.Stat(fpath)
 	if err != nil {
 		if os.IsNotExist(err) {
-			if err = CreateDatabase(fpath); err != nil {
+			if err = lib.CreateDatabase(fpath); err != nil {
 				return err
 			}
 			return nil

          
@@ 226,10 58,10 @@ func DatabaseInit(cmd *cobra.Command, ar
 	fmt.Printf("Database file %v exists...\n", dbFile)
 	if force == true {
 		fmt.Println("Force flag is set, removing existing database file.")
-		if err = BackupDatabase(fpath); err != nil {
+		if err = lib.BackupDatabase(fpath); err != nil {
 			return err
 		}
-		if err = CreateDatabase(fpath); err != nil {
+		if err = lib.CreateDatabase(fpath); err != nil {
 			return err
 		}
 	} else {

          
@@ 238,55 70,10 @@ func DatabaseInit(cmd *cobra.Command, ar
 	return nil
 }
 
-// GetDatabase returns an sqlx conn to db
-func GetDatabase() (*sqlx.DB, error) {
-	fpath := GetFullFilePath(dbFile)
-	_, err := os.Stat(fpath)
-	if err != nil {
-		return nil, err
-	}
-	sqldb, err := sqlx.Open("sqlite3", fpath)
-	if err != nil {
-		return nil, err
-	}
-	return sqldb, nil
-}
-
-// GetDBTransaction returns a db transaction
-func GetDBTransaction(db *sqlx.DB) (*sqlx.Tx, error) {
-	tx, err := db.Beginx()
-	if err != nil {
-		return nil, err
-	}
-	return tx, nil
-}
-
-// NewQueryManager returns a new QueryManager instance
-func NewQueryManager() (*QueryManager, error) {
-	db, err := GetDatabase()
-	if err != nil {
-		fmt.Fprintf(
-			os.Stderr,
-			"Unable to open database file. Perhaps you need to run: tago db init\n",
-		)
-		return nil, err
-	}
-	qm := &QueryManager{
-		DB:       db,
-		Commit:   true,
-		Rollback: true,
-	}
-	err = qm.SchemaCheck()
-	if err != nil {
-		return nil, err
-	}
-	return qm, nil
-}
-
 // UpgradeSchema upgrades db schema if needed
 func UpgradeSchema(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}

          
@@ 299,11 86,11 @@ func UpgradeSchema(cmd *cobra.Command, a
 		return fmt.Errorf("Unable to fetch schema version: %v", err)
 	}
 
-	if currentVersion < schemaVersion {
-		if err = BackupDatabase(GetFullFilePath(dbFile)); err != nil {
+	if currentVersion < lib.SchemaVersion {
+		if err = lib.BackupDatabase(lib.GetFullFilePath(dbFile)); err != nil {
 			return err
 		}
-		for currentVersion < schemaVersion {
+		for currentVersion < lib.SchemaVersion {
 			currentVersion++
 			switch currentVersion {
 			/* When changes are needed, something like:

          
@@ 315,10 102,10 @@ func UpgradeSchema(cmd *cobra.Command, a
 				fmt.Println("Schema version: ", currentVersion)
 			}
 			// Upgrade schema version after every iteration
-			WriteSchemaVersion(qm.DB, currentVersion)
+			lib.WriteSchemaVersion(qm.DB, currentVersion)
 		}
 	} else {
-		fmt.Println("Your schema is at the current version: ", schemaVersion)
+		fmt.Println("Your schema is at the current version: ", lib.SchemaVersion)
 	}
 
 	return nil

          
@@ 329,7 116,7 @@ func UpgradeSchema(cmd *cobra.Command, a
 /*
 func upgradeSchemaToVersion2(qm *QueryManager) error {
 	// Placeholder until an upgrade is actually needed
-	// WriteSchemaVersion
+	// lib.WriteSchemaVersion
 	return nil
 }
 */

          
M cmd/delete.go +9 -8
@@ 4,6 4,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 38,15 39,15 @@ func init() {
 // DeleteFiles deletes files from db
 func DeleteFiles(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}
 	defer qm.DB.Close()
 
-	file := NewFile(qm, args[0])
+	file := lib.NewFile(qm, args[0])
 	if err := file.Load(qm, false); err != nil {
-		if _, ok := err.(NotFoundError); ok {
+		if _, ok := err.(lib.NotFoundError); ok {
 			fmt.Println(err.Error())
 			return nil
 		}

          
@@ 56,27 57,27 @@ func DeleteFiles(cmd *cobra.Command, arg
 	if err = file.Delete(qm); err != nil {
 		return err
 	}
-	fmt.Printf("Successfully removed %s from the database.\n", RelativePath(file.FullPath()))
+	fmt.Printf("Successfully removed %s from the database.\n", lib.RelativePath(file.FullPath()))
 	return nil
 }
 
 // DeleteTags deletes tags from db
 func DeleteTags(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}
 	defer qm.DB.Close()
 
-	if _, err := ValidateTagName(args[0]); err != nil {
+	if _, err := lib.ValidateTagName(args[0]); err != nil {
 		fmt.Println(err)
 		return nil
 	}
 
-	tag := Tag{Name: args[0]}
+	tag := lib.Tag{Name: args[0]}
 	if err := tag.Fetch(qm); err != nil {
-		if _, ok := err.(NotFoundError); ok {
+		if _, ok := err.(lib.NotFoundError); ok {
 			fmt.Println(err.Error())
 			return nil
 		}

          
M cmd/files.go +8 -7
@@ 4,6 4,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 23,7 24,7 @@ func init() {
 func ListFiles(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
 	ctype, _ := cmd.Flags().GetString("ctype")
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}

          
@@ 31,7 32,7 @@ func ListFiles(cmd *cobra.Command, args 
 
 	vargs := make([]string, 0, len(args))
 	for _, arg := range args {
-		if tag, err := ValidateTagName(arg); err != nil {
+		if tag, err := lib.ValidateTagName(arg); err != nil {
 			fmt.Println(err)
 		} else {
 			vargs = append(vargs, tag)

          
@@ 42,19 43,19 @@ func ListFiles(cmd *cobra.Command, args 
 		return fmt.Errorf("No valid tags were given")
 	}
 
-	conf := &SearchFilesConfig{}
-	conf.tags = vargs
+	conf := &lib.SearchFilesConfig{}
+	conf.Tags = vargs
 	if ctype != "" {
-		conf.ctype = ctype
+		conf.Ctype = ctype
 	}
 
-	files, err := FileSearch(qm, conf)
+	files, err := lib.FileSearch(qm, conf)
 	if err != nil {
 		return err
 	}
 	if len(files) > 0 {
 		for _, file := range files {
-			fmt.Printf("%v\n", RelativePath(file.FullPath()))
+			fmt.Printf("%v\n", lib.RelativePath(file.FullPath()))
 		}
 	} else {
 		fmt.Println("There are no files with the given tags and options.")

          
M cmd/merge.go +5 -4
@@ 4,6 4,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 21,20 22,20 @@ func init() {
 // MergeTags merges all tags from old-tag into new-tag
 func MergeTags(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}
 	defer qm.DB.Close()
 
 	for _, arg := range args {
-		if _, err := ValidateTagName(arg); err != nil {
+		if _, err := lib.ValidateTagName(arg); err != nil {
 			return fmt.Errorf(err.Error())
 		}
 	}
 
-	otag := Tag{Name: args[0]}
-	ntag := Tag{Name: args[1]}
+	otag := lib.Tag{Name: args[0]}
+	ntag := lib.Tag{Name: args[1]}
 
 	if err := ntag.Merge(qm, &otag); err != nil {
 		return err

          
M cmd/root.go +5 -0
@@ 4,6 4,7 @@ import (
 	"os"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 var (

          
@@ 16,6 17,10 @@ var rootCmd = &cobra.Command{
 	Version: "0.1.0",
 	Short:   "Tag and index your system files.",
 	Long:    "Tag and index your system files.",
+	PersistentPreRun: func(cmd *cobra.Command, args []string) {
+		lib.TagoRunFlags.DbFile = dbFile
+		lib.TagoRunFlags.ShowFullPath = fullPath
+	},
 }
 
 func init() {

          
M cmd/search.go +8 -7
@@ 5,6 5,7 @@ import (
 
 	"github.com/google/shlex"
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 26,9 27,9 @@ func SearchFiles(cmd *cobra.Command, arg
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
 	ctype, _ := cmd.Flags().GetString("ctype")
 	ltags, _ := cmd.Flags().GetString("tags")
-	conf := &SearchFilesConfig{search: args[0]}
+	conf := &lib.SearchFilesConfig{Search: args[0]}
 
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}

          
@@ 44,7 45,7 @@ func SearchFiles(cmd *cobra.Command, arg
 		// Get tags
 		tags := make([]string, 0, len(ptags))
 		for _, arg := range ptags {
-			if tag, err := ValidateTagName(arg); err != nil {
+			if tag, err := lib.ValidateTagName(arg); err != nil {
 				fmt.Println(err)
 			} else {
 				tags = append(tags, tag)

          
@@ 54,21 55,21 @@ func SearchFiles(cmd *cobra.Command, arg
 		if len(tags) == 0 {
 			fmt.Println("No valid tags were given. Skipping tags.")
 		} else {
-			conf.tags = tags
+			conf.Tags = tags
 		}
 	}
 
 	if ctype != "" {
-		conf.ctype = ctype
+		conf.Ctype = ctype
 	}
 
-	files, err := FileSearch(qm, conf)
+	files, err := lib.FileSearch(qm, conf)
 	if err != nil {
 		return err
 	}
 	if len(files) > 0 {
 		for _, file := range files {
-			fmt.Printf("%v\n", RelativePath(file.FullPath()))
+			fmt.Printf("%v\n", lib.RelativePath(file.FullPath()))
 		}
 	} else {
 		fmt.Println("There are no files with the given query and options.")

          
M cmd/stats.go +3 -2
@@ 4,6 4,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 21,7 22,7 @@ func init() {
 // ListTags lists all tags in the database
 func ListTags(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}

          
@@ 40,7 41,7 @@ func ListTags(cmd *cobra.Command, args [
 		ORDER BY
 			file_count DESC, length(name), name`
 
-	var tags []Tag
+	var tags []lib.Tag
 	err = qm.DB.Select(&tags, q)
 	if err != nil {
 		return err

          
M cmd/tag.go +48 -23
@@ 5,6 5,7 @@ import (
 
 	"github.com/google/shlex"
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 40,7 41,7 @@ func TagFile(cmd *cobra.Command, args []
 	ltags, _ := cmd.Flags().GetString("tags")
 	desc, _ := cmd.Flags().GetString("desc")
 	index, _ := cmd.Flags().GetBool("index")
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}

          
@@ 48,19 49,12 @@ func TagFile(cmd *cobra.Command, args []
 
 	if ltags == "" {
 		// One file
-		file := NewFile(qm, args[0])
-		file.IsIndex = index
-		file.indexDesc = desc
-		if err := file.Load(qm, true); err != nil {
-			return err
-		}
-
-		tags := make([]Tag, 0, len(args[1:]))
+		tags := make([]lib.Tag, 0, len(args[1:]))
 		for _, arg := range args[1:] {
-			if tag, err := ValidateTagName(arg); err != nil {
+			if tag, err := lib.ValidateTagName(arg); err != nil {
 				fmt.Println(err)
 			} else {
-				tags = append(tags, Tag{Name: tag})
+				tags = append(tags, lib.Tag{Name: tag})
 			}
 		}
 

          
@@ 68,10 62,27 @@ func TagFile(cmd *cobra.Command, args []
 			return fmt.Errorf("No valid tags were given")
 		}
 
-		if err := file.Tag(qm, tags...); err != nil {
+		files, err := lib.NewFiles(qm, args[0])
+		if err != nil {
 			return err
 		}
-		fmt.Println(RelativePath(file.FullPath()))
+
+		for _, file := range files {
+			file.IsIndex = index
+			file.IndexDesc = desc
+			if err := file.Load(qm, true); err != nil {
+				if len(files) == 1 {
+					return err
+				}
+				fmt.Println(err)
+				continue
+			}
+
+			if err := file.Tag(qm, tags...); err != nil {
+				return err
+			}
+			fmt.Println(lib.RelativePath(file.FullPath()))
+		}
 	} else {
 		// Potentially multiple files
 		ptags, err := shlex.Split(ltags)

          
@@ 80,27 91,41 @@ func TagFile(cmd *cobra.Command, args []
 		}
 
 		// Get tags
-		tags := make([]Tag, 0, len(ptags))
+		tags := make([]lib.Tag, 0, len(ptags))
 		for _, arg := range ptags {
-			if tag, err := ValidateTagName(arg); err != nil {
+			if tag, err := lib.ValidateTagName(arg); err != nil {
 				fmt.Println(err)
 			} else {
-				tags = append(tags, Tag{Name: tag})
+				tags = append(tags, lib.Tag{Name: tag})
 			}
 		}
 
+		if len(tags) == 0 {
+			return fmt.Errorf("No valid tags were given")
+		}
+
 		// Tag each file
 		for _, arg := range args {
-			file := NewFile(qm, arg)
-			file.IsIndex = index
-			file.indexDesc = desc
-			if err := file.Load(qm, true); err != nil {
+			files, err := lib.NewFiles(qm, arg)
+			if err != nil {
 				return err
 			}
-			if err := file.Tag(qm, tags...); err != nil {
-				return err
+
+			for _, file := range files {
+				file.IsIndex = index
+				file.IndexDesc = desc
+				if err := file.Load(qm, true); err != nil {
+					if len(files) == 1 {
+						return err
+					}
+					fmt.Println(err)
+					continue
+				}
+				if err := file.Tag(qm, tags...); err != nil {
+					return err
+				}
+				fmt.Println(lib.RelativePath(file.FullPath()))
 			}
-			fmt.Println(RelativePath(file.FullPath()))
 		}
 	}
 	return nil

          
M cmd/tags.go +6 -5
@@ 4,6 4,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	"hg.code.netlandish.com/~petersanchez/tago/lib"
 )
 
 func init() {

          
@@ 21,15 22,15 @@ func init() {
 // ShowFileTags shows tags assigned to given file
 func ShowFileTags(cmd *cobra.Command, args []string) error {
 	cmd.SilenceUsage = true // Usage is correct, don't show on errors
-	qm, err := NewQueryManager()
+	qm, err := lib.NewQueryManager()
 	if err != nil {
 		return err
 	}
 	defer qm.DB.Close()
 
-	file := NewFile(qm, args[0])
+	file := lib.NewFile(qm, args[0])
 	if err := file.Load(qm, false); err != nil {
-		if _, ok := err.(NotFoundError); ok {
+		if _, ok := err.(lib.NotFoundError); ok {
 			fmt.Println(err.Error())
 			return nil
 		}

          
@@ 41,11 42,11 @@ func ShowFileTags(cmd *cobra.Command, ar
 	}
 
 	if len(tags) == 0 {
-		fmt.Printf("%v: File is not currently tagged.\n", RelativePath(file.FullPath()))
+		fmt.Printf("%v: File is not currently tagged.\n", lib.RelativePath(file.FullPath()))
 		return nil
 	}
 
-	fmt.Println(RelativePath(file.FullPath()) + ":")
+	fmt.Println(lib.RelativePath(file.FullPath()) + ":")
 	for _, tag := range tags {
 		fmt.Printf("\t%v\n", tag.Name)
 	}

          
A => lib/db.go +223 -0
@@ 0,0 1,223 @@ 
+package lib
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/jmoiron/sqlx"
+	_ "github.com/mattn/go-sqlite3" // sqlite3
+)
+
+// SchemaVersion is the current db schema version
+const SchemaVersion int = 1
+
+// WriteSchemaVersion write schema version to db
+func WriteSchemaVersion(db *sqlx.DB, version int) error {
+	tx, err := db.Beginx()
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback() // Ignored after Commit()
+
+	q := "UPDATE schema_version SET version = ?"
+	res, err := tx.Exec(q, version)
+	if err != nil {
+		return fmt.Errorf("Error with version query: %v", err)
+	}
+	cnt, err := res.RowsAffected()
+	if err != nil {
+		return err
+	}
+	if cnt != 1 {
+		// Not yet set, write the version
+		q = "INSERT INTO schema_version VALUES (?)"
+		_, err := tx.Exec(q, version)
+		if err != nil {
+			return fmt.Errorf("Writing schema version failed: %v", err)
+		}
+	}
+
+	// Commit the transaction
+	if err := tx.Commit(); err != nil {
+		return err
+	}
+	return nil
+}
+
+// TxExec Run the query
+func TxExec(tx *sqlx.Tx, query string, args ...interface{}) error {
+	_, err := tx.Exec(query, args...)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// CreateTables create db tables
+func CreateTables(db *sqlx.DB) error {
+	tx, err := db.Beginx()
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback() // Ignored after Commit()
+
+	// Schema version table
+	sql := `
+		CREATE TABLE IF NOT EXISTS schema_version (
+		    version INTEGER NOT NULL PRIMARY KEY
+		)`
+
+	if err = TxExec(tx, sql); err != nil {
+		return err
+	}
+
+	// Main file table
+	sql = `
+		CREATE TABLE IF NOT EXISTS file (
+		    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+		    directory TEXT NOT NULL,
+		    name TEXT NOT NULL,
+		    fingerprint TEXT NOT NULL,
+		    content_type TEXT NOT NULL,
+		    mod_time DATETIME NOT NULL,
+		    size INTEGER NOT NULL,
+		    is_dir BOOLEAN NOT NULL,
+			is_index BOOLEAN NOT NULL,
+		    CONSTRAINT con_file_path UNIQUE (directory, name)
+		)`
+
+	if err = TxExec(tx, sql); err != nil {
+		return err
+	}
+
+	sql = `
+		CREATE VIRTUAL TABLE file_index USING fts5(
+			file_id UNINDEXED,
+			filename,
+			desc,
+			contents
+		)`
+
+	if err = TxExec(tx, sql); err != nil {
+		return err
+	}
+
+	sql = `
+		CREATE TABLE IF NOT EXISTS tag (
+		    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+		    name TEXT NOT NULL,
+		    CONSTRAINT con_tag_name UNIQUE (name)
+		)`
+
+	if err = TxExec(tx, sql); err != nil {
+		return err
+	}
+
+	sql = "CREATE INDEX IF NOT EXISTS idx_tag_name ON tag(name)"
+	if err = TxExec(tx, sql); err != nil {
+		return err
+	}
+
+	// Tag mapping table
+	sql = `
+		CREATE TABLE IF NOT EXISTS tag_map (
+		    file_id INTEGER NOT NULL,
+		    tag_id INTEGER NOT NULL,
+		    PRIMARY KEY (file_id, tag_id),
+		    FOREIGN KEY (file_id) REFERENCES file(id),
+		    FOREIGN KEY (tag_id) REFERENCES tag(id)
+		    CONSTRAINT con_file_tag UNIQUE (file_id, tag_id)
+		)`
+	if err = TxExec(tx, sql); err != nil {
+		return err
+	}
+
+	// Commit the transaction
+	if err := tx.Commit(); err != nil {
+		return err
+	}
+	return nil
+}
+
+// CreateDatabase creates the db file
+func CreateDatabase(dbfile string) error {
+	fmt.Printf("Creating database %v...\n", dbfile)
+
+	dir := filepath.Dir(dbfile)
+
+	_, err := os.Stat(dir)
+	if err != nil {
+		if os.IsNotExist(err) {
+			// Create directories
+			fmt.Printf("Base directory %v doesn't exist. Creating...\n", dir)
+			if err = os.MkdirAll(dir, 0755); err != nil {
+				return err
+			}
+		} else {
+			return err
+		}
+	}
+
+	file, err := os.Create(dbfile)
+	if err != nil {
+		return err
+	}
+	file.Close()
+
+	sqldb, _ := sqlx.Open("sqlite3", dbfile)
+	defer sqldb.Close()
+
+	if err = CreateTables(sqldb); err != nil {
+		return err
+	}
+	if err = WriteSchemaVersion(sqldb, SchemaVersion); err != nil {
+		return err
+	}
+	return nil
+}
+
+// GetDatabase returns an sqlx conn to db
+func GetDatabase(dbFile string) (*sqlx.DB, error) {
+	fpath := GetFullFilePath(dbFile)
+	_, err := os.Stat(fpath)
+	if err != nil {
+		return nil, err
+	}
+	sqldb, err := sqlx.Open("sqlite3", fpath)
+	if err != nil {
+		return nil, err
+	}
+	return sqldb, nil
+}
+
+// GetDBTransaction returns a db transaction
+func GetDBTransaction(db *sqlx.DB) (*sqlx.Tx, error) {
+	tx, err := db.Beginx()
+	if err != nil {
+		return nil, err
+	}
+	return tx, nil
+}
+
+// NewQueryManager returns a new QueryManager instance
+func NewQueryManager() (*QueryManager, error) {
+	db, err := GetDatabase(TagoRunFlags.DbFile)
+	if err != nil {
+		fmt.Fprintf(
+			os.Stderr,
+			"Unable to open database file. Perhaps you need to run: tago db init\n",
+		)
+		return nil, err
+	}
+	qm := &QueryManager{
+		DB:       db,
+		Commit:   true,
+		Rollback: true,
+	}
+	err = qm.SchemaCheck()
+	if err != nil {
+		return nil, err
+	}
+	return qm, nil
+}

          
A => lib/flags.go +10 -0
@@ 0,0 1,10 @@ 
+package lib
+
+// CLIFlags are global flags set on the command line
+type CLIFlags struct {
+	DbFile       string
+	ShowFullPath bool
+}
+
+// TagoRunFlags are global flags set on the CLI
+var TagoRunFlags = &CLIFlags{}

          
M cmd/helpers.go => lib/helpers.go +22 -2
@@ 1,4 1,4 @@ 
-package cmd
+package lib
 
 import (
 	"fmt"

          
@@ 68,6 68,26 @@ func NewFile(qm *QueryManager, fname str
 	return file
 }
 
+// NewFiles returns a slice of *File objects
+func NewFiles(qm *QueryManager, fname string) ([]*File, error) {
+	files := make([]*File, 0)
+	matches, err := filepath.Glob(fname)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(matches) > 0 {
+		for _, match := range matches {
+			file := NewFile(qm, match)
+			files = append(files, file)
+		}
+	} else {
+		file := NewFile(qm, fname)
+		files = append(files, file)
+	}
+	return files, nil
+}
+
 // ValidateTagName ensures given tag(s) are valid
 func ValidateTagName(tag string) (string, error) {
 	tag = strings.ToLower(tag) // Always lowercase

          
@@ 91,7 111,7 @@ func ValidateTagName(tag string) (string
 
 // RelativePath shows relative path from the current directory
 func RelativePath(path string) string {
-	if fullPath {
+	if TagoRunFlags.ShowFullPath {
 		// Do not use relative path with fullPath is switched on
 		return path
 	}

          
M cmd/types.go => lib/types.go +18 -18
@@ 1,4 1,4 @@ 
-package cmd
+package lib
 
 import (
 	"bytes"

          
@@ 70,7 70,7 @@ func (q *QueryManager) SchemaCheck() err
 		return err
 	}
 
-	if curVer != schemaVersion {
+	if curVer != SchemaVersion {
 		return fmt.Errorf(
 			"Schema version mismatch. Perhaps you need to run: tago db upgrade",
 		)

          
@@ 114,10 114,10 @@ type File struct {
 	Size        int64     `db:"size"`
 	IsDir       bool      `db:"is_dir"`
 	IsIndex     bool      `db:"is_index"`
+	IndexDesc   string    `db:"-"`
 
 	// Internal use
-	needsUpdate bool   `db:"-"`
-	indexDesc   string `db:"-"`
+	needsUpdate bool `db:"-"`
 }
 
 // FullPath return file full path

          
@@ 338,7 338,7 @@ func (f *File) Write(qm *QueryManager) (
 				WHERE
 					file_id = ?
 				`
-			res, err = qm.Exec(q, f.Name, f.indexDesc, data, f.ID)
+			res, err = qm.Exec(q, f.Name, f.IndexDesc, data, f.ID)
 			if err != nil {
 				return res, err
 			}

          
@@ 355,7 355,7 @@ func (f *File) Write(qm *QueryManager) (
 					q,
 					f.ID,
 					f.Name,
-					f.indexDesc,
+					f.IndexDesc,
 					data,
 				)
 				if err != nil {

          
@@ 744,9 744,9 @@ type ftsMatch struct {
 
 // SearchFilesConfig submits search terms, etc.
 type SearchFilesConfig struct {
-	search string
-	ctype  string
-	tags   []string
+	Search string
+	Ctype  string
+	Tags   []string
 }
 
 // FileSearch will search db files and content indexes

          
@@ 759,7 759,7 @@ func FileSearch(qm *QueryManager, conf *
 		ftsMatches []ftsMatch
 	)
 
-	if conf.search == "" && len(conf.tags) == 0 {
+	if conf.Search == "" && len(conf.Tags) == 0 {
 		return nil, fmt.Errorf("Must specify either a search query or tag(s)")
 	}
 

          
@@ 769,7 769,7 @@ func FileSearch(qm *QueryManager, conf *
         FROM
             file AS f`
 
-	if conf.search != "" {
+	if conf.Search != "" {
 		q = `
 			SELECT
 				file_id,

          
@@ 780,7 780,7 @@ func FileSearch(qm *QueryManager, conf *
 				file_index
 			WHERE
 				file_index MATCH ?`
-		err = qm.DB.Select(&ftsMatches, q, conf.search)
+		err = qm.DB.Select(&ftsMatches, q, conf.Search)
 		if err != nil {
 			return nil, err
 		}

          
@@ 789,7 789,7 @@ func FileSearch(qm *QueryManager, conf *
 		}
 	}
 
-	if len(conf.tags) > 0 {
+	if len(conf.Tags) > 0 {
 		query += `
 			INNER JOIN
 				tag AS t

          
@@ 801,7 801,7 @@ func FileSearch(qm *QueryManager, conf *
 				tm.file_id = f.id
 			AND
 				tm.tag_id = t.id`
-		nargs = append(nargs, conf.tags)
+		nargs = append(nargs, conf.Tags)
 	}
 
 	if len(ftsMatches) > 0 {

          
@@ 815,9 815,9 @@ func FileSearch(qm *QueryManager, conf *
 		nargs = append(nargs, fids)
 	}
 
-	if conf.ctype != "" {
+	if conf.Ctype != "" {
 		// User specified content type filter
-		ctypeMeta = GetContentTypeQuery(conf.ctype)
+		ctypeMeta = GetContentTypeQuery(conf.Ctype)
 		if len(ftsMatches) > 0 {
 			query += `
 				AND

          
@@ 845,11 845,11 @@ func FileSearch(qm *QueryManager, conf *
 		GROUP BY
 			f.id`
 
-	if len(conf.tags) > 0 {
+	if len(conf.Tags) > 0 {
 		query += `
 			HAVING
 				COUNT(f.id) = ?`
-		nargs = append(nargs, len(conf.tags))
+		nargs = append(nargs, len(conf.Tags))
 	}
 
 	query, nargs, err = sqlx.In(query, nargs...)