diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65075c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# YACC +y.output + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b7f7de --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# tagger diff --git a/api.go b/api.go new file mode 100644 index 0000000..965f956 --- /dev/null +++ b/api.go @@ -0,0 +1,98 @@ +package tagger + +import ( + "code.google.com/p/go-uuid/uuid" + "errors" + "io" +) + +type ( + // StorageProvider specifies the interface that must be implemented by tag storage backends. + StorageProvider interface { + io.Closer + + GetFile(u uuid.UUID) (File, error) + GetFileForPath(path string) (File, error) + GetAllFiles() ([]File, error) + GetMatchingFiles(f Filter) ([]File, error) + + UpdateTag(f File, t Tag) error + RemoveTag(f File, t Tag) error + GetTags(f File) ([]Tag, error) + // GetAllTags() ([]Tag, error) // TODO: Reconsider this method. Maybe split into two? (tags, values) + + UpdateFile(f File, t []Tag) error + RemoveFile(f File) error + } + + // File is a structure that represents a file in the database + File struct { + uuid uuid.UUID + path string + } + + // Tag is an interface representing the needed methods on a tag + Tag interface { + Name() string + HasValue() bool + Value() int + } + + // NamedTag is a tag with just a name + NamedTag struct { + name string + } + + // ValueTag is a tag with both a name and a value + ValueTag struct { + name string + value int + } +) + +// NewFile creates a new file struct an populates it's fields +func NewFile(uuid_ uuid.UUID, path string) File { + return File{uuid: uuid_, path: path} +} + +// NewNamedTag creates a new NamedTag struct an populates it's fields +func NewNamedTag(name string) *NamedTag { + return &NamedTag{name: name} +} + +// NewValueTag creates a new ValueTag struct an populates it's fields +func NewValueTag(name string, value int) *ValueTag { + return &ValueTag{name: name, value: value} +} + +// UUID returns the UUID of a file +func (f File) UUID() uuid.UUID { return f.uuid } + +// Path returns the path of a file +func (f File) Path() string { return f.path } + +// Name returns the name of a tag +func (t NamedTag) Name() string { return t.name } + +// Name returns the name of a tag +func (t ValueTag) Name() string { return t.name } + +// HasValue returns whether the tag has a value +func (t NamedTag) HasValue() bool { return false } + +// HasValue returns whether the tag has a value +func (t ValueTag) HasValue() bool { return true } + +// Value returns -1 on a named tag +func (t NamedTag) Value() int { return -1 } + +// Value returns the value of a value tag +func (t ValueTag) Value() int { return t.value } + +// Errors +var ( + ErrNoFile = errors.New("tagger: No such file in storage") + ErrNoTag = errors.New("tagger: No such tag on file") + ErrNoMatches = errors.New("tagger: No matching files in storage") + ErrInvalidValue = errors.New("tagger: Invalid tag value") +) diff --git a/cmd/tagger-cli/main.go b/cmd/tagger-cli/main.go new file mode 100644 index 0000000..1475cf4 --- /dev/null +++ b/cmd/tagger-cli/main.go @@ -0,0 +1,323 @@ +package main + +import ( + "code.google.com/p/go-uuid/uuid" + "flag" + "fmt" + "github.com/kiljacken/tagger" + "github.com/kiljacken/tagger/storage" + "os" + "strconv" + "strings" +) + +const NAME = "tagger-cli" +const VERSION = "0.0.1-alpha" +const ARG_OFFSET = 1 + +type command struct { + f func() error + name string + desc string +} + +var commands []command +var commandMap map[string]command + +func init() { + commands = []command{ + {usage, "help", "prints a helpful usage message"}, + {version, "version", "prints version information"}, + // File manipulation + {addFile, "add", "adds a file to the tag database"}, + {removeFile, "remove", "removes a file from the tag database"}, + {moveFile, "move", "moves a file to a new location"}, + // Tag manipulation + {setTag, "set", "sets a tag on a file"}, + {unsetTag, "unset", "unsets a tag on a file"}, + // Querying + {match, "match", "find files matching filter"}, + {get, "get", "gets the tags on a file"}, + {files, "files", "gets all files in database"}, + } + + commandMap = map[string]command{} + for _, cmd := range commands { + commandMap[cmd.name] = cmd + } +} + +var provider tagger.StorageProvider + +func main() { + // TODO: os.Exit(?) prohibits defers from executing. this could be bad + flag.Parse() + + if flag.NArg() < 1 { + usage() + os.Exit(1) + } + + passedCmd := flag.Arg(0) + cmd, ok := commandMap[passedCmd] + if !ok { + fmt.Printf("Unknown command: %s\n", passedCmd) + usage() + os.Exit(1) + } + + // Setup storage provider + prov, err := storage.NewSqliteStorage("./test.db") //":memory:") + if err != nil { + fmt.Printf("Error while opening storage: %s\n", err) + os.Exit(1) + } + provider = prov + defer provider.Close() + + // Execute the command + if err = cmd.f(); err != nil { + fmt.Printf("Error while executing command: %s\n", err) + os.Exit(1) + } + + // Exit with error code 0 + os.Exit(0) +} + +func getFileFromArg(arg string) (tagger.File, error) { + // If path contains the prefix 'uuid:' consider it an uuid + if strings.HasPrefix(arg, "uuid:") { + // Get the file matching the uuid + return provider.GetFile(uuid.Parse(arg[5:])) + } else { + // Get the file matching the file + return provider.GetFileForPath(arg) + } +} + +func ensureArgs(n int, msg string) error { + if flag.NArg() < ARG_OFFSET+n { + return fmt.Errorf("Expected %d arguments, got %d.\nUsage: %s", n, flag.NArg()-ARG_OFFSET, msg) + } + return nil +} + +func usage() error { + fmt.Printf("Usage: tagger-cli [command] \n") + fmt.Printf("\n") + fmt.Printf("Available commands:\n") + for _, cmd := range commands { + fmt.Printf(" %s: %s\n", cmd.name, cmd.desc) + } + + return nil +} + +func version() error { + fmt.Printf("%s v%s\n", NAME, VERSION) + return nil +} + +func addFile() error { + // Ensure we have enough arguments + if err := ensureArgs(1, "add [path]"); err != nil { + return err + } + + path := flag.Arg(ARG_OFFSET) + + // Create the new file + file := tagger.NewFile(uuid.NewUUID(), path) + + // Update the file, an return if an error occurs + err := provider.UpdateFile(file, []tagger.Tag{}) + if err != nil { + return err + } + + // Print the new uuid to the user + fmt.Printf("%s\n", file.UUID()) + + return nil +} + +func removeFile() error { + // Ensure we have enough arguments + if err := ensureArgs(1, "remove [path]"); err != nil { + return err + } + + path := flag.Arg(ARG_OFFSET) + + // Get the file matching the supplied argument + file, err := getFileFromArg(path) + if err != nil { + return err + } + + // Remove the file and return the error value + return provider.RemoveFile(file) +} + +func moveFile() error { + // Ensure we have enough arguments + if err := ensureArgs(2, "move [source] [destination]"); err != nil { + return err + } + + src := flag.Arg(ARG_OFFSET) + dst := flag.Arg(ARG_OFFSET + 1) + + // Get the file matching the supplied argument + file, err := getFileFromArg(src) + if err != nil { + return err + } + + // Get the tags of the file + tags, err := provider.GetTags(file) + if err != nil { + return err + } + + // Update the file path + file = tagger.NewFile(file.UUID(), dst) + + // Update the file and return the error value + return provider.UpdateFile(file, tags) +} + +func setTag() error { + // Ensure we have enough arguments + if err := ensureArgs(2, "set [path] [tag] (value)"); err != nil { + return err + } + + path := flag.Arg(ARG_OFFSET) + name := flag.Arg(ARG_OFFSET + 1) + + // Get specified file + file, err := getFileFromArg(path) + if err != nil { + return err + } + + // Depending on the amount of arguments, create a value tag or a named tag + var tag tagger.Tag + if flag.NArg() > ARG_OFFSET+2 { + // Parse tag value + value, err := strconv.Atoi(flag.Arg(ARG_OFFSET + 2)) + if err != nil { + return tagger.ErrInvalidValue + } + + // Create a value tag + tag = tagger.NewValueTag(name, int(value)) + } else { + // Create a named tag + tag = tagger.NewNamedTag(name) + } + + // Update the tag and return any errors + return provider.UpdateTag(file, tag) +} + +func unsetTag() error { + // Ensure we have enough arguments + if err := ensureArgs(2, "unset [path] [tag]"); err != nil { + return err + } + + path := flag.Arg(ARG_OFFSET) + name := flag.Arg(ARG_OFFSET + 1) + + // Get specified file + file, err := getFileFromArg(path) + if err != nil { + return err + } + + tag := tagger.NewNamedTag(name) + + // Update the tag and return any errors + return provider.RemoveTag(file, tag) +} + +func match() error { + if err := ensureArgs(1, "match [filter]"); err != nil { + return err + } + + // Stich filter together from arguments for user convinience + arg := "" + for i := ARG_OFFSET; i < flag.NArg(); i++ { + arg = fmt.Sprintf("%s %s", arg, flag.Arg(i)) + } + + // Parse the filter + r := strings.NewReader(arg) + filter, err := tagger.ParseFilter(r) + if err != nil { + return err + } + + // Get all files matching the filter + files, err := provider.GetMatchingFiles(filter) + if err != nil { + return err + } + + // Print all matched files + for _, file := range files { + fmt.Printf("%s %s\n", file.UUID(), file.Path()) + } + + return nil +} + +func get() error { + if err := ensureArgs(1, "get [file]"); err != nil { + return err + } + path := flag.Arg(ARG_OFFSET) + + // Get the provided file + file, err := getFileFromArg(path) + if err != nil { + return nil + } + + // Get the tags for the file + tags, err := provider.GetTags(file) + if err != nil { + return err + } + + // Loop through each tag and print it out + for _, tag := range tags { + if tag.HasValue() { + fmt.Printf("%s=%d ", tag.Name(), tag.Value()) + } else { + fmt.Printf("%s ", tag.Name()) + } + } + fmt.Printf("\n") + + return nil +} + +func files() error { + // Get the list of all files + files, err := provider.GetAllFiles() + if err != nil { + return err + } + + // Loop through each file and print their UUID and path + for _, file := range files { + fmt.Printf("%s %s\n", file.UUID(), file.Path()) + } + + return nil +} diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..0fa8b15 --- /dev/null +++ b/filter.go @@ -0,0 +1,184 @@ +package tagger + +import ( + "fmt" + "strings" +) + +// TODO: Investigate negate/invert filter and it's sql equivalent + +// Filter provides an interface to filter files based on their tags +type Filter interface { + fmt.Stringer + // TODO: This interface might not if databases engines want to optimize filtering + Matches(t []Tag) bool +} + +// NameFilter filters tags on their names +type NameFilter struct { + Name string +} + +// Matches check if the filter matches the given tags +func (n NameFilter) Matches(tags []Tag) bool { + for _, tag := range tags { + if tag.Name() == n.Name { + return true + } + } + return false +} + +// Comparator describes a way to compare two integer values +type Comparator int + +// Definitions of various comparinson operators +const ( + Invalid Comparator = iota + Equals + NotEquals + LessThan + GreaterThan + LessThanOrEqual + GreaterThanOrEqual +) + +func ComparatorFromString(val string) Comparator { + switch val { + case "==": + return Equals + case "!=": + return NotEquals + case "<": + return LessThan + case ">": + return GreaterThan + case "<=": + return LessThanOrEqual + case ">=": + return GreaterThanOrEqual + default: + return Invalid + } +} + +// ComparinsonFilter filters value tags based on their value +type ComparinsonFilter struct { + Name string + Value int + Function Comparator +} + +// Matches check if the filter matches the given tags +func (c ComparinsonFilter) Matches(tags []Tag) bool { + for _, tag := range tags { + if tag.Name() == c.Name { + if !tag.HasValue() { + return false + } + + switch c.Function { + case Equals: + return tag.Value() == c.Value + + case NotEquals: + return tag.Value() != c.Value + + case LessThan: + return tag.Value() < c.Value + + case GreaterThan: + return tag.Value() > c.Value + + case LessThanOrEqual: + return tag.Value() <= c.Value + + case GreaterThanOrEqual: + return tag.Value() >= c.Value + } + } + } + + return false +} + +// AndFilter allows the joining of two or more filters, all which must match +type AndFilter struct { + Filters []Filter +} + +// Matches check if the filter matches the given tags +func (a AndFilter) Matches(tags []Tag) bool { + for _, filter := range a.Filters { + if !filter.Matches(tags) { + return false + } + } + + return true +} + +// OrFilter allows the joining of two or more filters, one of which must match +type OrFilter struct { + Filters []Filter +} + +// Matches check if the filter matches the given tags +func (o OrFilter) Matches(tags []Tag) bool { + for _, filter := range o.Filters { + if filter.Matches(tags) { + return true + } + } + + return false +} + +// Debuggg + +func (c Comparator) String() string { + switch c { + case Equals: + return "==" + + case NotEquals: + return "!=" + + case LessThan: + return "<" + + case GreaterThan: + return ">" + + case LessThanOrEqual: + return "<=" + + case GreaterThanOrEqual: + return ">=" + } + return "INVALID" +} + +func (n NameFilter) String() string { + return fmt.Sprintf("%s", n.Name) +} + +func (c ComparinsonFilter) String() string { + return fmt.Sprintf("%s %s %d", c.Name, c.Function, c.Value) +} + +func (a AndFilter) String() string { + subs := make([]string, 0) + for _, f := range a.Filters { + subs = append(subs, f.String()) + } + return fmt.Sprintf("(%s)", strings.Join(subs, ", ")) +} + +func (a OrFilter) String() string { + subs := make([]string, 0) + for _, f := range a.Filters { + subs = append(subs, f.String()) + } + return fmt.Sprintf("(%s)", strings.Join(subs, ", ")) +} diff --git a/filterparse.go b/filterparse.go new file mode 100644 index 0000000..47c55cb --- /dev/null +++ b/filterparse.go @@ -0,0 +1,210 @@ +package tagger + +//go:generate go tool yacc -o filterparse_gen.go filterparse.y + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "regexp" + "strconv" +) + +var ( + ErrEOF = errors.New("tagger: Unexpected EOF") +) + +func makeErr(part, msg string) error { + return fmt.Errorf("tagger: Error while parsing %s:\n\t%s", part, msg) +} + +// ParseFilter parses a filter from a reader, and returns the described +// filter. +// +// The format of the filter is a simple logic language. +// There is a simple precedence heirachy: +// 1. Parentheses +// 2. And expressions +// 3. Or expressions +// 4. Tags and comparators +// +// This means that "tag1 && tag2 && tag3 || tag4" parses as equivalent to +// "(tag1 && tag2 && tag3) || tag4". +// +// Examples of filters: +// "picture && year > 2007 && year < 2009" +// "todo && (important || easy)" +func ParseFilter(reader io.Reader) (Filter, error) { + // Lex the input + tokens, err := lexer(reader) + if err != nil { + return nil, err + } + + // Call the generated parser + ret := yyParse(tokens) + if ret != 0 { + // TODO: Make a proper error message + return nil, errors.New("tagger: something bad happened yo") + } + + // Return the generated filter + return tokens.filter, nil +} + +// tokenType is a wrapper around the tokens generated by yacc +type tokenType int + +const ( + tokLparen tokenType = LPAREN + tokRparen = RPAREN + tokAnd = AND + tokOr = OR + tokComp = COMP + tokTag = TAG + tokVal = VAL +) + +func (t tokenType) String() string { + switch t { + case tokLparen: + return "LPAREN" + case tokRparen: + return "RPAREN" + case tokAnd: + return "AND" + case tokOr: + return "OR" + case tokComp: + return "COMP" + case tokTag: + return "TAG" + case tokVal: + return "VAL" + default: + return "INVALID" + } +} + +// token describes an indivdual token +type token struct { + typ tokenType + value string +} + +// tokenDef defines which pattern matches a given token type +type tokenDef struct { + pattern string + typ tokenType +} + +var tokenDefs = []tokenDef{ + {`[ \"\'\n\r]+`, -1}, + {`\(`, tokLparen}, + {`\)`, tokRparen}, + {`&&`, tokAnd}, + {`\|\|`, tokOr}, + {`==|!=|>=|<=|>|<`, tokComp}, + {`[a-zA-Z][a-zA-Z0-9_\-\?\(\)]*`, tokTag}, + {`-?[0-9]+`, tokVal}, +} + +func lexer(reader io.Reader) (*lex, error) { + // Read the whole input stream into a string + input_bytes, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + + // Convert input to string + input := string(input_bytes) + + // Prepare an empty array of tokens + tokens := make([]token, 0) + +outer: + // Loop until we reach the end of our input + for pos := 0; pos < len(input); { + // Loop through all token definitions + for _, tokenDef := range tokenDefs { + // Compile the pattern + // TODO: Precompile for increased performance + patt := regexp.MustCompile(tokenDef.pattern) + + // If the pattern matches, and starts at the first character (0-indexed) + if matches := patt.FindStringIndex(input[pos:]); matches != nil && matches[0] == 0 { + // Capture the start and end position + start, end := matches[0], matches[1] + + // Extract the value from our input + value := input[pos+start : pos+end] + + // Increment our input position + pos += len(value) + + // If the token is not whitespace, add it to the array of tokens + if tokenDef.typ != -1 { + token := token{value: value, typ: tokenDef.typ} + tokens = append(tokens, token) + } + + // Continue with next iteration of the outer loop, skipping the rest of the code + continue outer + } + } + + // This point should only be reached if we reach something not matching + // any token patterns, so we return an error. + return nil, fmt.Errorf("tagger: Invalid char in filter at pos %d: %s", pos, input[pos:]) + } + + // Return the tokens + return &lex{tokens: tokens}, nil +} + +type lex struct { + tokens []token + filter Filter +} + +func (l *lex) Lex(lval *yySymType) int { + // If we didn't parse any tokens, return 0 + if len(l.tokens) == 0 { + return 0 + } + + // Pop the first token from the list + v := l.tokens[0] + l.tokens = l.tokens[1:] + + // Process the token depending on type + switch v.typ { + // If token is a comparator, convert the comparator to it's typed + // representation and store it in the destination struct + case tokComp: + lval.comp = ComparatorFromString(v.value) + + // If the token is a tag store it's value in the destination struct + case tokTag: + lval.tag = v.value + + // If the token is a value, convert it to an integer and store it in the + // destination struct + case tokVal: + n, err := strconv.Atoi(v.value) + if err != nil { + // TODO: Find a way to handle gracefully + log.Fatal("tagger: invalid integer value") + } + lval.val = n + } + + // Return the type of the tag + return int(v.typ) +} + +func (l *lex) Error(e string) { + log.Fatal(e) +} diff --git a/filterparse.y b/filterparse.y new file mode 100644 index 0000000..5cdde6c --- /dev/null +++ b/filterparse.y @@ -0,0 +1,69 @@ +%{ +package tagger +%} + +%union{ + filter Filter + tag string + val int + comp Comparator +} + +%token TAG VAL COMP +%token AND OR +%token LPAREN RPAREN + +%type expr paren and_expr or_expr comp tag +%type VAL +%type TAG +%type COMP + +%% + +goal: + expr { yylex.(*lex).filter = $1 } + +expr: + paren +| and_expr +| or_expr +| comp +| tag + +paren: + LPAREN expr RPAREN { $$ = $2 } + +and_expr: + and_expr AND expr + { + $$ = AndFilter{Filters: append($1.(AndFilter).Filters, $3)} + } +| expr AND expr + { + $$ = AndFilter{Filters: []Filter{$1, $3}} + } + +or_expr: + or_expr OR expr + { + $$ = OrFilter{Filters: append($1.(OrFilter).Filters, $3)} + } +| expr OR expr + { + $$ = OrFilter{Filters: []Filter{$1, $3}} + } + +comp: + TAG COMP VAL + { + $$ = ComparinsonFilter{Name: $1, Value: $3, Function: $2} + } + +tag: + TAG + { + $$ = NameFilter{Name: $1} + } + +%% + diff --git a/filterparse_gen.go b/filterparse_gen.go new file mode 100644 index 0000000..011acd1 --- /dev/null +++ b/filterparse_gen.go @@ -0,0 +1,383 @@ +//line filterparse.y:2 +package tagger + +import __yyfmt__ "fmt" + +//line filterparse.y:2 +//line filterparse.y:5 +type yySymType struct { + yys int + filter Filter + tag string + val int + comp Comparator +} + +const TAG = 57346 +const VAL = 57347 +const COMP = 57348 +const AND = 57349 +const OR = 57350 +const LPAREN = 57351 +const RPAREN = 57352 + +var yyToknames = []string{ + "TAG", + "VAL", + "COMP", + "AND", + "OR", + "LPAREN", + "RPAREN", +} +var yyStatenames = []string{} + +const yyEofCode = 1 +const yyErrCode = 2 +const yyMaxDepth = 200 + +//line filterparse.y:68 + +//line yacctab:1 +var yyExca = []int{ + -1, 1, + 1, -1, + -2, 0, +} + +const yyNprod = 14 +const yyPrivate = 57344 + +var yyTokenNames []string +var yyStates []string + +const yyLast = 23 + +var yyAct = []int{ + + 2, 10, 11, 9, 20, 10, 11, 12, 8, 14, + 13, 16, 17, 18, 19, 15, 21, 1, 7, 6, + 5, 4, 3, +} +var yyPact = []int{ + + -1, -1000, -2, -1000, 0, 2, -1000, -1000, -1, 9, + -1, -1, -1, -1, -6, 11, -2, -2, -2, -2, + -1000, -1000, +} +var yyPgo = []int{ + + 0, 0, 22, 21, 20, 19, 18, 17, +} +var yyR1 = []int{ + + 0, 7, 1, 1, 1, 1, 1, 2, 3, 3, + 4, 4, 5, 6, +} +var yyR2 = []int{ + + 0, 1, 1, 1, 1, 1, 1, 3, 3, 3, + 3, 3, 3, 1, +} +var yyChk = []int{ + + -1000, -7, -1, -2, -3, -4, -5, -6, 9, 4, + 7, 8, 7, 8, -1, 6, -1, -1, -1, -1, + 10, 5, +} +var yyDef = []int{ + + 0, -2, 1, 2, 3, 4, 5, 6, 0, 13, + 0, 0, 0, 0, 0, 0, 9, 11, 8, 10, + 7, 12, +} +var yyTok1 = []int{ + + 1, +} +var yyTok2 = []int{ + + 2, 3, 4, 5, 6, 7, 8, 9, 10, +} +var yyTok3 = []int{ + 0, +} + +//line yaccpar:1 + +/* parser for yacc output */ + +var yyDebug = 0 + +type yyLexer interface { + Lex(lval *yySymType) int + Error(s string) +} + +const yyFlag = -1000 + +func yyTokname(c int) string { + // 4 is TOKSTART above + if c >= 4 && c-4 < len(yyToknames) { + if yyToknames[c-4] != "" { + return yyToknames[c-4] + } + } + return __yyfmt__.Sprintf("tok-%v", c) +} + +func yyStatname(s int) string { + if s >= 0 && s < len(yyStatenames) { + if yyStatenames[s] != "" { + return yyStatenames[s] + } + } + return __yyfmt__.Sprintf("state-%v", s) +} + +func yylex1(lex yyLexer, lval *yySymType) int { + c := 0 + char := lex.Lex(lval) + if char <= 0 { + c = yyTok1[0] + goto out + } + if char < len(yyTok1) { + c = yyTok1[char] + goto out + } + if char >= yyPrivate { + if char < yyPrivate+len(yyTok2) { + c = yyTok2[char-yyPrivate] + goto out + } + } + for i := 0; i < len(yyTok3); i += 2 { + c = yyTok3[i+0] + if c == char { + c = yyTok3[i+1] + goto out + } + } + +out: + if c == 0 { + c = yyTok2[1] /* unknown char */ + } + if yyDebug >= 3 { + __yyfmt__.Printf("lex %s(%d)\n", yyTokname(c), uint(char)) + } + return c +} + +func yyParse(yylex yyLexer) int { + var yyn int + var yylval yySymType + var yyVAL yySymType + yyS := make([]yySymType, yyMaxDepth) + + Nerrs := 0 /* number of errors */ + Errflag := 0 /* error recovery flag */ + yystate := 0 + yychar := -1 + yyp := -1 + goto yystack + +ret0: + return 0 + +ret1: + return 1 + +yystack: + /* put a state and value onto the stack */ + if yyDebug >= 4 { + __yyfmt__.Printf("char %v in %v\n", yyTokname(yychar), yyStatname(yystate)) + } + + yyp++ + if yyp >= len(yyS) { + nyys := make([]yySymType, len(yyS)*2) + copy(nyys, yyS) + yyS = nyys + } + yyS[yyp] = yyVAL + yyS[yyp].yys = yystate + +yynewstate: + yyn = yyPact[yystate] + if yyn <= yyFlag { + goto yydefault /* simple state */ + } + if yychar < 0 { + yychar = yylex1(yylex, &yylval) + } + yyn += yychar + if yyn < 0 || yyn >= yyLast { + goto yydefault + } + yyn = yyAct[yyn] + if yyChk[yyn] == yychar { /* valid shift */ + yychar = -1 + yyVAL = yylval + yystate = yyn + if Errflag > 0 { + Errflag-- + } + goto yystack + } + +yydefault: + /* default state action */ + yyn = yyDef[yystate] + if yyn == -2 { + if yychar < 0 { + yychar = yylex1(yylex, &yylval) + } + + /* look through exception table */ + xi := 0 + for { + if yyExca[xi+0] == -1 && yyExca[xi+1] == yystate { + break + } + xi += 2 + } + for xi += 2; ; xi += 2 { + yyn = yyExca[xi+0] + if yyn < 0 || yyn == yychar { + break + } + } + yyn = yyExca[xi+1] + if yyn < 0 { + goto ret0 + } + } + if yyn == 0 { + /* error ... attempt to resume parsing */ + switch Errflag { + case 0: /* brand new error */ + yylex.Error("syntax error") + Nerrs++ + if yyDebug >= 1 { + __yyfmt__.Printf("%s", yyStatname(yystate)) + __yyfmt__.Printf(" saw %s\n", yyTokname(yychar)) + } + fallthrough + + case 1, 2: /* incompletely recovered error ... try again */ + Errflag = 3 + + /* find a state where "error" is a legal shift action */ + for yyp >= 0 { + yyn = yyPact[yyS[yyp].yys] + yyErrCode + if yyn >= 0 && yyn < yyLast { + yystate = yyAct[yyn] /* simulate a shift of "error" */ + if yyChk[yystate] == yyErrCode { + goto yystack + } + } + + /* the current p has no shift on "error", pop stack */ + if yyDebug >= 2 { + __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys) + } + yyp-- + } + /* there is no state on the stack with an error shift ... abort */ + goto ret1 + + case 3: /* no shift yet; clobber input char */ + if yyDebug >= 2 { + __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yychar)) + } + if yychar == yyEofCode { + goto ret1 + } + yychar = -1 + goto yynewstate /* try again in the same state */ + } + } + + /* reduction by production yyn */ + if yyDebug >= 2 { + __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate)) + } + + yynt := yyn + yypt := yyp + _ = yypt // guard against "declared and not used" + + yyp -= yyR2[yyn] + yyVAL = yyS[yyp+1] + + /* consult goto table to find next state */ + yyn = yyR1[yyn] + yyg := yyPgo[yyn] + yyj := yyg + yyS[yyp].yys + 1 + + if yyj >= yyLast { + yystate = yyAct[yyg] + } else { + yystate = yyAct[yyj] + if yyChk[yystate] != -yyn { + yystate = yyAct[yyg] + } + } + // dummy call; replaced with literal code + switch yynt { + + case 1: + //line filterparse.y:24 + { + yylex.(*lex).filter = yyS[yypt-0].filter + } + case 2: + yyVAL.filter = yyS[yypt-0].filter + case 3: + yyVAL.filter = yyS[yypt-0].filter + case 4: + yyVAL.filter = yyS[yypt-0].filter + case 5: + yyVAL.filter = yyS[yypt-0].filter + case 6: + yyVAL.filter = yyS[yypt-0].filter + case 7: + //line filterparse.y:34 + { + yyVAL.filter = yyS[yypt-1].filter + } + case 8: + //line filterparse.y:38 + { + yyVAL.filter = AndFilter{Filters: append(yyS[yypt-2].filter.(AndFilter).Filters, yyS[yypt-0].filter)} + } + case 9: + //line filterparse.y:42 + { + yyVAL.filter = AndFilter{Filters: []Filter{yyS[yypt-2].filter, yyS[yypt-0].filter}} + } + case 10: + //line filterparse.y:48 + { + yyVAL.filter = OrFilter{Filters: append(yyS[yypt-2].filter.(OrFilter).Filters, yyS[yypt-0].filter)} + } + case 11: + //line filterparse.y:52 + { + yyVAL.filter = OrFilter{Filters: []Filter{yyS[yypt-2].filter, yyS[yypt-0].filter}} + } + case 12: + //line filterparse.y:58 + { + yyVAL.filter = ComparinsonFilter{Name: yyS[yypt-2].tag, Value: yyS[yypt-0].val, Function: yyS[yypt-1].comp} + } + case 13: + //line filterparse.y:64 + { + yyVAL.filter = NameFilter{Name: yyS[yypt-0].tag} + } + } + goto yystack /* stack new state and value */ +} diff --git a/storage/sqlite.go b/storage/sqlite.go new file mode 100644 index 0000000..cc605d4 --- /dev/null +++ b/storage/sqlite.go @@ -0,0 +1,388 @@ +package storage + +import ( + "code.google.com/p/go-uuid/uuid" + "database/sql" + "github.com/kiljacken/tagger" + _ "github.com/mattn/go-sqlite3" + "log" +) + +type SqliteStorage struct { + db *sql.DB +} + +// NewSqliteStorage returns a new storage engine backed by an in memory sqlite database +// TODO: Support file databases, maybe by passthrough of connection descriptor +func NewSqliteStorage(descriptor string) (*SqliteStorage, error) { + // Open up a sqlite memory connection + db, err := sql.Open("sqlite3", descriptor) + if err != nil { + // If an error occurs, returns this error + return nil, err + } + + // Create a empty sqlite storage struct, and store the db connection in it + storage := new(SqliteStorage) + storage.db = db + + // Setup database tables + storage.init() + + // Return the new storage engine + return storage, nil +} + +func (s *SqliteStorage) init() { + setupStmt := ` + PRAGMA foreign_keys = ON; + ` + + // Setup database settings + _, err := s.db.Exec(setupStmt) + if err != nil { + // If an error occurs die with an error message + log.Fatal(err) + } + + tableStmt := ` + CREATE TABLE IF NOT EXISTS file( + uuid TEXT NOT NULL, + path TEXT, + PRIMARY KEY (uuid) + UNIQUE(path) ON CONFLICT REPLACE + ); + CREATE TABLE IF NOT EXISTS tags( + uuid TEXT NOT NULL, + name TEXT NOT NULL, + value INTEGER, + FOREIGN KEY(uuid) REFERENCES file(uuid) + PRIMARY KEY (uuid, name) + ); + ` + /* + CREATE TABLE named_tags( + uuid TEXT NOT NULL, + name TEXT NOT NULL, + FOREIGN KEY(uuid) REFERENCES file(uuid) + PRIMARY KEY (uuid, name) + ); + CREATE TABLE value_tags( + uuid TEXT NOT NULL, + name TEXT NOT NULL, + value INTEGER NOT NULL, + FOREIGN KEY(uuid) REFERENCES file(uuid) + PRIMARY KEY (uuid, name) + ); + */ + + // Setup database tables + _, err = s.db.Exec(tableStmt) + if err != nil { + // If an error occurs die with an error message + log.Fatal(err) + } +} + +func (s *SqliteStorage) Close() error { + return s.db.Close() +} + +const getFileStmt = `SELECT * FROM file WHERE uuid = ?` + +func (s *SqliteStorage) GetFile(u uuid.UUID) (tagger.File, error) { + // Prepare the statement + st, err := s.db.Prepare(getFileStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // Fetch the row with the file + row := st.QueryRow(u.String()) + + // Get the values from the row + var rowUuid, path sql.NullString + err = row.Scan(&rowUuid, &path) + if err == sql.ErrNoRows { + // If no row was found, no such file exists + return tagger.File{}, tagger.ErrNoFile + } else if err != nil { + // If another error occurs return it + return tagger.File{}, err + } + + // Construct a file struct and return it + return tagger.NewFile(uuid.Parse(rowUuid.String), path.String), nil +} + +const getFileForPathStmt = `SELECT * FROM file WHERE path = ?` + +func (s *SqliteStorage) GetFileForPath(path string) (tagger.File, error) { + // Prepare the statement + st, err := s.db.Prepare(getFileForPathStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // Fetch the row with the file + row := st.QueryRow(path) + + // Get the values from the row + var rowUuid, rowPath sql.NullString + err = row.Scan(&rowUuid, &rowPath) + if err == sql.ErrNoRows { + // If no row was found, no such file exists + return tagger.File{}, tagger.ErrNoFile + } else if err != nil { + // If another error occurs return it + return tagger.File{}, err + } + + // Construct a file struct and return it + return tagger.NewFile(uuid.Parse(rowUuid.String), rowPath.String), nil +} + +const getAllFilesStmt = `SELECT * FROM file` + +func (s *SqliteStorage) GetAllFiles() ([]tagger.File, error) { + // Prepare the statement + st, err := s.db.Prepare(getAllFilesStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // Fetch the row with the file + rows, err := st.Query() + if err != nil { + // An error shouldn't happen here according to docs. + // If no row was found row.Scan will return ErrNoRow. + return nil, err + } + defer rows.Close() + + // Create an empty array of files + files := make([]tagger.File, 0) + + // Loop through each row in the query + for rows.Next() { + // Get the values from the row + var rowUuid, path sql.NullString + err = rows.Scan(&rowUuid, &path) + if err != nil { + // If an error occured, return the error + return nil, err + } + + files = append(files, tagger.NewFile(uuid.Parse(rowUuid.String), path.String)) + } + + // If an error occured during the query, return the error + if rows.Err() != nil { + return nil, rows.Err() + } + + // Return the array of files + return files, nil +} + +func (s *SqliteStorage) GetMatchingFiles(f tagger.Filter) ([]tagger.File, error) { + // XXX: This is really bad practice. Database engines should make optimized + // sql statements for filtering. + matches := make([]tagger.File, 0) + + // Get ALL files + files, err := s.GetAllFiles() + if err != nil { + return nil, err + } + + // Loop through ALL files + for _, file := range files { + // Get the files tags + tags, err := s.GetTags(file) + if err != nil { + // TODO: We fail fast now, maybe try other files first? + return nil, err + } + + // Add file to result only if it's tags match the filter + if f.Matches(tags) { + matches = append(matches, file) + } + } + + return matches, nil +} + +const updateTagStmt = `INSERT OR REPLACE INTO tags (uuid, name, value) VALUES (?, ?, ?)` + +func (s *SqliteStorage) UpdateTag(f tagger.File, t tagger.Tag) error { + // Prepare the statement + st, err := s.db.Prepare(updateTagStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + if t.HasValue() { + // If the tag has a value, update with value + _, err = st.Exec(f.UUID().String(), t.Name(), t.Value()) + } else { + // If the tag doesn't have a value, update value to NULL + _, err = st.Exec(f.UUID().String(), t.Name(), nil) + } + + // If an error occurs, return it + if err != nil { + return err + } + + return nil +} + +const removeTagStmt = `DELETE FROM tags WHERE uuid = ? AND name = ?` + +func (s *SqliteStorage) RemoveTag(f tagger.File, t tagger.Tag) error { + // Prepare the statement + st, err := s.db.Prepare(removeTagStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // Execute the statement + _, err = st.Exec(f.UUID().String(), t.Name()) + + // If an error occurs, return it + if err != nil { + return err + } + + return nil +} + +const getTagsStmt = `SELECT name, value FROM tags WHERE uuid = ?` + +func (s *SqliteStorage) GetTags(f tagger.File) ([]tagger.Tag, error) { + // Prepare the statement + st, err := s.db.Prepare(getTagsStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // Execute the query + rows, err := st.Query(f.UUID().String()) + if err != nil { + // An error shouldn't happen here according to docs. + // If no row was found row.Scan will return ErrNoRow. + return nil, err + } + defer rows.Close() + + // Create an empty array of tags + tags := make([]tagger.Tag, 0) + + // Loop through each row in the query + for rows.Next() { + // Get the values from the row + var name sql.NullString + var value sql.NullInt64 + err = rows.Scan(&name, &value) + if err != nil { + // If an error occured, return the error + return nil, err + } + + // Depending on if we have a value, create a value tag or a name tag + var tag tagger.Tag + if value.Valid { + tag = tagger.NewValueTag(name.String, int(value.Int64)) + } else { + tag = tagger.NewNamedTag(name.String) + } + + // Add the tag to our array + tags = append(tags, tag) + } + + // If an error occured during the query, return the error + if rows.Err() != nil { + return nil, rows.Err() + } + + // Return the array of tags + return tags, nil +} + +const updateFileStmt = `INSERT OR REPLACE INTO file (uuid, path) VALUES (?, ?)` + +func (s *SqliteStorage) UpdateFile(f tagger.File, t []tagger.Tag) error { + // Prepare the statement + st, err := s.db.Prepare(updateFileStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // If the tag has a value, update with value + _, err = st.Exec(f.UUID().String(), f.Path()) + // If an error occurs, return it + if err != nil { + return err + } + + // For each tag associated with file, update the tag. + for _, tag := range t { + err := s.UpdateTag(f, tag) + // If an error occurs return it + if err != nil { + return err + } + } + + return nil +} + +const removeFileStmt = `DELETE FROM file WHERE uuid = ?` + +func (s *SqliteStorage) RemoveFile(f tagger.File) error { + // Loop through all tags associated with the file and remove them + tags, err := s.GetTags(f) + if err != nil { + return err + } + + for _, tag := range tags { + err := s.RemoveTag(f, tag) + if err != nil { + return err + } + } + + // Prepare the statement + st, err := s.db.Prepare(removeFileStmt) + if err != nil { + // If we get an error here its due to programmer error + log.Fatal(err) + } + defer st.Close() + + // Execute the query + _, err = st.Exec(f.UUID().String()) + if err != nil { + return err + } + + return nil +}