-
Notifications
You must be signed in to change notification settings - Fork 0
/
gtagd.py
executable file
·321 lines (257 loc) · 10.3 KB
/
gtagd.py
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/usr/bin/env python3
# the gutentag daemon
import threading
import sqlite3
import os
import sys
import traceback
import parser
import re
import unittest
from xmlrpc.server import SimpleXMLRPCServer
from gtagmount import GutenTagMount
import gtag_common
SQLITE3_FILE = os.path.join(os.getenv("HOME"), ".gtagdb.sqlite3")
def checkfiles(files):
"""
check each file for existence, and throw if not
"""
for f in files:
if not os.path.isfile(f):
raise Exception("file does not exist: %s" %(str(f)))
def evalTagterm(tagterm, tags):
"""
Do the tags match the tagterm?
"""
# print("---")
# print("tagterm: '{}'".format(tagterm))
# replace all tags in tagterm with True
for t in tags:
tagterm = tagterm.replace(t, "True")
tagterm = tagterm.replace('"True"', "True")
# make a list of all remaining tags in tagterm
# replace all other tags with False
quotedusedtags = re.findall('"([^\"]+)"', tagterm)
for t in quotedusedtags:
tagterm = tagterm.replace('"{}"'.format(t), "False")
usedtags = re.findall("([^/(/)\"|&! ]+)", tagterm)
for t in usedtags:
if t == "True":
continue
tagterm = tagterm.replace(t, "False")
# check if there are invalid tags
for t in tags:
if t == "True" or t == "False":
raise Exception("Dont use 'True' or 'False' as tags")
tagterm = tagterm.replace("!", " not ")
tagterm = tagterm.replace("|", " or ")
tagterm = tagterm.replace("&", " and ")
# print("quotedusedtags: {}".format(quotedusedtags))
# print("usedtags: {}".format(usedtags))
# print("tags: {}".format(tags))
# print("eval: '{}'".format(tagterm))
# hopefully the tagterm is now valid python syntax
return eval(tagterm)
class EvalTagtermTester(unittest.TestCase):
def test_tagterms(self):
self.assertTrue(evalTagterm("mietz", ["mietz", "katz"]))
self.assertTrue(evalTagterm("mietz&katz", ["mietz", "katz"]))
self.assertTrue(evalTagterm("mietz|katz", ["mietz", "katz"]))
self.assertFalse(evalTagterm("mietz", ["katz"]))
self.assertFalse(evalTagterm("mietz&katz", ["katz"]))
self.assertTrue(evalTagterm("mietz|katz", ["katz"]))
self.assertFalse(evalTagterm("!mietz", ["mietz", "katz"]))
self.assertTrue(evalTagterm("lauf!", ["lauf!"]))
self.assertTrue(evalTagterm("!lauf!", ["!lauf!"]))
self.assertFalse(evalTagterm("schau", ["schau mal"]))
self.assertRaises(SyntaxError, evalTagterm, "schau mal", ["schau"])
self.assertTrue(evalTagterm('"schau mal"', ["schau mal"]))
def specToTree(spec):
"""
spec is a string, e.g. "Music | (Photo & Jara)"
Creates a Tree with Nodes and Leafs, where a nodes is either a AND node or a OR node, both of which have two children
"""
# TODO: see the code in ProtectorRuleParser.py:defineRules() to transform the string into the tree
# TODO: see FeatureTree.py for a treeclass
pass
class GutenTagDb:
def __init__(self, dbfile):
self._parser = parser.Parser()
self._dbfile = dbfile
def setMount(self, mount):
self._mount = mount
def openDb(self):
"""call it from within the thread to run the xmlrpc server"""
# open sqlite3 database
self._dbcxn = sqlite3.connect(self._dbfile)
# create the table if not exists
cur = self._dbcxn.cursor()
# cur.execute("CREATE TABLE IF NOT EXISTS files_tags(id INTEGER PRIMARY KEY, file TEXT, tag TEXT)")
cur.execute("CREATE TABLE IF NOT EXISTS files(id INTEGER PRIMARY KEY, path TEXT)")
cur.execute("CREATE TABLE IF NOT EXISTS tags(id INTEGER PRIMARY KEY, label TEXT)")
cur.execute("CREATE TABLE IF NOT EXISTS file_tags(id INTEGER PRIMARY KEY, file_id INTEGER, tag_id INTEGER)")
self._dbcxn.commit()
def shutdown(self, num, stackframe):
print("shutting down")
if hasattr(self, self._server):
self._server.shutdown()
def delete_mount(self, tagterm):
if hasattr(self, "_mount"):
self._mount.delete_mount(tagterm)
return True
return False
def add_mount(self, tagterm):
if hasattr(self, "_mount"):
self._mount.add_mount(tagterm)
return True
return False
def add(self, files, tags):
"""
files: list of files
tags: list of tags
adds all the tags to all the specified files (dont care for double entries)
"""
print("tag %i files with %i tags" % (len(files), len(tags)))
cur = self._dbcxn.cursor()
try:
for f in files:
# insert file or retrieve id from existent
cur.execute('SELECT id FROM files WHERE path = ?', [f])
res = cur.fetchone()
if not res:
cur.execute("INSERT OR IGNORE INTO files(path) VALUES (?)", [f])
f_id = cur.lastrowid
else:
f_id = res[0]
for t in tags:
# insert tag or retrieve id from existent
cur.execute('SELECT id FROM tags WHERE label = ?', [t])
res = cur.fetchone()
if not res:
cur.execute("INSERT OR IGNORE INTO tags(label) VALUES (?)", [t])
t_id = cur.lastrowid
else:
t_id = res[0]
cur.execute('INSERT INTO file_tags(file_id, tag_id) VALUES(?, ?)', [f_id, t_id])
self._dbcxn.commit()
except:
if self._dbcxn:
self._dbcxn.rollback()
traceback.print_exc()
raise Exception("Daemon Error")
return True
def remove(self, files, tags):
"""
files: list of files
tags: list of tags
removes all the tags from all the specified files (dont care for unknown tags or files).
if one of the lists is empty, the items in the other one will be completely deleted from db.
in the other case only the correlation are delete (from file_tags table)
"""
print("untag %i files with %i tags" % (len(files), len(tags)))
cur = self._dbcxn.cursor()
try:
if files == []:
for t in tags:
# retrieve tag id
cur.execute('SELECT id FROM tags WHERE label = ?', [t])
res = cur.fetchone()
if not res:
print("tag not registered {}, ignoring".format(t))
continue
t_id = res[0]
cur.execute("DELETE FROM file_tags WHERE tag_id = ?", [t_id])
cur.execute("DELETE FROM tags WHERE id = ?", [t_id])
for f in files:
# retrieve file id
cur.execute('SELECT id FROM files WHERE path = ?', [f])
res = cur.fetchone()
if not res:
print("file not registered {}, ignoring".format(f))
continue
f_id = res[0]
if tags == []:
cur.execute("DELETE FROM file_tags WHERE file_id = ?", [f_id])
cur.execute("DELETE FROM files WHERE id = ?", [f_id])
for t in tags:
# retrieve tag id
cur.execute('SELECT id FROM tags WHERE label = ?', [t])
res = cur.fetchone()
if not res:
print("tag not registered {}, ignoring".format(t))
continue
t_id = res[0]
cur.execute("DELETE FROM file_tags WHERE file_id = ? AND tag_id = ?", [f_id, t_id])
self._dbcxn.commit()
except:
if self._dbcxn:
self._dbcxn.rollback()
traceback.print_exc()
raise Exception("Daemon Error")
return True
def tags(self, filename):
cur = self._dbcxn.cursor()
tags = []
try:
if filename == "":
cur.execute('SELECT label FROM tags')
else:
cur.execute('SELECT label FROM tags LEFT JOIN file_tags ON tags.id = file_tags.tag_id LEFT JOIN files ON file_tags.file_id = files.id WHERE path = ?', [filename])
rows = cur.fetchall()
for r in rows:
tags.append(r[0])
except:
traceback.print_exc()
raise Exception("Daemon Error")
return list(set(tags))
def files(self, tagterm):
cur = self._dbcxn.cursor()
matches = []
try:
# get list fo files
files = []
cur.execute('SELECT path FROM files')
rows = cur.fetchall()
for r in rows:
files.append(r[0])
files = set(files)
if tagterm == "":
matches = list(files)
else:
for f in files:
tags = self.tags(f)
if evalTagterm(tagterm, tags):
matches.append(f)
except:
traceback.print_exc()
raise Exception("Daemon Error")
return matches
def pid(self):
return os.getpid()
class GutenTagServerThread(threading.Thread):
def __init__(self, gtdb):
threading.Thread.__init__(self)
self._gtdb = gtdb
self._server = SimpleXMLRPCServer(("localhost", gtag_common.RPC_PORT))
self._server.register_introspection_functions()
self._server.register_instance(self._gtdb)
def run(self):
self._gtdb.openDb()
self._server.serve_forever()
def main():
dbfile = SQLITE3_FILE
# check if there is an db file in the arguments list
if len(sys.argv) > 1:
dbfile = sys.argv[1]
print("Using database file: {}".format(dbfile))
# need two db connections, cause they run in different threads
gtdb_mount = GutenTagDb(dbfile)
gtdb_server = GutenTagDb(dbfile)
server_thread = GutenTagServerThread(gtdb_server)
mount = GutenTagMount(gtdb_mount)
gtdb_server.setMount(mount)
server_thread.start()
mount.start() # this blocks, so call it at last
# signal.signal(signal.SIGTERM, self.shutdown)
if __name__ == "__main__":
main()