summaryrefslogtreecommitdiff
path: root/trunk/dimbola/db.py
blob: 13c7e2784bf2ebafc910a8f7743745fa75e25126 (plain)
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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# Copyright (C) 2009  Lars Wirzenius <liw@liw.fi>
# 
# 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/>.


import logging
import os
import sqlite3

import dimbola


class Database(object):

    '''Interface to the database.
    
    This provides a thin layer of abstraction to the Sqlite3 database
    file we use to store data. It makes testing those parts that access
    the database easier: all actual SQL queries are in this class, so
    it is easy to mock up when testing upper level.
    
    '''
    
    SUPPORTED = 2
    
    def __init__(self, filename):
        self.conn = sqlite3.connect(filename)
        self.transaction = None
        
    def init_db(self):
        '''Create tables in the database.'''

        logging.debug('Creating database tables.')
        
        self.begin_transaction()
        
        self.execute('''create table if not exists dbmeta (version integer)''')
        version = self.query_first('select version from dbmeta')
        if version is None:
            version = 1
        if version > self.SUPPORTED:
            raise Exception('Database is version %s, we support up to %s' %
                            (version, self.SUPPORTED))
        if version < self.SUPPORTED:
            self.execute('delete from dbmeta')
            self.execute('insert into dbmeta (version) values (?)',
                         (self.SUPPORTED,))
        
        self.execute('''create table if not exists photos
                        (photoid integer primary key autoincrement,
                         folderid integer,
                         basename string,
                         rating integer,
                         rotate integer)''')
        if version < 2:
            self.execute('alter table photos add column sha1 string')
        
        self.execute('''create table if not exists folders
                        (folderid integer primary key autoincrement,
                         foldername string unique)''')
                      
        self.execute('''create table if not exists thumbnails
                        (photoid integer unique,
                         thumbnail blob)''')
                      
        self.execute('''create table if not exists previews
                        (photoid integer unique,
                         preview blob)''')
        
        self.execute('''create table if not exists tagnames
                        (tagid integer primary key,
                         tagname string,
                         tagparent integer)''')
                      
        self.execute('''create table if not exists tags
                        (tagid integer,
                         photoid integer)''')
                      
        self.execute('''create table if not exists exif
                        (exifid integer,
                         photoid integer,
                         exifvalue string)''')
                      
        self.execute('''create table if not exists exifnames
                        (exifid integer primary key,
                         exifname string unique)''')
                      
        self.end_transaction()

    def begin_transaction(self):
        '''Start a transaction.
        
        Any multiple-query thing should happen within a transaction.
        
        '''
        
        assert self.transaction is None
        self.transaction = self.conn.cursor()
        
    def end_transaction(self):
        '''End the current transaction.'''
        
        assert self.transaction is not None
        self.transaction = None
        self.conn.commit()
        
    def __enter__(self):
        self.begin_transaction()
        
    def __exit__(self, exc_type, exc_value, exc_traceback):
        if exc_type is None:
            self.end_transaction()
        else:
            self.conn.rollback()
            self.transaction = None

    def execute(self, sql, args=None):
        '''Execute some SQL within the current transaction.'''
        logging.debug('Executing SQL: "%s" with arguments %s' % (sql, args))
        if args is None:
            self.transaction.execute(sql)
        else:
            self.transaction.execute(sql, args)

    def query(self, sql, args=None):
        '''Query the database within the current transaction.
        
        Generate result rows, one by one.
        
        '''
        
        self.execute(sql, args)
        for row in self.transaction:
            yield row

    def query_first(self, sql, args=None):
        '''Like query, but return first column of first match, or None.'''
        for row in self.query(sql, args):
            return row[0]
        return None

    def find_folder(self, foldername):
        '''Return folderid corresponding to a foldername, or None.'''
        sql = 'select folderid from folders where foldername = ?'
        return self.query_first(sql, (foldername,))

    def get_folder_name(self, folderid):
        '''Return foldername that corresponds to folder id.'''
        sql = 'select foldername from folders where folderid = ?'
        return self.query_first(sql, (folderid,))

    def add_folder(self, foldername):
        '''Add a new folder to the database, return its id.'''
        self.execute('insert into folders (foldername) values (?)',
                     (foldername,))
        folderid = self.transaction.lastrowid
        logging.debug('Added folder %s as %s' % (foldername, folderid))
        return folderid

    def find_photoids(self):
        '''Return list of ids of all photos in database.'''
        return [row[0] for row in self.query('select photoid from photos')]

    def find_photoids_in_folder(self, folderid):
        '''Return ids of photos in a folder.'''
        sql = 'select photoid from photos where folderid = ?'
        return [row[0] for row in self.query(sql, (folderid,))]

    def add_photo(self, folderid, basename, rating, rotate):
        '''Add a new photo to the database, return its id.'''
        self.execute('''insert into photos 
                        (folderid, basename, rating, rotate)
                        values (:folderid, :basename, :rating, :rotate)''',
                     { 'folderid': folderid,
                       'basename': basename,
                       'rating': rating,
                       'rotate': rotate })
        photoid = self.transaction.lastrowid
        logging.debug('Added photo %s in folder %s as %s' % 
                      (basename, folderid, photoid))
        return photoid
        
    def remove_photo(self, photoid):
        '''Remove photo from database.
        
        This also removes all tags and other metadata related to the
        photo.
        
        '''
        
        self.execute('delete from photos where photoid = ?', (photoid,))
        self.execute('delete from thumbnails where photoid = ?', (photoid,))
        self.execute('delete from previews where photoid = ?', (photoid,))
        self.execute('delete from tags where photoid = ?', (photoid,))
        self.execute('delete from exif where photoid = ?', (photoid,))
        
    def set_sha1(self, photoid, sha1):
        '''Set the sha1 checksum for a photo.'''
        self.execute('update photos set sha1 = ? where photoid = ?',
                     (sha1, photoid))

    def get_sha1(self, photoid):
        '''Return the sha1 checksum for a photo.'''
        return self.query_first('select sha1 from photos where photoid = ?',
                                (photoid,))
    
    def find_photos_without_checksum(self):
        '''Return list of photos without checksums.'''
        sql = 'select photoid from photos where sha1 isnull'
        return [row[0] for row in self.query(sql)]
    
    def get_basic_photo_metadata(self, photoid):
        '''Return the basic metadata about a photo.
        
        Return folderid, basename, rating, rotate.
        
        '''
        
        sql = ('select folderid, basename, rating, rotate from photos '
               'where photoid = ?')
        for row in self.query(sql, (photoid,)):
            return row
        return None, None, None, None

    def get_photo_pathname(self, photoid):
        '''Return the full pathname of a photo.'''
        folderid, basename, c, d = self.get_basic_photo_metadata(photoid)
        foldername = self.get_folder_name(folderid)
        return os.path.join(foldername, basename)

    def find_exifname(self, exifname):
        '''Return exifid corresponding to exifname, or None.'''
        sql = 'select exifid from exifnames where exifname = ?'
        return self.query_first(sql, (exifname,))

    def add_exifname(self, exifname):
        '''Add a new exifname to the database, return its id.'''
        self.execute('insert into exifnames (exifname) values (?)',
                     (exifname,))
        exifid = self.transaction.lastrowid
        logging.debug('Added exifname %s as %s' % (exifname, exifid))
        return exifid

    def add_exif(self, photoid, exifid, exifvalue):
        '''Add a new exif header for a photo.'''
        self.execute('''insert into exif (photoid, exifid, exifvalue)
                        values (:photoid, :exifid, :exifvalue)''',
                     { 'photoid': photoid,
                       'exifid': exifid,
                       'exifvalue': exifvalue })

    def get_exif(self, photoid, exifname):
        '''Return a given exif header for a given photo.'''
        exifid = self.find_exifname(exifname)
        sql = 'select exifvalue from exif where photoid = ? and exifid = ?'
        value = self.query_first(sql, (photoid, exifid))
        if value is not None:
            value = str(value)
        return value

    def add_thumbnail(self, photoid, thumbnail):
        '''Add a new thumbnail for a photo.'''
        self.execute('''insert into thumbnails (photoid, thumbnail)
                        values (:photoid, :thumbnail)''',
                     { 'photoid': photoid,
                       'thumbnail': buffer(thumbnail) })

    def get_thumbnail(self, photoid):
        '''Return the thumbnail for a given photo, or None.'''
        sql = 'select thumbnail from thumbnails where photoid = ?'
        return self.query_first(sql, (photoid,))

    def add_preview(self, photoid, preview):
        '''Add a new preview for a photo.'''
        self.execute('''insert into previews (photoid, preview)
                        values (:photoid, :preview)''',
                     { 'photoid': photoid,
                       'preview': buffer(preview) })

    def get_preview(self, photoid):
        '''Return the preview for a given photo, or None.'''
        sql = 'select preview from previews where photoid = ?'
        return self.query_first(sql, (photoid,))
        
    def get_tagnames(self):
        '''Return generator to go through all tagid, name, parentid tuples.'''
        sql = 'select tagid, tagname, tagparent from tagnames'
        for tagid, tagname, tagparent in self.query(sql):
            yield tagid, tagname, tagparent

    def get_tagname(self, tagid):
        '''Return name corresponding to a tagid.'''
        sql = 'select tagname from tagnames where tagid = ?'
        return self.query_first(sql, (tagid,))

    def add_tagname(self, tagname):
        '''Add a new tagname to the database, return its id.'''
        self.execute('insert into tagnames (tagname) values (?)',
                     (tagname,))
        tagid = self.transaction.lastrowid
        logging.debug('Added tagname %s as %s' % (tagname, tagid))
        return tagid

    def remove_tagname(self, tagid):
        '''Remove a tag name from the database.
        
        This also removes the tag from the photos that have it.
        
        '''
        self.execute('delete from tags where tagid = ?', (tagid,))
        self.execute('delete from tagnames where tagid = ?', (tagid,))

    def change_tagname(self, tagid, tagname):
        '''Change the name of a tag.'''
        self.execute('update tagnames set tagname = ? where tagid = ?', 
                     (tagname, tagid))
    
    def set_tagparent(self, tagid, parentid):
        '''Set parent of a tag.'''
        self.execute('update tagnames set tagparent = ? where tagid = ?',
                     (parentid, tagid))

    def get_tagchildren(self, tagid):
        '''Return ids of all child tags of a given tag.'''
        sql = 'select tagid from tagnames where tagparent = ?'
        for childid in self.query(sql, (tagid,)):
            yield childid[0]
    
    def get_tagids(self, photoid):
        '''Return list of tagids that apply to a photo.'''
        sql = 'select tagid from tags where photoid = ?'
        tagids = []
        for row in self.query(sql, (photoid,)):
            tagids.append(row[0])
        return tagids

    def add_tagid(self, photoid, tagid):
        '''Add a tagid for a photo.'''
        sql = 'insert into tags (photoid, tagid) values (?, ?)'
        self.execute(sql, (photoid, tagid))

    def remove_tagid(self, photoid, tagid):
        '''Remove a tagid for a photo.'''
        sql = 'delete from tags where photoid = ? and tagid = ?'
        self.execute(sql, (photoid, tagid))
        
    def set_rating(self, photoid, rating):
        '''Set rating for a photo.'''
        sql = 'update photos set rating = ? where photoid = ?'
        self.execute(sql, (rating, photoid))

    def set_rotate(self, photoid, rotate):
        '''Set rotation angle for a photo.'''
        sql = 'update photos set rotate = ? where photoid = ?'
        self.execute(sql, (rotate, photoid))