Changeset 4419

Show
Ignore:
Timestamp:
11/23/05 20:12:52 (3 years ago)
Author:
nk
Message:

log system rewrite to use sqlite database instead of plain ascii files. this allows us to scale better (be faster), provide search in history, and save logs for JIDs that are non-ASCII. PLEASE read http://trac.gajim.org/wiki/MigrateLogToDot9DB

Location:
trunk
Files:
1 added
10 modified

Legend:

Unmodified
Added
Removed
  • trunk/po/POTFILES.in

    r4210 r4419  
    3030src/tooltips.py 
    3131src/vcard.py 
     32src/common/check_paths.py 
    3233src/common/GnuPG.py 
    3334src/common/GnuPGInterface.py 
  • trunk/README

    r4391 r4419  
    55pygtk2.6 or higher 
    66python-libglade 
    7 python-pysqlite2 
     7pysqlite2 (aka. python-pysqlite2) 
    88 
    99some distros also split too much python standard library. 
  • trunk/scripts/migrate_logs_to_dot9_db.py

    r4410 r4419  
    176176        f.write('Thank you\n') 
    177177        f.close() 
    178         # after huge import create the indices (they are slow on massive insert) 
    179         cur.executescript( 
    180                 ''' 
    181                 CREATE UNIQUE INDEX jids_already_index ON jids (jid); 
    182                 CREATE INDEX jid_id_index ON logs (jid_id); 
    183                 ''' 
    184         ) 
  • trunk/src/common/connection.py

    r4408 r4419  
    365365                                        return 
    366366                                self.dispatch('GC_MSG', (frm, msgtxt, tim)) 
    367                                 gajim.logger.write('gc', msgtxt, frm, tim = tim) 
     367                                gajim.logger.write('gc_msg', frm, msgtxt, tim = tim) 
    368368                elif mtype == 'normal': # it's single message 
    369369                        log_msgtxt = msgtxt 
    370370                        if subject: 
    371371                                log_msgtxt = _('Subject: %s\n%s') % (subject, msgtxt) 
    372                         gajim.logger.write('incoming', log_msgtxt, frm, tim = tim) 
     372                        gajim.logger.write('single_msg_recv', frm, log_msgtxt, tim = tim) 
    373373                        if invite is not None: 
    374374                                item = invite.getTag('invite') 
     
    388388                                log_msgtxt = _('Subject: %s\n%s') % (subject, msgtxt) 
    389389                        if msg.getTag('body'): 
    390                                 gajim.logger.write('incoming', log_msgtxt, frm, tim = tim) 
     390                                gajim.logger.write('chat_msg_recv', frm, log_msgtxt, tim = tim) 
    391391                        self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject, 
    392392                                chatstate)) 
     
    470470                                                errmsg, errcode)) 
    471471                        if not ptype or ptype == 'unavailable': 
    472                                 gajim.logger.write('status', status, who, show) 
     472                                gajim.logger.write('gcstatus', who, status, show) 
    473473                                self.dispatch('GC_NOTIFY', (jid_stripped, show, status, resource, 
    474474                                        prs.getRole(), prs.getAffiliation(), prs.getJid(), 
     
    518518                                self.vcard_shas[jid_stripped] = avatar_sha 
    519519                if not ptype or ptype == 'unavailable': 
    520                         gajim.logger.write('status', status, jid_stripped, show) 
     520                        gajim.logger.write('status', jid_stripped, status, show) 
    521521                        self.dispatch('NOTIFY', (jid_stripped, show, status, resource, prio, 
    522522                                keyID)) 
     
    18991899                        log_msg = _('Subject: %s\n%s') % (subject, msg) 
    19001900                if log_msg: 
    1901                         gajim.logger.write('outgoing', log_msg, jid) 
     1901                        if type == 'chat': 
     1902                                kind = 'chat_msg_sent' 
     1903                        else: 
     1904                                kind = 'single_msg_sent' 
     1905                        gajim.logger.write(kind, jid, log_msg) 
    19021906                self.dispatch('MSGSENT', (jid, msg, keyID)) 
    19031907 
  • trunk/src/common/gajim.py

    r4416 r4419  
    2424 
    2525import common.config 
    26 import common.logger 
     26 
    2727 
    2828interface = None # The actual interface (the gtk one for the moment) 
     
    3838log.addHandler(h) 
    3939 
     40import common.logger 
    4041logger = common.logger.Logger() # init the logger 
    4142 
     
    4647                DATA_DIR = os.path.join('..', 'data') 
    4748        try: 
    48                 # Documents and Settings\[User Name]\Application Data\Gajim\logs 
     49                # Documents and Settings\[User Name]\Application Data\Gajim 
    4950                LOGPATH = os.path.join(os.environ['appdata'], 'Gajim', 'Logs') # deprecated 
    50                 LOG_DB_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'logs.db') 
    5151                VCARDPATH = os.path.join(os.environ['appdata'], 'Gajim', 'Vcards') 
    5252        except KeyError: 
    53                 # win9x, ./Logs etc 
     53                # win9x, in cwd 
    5454                LOGPATH = 'Logs' # deprecated 
    55                 LOG_DB_PATH = 'logs.db' 
    5655                VCARDPATH = 'Vcards' 
    5756else: # Unices 
    5857        DATA_DIR = '../data' 
    5958        LOGPATH = os.path.expanduser('~/.gajim/logs') # deprecated 
    60         LOG_DB_PATH = os.path.expanduser('~/.gajim/logs.db') 
    6159        VCARDPATH = os.path.expanduser('~/.gajim/vcards') 
    6260 
     
    6462        LOGPATH = LOGPATH.decode(sys.getfilesystemencoding()) 
    6563        VCARDPATH = VCARDPATH.decode(sys.getfilesystemencoding()) 
    66         LOG_DB_PATH = LOG_DB_PATH.decode(sys.getfilesystemencoding()) 
    6764except: 
    6865        pass 
  • trunk/src/common/helpers.py

    r4393 r4419  
    2424import sys 
    2525import stat 
     26from pysqlite2 import dbapi2 as sqlite 
    2627 
    2728import gajim 
     29import logger 
    2830from common import i18n 
    2931from common.xmpp_stringprep import nodeprep, resourceprep, nameprep 
     
    5052 
    5153        # Search for delimiters 
    52         user_sep = jidstring.find("@") 
    53         res_sep  = jidstring.find("/") 
     54        user_sep = jidstring.find('@') 
     55        res_sep  = jidstring.find('/') 
    5456 
    5557        if user_sep == -1:               
     
    136138                        else: 
    137139                                raise 
    138  
    139 def check_paths(): 
    140         LOGPATH = gajim.LOGPATH 
    141         VCARDPATH = gajim.VCARDPATH 
    142         dot_gajim = os.path.dirname(LOGPATH) 
    143         if os.path.isfile(dot_gajim): 
    144                 print _('%s is file but it should be a directory') % dot_gajim 
    145                 print _('Gajim will now exit') 
    146                 sys.exit() 
    147         elif os.path.isdir(dot_gajim): 
    148                 s = os.stat(dot_gajim) 
    149                 if s.st_mode & stat.S_IROTH: # others have read permission! 
    150                         os.chmod(dot_gajim, 0700) # rwx------ 
    151  
    152                 if not os.path.exists(LOGPATH): 
    153                         print _('creating %s directory') % LOGPATH 
    154                         os.mkdir(LOGPATH, 0700) 
    155                 elif os.path.isfile(LOGPATH): 
    156                         print _('%s is file but it should be a directory') % LOGPATH 
    157                         print _('Gajim will now exit') 
    158                         sys.exit() 
    159                 elif os.path.isdir(LOGPATH): 
    160                                 s = os.stat(LOGPATH) 
    161                                 if s.st_mode & stat.S_IROTH: # others have read permission! 
    162                                         os.chmod(LOGPATH, 0700) # rwx------ 
    163  
    164                 if not os.path.exists(VCARDPATH): 
    165                         print _('creating %s directory') % VCARDPATH 
    166                         os.mkdir(VCARDPATH, 0700) 
    167                 elif os.path.isfile(VCARDPATH): 
    168                         print _('%s is file but it should be a directory') % VCARDPATH 
    169                         print _('Gajim will now exit') 
    170                         sys.exit() 
    171                 elif os.path.isdir(VCARDPATH): 
    172                                 s = os.stat(VCARDPATH) 
    173                                 if s.st_mode & stat.S_IROTH: # others have read permission! 
    174                                         os.chmod(VCARDPATH, 0700) # rwx------ 
    175         else: # dot_gajim doesn't exist 
    176                 if dot_gajim: # is '' on win9x so avoid that 
    177                         print _('creating %s directory') % dot_gajim 
    178                         os.mkdir(dot_gajim, 0700) 
    179                 if not os.path.isdir(LOGPATH): 
    180                         print _('creating %s directory') % LOGPATH 
    181                         os.mkdir(LOGPATH, 0700) 
    182                 if not os.path.isdir(VCARDPATH): 
    183                         print _('creating %s directory') % VCARDPATH 
    184                         os.mkdir(VCARDPATH, 0700) 
    185140 
    186141def convert_bytes(string): 
     
    453408        if not os.path.exists(path_to_soundfile): 
    454409                return 
    455         if os.name  == 'nt': 
     410        if os.name == 'nt': 
    456411                try: 
    457412                        winsound.PlaySound(path_to_soundfile, 
  • trunk/src/common/logger.py

    r4354 r4419  
    1919 
    2020import os 
     21import sys 
    2122import time 
    22  
    23 import common.gajim 
     23import datetime 
     24 
    2425from common import i18n 
    2526_ = i18n._ 
    26 import helpers 
    27  
     27 
     28try: 
     29        from pysqlite2 import dbapi2 as sqlite 
     30except ImportError: 
     31        error = _('pysqlite2 (aka python-pysqlite2) dependency is missing. '\ 
     32                'After you install pysqlite3, if you want to migrate your logs '\ 
     33                'to the new database, please read: http://trac.gajim.org/wiki/MigrateLogToDot9DB ' 
     34                'Exiting...' 
     35                ) 
     36        print >> sys.stderr, error 
     37        sys.exit() 
     38 
     39GOT_JIDS_ALREADY_IN_DB = False 
     40 
     41if os.name == 'nt': 
     42        try: 
     43                # Documents and Settings\[User Name]\Application Data\Gajim\logs.db 
     44                LOG_DB_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'logs.db') 
     45        except KeyError: 
     46                # win9x, ./logs.db 
     47                LOG_DB_PATH = 'logs.db' 
     48else: # Unices 
     49        LOG_DB_PATH = os.path.expanduser('~/.gajim/logs.db') 
     50 
     51try: 
     52        LOG_DB_PATH = LOG_DB_PATH.decode(sys.getfilesystemencoding()) 
     53except: 
     54        pass 
    2855 
    2956class Logger: 
    3057        def __init__(self): 
    31                 pass 
    32  
    33         def write(self, kind, msg, jid, show = None, tim = None): 
     58                if not os.path.exists(LOG_DB_PATH): 
     59                        # this can happen only the first time (the time we create the db) 
     60                        # db is created in src/common/checks_paths.py 
     61                        return 
     62                 
     63                self.get_jids_already_in_db() 
     64 
     65        def get_jids_already_in_db(self): 
     66                con = sqlite.connect(LOG_DB_PATH) 
     67                cur = con.cursor() 
     68                cur.execute('SELECT jid FROM jids') 
     69                rows = cur.fetchall() # list of tupples: (u'aaa@bbb',), (u'cc@dd',)] 
     70                self.jids_already_in = [] 
     71                for row in rows: 
     72                        # row[0] is first item of row (the only result here, the jid) 
     73                        self.jids_already_in.append(row[0]) 
     74                con.close() 
     75                GOT_JIDS_ALREADY_IN_DB = True 
     76 
     77        def jid_is_from_pm(cur, jid): 
     78                '''if jid is gajim@conf/nkour it's likely a pm one, how we know 
     79                gajim@conf is not a normal guy and nkour is not his resource? 
     80                we ask if gajim@conf is already in jids (as room) 
     81                this fails if user disable logging for room and only enables for 
     82                pm (so higly unlikely) and if we fail we do not force chaos 
     83                (user will see the first pm as if it was message in room's public chat)''' 
     84                 
     85                possible_room_jid, possible_nick = jid.split('/', 1) 
     86                 
     87                cur.execute('SELECT jid_id FROM jids WHERE jid="%s"' % possible_room_jid) 
     88                jid_id = cur.fetchone()[0] 
     89                if jid_id: 
     90                        return True 
     91                else: 
     92                        return False 
     93         
     94        def get_jid_id(self, jid): 
     95                '''jids table has jid and jid_id 
     96                logs table has log_id, jid_id, contact_name, time, kind, show, message 
     97                so to ask logs we need jid_id that matches our jid in jids table 
     98                this method asks jid and returns the jid_id for later sql-ing on logs 
     99                ''' 
     100                con = sqlite.connect(LOG_DB_PATH) 
     101                cur = con.cursor() 
     102                 
     103                if jid in self.jids_already_in: # we already have jids in DB 
     104                        cur.execute('SELECT jid_id FROM jids WHERE jid="%s"' % jid) 
     105                        jid_id = cur.fetchone()[0] 
     106                else: # oh! a new jid :), we add him now 
     107                        cur.execute('INSERT INTO jids (jid) VALUES (?)', (jid,)) 
     108                        con.commit() 
     109                        jid_id = cur.lastrowid 
     110                        self.jids_already_in.append(jid) 
     111                return jid_id 
     112         
     113        def write(self, kind, jid, message = None, show = None, tim = None): 
     114                '''write a row (status, gcstatus, message etc) to logs database 
     115                kind can be status, gcstatus, gc_msg, (we only recv for those 3), 
     116                single_msg_recv, chat_msg_recv, chat_msg_sent, single_msg_sent 
     117                we cannot know if it is pm or normal chat message, we try to guess 
     118                see jid_is_from_pm() 
     119                 
     120                we analyze jid and store it as follows: 
     121                jids.jid text column will hold JID if TC-related, room_jid if GC-related, 
     122                ROOM_JID/nick if pm-related.''' 
     123                 
     124                if not GOT_JIDS_ALREADY_IN_DB: 
     125                        self.get_jids_already_in_db() 
     126         
     127                con = sqlite.connect(LOG_DB_PATH) 
     128                cur = con.cursor() 
     129                 
    34130                jid = jid.lower() 
    35                 if not tim: 
    36                         tim = time.time() 
     131                contact_name_col = None # holds nickname for kinds gcstatus, gc_msg 
     132                # message holds the message unless kind is status or gcstatus, 
     133                # then it holds status message 
     134                message_col = message 
     135                show_col = show 
     136                if tim: 
     137                        time_col = int(float(time.mktime(tim))) 
    37138                else: 
    38                         tim = time.mktime(tim) 
    39  
    40                 if not msg: 
    41                         msg = '' 
    42  
    43                 msg = helpers.to_one_line(msg) 
    44                 if len(jid.split('/')) > 1: 
    45                         ji, nick = jid.split('/', 1) 
    46                 else: 
    47                         ji = jid 
    48                         nick = '' 
    49                 files = [] 
    50                 if kind == 'status': # we save time:jid:show:msg 
    51                         if not show: 
    52                                 show = 'online' 
    53                         if common.gajim.config.get('log_notif_in_user_file'): 
    54                                 path_to_file = os.path.join(common.gajim.LOGPATH, ji) 
    55                                 if os.path.isdir(path_to_file): 
    56                                         jid = 'gcstatus' 
    57                                         msg = show + ':' + msg 
    58                                         show = nick 
    59                                         files.append(ji + '/' + ji) 
    60                                         if os.path.isfile(jid): 
    61                                                 files.append(jid) 
    62                                 else: 
    63                                         files.append(ji) 
    64                         if common.gajim.config.get('log_notif_in_sep_file'): 
    65                                 files.append('notify.log') 
    66                 elif kind == 'incoming': # we save time:recv:message 
    67                         path_to_file = os.path.join(common.gajim.LOGPATH, ji) 
    68                         if os.path.isdir(path_to_file): 
    69                                 files.append(jid) 
     139                        time_col = int(float(time.time())) 
     140 
     141                def commit_to_db(values, cur = cur): 
     142                        sql = 'INSERT INTO logs (jid_id, contact_name, time, kind, show, message) '\ 
     143                                        'VALUES (?, ?, ?, ?, ?, ?)' 
     144                        cur.execute(sql, values) 
     145                        cur.connection.commit() 
     146                 
     147                jid_id = self.get_jid_id(jid) 
     148                                         
     149                if kind == 'status': # we store (not None) time, jid, show, msg 
     150                        # status for roster items 
     151                        if show is None: 
     152                                show_col = 'online' 
     153 
     154                        values = (jid_id, contact_name_col, time_col, kind, show_col, message_col) 
     155                        commit_to_db(values) 
     156                elif kind == 'gcstatus': 
     157                        # status in ROOM (for pm status see status) 
     158                        if show is None: 
     159                                show_col = 'online' 
     160                         
     161                        jid, nick = jid.split('/', 1) 
     162                         
     163                        jid_id = self.get_jid_id(jid) # re-get jid_id for the new jid 
     164                        contact_name_col = nick 
     165                        values = (jid_id, contact_name_col, time_col, kind, show_col, message_col) 
     166                        commit_to_db(values) 
     167                elif kind == 'gc_msg': 
     168                        if jid.find('/') != -1: # if it has a / 
     169                                jid, nick = jid.split('/', 1) 
    70170                        else: 
    71                                 files.append(ji) 
    72                         jid = 'recv' 
    73                         show = msg 
    74                         msg = '' 
    75                 elif kind == 'outgoing': # we save time:sent:message 
    76                         path_to_file = os.path.join(common.gajim.LOGPATH, ji) 
    77                         if os.path.isdir(path_to_file): 
    78                                 files.append(jid) 
    79                         else: 
    80                                 files.append(ji) 
    81                         jid = 'sent' 
    82                         show = msg 
    83                         msg = '' 
    84                 elif kind == 'gc': # we save time:gc:nick:message 
    85                         # create the folder if needed 
    86                         ji_fn = os.path.join(common.gajim.LOGPATH, ji) 
    87                         if os.path.isfile(ji_fn): 
    88                                 os.remove(ji_fn) 
    89                         if not os.path.isdir(ji_fn): 
    90                                 os.mkdir(ji_fn, 0700) 
    91                         files.append(ji + '/' + ji) 
    92                         jid = 'gc' 
    93                         show = nick 
    94                 # convert to utf8 before writing to file if needed 
    95                 if isinstance(tim, unicode): 
    96                         tim = tim.encode('utf-8') 
    97                 if isinstance(jid, unicode): 
    98                         jid = jid.encode('utf-8') 
    99                 if isinstance(show, unicode): 
    100                         show = show.encode('utf-8') 
    101                 if msg and isinstance(msg, unicode): 
    102                         msg = msg.encode('utf-8') 
    103                 for f in files: 
    104                         path_to_file = os.path.join(common.gajim.LOGPATH, f) 
    105                         if os.path.isdir(path_to_file): 
    106                                 return 
    107                         # this does it rw-r-r by default but is in a dir with 700 so it's ok 
    108                         fil = open(path_to_file, 'a') 
    109                         fil.write('%s:%s:%s' % (tim, jid, show)) 
    110                         if msg: 
    111                                 fil.write(':' + msg) 
    112                         fil.write('\n') 
    113                         fil.close() 
    114  
    115         def __get_path_to_file(self, fjid): 
    116                 jid = fjid.split('/')[0] 
    117                 path_to_file = os.path.join(common.gajim.LOGPATH, jid) 
    118                 if os.path.isdir(path_to_file): 
    119                         if fjid == jid: # we want to read the gc history 
    120                                 path_to_file = os.path.join(common.gajim.LOGPATH, jid + '/' + jid) 
    121                         else: #we want to read pm history 
    122                                 path_to_file = os.path.join(common.gajim.LOGPATH, fjid) 
    123                 return path_to_file 
    124  
    125         def get_no_of_lines(self, fjid): 
    126                 '''returns total number of lines in a log file 
    127                 returns 0 if log file does not exist''' 
    128                 fjid = fjid.lower() 
    129                 path_to_file = self.__get_path_to_file(fjid) 
    130                 if not os.path.isfile(path_to_file): 
    131                         return 0 
    132                 f = open(path_to_file, 'r') 
    133                 return len(f.readlines()) # number of lines 
    134  
    135         # FIXME: remove me when refactor in TC is done 
    136         def read_from_line_to_line(self, fjid, begin_from_line, end_line): 
    137                 '''returns the text in the lines (list), 
    138                 returns empty list if log file does not exist''' 
    139                 fjid = fjid.lower() 
    140                 path_to_file = self.__get_path_to_file(fjid) 
    141                 if not os.path.isfile(path_to_file): 
    142                         return [] 
    143  
    144                 lines = [] 
    145                  
    146                 fil = open(path_to_file, 'r') 
    147                 #fil.readlines(begin_from_line) # skip the previous lines 
    148                 no_of_lines = begin_from_line # number of lines between being and end 
    149                 while (no_of_lines < begin_from_line and fil.readline()): 
    150                         no_of_lines += 1 
    151                  
    152                 print begin_from_line, end_line 
    153                 while no_of_lines < end_line: 
    154                         line = fil.readline().decode('utf-8') 
    155                         print `line`, '@', no_of_lines 
    156                         if line: 
    157                                 line = helpers.from_one_line(line) 
    158                                 lineSplited = line.split(':') 
    159                                 if len(lineSplited) > 2: 
    160                                         lines.append(lineSplited) 
    161                                 no_of_lines += 1 
    162                         else: # emplty line (we are at the end of file) 
    163                                 break 
    164                 return lines 
    165  
    166         def get_last_conversation_lines(self, jid, how_many_lines, timeout): 
    167                 '''accepts how many lines to restore and when to time them out 
    168                 (mark them as too old), returns the lines (list), empty list if log file 
    169                 does not exist''' 
    170                 fjid = fjid.lower() 
    171                 path_to_file = self.__get_path_to_file(fjid) 
    172                 if not os.path.isfile(path_to_file): 
    173                         return [] 
    174                  
    175  
    176         def get_conversation_for_date(self, fjid, year, month, day): 
    177                 '''returns the text in the lines (list), 
    178                 returns empty list if log file does not exist''' 
    179                 fjid = fjid.lower() 
    180                 path_to_file = self.__get_path_to_file(fjid) 
    181                 if not os.path.isfile(path_to_file): 
    182                         return [] 
    183                  
    184                 lines = [] 
    185                 f = open(path_to_file, 'r') 
    186                 done = False 
    187                 found_first_line_that_matches = False 
    188                 while not done: 
    189                         # it should be utf8 (I don't decode for optimization reasons) 
    190                         line = f.readline() 
    191                         if line: 
    192                                 line = helpers.from_one_line(line) 
    193                                 splitted_line = line.split(':') 
    194                                 if len(splitted_line) > 2: 
    195                                         # line[0] is date, line[1] is type of message 
    196                                         # line[2:] is message 
    197                                         date = splitted_line[0] 
    198                                         date = time.localtime(float(date)) 
    199                                         # eg. 2005 
    200                                         line_year = int(time.strftime('%Y', date)) 
    201                                         # (01 - 12) 
    202                                         line_month = int(time.strftime('%m', date)) 
    203                                         # (01 - 31) 
    204                                         line_day = int(time.strftime('%d', date)) 
    205                                          
    206                                         # now check if that line is one of the lines we want 
    207                                         # (if it is in the date we want) 
    208                                         if line_year == year and line_month == month and line_day == day: 
    209                                                 if found_first_line_that_matches is False: 
    210                                                         found_first_line_that_matches = True 
    211                                                 lines.append(splitted_line) 
    212                                         else: 
    213                                                 if found_first_line_that_matches: # we had a match before 
    214                                                         done = True # but no more. so we're done with that date 
     171                                # it's server message f.e. error message 
     172                                # when user tries to ban someone but he's not allowed to 
     173                                nick = None 
     174                        jid_id = self.get_jid_id(jid) # re-get jid_id for the new jid 
     175                        contact_name_col = nick 
    215176