-
Notifications
You must be signed in to change notification settings - Fork 115
/
files.go
213 lines (173 loc) · 6.72 KB
/
files.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// Copyright 2011-2018 Paul Ruane.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cli
import (
"fmt"
"github.com/oniony/TMSU/common/log"
"github.com/oniony/TMSU/common/path"
"github.com/oniony/TMSU/entities"
"github.com/oniony/TMSU/query"
"github.com/oniony/TMSU/storage"
"path/filepath"
"strings"
)
var FilesCommand = Command{
Name: "files",
Aliases: []string{"query"},
Synopsis: "List files with particular tags",
Usages: []string{"tmsu files [OPTION]... [QUERY]"},
Description: `Lists the files in the database that match the QUERY specified. If no query is specified, all files in the database are listed.
QUERY may contain tag names to match, operators and parentheses. Operators are: and or not == != < > <= >= eq ne lt gt le ge.
Queries are run against the database so the results may not reflect the current state of the filesystem. Only tagged files are matched: to identify untagged files use the 'untagged' subcommand.
Note: If your tag or value name contains whitespace, operators (e.g. '<') or parentheses ('(' or ')'), these must be escaped with a backslash '\', e.g. '\<tag\>' matches the tag name '<tag>'. Your shell, however, may use some punctuation for its own purposes: this can normally be avoided by enclosing the query in single quotation marks or by escaping the problem characters with a backslash.`,
Examples: []string{"$ tmsu files music mp3 # files with both 'music' and 'mp3'",
"$ tmsu files music and mp3 # same query but with explicit 'and'",
"$ tmsu files music and not mp3",
`$ tmsu files "music and (mp3 or flac)"`,
`$ tmsu files "year == 2017"`,
`$ tmsu files "year < 2017"`,
`$ tmsu files year lt 2017`,
`$ tmsu files year`,
`$ tmsu files --path=/home/bob music`,
`$ tmsu files 'contains\=equals'`,
`$ tmsu files '\<tag\>'`},
Options: Options{{"--directory", "-d", "list only items that are directories", false, ""},
{"--file", "-f", "list only items that are files", false, ""},
{"--print0", "-0", "delimit files with a NUL character rather than newline.", false, ""},
{"--count", "-c", "lists the number of files rather than their names", false, ""},
{"--path", "-p", "list only items under PATH", true, ""},
{"--explicit", "-e", "list only explicitly tagged files", false, ""},
{"--sort", "-s", "sort output: id, none, name, size, time", true, ""},
{"--ignore-case", "-i", "ignore the case of tag and value names", false, ""}},
Exec: filesExec,
}
// unexported
func filesExec(options Options, args []string, databasePath string) (error, warnings) {
dirOnly := options.HasOption("--directory")
fileOnly := options.HasOption("--file")
print0 := options.HasOption("--print0")
showCount := options.HasOption("--count")
hasPath := options.HasOption("--path")
explicitOnly := options.HasOption("--explicit")
ignoreCase := options.HasOption("--ignore-case")
sort := "name"
if options.HasOption("--sort") {
sort = options.Get("--sort").Argument
}
absPath := ""
if hasPath {
relPath := options.Get("--path").Argument
var err error
absPath, err = filepath.Abs(relPath)
if err != nil {
return fmt.Errorf("could not get absolute path of '%v': %v'", relPath, err), nil
}
}
store, err := openDatabase(databasePath)
if err != nil {
return err, nil
}
defer store.Close()
tx, err := store.Begin()
if err != nil {
return err, nil
}
defer tx.Commit()
queryText := strings.Join(args, " ")
return listFilesForQuery(store, tx, queryText, absPath, dirOnly, fileOnly, print0, showCount, explicitOnly, ignoreCase, sort)
}
// unexported
func listFilesForQuery(store *storage.Storage, tx *storage.Tx, queryText, path string, dirOnly, fileOnly, print0, showCount, explicitOnly, ignoreCase bool, sort string) (error, warnings) {
log.Info(2, "parsing query")
expression, err := query.Parse(queryText)
if err != nil {
return fmt.Errorf("could not parse query: %v", err), nil
}
log.Info(2, "checking tag names")
warnings := make(warnings, 0, 10)
tagNames, err := query.TagNames(expression)
if err != nil {
return fmt.Errorf("could not identify tag names: %v", err), nil
}
tags, err := store.TagsByCasedNames(tx, tagNames, ignoreCase)
for _, tagName := range tagNames {
if err := entities.ValidateTagName(tagName); err != nil {
warnings = append(warnings, err.Error())
continue
}
if !tags.ContainsCasedName(tagName, ignoreCase) {
warnings = append(warnings, fmt.Sprintf("no such tag '%v'", tagName))
continue
}
}
valueNames, err := query.ExactValueNames(expression)
if err != nil {
return fmt.Errorf("could not identify value names: %v", err), nil
}
values, err := store.ValuesByCasedNames(tx, valueNames, ignoreCase)
for _, valueName := range valueNames {
if err := entities.ValidateValueName(valueName); err != nil {
warnings = append(warnings, err.Error())
continue
}
if !values.ContainsCasedName(valueName, ignoreCase) {
warnings = append(warnings, fmt.Sprintf("no such value '%v'", valueName))
continue
}
}
log.Info(2, "querying database")
files, err := store.FilesForQuery(tx, expression, path, explicitOnly, ignoreCase, sort)
if err != nil {
if strings.Index(err.Error(), "parser stack overflow") > -1 {
return fmt.Errorf("the query is too complex (see the troubleshooting wiki for how to increase the stack size)"), warnings
}
return fmt.Errorf("could not query files: %v", err), warnings
}
if err = listFiles(tx, files, dirOnly, fileOnly, print0, showCount); err != nil {
return err, warnings
}
return nil, warnings
}
func listFiles(tx *storage.Tx, files entities.Files, dirOnly, fileOnly, print0, showCount bool) error {
relPaths := make([]string, 0, len(files))
for _, file := range files {
if fileOnly && file.IsDir {
continue
}
if dirOnly && !file.IsDir {
continue
}
absPath := file.Path()
relPath := path.Rel(absPath)
relPaths = append(relPaths, relPath)
}
if showCount {
fmt.Println(len(relPaths))
} else {
for _, relPath := range relPaths {
if print0 {
fmt.Printf("%v\000", relPath)
} else {
fmt.Println(relPath)
}
}
}
return nil
}
func containsTag(tags []string, tag string) bool {
for _, iteratedTag := range tags {
if iteratedTag == tag {
return true
}
}
return false
}