From 21054959b052c24acc37eeb9bd1960675d81f726 Mon Sep 17 00:00:00 2001 From: Sina Siadat Date: Thu, 23 Apr 2015 23:21:51 +0430 Subject: [PATCH] first commit --- commands.go | 408 +++++++++++++++++++++++++++ filesystem.go | 107 +++++++ main.go | 140 +++++++++ models.attr.go | 749 +++++++++++++++++++++++++++++++++++++++++++++++++ options.go | 129 +++++++++ 5 files changed, 1533 insertions(+) create mode 100644 commands.go create mode 100644 filesystem.go create mode 100644 main.go create mode 100644 models.attr.go create mode 100644 options.go diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..1b9d8cc --- /dev/null +++ b/commands.go @@ -0,0 +1,408 @@ +package main + +import ( + "bufio" + "database/sql" + "fmt" + _ "github.com/hanwen/go-fuse/fuse" + _ "github.com/mattn/go-sqlite3" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + "text/tabwriter" +) + +var globalDB *sql.DB +var globalOpts Options + +const orderby = "-frequency, -mark, CASE WHEN updated_at IS NULL THEN created_at ELSE updated_at END DESC" + +func cmdShow(db *sql.DB, opts Options) bool { + if len(opts.IDs) == 0 && len(opts.Aliases) == 0 { + opts.IDs = append(opts.IDs, int64(getLastAttrID(db))) + } + + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + //fmt.Printf(attr.GetValue()) + printToLess(attr.GetValue()) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + //fmt.Printf(attr.GetValue()) + printToLess(attr.GetValue()) + } + return true +} + +func cmdCat(db *sql.DB, opts Options) bool { + if len(opts.IDs) == 0 && len(opts.Aliases) == 0 { + opts.IDs = append(opts.IDs, int64(getLastAttrID(db))) + } + + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + fmt.Printf(attr.GetValue()) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + fmt.Printf(attr.GetValue()) + } + return true +} + +func cmdMount(db *sql.DB, opts Options) bool { + // check if opts.MountPoint directory exists, if it exists do nothing, if it doesn't create it and continue + globalDB = db + globalOpts = opts + globalOpts.Offset = 0 + globalOpts.Limit = 40 + Mount(opts.MountPoint) + return true +} + +func cmdAddFiles(db *sql.DB, files []string) bool { + tx, err := db.Begin() + + // stmt, err := tx.Prepare("INSERT INTO attributes (name, pwd, value_text, value_blob) VALUES (?, ?, ?, ?)") + stmt, err := tx.Prepare("INSERT INTO attributes (name, value_text, value_blob) VALUES (?, ?, ?)") + + if err != nil { + log.Fatal(err) + } + + defer stmt.Close() + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + content, err := ioutil.ReadFile(file) + if err != nil { + log.Fatal(err) + } + + fileAbsPath, err := filepath.Abs(file) + // fileRelPath, err := filepath.Rel(pwd, fileAbsPath) + + if err != nil { + log.Fatal(err) + } + + //_, err = stmt.Exec("file", pwd, fileRelPath, content) + _, err = stmt.Exec("file", fileAbsPath, content) + if err != nil { + log.Fatal(err) + } + } + + tx.Commit() + return true +} + +func cmdLs(db *sql.DB, w *tabwriter.Writer, opts Options) bool { + attrs := listWithFilters(db, opts) + for _, attr := range attrs { + if opts.ListFilepaths { + fmt.Println(attr.Filepath()) + } else { + attr.Print(w, opts.Recursive, opts.Indent, opts.Filters, opts.AfterLinesCount) + } + } + return true +} + +func cmdNew(db *sql.DB, opts Options) bool { + + var value_text string + + if opts.FromStdin { + lines := make([]string, 0, 0) + reader := bufio.NewReader(os.Stdin) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + go func() { + for _ = range c { + // CTRL-c + // Do nothing, this will continue executing the rest of the code + } + }() + + for { + line, _, err := reader.ReadLine() + if err != nil { + // EOF + break + } + lines = append(lines, string(line)) + log.Printf("%s\n", prettyAttr("eton", string(line))) + } + value_text = strings.Join(lines, "\n") + } else if len(opts.Note) > 0 { + value_text = opts.Note + } else { + f, err := ioutil.TempFile("", "eton-edit") + check(err) + + openEditor(f.Name()) + value_text = readFile(f.Name()) + } + + saveString(db, value_text) + + return true +} + +func cmdAdd(db *sql.DB, id int, attrs []string) bool { + // TODO + return false +} + +func cmdAddAttr(db *sql.DB, id int, attrs []string) bool { + var stmt *sql.Stmt + + tx, err := db.Begin() + + if id == -1 { + stmt, err = tx.Prepare("INSERT INTO attributes (name, value_text) VALUES (?, ?)") + } else { + stmt, err = tx.Prepare("INSERT INTO attributes (name, value_text, parent_id) VALUES (?, ?, ?)") + } + + if err != nil { + log.Fatal(err) + } + + defer stmt.Close() + + if err != nil { + log.Fatal(err) + } + + for _, attr := range attrs { + name := "" + value := "" + + nameValuePair := strings.SplitN(attr, ":", 2) + + switch len(nameValuePair) { + case 1: + value = nameValuePair[0] + case 2: + name = nameValuePair[0] + value = nameValuePair[1] + } + + if id == -1 { + _, err = stmt.Exec(name, value) + } else { + _, err = stmt.Exec(name, value, id) + } + if err != nil { + log.Fatal(err) + } + } + + tx.Commit() + + return true +} + +func cmdUnalias(db *sql.DB, opts Options) bool { + attr := findAttributeByAlias(db, opts.Alias) + if attr.GetID() == -1 { + log.Fatalf("alias \"%s\" not found", opts.Alias) + } else { + attr.SetAlias(db, "") + } + return true +} + +func cmdAlias(db *sql.DB, opts Options) bool { + if !(opts.ID > 0 && len(opts.Alias1) > 0 || len(opts.Alias2) > 0) && !(len(opts.Alias1) > 0 && len(opts.Alias2) > 0) { + return false + } + + var attr Attr + + if opts.ID > 0 { + attr = findAttributeByID(db, opts.ID) + if len(opts.Alias1) > 0 { + attr.SetAlias(db, opts.Alias1) + } else if len(opts.Alias2) > 0 { + attr.SetAlias(db, opts.Alias2) + } + } else if len(opts.Alias1) > 0 && len(opts.Alias2) > 0 { + attr1 := findAttributeByAlias(db, opts.Alias1) + attr2 := findAttributeByAlias(db, opts.Alias2) + + if attr1.GetID() > 0 && attr2.GetID() <= 0 { + attr1.SetAlias(db, opts.Alias2) + } else if attr1.GetID() <= 0 && attr2.GetID() > 0 { + attr2.SetAlias(db, opts.Alias1) + } else { + log.Println("not changing anything", attr1.GetID(), attr2.GetID()) + } + } + return true +} + +func cmdEdit(db *sql.DB, opts Options) bool { + var totalUpdated int64 + + if len(opts.IDs) == 0 && len(opts.Aliases) == 0 { + opts.IDs = append(opts.IDs, int64(getLastAttrID(db))) + } + + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + totalUpdated += attr.Edit(db) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + totalUpdated += attr.Edit(db) + } + + if totalUpdated > 0 { + fmt.Println(totalUpdated, "records updated") + } + + return true +} + +func cmdRm(db *sql.DB, opts Options) bool { + + var totalUpdated int64 + + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + totalUpdated += attr.Rm(db) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + totalUpdated += attr.Rm(db) + } + + if totalUpdated > 0 { + fmt.Println(totalUpdated, "deleted") + } + + return true +} + +func cmdUnrm(db *sql.DB, opts Options) bool { + var totalUpdated int64 + + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + totalUpdated += attr.Unrm(db) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + totalUpdated += attr.Unrm(db) + } + + if totalUpdated > 0 { + fmt.Println(totalUpdated, "recovered") + } + + return true +} + +func cmdInit(db *sql.DB) bool { + InitializeDatabase(db) + return true +} + +func cmdMark(db *sql.DB, opts Options) bool { + var totalUpdated int64 + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + totalUpdated += attr.SetMark(db, 1) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + totalUpdated += attr.SetMark(db, 1) + } + + fmt.Println(totalUpdated, "marked") + return true +} + +func cmdUnmark(db *sql.DB, opts Options) bool { + var totalUpdated int64 + for _, id := range opts.IDs { + attr := findAttributeByID(db, id) + totalUpdated += attr.SetMark(db, 0) + } + + for _, alias := range opts.Aliases { + attr := findAttributeByAlias(db, alias) + totalUpdated += attr.SetMark(db, 0) + } + + fmt.Println(totalUpdated, "marked") + return true +} + +/******************************************************************************/ + +func openEditor(filepath string) { + cmd := exec.Command("/usr/bin/env", "vim", filepath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Run() +} + +func readFile(filepath string) string { + data, err := ioutil.ReadFile(filepath) + check(err) + return string(data) +} + +func check(e error) { + if e != nil { + // log.Fatal(e) + panic(e) + } +} + +func printToLess(text string) { + // declare your pager + cmd := exec.Command("/usr/bin/env", "less") + // create a pipe (blocking) + r, stdin := io.Pipe() + // Set your i/o's + cmd.Stdin = r + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Create a blocking chan, Run the pager and unblock once it is finished + c := make(chan struct{}) + go func() { + defer close(c) + cmd.Run() + }() + + // Pass anything to your pipe + fmt.Fprintf(stdin, text) + + // Close stdin (result in pager to exit) + stdin.Close() + + // Wait for the pager to be finished + <-c +} diff --git a/filesystem.go b/filesystem.go new file mode 100644 index 0000000..6e5d36d --- /dev/null +++ b/filesystem.go @@ -0,0 +1,107 @@ +package main + +import ( + "github.com/hanwen/go-fuse/fuse" + "github.com/hanwen/go-fuse/fuse/nodefs" + "github.com/hanwen/go-fuse/fuse/pathfs" + "log" + "os" + "os/signal" + //"path/filepath" +) + +type HelloFs struct { + pathfs.FileSystem +} + +func (me *HelloFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) { + log.Printf("GetAttr for %v\n", name) + switch name { + case "": + return &fuse.Attr{ + Mode: fuse.S_IFDIR | 0755, + }, fuse.OK + default: + attr := findAttributeByAliasOrID(globalDB, name) + size := 0 + if attr.GetID() > 0 { + size = len(attr.GetValue()) + } + return &fuse.Attr{ + Mode: fuse.S_IFREG | 0644, + Size: uint64(size), + }, fuse.OK + } + return nil, fuse.ENOENT +} + +func (me *HelloFs) OpenDir(name string, context *fuse.Context) (c []fuse.DirEntry, code fuse.Status) { + log.Printf("OpenDir for %v\n", name) + if name == "" { + //c = []fuse.DirEntry{{Name: "file.txt", Mode: fuse.S_IFREG}} + attrs := listWithFilters(globalDB, globalOpts) + c = make([]fuse.DirEntry, len(attrs), len(attrs)) + + for i, attr := range attrs { + var d fuse.DirEntry + d.Name = attr.GetIdentifier() + //log.Println(d.Name) + d.Mode = fuse.S_IFREG | 0644 + c[i] = fuse.DirEntry(d) + //c = append(c, ) + } + + return c, fuse.OK + } + return nil, fuse.ENOENT +} + +func (me *HelloFs) Open(name string, flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) { + log.Printf("Open for %v\n", name) + if flags&fuse.O_ANYWRITE != 0 { + return nil, fuse.EPERM + } + attr := findAttributeByAliasOrID(globalDB, name) + if attr.GetID() <= 0 { + return nil, fuse.ENOENT + } + bytes := []byte(attr.GetValue()) + return nodefs.NewDataFile(bytes), fuse.OK +} + +func Mount(mountpoint string) { + + log.Println("NOTE: This is just an experiment") + log.Println("mounting on", mountpoint) + if _, err := os.Stat(mountpoint); err == nil { + log.Println("directory exists:", mountpoint) + log.Println("move directory and try again") + return + } else { + os.Mkdir(mountpoint, os.ModeDir) + } + + nfs := pathfs.NewPathNodeFs(&HelloFs{FileSystem: pathfs.NewDefaultFileSystem()}, nil) + server, _, err := nodefs.MountRoot(mountpoint, nfs.Root(), nil) + check(err) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + defer func() { + server.Unmount() + //mountpointAbsolutepath, _ := filepath.Abs(mountpoint) + os.Remove(mountpoint) + log.Println("Removed mount point", mountpoint) + }() + + go func() { + for _ = range c { + // CTRL-c + // Do nothing, this will continue executing the rest of the code + server.Unmount() + } + }() + + server.Serve() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..65cd028 --- /dev/null +++ b/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "bufio" + "database/sql" + "github.com/docopt/docopt-go" + _ "github.com/mattn/go-sqlite3" + "log" + "math/rand" + "os" + "path/filepath" + "strconv" + "text/tabwriter" +) + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +var pwd string + +const dbfilename string = ".etondb" + +const usage string = `Usage: + eton new [-|] + eton (ls|grep) [...] [--offset OFFSET] [--limit LIMIT] [--removed-only] [--short] [--all] [--exec COMMAND] [--edit] [--list-files] [--after AFTER] + eton edit [...] + eton alias + eton unalias + eton mark ... + eton unmark ... + eton cat [...] + eton show [...] + eton (rm|remove) ... + eton (unrm|unremove|recover) ... + eton addfile (-|...) + eton mount [] + +Options: + -A, --after AFTER lines to print after a match [default: 0] + -o, --offset OFFSET offset for the items listed [default: 0] + -L, --limit LIMIT maximum number of rows returned, pass -Lall to list everything [default: 10] + -r, --recursive recursive mode + -l, --list-files list items as filenames + -s, --short short mode lists rows with aliases only +` + +func main() { + args, _ := docopt.Parse(usage, nil, true, "version 0.0.0", false, true) + + opts := OptionsFromArgs(args) + + //pwd, _ = os.Getwd() + + dbfile := filepath.Join(homeDirectory(), dbfilename) + var db *sql.DB + + dbfileExists := false + + if _, err := os.Stat(dbfile); err == nil { + dbfileExists = true + } + + //if dbfileExists || args["init"].(bool) { + if true { + var err error + db, err = sql.Open("sqlite3", dbfile) + if err != nil { + log.Fatal(err) + } + defer db.Close() + } else { + log.Fatal(`database file not found, use "init" command`) + } + + if !dbfileExists { + cmdInit(db) + } + + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 0, 2, ' ', 0) + + switch true { + // case args["init"].(bool): + // if dbfileExists { + // log.Fatal("database already exists, command ignored.") + // } + // cmdInit(db) + case args["mount"].(bool): + cmdMount(db, opts) + case args["new"].(bool): + cmdNew(db, opts) + case args["addfile"].(bool): + if len(args[""].([]string)) > 0 { + cmdAddFiles(db, args[""].([]string)) + } else { + reader := bufio.NewReader(os.Stdin) + for { + line, _, err := reader.ReadLine() + if err != nil { + break + } + sline := string(line) + cmdAddFiles(db, []string{sline}) + } + } + case args["ls"].(bool) || args["grep"].(bool): + cmdLs(db, w, opts) + case args["cat"].(bool): + cmdCat(db, opts) + case args["show"].(bool): + cmdShow(db, opts) + case args["rm"].(bool) || args["remove"].(bool): + cmdRm(db, opts) + case args["unrm"].(bool) || args["unremove"].(bool) || args["recover"].(bool): + cmdUnrm(db, opts) + case args["edit"].(bool): + cmdEdit(db, opts) + case args["mark"].(bool): + cmdMark(db, opts) + case args["unmark"].(bool): + cmdUnmark(db, opts) + case args["alias"].(bool): + cmdAlias(db, opts) + case args["unalias"].(bool): + cmdUnalias(db, opts) + case args["addattr"].(bool): + id, _ := strconv.Atoi(args[""].(string)) + cmdAddAttr(db, id, args[""].([]string)) + default: + log.Println("Never reached") + } + + //w.Flush() +} + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} diff --git a/models.attr.go b/models.attr.go new file mode 100644 index 0000000..3182252 --- /dev/null +++ b/models.attr.go @@ -0,0 +1,749 @@ +package main + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "github.com/andrew-d/go-termutil" + "github.com/mgutz/ansi" + "io/ioutil" + "log" + "os" + "regexp" + "strconv" + "strings" + "text/tabwriter" + "time" +) + +// Attr holds the data fetched from a row +// Only 1 ValueXxx field should have value, the others should be nil +type Attr struct { + // Meta + ID sql.NullInt64 + ParentID sql.NullInt64 + Name sql.NullString + Alias sql.NullString + Path sql.NullString + Frequency sql.NullInt64 + Mark sql.NullInt64 + + // Values + ValueText sql.NullString + ValueBlob []byte + ValueInt sql.NullInt64 + ValueReal sql.NullFloat64 + ValueTime time.Time + + // Timestamps + CreatedAt NullTime + UpdatedAt NullTime + AccessedAt NullTime + DeletedAt NullTime +} + +const sqlSelect = "id, value_text, name, parent_id, alias, mark, value_blob, created_at, updated_at" + +// GetID returns the int64 value of attr's ID. +func (attr Attr) GetID() int64 { + //var err error + if value, err := attr.ID.Value(); err == nil && value != nil { + return value.(int64) + } + return -1 +} + +func (attr Attr) GetCreatedAt() (t time.Time) { + //var err error + if value, err := attr.CreatedAt.Value(); err == nil && value != nil { + t = value.(time.Time) + return + } + return t +} + +func (attr Attr) GetUpdatedAt() (t time.Time) { + //var err error + if value, err := attr.UpdatedAt.Value(); err == nil && value != nil { + t = value.(time.Time) + return + } + return t +} + +func (attr Attr) GetAccessedAt() (t time.Time) { + //var err error + if value, err := attr.AccessedAt.Value(); err == nil && value != nil { + t = value.(time.Time) + return + } + return t +} + +func (attr Attr) GetDeletedAt() (t time.Time) { + //var err error + if value, err := attr.DeletedAt.Value(); err == nil && value != nil { + t = value.(time.Time) + return + } + return t +} + +// GetIDString returns the string value of attr's ID. +func (attr Attr) GetIDString() string { + var err error + if value, err := attr.ID.Value(); err == nil && value != nil { + return strconv.Itoa(int(value.(int64))) + } + log.Fatal("Attr is not loaded, has no id") + check(err) + return "" +} + +// GetMark returns the int value of attr's mark +func (attr Attr) GetMark() int { + var err error + if value, err := attr.Mark.Value(); err == nil && value != nil { + return int(value.(int64)) + } + log.Fatal("Mark is not loaded, has no 'mark'") + check(err) + return 0 +} + +// GetIdentifier returns attr's ID, or its Alias if it is not nil. +func (attr Attr) GetIdentifier() string { + alias := attr.GetAlias() + if len(alias) > 0 { + return alias + } else { + return attr.GetIDString() + } +} + +// GetName is a helper to get attr's Name as string +func (attr Attr) GetName() string { + var err error + + if value, err := attr.Name.Value(); err == nil && value != nil { + return value.(string) + } + log.Fatal("Attr is not loaded, has no name") + check(err) + return "" +} + +// GetName is a helper to get attr's Name as string +func (attr Attr) GetAlias() string { + var err error + + if value, err := attr.Alias.Value(); err == nil && value != nil { + return value.(string) + } + check(err) + return "" +} + +// GetTextValue returns a string representation of attr's value, whatever type it is +func (attr Attr) GetTextValue() string { + var err error + if value, err := attr.ValueText.Value(); err == nil && value != nil { + return value.(string) + } + check(err) + return "" +} + +// GetValue returns a string representation of attr's value, in order of +// preference: first ValueBlob, then ValueText, then ValueInt, then ValueReal +func (attr Attr) GetValue() string { + var err error + + // if ValueBlov exists + if len(attr.ValueBlob) > 0 { + return string(attr.ValueBlob) + } + + if value, err := attr.ValueText.Value(); err == nil && value != nil { + return value.(string) + } + check(err) + + if value, err := attr.ValueInt.Value(); err == nil && value != nil { + return strconv.Itoa(value.(int)) + } + check(err) + + if value, err := attr.ValueReal.Value(); err == nil && value != nil { + return strconv.FormatFloat(value.(float64), 'f', 2, 32) + } + check(err) + + log.Fatal("Attr is not loaded, has no value") + + return "" +} + +// Print pretty-prints attr's field values. +func (attr Attr) Print(w *tabwriter.Writer, verbose bool, indent int, highlighteds []string, after int) { + debug := false + + if debug { + if value, err := attr.ParentID.Value(); err == nil && value != nil { + fmt.Fprintf(w, "%s:%d\t", "ParentID", value) + } else { + fmt.Fprintf(w, "%s:%s\t", "ParentID", novalue) + } + + if value, err := attr.Name.Value(); err == nil && value != nil { + fmt.Fprintf(w, "%s:%s\t", "Name", value) + } else { + fmt.Fprintf(w, "%s:%s\t", "Name", novalue) + } + + if value, err := attr.ValueText.Value(); err == nil && value != nil { + fmt.Fprintf(w, "%s:%s\t", "ValueText", value) + } else { + fmt.Fprintf(w, "%s:%s\t", "ValueText", novalue) + } + + if attr.ValueBlob != nil { + fmt.Fprintf(w, "%s:%d\t", "ValueBlob-len", len(attr.ValueBlob)) + } else { + fmt.Fprintf(w, "%s:%s\t", "ValueBlob-len", novalue) + } + } else { + // Last modifier: + //fmt.Fprintf(w, "%s\t", prettyAttr("at", attr.prettyAt())) + + // Name: + //fmt.Fprintf(w, "%s\t", prettyAttr("name", attr.GetName())) + + // Value: + //fmt.Printf(strings.Repeat(" ", indent)) + + if attr.GetMark() == 0 { + fmt.Printf("%s%s %s\n", Color("ID:", "default"), Color(attr.GetIdentifier(), "yellow+b"), attr.Title()) + } else { + fmt.Printf("%s%s %s\n", Color("ID:", "default"), Color(attr.GetIdentifier(), "yellow+b"), Color(attr.Title(), "default+u")) + } + if len(highlighteds) > 0 { + fmt.Println(attr.PrettyMatches(highlighteds, after)) + } + } +} + +func (attr Attr) PrettyMatches(highlighteds []string, after int) string { + var valueText string + if len(highlighteds) == 0 { + valueText = attr.Title() + } else { + valueText = strings.TrimSpace(attr.GetValue()) + + matchinglines := make([]string, 0, 0) + + lastMatchingLine := -1 + var matchCounter int + for linenumber, line := range strings.Split(valueText, "\n") { + line = strings.TrimSpace(line) + isCoveredByLastMatch := lastMatchingLine != -1 && linenumber <= lastMatchingLine+after + + line, matched := highlightLine(line, highlighteds) + if matched { + lastMatchingLine = linenumber + if true { // !isCoveredByLastMatch { + matchCounter += 1 + } + } + if matched || isCoveredByLastMatch { + prefix := fmt.Sprintf("%s L%s:", strings.Repeat(" ", len(attr.GetIdentifier())), strconv.Itoa(linenumber+1)) + matchinglines = append(matchinglines, Color(prefix, "black")+line) + if maximumShownMatches != -1 && matchCounter >= maximumShownMatches { + break + } + } + } + + valueText = strings.Join(matchinglines, "\n") + } + return valueText + "\n" +} + +func (attr Attr) Title() string { + valueText := strings.TrimSpace(attr.GetTextValue()) + firstLineEndIndex := strings.Index(valueText, "\n") + + if firstLineEndIndex >= 0 { + valueText = valueText[0:firstLineEndIndex] + } else { + if len(valueText) > 80 { + valueText = valueText[0:80] + ellipsis + } + } + return valueText +} + +func (attr Attr) prettyAt() string { + if attr.GetUpdatedAt().IsZero() { + return attr.GetCreatedAt().Local().Format(datelayout) // + " " + } else { + return attr.GetUpdatedAt().Local().Format(datelayout) // + "*" + } +} + +func (attr Attr) prettyCreatedAt() string { + return attr.GetCreatedAt().Local().Format(datelayout) +} + +func (attr Attr) prettyUpdatedAt() string { + if !attr.GetUpdatedAt().IsZero() { + return attr.GetUpdatedAt().Local().Format(datelayout) + } else { + return "" + } +} + +func (attr Attr) Filepath() string { + f, err := ioutil.TempFile("", "eton-edit") + check(err) + writeToFile(f.Name(), attr.GetValue()) + return f.Name() +} + +// SetAlias sets attr's Alias to the given alias. +// If give alias is empty string, it will unset the alias (set it to NULL in the database). +func (attr Attr) SetAlias(db *sql.DB, alias string) { + + unset := len(alias) == 0 + if !unset { + var validAlias = regexp.MustCompile(`[^\s\d]+`) + if !validAlias.MatchString(alias) { + fmt.Println("Alias must contain a non-numeric character") + return + } + } + + stmt, err := db.Prepare("UPDATE attributes SET alias = ? WHERE id = ?") + check(err) + + //var result sql.Result + if !unset { + _, err = stmt.Exec(alias, attr.GetID()) + } else { + _, err = stmt.Exec(nil, attr.GetID()) + } + //check(err) + if err == nil { + if unset { + fmt.Printf("alias NULL set for ID:%d\n", attr.GetID()) + } else { + fmt.Printf("alias \"%s\" set for ID:%d\n", alias, attr.GetID()) + } + } else { + log.Fatalf("error while setting alias \"%s\" for ID:%d -- alias must be unique\n", alias, attr.GetID()) // , err) + } + //rowsAffected, err := result.RowsAffected() +} + +func (attr Attr) SetMark(db *sql.DB, mark int) (rowsAffected int64) { + stmt, err := db.Prepare("UPDATE attributes SET mark = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL") + check(err) + + result, err := stmt.Exec(mark, attr.GetID()) + check(err) + rowsAffected, err = result.RowsAffected() + check(err) + + return rowsAffected +} + +func (attr Attr) Rm(db *sql.DB) (rowsAffected int64) { + stmt, err := db.Prepare("UPDATE attributes SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL") + check(err) + + result, err := stmt.Exec(attr.GetID()) + check(err) + rowsAffected, err = result.RowsAffected() + check(err) + + return rowsAffected +} + +func (attr Attr) Unrm(db *sql.DB) (rowsAffected int64) { + stmt, err := db.Prepare("UPDATE attributes SET deleted_at = NULL WHERE id = ? AND deleted_at IS NOT NULL") + check(err) + + result, err := stmt.Exec(attr.GetID()) + check(err) + rowsAffected, err = result.RowsAffected() + check(err) + + return rowsAffected +} + +func (attr Attr) IncrementFrequency(db *sql.DB) (rowsAffected int64) { + stmt, err := db.Prepare("UPDATE attributes SET frequency = frequency + 1 WHERE id = ? AND deleted_at IS NULL") + check(err) + + result, err := stmt.Exec(attr.GetID()) + check(err) + + rowsAffected, err = result.RowsAffected() + check(err) + return +} + +func (attr Attr) Edit(db *sql.DB) (rowsAffected int64) { + // f, err := ioutil.TempFile("", "eton-edit") + // check(err) + // writeToFile(f.Name(), attr.GetValue()) + filepath := attr.Filepath() + + openEditor(filepath) + value_text := readFile(filepath) + if value_text == attr.GetValue() { + fmt.Printf("Nothing changed for ID:%d\n", attr.GetID()) + } else { + update_stmt, err := db.Prepare("UPDATE attributes SET value_text = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?") + check(err) + + result, err := update_stmt.Exec(value_text, attr.GetID()) + check(err) + rowsAffected, err = result.RowsAffected() + check(err) + fmt.Printf("Updated ID:%d\n", attr.GetID()) + } + return +} + +/******************************************************************************/ + +func writeToFile(filepath string, content string) { + err := ioutil.WriteFile(filepath, []byte(content), 0644) + check(err) +} + +func highlightLine(line string, highlighteds []string) (string, bool) { + if len(highlighteds) == 0 { + return line, false + } else { + reFlags := "(?i)" + re := regexp.MustCompile(reFlags + "(" + strings.Join(highlighteds, "|") + ")") + if indexes := re.FindStringIndex(line); indexes != nil { + var indexBegin int + var indexEnd int + var beforeStr string + var afterStr string + + if len(indexes) > 0 { + firstIndex := indexes[0] + indexBegin = firstIndex - 40 + if indexBegin < 0 { + indexBegin = 0 + } + if indexBegin != 0 { + beforeStr = ellipsis + } + indexEnd = firstIndex + 40 + } else { + indexEnd = 80 + } + + if indexEnd > indexBegin+80 { + indexEnd = indexBegin + 80 + } + if indexEnd > len(line) { + indexEnd = len(line) + } + if indexEnd != len(line) { + afterStr = ellipsis + } + + line = re.ReplaceAllString(line[indexBegin:indexEnd], Color("$0", "black+b:green")) + return beforeStr + line + afterStr, true + } + return line, false + } +} + +func prettyAttr(name, value string) string { + if len(name) > 0 { + name = name + ":" + } + if termutil.Isatty(os.Stdout.Fd()) { + return ansi.Color(name, "black") + ansi.Color(value, "default") + } else { + return name + value + } +} + +func prettyAttr2(name, value string) string { + if termutil.Isatty(os.Stdout.Fd()) { + return ansi.Color(name+":", "black") + ansi.Color(value, "blue") + } else { + return name + ":" + value + } +} + +// Color is the same as ansi.Color but only if STDOUT is a TTY +func Color(str, color string) string { + if termutil.Isatty(os.Stdout.Fd()) { + return ansi.Color(str, color) + } else { + return str + } +} + +func findAttributeByID(db *sql.DB, ID int64) (attr Attr) { + var err error + var stmt *sql.Stmt + + defer func() { + if err == nil { + attr.IncrementFrequency(db) + } + }() + + stmt, err = db.Prepare("SELECT " + sqlSelect + " FROM attributes WHERE id = ? AND deleted_at IS NULL LIMIT 1") + check(err) + + err = stmt.QueryRow(ID).Scan(&attr.ID, &attr.ValueText, &attr.Name, &attr.ParentID, &attr.Alias, &attr.Mark, &attr.ValueBlob, &attr.CreatedAt, &attr.UpdatedAt) + if err != nil { + log.Fatalln("No record found with id", ID, err) + } + return +} + +func findAttributeByAlias(db *sql.DB, alias string) (attr Attr) { + var err error + var stmt *sql.Stmt + + defer func() { + if err == nil { + attr.IncrementFrequency(db) + } + }() + + // Exact match + stmt, err = db.Prepare("SELECT " + sqlSelect + " FROM attributes WHERE alias = ? ORDER BY " + orderby + " LIMIT 1") + check(err) + err = stmt.QueryRow(alias).Scan(&attr.ID, &attr.ValueText, &attr.Name, &attr.ParentID, &attr.Alias, &attr.Mark, &attr.ValueBlob, &attr.CreatedAt, &attr.UpdatedAt) + if err == nil { + return + } + + stmt, err = db.Prepare("SELECT " + sqlSelect + " FROM attributes WHERE alias LIKE ? ORDER BY " + orderby + " LIMIT 1") + check(err) + + // Prefix match + err = stmt.QueryRow(alias+"%").Scan(&attr.ID, &attr.ValueText, &attr.Name, &attr.ParentID, &attr.Alias, &attr.Mark, &attr.ValueBlob, &attr.CreatedAt, &attr.UpdatedAt) + if err == nil { + return + } + + // Postfix match + err = stmt.QueryRow("%"+alias).Scan(&attr.ID, &attr.ValueText, &attr.Name, &attr.ParentID, &attr.Alias, &attr.Mark, &attr.ValueBlob, &attr.CreatedAt, &attr.UpdatedAt) + if err == nil { + return + } + + prunes := strings.Split(alias, "") + + // Fuzzy match + err = stmt.QueryRow("%"+strings.Join(prunes, "%")+"%").Scan(&attr.ID, &attr.ValueText, &attr.Name, &attr.ValueBlob) + if err == nil { + return + } + + return +} + +func findAttributeByAliasOrID(db *sql.DB, alias_or_id string) (attr Attr) { + attr = findAttributeByAlias(db, alias_or_id) + if attr.GetID() <= 0 { + + intID, err := strconv.Atoi(alias_or_id) + + if err != nil { + return attr + } + + attr = findAttributeByID(db, int64(intID)) + } + + return attr +} + +func listWithFilters(db *sql.DB, opts Options) (attrs []Attr) { + var stmt *sql.Stmt + var rows *sql.Rows + var nolimit = opts.Limit == -1 + + var sqlConditions string + var sqlLimit string + + queryValues := make([]interface{}, 0, 5) + + if opts.RemovedOnly { + sqlConditions = "deleted_at IS NOT NULL" + } else { + sqlConditions = "deleted_at IS NULL" + } + + if opts.RootID == -1 { + sqlConditions += " AND parent_id IS NULL" + } else { + nolimit = true + sqlConditions += fmt.Sprintf(" AND parent_id = %d ", opts.RootID) + } + + if !opts.Recursive { + sqlConditions += " AND parent_id IS NULL" + } + + if opts.AliasedOrMarkedOnly { + sqlConditions += " AND ((alias IS NOT NULL AND alias != '') OR mark > 0)" + } + + if opts.RootID == -1 && len(opts.Filters) > 0 { + nolimit = true + nameOrVal := make([]string, 0, 0) + + for _, filter := range opts.Filters { + likeValue := "%" + filter + "%" + // queryValues = append(queryValues, likeValue, likeValue, likeValue) + queryValues = append(queryValues, likeValue, likeValue) + + // nameOrVal = append(nameOrVal, "(value_text LIKE ? OR name LIKE ? OR alias LIKE ?)") + nameOrVal = append(nameOrVal, "(value_text LIKE ? OR alias LIKE ?)") + } + + sqlConditions += " AND ( " + strings.Join(nameOrVal, " AND ") + " )" + } + + if nolimit { + sqlLimit = "" + } else { + queryValues = append(queryValues, opts.Offset) + queryValues = append(queryValues, opts.Limit) + sqlLimit = "LIMIT ?, ?" + } + + // =========================================================================== + + tx, err := db.Begin() + check(err) + stmt, err = tx.Prepare("SELECT " + sqlSelect + " FROM attributes WHERE " + sqlConditions + " ORDER BY " + orderby + " " + sqlLimit) + check(err) + defer stmt.Close() + rows, err = stmt.Query(queryValues...) + check(err) + defer rows.Close() + + attrs = make([]Attr, 0, 0) + + for rows.Next() { + attr := Attr{} + err = rows.Scan(&attr.ID, &attr.ValueText, &attr.Name, &attr.ParentID, &attr.Alias, &attr.Mark, &attr.ValueBlob, &attr.CreatedAt, &attr.UpdatedAt) + check(err) + attrs = append(attrs, attr) + + var optsNew Options + optsNew = opts + optsNew.RootID = attr.GetID() + optsNew.Indent += 2 + //cmdLs(db, w, optsNew) + } + + tx.Commit() + return attrs +} + +func getLastAttrID(db *sql.DB) int64 { + // Experimental + var ID int64 + + stmt, err := db.Prepare("SELECT id FROM attributes WHERE deleted_at IS NULL ORDER BY " + orderby + " LIMIT 1") + check(err) + + err = stmt.QueryRow().Scan(&ID) + check(err) + return ID +} + +func saveString(db *sql.DB, value_text string) { + new_stmt, err := db.Prepare("INSERT INTO attributes (name, value_text) VALUES ('note', ?)") + check(err) + + result, err := new_stmt.Exec(value_text) + check(err) + + lastInsertId, err := result.LastInsertId() + check(err) + + fmt.Printf("New note ID:%d\n", lastInsertId) +} + +func InitializeDatabase(db *sql.DB) bool { + sqlStmt := ` + DROP TABLE IF EXISTS attributes; + CREATE TABLE attributes ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT, + alias TEXT, + parent_id INTEGER, + frequency INTEGER DEFAULT 0, + mark INTEGER DEFAULT 0, + -- pwd TEXT, + + value_text TEXT, + value_blob BLOB, + value_int INTEGER, + value_real REAL, + value_time DATETIME, + + accessed_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE UNIQUE INDEX IF NOT EXISTS index_on_alias ON attributes (alias); + CREATE INDEX IF NOT EXISTS index_on_name ON attributes (name); + CREATE INDEX IF NOT EXISTS index_on_value_text ON attributes (value_text); + CREATE INDEX IF NOT EXISTS index_on_value_blob ON attributes (value_blob); + CREATE INDEX IF NOT EXISTS index_on_value_int ON attributes (value_int); + CREATE INDEX IF NOT EXISTS index_on_value_real ON attributes (value_real); + CREATE INDEX IF NOT EXISTS index_on_accessed_at ON attributes (accessed_at); + CREATE INDEX IF NOT EXISTS index_on_deleted_at ON attributes (deleted_at); + CREATE INDEX IF NOT EXISTS index_on_frequency ON attributes (frequency); + CREATE INDEX IF NOT EXISTS index_on_mark ON attributes (mark); + ` + _, err := db.Exec(sqlStmt) + if err != nil { + log.Fatal(err) + return false + } + fmt.Println("repository initiated") + return true +} + +// NullTime allows timestamps to be NULL +type NullTime struct { + Time time.Time + Valid bool // Valid is true if Time is not NULL +} + +// Scan implements the Scanner interface. +func (nt *NullTime) Scan(value interface{}) error { + nt.Time, nt.Valid = value.(time.Time) + return nil +} + +// Value implements the driver Valuer interface. +func (nt NullTime) Value() (driver.Value, error) { + if !nt.Valid { + return nil, nil + } + return nt.Time, nil +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..8268b8a --- /dev/null +++ b/options.go @@ -0,0 +1,129 @@ +package main + +import ( + "os/user" + "path/filepath" + "strconv" +) + +const novalue string = "nil" +const datelayout string = "06/01/02 03:04pm" +const ellipsis = "…" +const maximumShownMatches = -1 // -1 + +type Options struct { + ID int64 + Alias string + IDs []int64 + Aliases []string + Limit int + Offset int + RootID int64 + Indent int + Filters []string + FromStdin bool + Recursive bool + RemovedOnly bool + AliasedOrMarkedOnly bool + ListFilepaths bool + MountPoint string + Note string + AfterLinesCount int + Alias1 string + Alias2 string +} + +func OptionsFromArgs(args map[string]interface{}) (opts Options) { + // log.Printf("%v\n", args) + var err error + + opts.RootID = -1 + opts.Indent = 0 + + opts.Offset, err = strconv.Atoi(args["--offset"].(string)) + check(err) + + opts.ListFilepaths = args["--list-files"].(bool) + + if args[""] != nil { + opts.Note = args[""].(string) + } + + opts.AfterLinesCount, err = strconv.Atoi(args["--after"].(string)) + check(err) + + if args["--limit"].(string) == "all" { + opts.Limit = -1 + } else { + opts.Limit, err = strconv.Atoi(args["--limit"].(string)) + check(err) + } + + if args[""] != nil { + intID, err := strconv.Atoi(args[""].(string)) + if err == nil { + opts.ID = int64(intID) + } else { + opts.Alias1 = args[""].(string) + } + } + + if args[""] != nil { + opts.MountPoint = args[""].(string) + } else { + opts.MountPoint = filepath.Join(homeDirectory(), "eton-default-mount-point") + } + + if args[""] != nil { + intID, err := strconv.Atoi(args[""].(string)) + if err == nil { + opts.ID = int64(intID) + } else { + opts.Alias2 = args[""].(string) + } + } + + if args[""] != nil { + intID, err := strconv.Atoi(args[""].(string)) + if err == nil { + opts.ID = int64(intID) + } else { + opts.Alias = args[""].(string) + } + } + + for _, id := range args[""].([]string) { + intID, err := strconv.Atoi(id) + if err == nil { + opts.IDs = append(opts.IDs, int64(intID)) + } else { + opts.Aliases = append(opts.Aliases, id) + } + } + + if args[""] != nil { + opts.Alias = args[""].(string) + } + + opts.Filters = args[""].([]string) + opts.FromStdin = args["-"].(bool) + opts.Recursive = false // args["--recursive"].(bool) + opts.RemovedOnly = args["--removed-only"].(bool) + + opts.AliasedOrMarkedOnly = args["--short"].(bool) + return +} + +func (opts Options) GetIDsArrayOfInterface() []interface{} { + var interfaceIds = make([]interface{}, len(opts.IDs), len(opts.IDs)) + for i, id := range opts.IDs { + interfaceIds[i] = id + } + return interfaceIds +} + +func homeDirectory() string { + usr, err := user.Current() + check(err) + return usr.HomeDir +}