d7b2f47bdb975732f4c683beda80a933af17ae4e
[cs-p2p-next.git] / ppf / new / storage.py
1 """
2 Storage class for P2P logging information.
3
4 Built on previous work by Adriana Draghici and Razvan Deaconescu.
5
6 2011, Razvan Deaconescu, razvan.deaconescu@cs.pub.ro
7 2011, Mariana Marasoiu, mariana.marasoiu@gmail.com
8 """
9
10 import os
11 import os.path
12 import re
13 import logging
14 import sqlite3
15 import MySQLdb
16 import datetime
17
18 #
19 # Logging code heavily inspired by Logging HOWTO documentation:
20 #     http://docs.python.org/dev/howto/logging.html#configuring-logging
21 #
22
23 # Create logger; default logging level is DEBUG.
24 logger = logging.getLogger(__name__)
25 logger.setLevel(logging.DEBUG)
26
27 # Create console handler and set level to ERROR.
28 ch = logging.StreamHandler()
29 ch.setLevel(logging.DEBUG)
30
31 # Create formatter.
32 formatter = logging.Formatter('%(filename)s:%(lineno)s - '
33                               '%(levelname)s: %(message)s')
34
35 # Add formatter to console handler.
36 ch.setFormatter(formatter)
37
38 # Add console handler to logger.
39 logger.addHandler(ch)
40
41
42 message_types = {
43         'CHOKE': {'id': 1, 'parameters': None},
44         'UNCHOKE': {'id': 2, 'parameters': None},
45         'INTERESTED': {'id': 3, 'parameters': None},
46         'NOT_INTERESTED': {'id': 4, 'parameters': None},
47         'HAVE': {'id': 5, 'parameters': None},
48         'BITFIELD': {'id': 6, 'parameters': None},
49         'REQUEST': {'id': 7, 'parameters': None},
50         'PIECE': {'id': 8, 'parameters': None},
51         'CANCEL': {'id': 9, 'parameters': None},
52         'DHT_PORT': {'id': 10, 'parameters': None}
53 }
54
55 bittorrent_clients = {
56         'Tribler': {
57             'id': 1,
58             'language': 'Python',
59             'url': 'http://www.tribler.org/trac',
60             'dht_support': True,
61             'streaming_support': True,
62             'pxe_support': None,
63             'features': None
64         },
65         'NextShare': {
66             'id': 2,
67             'language': 'Python',
68             'url': 'https://trac.p2p-next.org/',
69             'dht_support': True,
70             'streaming_support': True,
71             'pxe_support': None,
72             'features': None
73         },
74         'libtorrent-rasterbar': {
75             'id': 3,
76             'language': 'C++',
77             'url': 'http://www.rasterbar.com/products/libtorrent/',
78             'dht_support': True,
79             'streaming_support': True,
80             'pxe_support': None,
81             'features': None
82         },
83         'Vuze': {
84             'id': 4,
85             'language': 'Java',
86             'url': 'http://www.vuze.com/',
87             'dht_support': True,
88             'streaming_support': True,
89             'pxe_support': None,
90             'features': None
91         },
92         'Transmission': {
93             'id': 5,
94             'language': 'C',
95             'url': 'http://www.transmissionbt.com/',
96             'dht_support': True,
97             'streaming_support': False,
98             'pxe_support': None,
99             'features': None
100         },
101         'Aria': {
102             'id': 6,
103             'language': 'C',
104             'url': 'http://aria2.sourceforge.net/',
105             'dht_support': True,
106             'streaming_support': False,
107             'pxe_support': None,
108             'features': None
109         },
110         'Mainline': {
111             'id': 7,
112             'language': 'Python',
113             'url': 'http://www.bittorrent.com/',
114             'dht_support': True,
115             'streaming_support': False,
116             'pxe_support': None,
117             'features': None
118         }
119 }
120
121 transfer_directions = {
122         'receive': 1,
123         'send': 2
124 }
125
126 class Swarm(object):
127     """ Class mimics a C structure. """
128     def __init__(self, torrent_filename=None, data_size=None,
129             description=None):
130         self.torrent_filename = torrent_filename
131         self.data_size = data_size
132         self.description = description
133
134 class ClientSession(object):
135     """ Class mimics a C structure. """
136     # TODO: Add timezone.
137     def __init__(self, swarm_id=None, btclient=None, system_os=None,
138             system_os_version=None, system_ram=None, system_cpu=None,
139             public_ip=None, public_port=None, ds_limit=None, us_limit=None,
140             start_time=None, dht_enabled=None, pxe_enabled=None,
141             streaming_enabled=None, features=None, description=None):
142         self.swarm_id = swarm_id
143         self.btclient = btclient
144         self.system_os = system_os
145         self.system_os_version = system_os_version
146         self.system_ram = system_ram
147         self.system_cpu = system_cpu
148         self.public_ip = public_ip
149         self.public_port = public_port
150         self.ds_limit = ds_limit
151         self.us_limit = us_limit
152         self.start_time = start_time
153         self.dht_enabled = dht_enabled
154         self.pxe_enabled = pxe_enabled
155         self.streaming_enabled = streaming_enabled
156         self.features = features
157         self.description = description
158
159 class PeerStatusMessage(object):
160     """ Class mimics a C structure. """
161     def __init__(self, swarm_id=None, client_session_id=None, timestamp=None,
162             peer_ip=None, peer_port=None, download_speed=None,
163             upload_speed=None):
164         self.swarm_id = swarm_id
165         self.client_session_id = client_session_id
166         self.timestamp = timestamp
167         self.peer_ip = peer_ip
168         self.peer_port = peer_port
169         self.download_speed = download_speed
170         self.upload_speed = upload_speed
171
172 class StatusMessage(object):
173     """ Class mimics a C structure. """
174     def __init__(self, swarm_id=None, client_session_id=None, timestamp=None,
175             time=None, num_peers=None, num_dht_peers=None,
176             download_speed=None, upload_speed=None, download_size=None,
177             upload_size=None, eta=None):
178         self.swarm_id = swarm_id
179         self.client_session_id = client_session_id
180         self.timestamp = timestamp
181         self.num_peers = num_peers
182         self.num_dht_peers = num_dht_peers
183         self.download_speed = download_speed
184         self.upload_speed = upload_speed
185         self.download_size = download_size
186         self.upload_size = upload_size
187         self.eta = eta
188
189 class VerboseMessage(object):
190     """ Class mimics a C structure. """
191     def __init__(self, swarm_id=None, client_session_id=None, timestamp=None,
192             transfer_direction=None, peer_ip=None, peer_port=None,
193             message_type=None, index=None, begin=None, length=None,
194             listen_port=None):
195         self.swarm_id = swarm_id
196         self.client_session_id = client_session_id
197         self.timestamp = timestamp
198         self.transfer_direction = transfer_direction
199         self.peer_ip = peer_ip
200         self.peer_port = peer_port
201         self.message_type = message_type
202         self.index = index
203         self.begin = begin
204         self.length = length
205         self.listen_port = listen_port
206
207
208 class SwarmDataAccess(object):
209
210     """Base class for swarm information storage.
211
212     Define main operations on storage objects: Swarm, ClientSession,
213     StatusMessage, PeerStatusMessage, VerboseMessage.
214     Each object has four corresponding operations: add, remove, get, update.
215     """
216
217     def __init__(self):
218         pass
219
220     def add_swarm(self, swarm):
221         pass
222
223     def remove_swarm(self):
224         pass
225
226     def get_swarm(self):
227         pass
228
229     def update_swarm(self):
230         pass
231
232     def add_client_session(self, session):
233         pass
234
235     def remove_client_session(self):
236         pass
237
238     def get_client_session(self):
239         pass
240
241     def update_client_session(self):
242         pass
243
244     def add_peer_status_message(self, msg):
245         pass
246
247     def remove_peer_status_message(self):
248         pass
249
250     def get_peer_status_message(self):
251         pass
252
253     def update_peer_status_message(self):
254         pass
255
256     def add_status_message(self, msg):
257         pass
258
259     def remove_status_message(self):
260         pass
261
262     def get_status_message(self):
263         pass
264
265     def update_status_message(self):
266         pass
267
268     def add_verbose_message(self, msg):
269         pass
270
271     def remove_verbose_message(self):
272         pass
273
274     def get_verbose_message(self):
275         pass
276
277     def update_verbose_message(self):
278         pass
279
280
281 class FileAccess(SwarmDataAccess):
282
283     """Subclass of SwarmDataAccess providing access for file storage."""
284
285     def __init__(self, path):
286         """Add base_path attribute received as argument."""
287         self.base_path = path
288
289
290 def find_last_numeric_subfolder(path):
291     """Find last numeric folder in base_path folder.
292
293     The last numeric folder is the last swarm_id.
294     """
295
296     dir_list = []
297     pattern = re.compile("[0-9]+")
298
299     # Browse entries in base_path folder.
300     listing = os.listdir(path)
301     for entry in listing:
302         # If directory name is a number (id) add it to the list.
303         if os.path.isdir(os.path.join(path, entry)):
304             if pattern.match(entry):
305                 dir_list.append(int(entry))
306
307     if not dir_list:
308         return None
309     else:
310         dir_list.sort()
311         return dir_list[len(dir_list)-1]
312
313
314 class TreeTextFileAccess(FileAccess):
315
316     """Child class of FileAccess for directory tree structure access."""
317
318     def __init__(self, path):
319         """Use superclass method for initialisation of constructor."""
320         super(TreeTextFileAccess, self).__init__(path)
321
322     def add_swarm(self, swarm):
323         """
324         Create a subfolder with an unique id. Add 1 to the last numeric
325         subfolder id. In case none exists, use 1 as id.
326         """
327         id = find_last_numeric_subfolder(self.base_path)
328         if id == None:
329             id = 1
330         else:
331             id = id+1
332
333         swarm_path = os.path.join(self.base_path, str(id))
334         os.mkdir(swarm_path)
335
336         swarm_config = os.path.join(swarm_path, "swarm.conf")
337         f = open(swarm_config, 'w')
338         f.write("""id = %s
339         torrent_filename = %s
340         data_size = %s
341         description = %s
342         """ %(id, swarm.torrent_filename, swarm.data_size, swarm.description))
343         f.close()
344
345     def add_client_session(self, session):
346         """
347         Create session subfolder in swarm subfolder and add config file.
348         TODO: Throw exception in case swarm subfolder doesn't exist.
349         """
350         swarm_path = os.path.join(self.base_path, str(session.swarm_id))
351
352         # Search first available folder in swarm_path.
353         id = find_last_numeric_subfolder(swarm_path)
354         if id == None:
355             id = 1
356         else:
357             id = id+1
358
359         # Create session subfolder.
360         session_path = os.path.join(swarm_path, str(id))
361         os.mkdir(session_path)
362
363         # Create and populate configuration file.
364         session_config = os.path.join(session_path, "client_session.conf")
365         f = open(session_config, 'w')
366         f.write("""id = %s
367         swarm_id = %s
368         btclient = %s
369         system_os = %s
370         system_os_version = %s
371         system_ram = %s
372         system_cpu = %s
373         public_ip = %s
374         public_port = %s
375         ds_limit = %s
376         us_limit = %s
377         start_time = %s
378         dht_enabled = %s
379         pxe_enabled = %s
380         streaming_enabled = %s
381         features = %s
382         description = %s
383         """ %(id, session.swarm_id, session.btclient, session.system_os,
384             session.system_os_version, session.system_ram, session.system_cpu,
385             session.public_ip, session.public_port, session.ds_limit,
386             session.us_limit, session.start_time, session.dht_enabled,
387             session.pxe_enabled, session.streaming_enabled,
388             session.features, session.description))
389         f.close()
390
391     def add_peer_status_message(self, msg):
392         """Add peer status msg line to file. msg type is PeerStatusMessage."""
393         # TODO: id is number of lines in file.
394         swarm_path = os.path.join(self.base_path, str(msg.swarm_id))
395         session_path = os.path.join(swarm_path, str(msg.client_session_id))
396         message_file = os.path.join(session_path, "peer_status.txt")
397
398         f = open(message_file, 'a')
399         f.write("""%s,%s,%s,%s,%s,%s,%s,%s\n"""
400                 %(1, msg.swarm_id, msg.client_session_id, msg.timestamp,
401                     msg.peer_ip, msg.peer_port, msg.download_speed,
402                     msg.upload_speed))
403         f.close()
404
405     def add_status_message(self, msg):
406         """Add status msg line to file. msg type is StatusMessage."""
407         # TODO: id is number of lines in file.
408         swarm_path = os.path.join(self.base_path, str(msg.swarm_id))
409         session_path = os.path.join(swarm_path, str(msg.client_session_id))
410         message_file = os.path.join(session_path, "status.txt")
411
412         f = open(message_file, 'a')
413         f.write("""%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"""
414                 %(1, msg.swarm_id, msg.client_session_id, msg.timestamp,
415                     msg.num_peers, msg.num_dht_peers, msg.download_speed,
416                     msg.upload_speed, msg.download_size, msg.upload_size,
417                     msg.eta))
418         f.close()
419
420     def add_verbose_message(self, msg):
421         """Add verbose msg line to file. msg type is VerboseMessage."""
422         # TODO: id is number of lines in file.
423         swarm_path = os.path.join(self.base_path, str(msg.swarm_id))
424         session_path = os.path.join(swarm_path, str(msg.client_session_id))
425         message_file = os.path.join(session_path, "verbose.txt")
426
427         f = open(message_file, 'a')
428         f.write("""%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"""
429                 %(1, msg.swarm_id, msg.client_session_id, msg.timestamp,
430                     msg.transfer_direction, msg.peer_ip, msg.peer_port,
431                     msg.message_type, msg.index, msg.begin, msg.length,
432                     msg.listen_port))
433         f.close()
434
435 class DatabaseAccess(SwarmDataAccess):
436
437     """Subclass of SwarmDataAccess providing functions for database usage."""
438
439     def __init__(self, database):
440         """Add database attribute received as argument.
441
442         The argument may be a name or a list with options for connection.
443         """
444
445         self.database = database
446
447     def connect(self):
448         """Initialize access attributes."""
449         self.conn = None
450         self.cursor = None
451
452     def disconnect(self):
453         """Close connection with database."""
454         self.cursor.close()
455         self.conn.close()
456
457 class SQLiteDatabaseAccess(DatabaseAccess):
458
459     """Child class of DatabaseAccess for SQLite access."""
460
461     def __init__(self, database):
462         """Use superclass method for initialisation of constructor."""
463         super(SQLiteDatabaseAccess, self).__init__(database)
464
465     def connect(self):
466         """Connect to sqlite3 database."""
467         self.conn = sqlite3.connect(self.database)
468         self.cursor = self.conn.cursor()
469         # Use foreign key support if available.
470         self.cursor.execute("PRAGMA foreign_keys = ON")
471         self.conn.commit()
472
473     def reset_query(self):
474         self.columns = ""
475         self.values = ""
476
477     def append_to_insert_query(self, data_name, data_value):
478         if data_value is not None:
479             self.columns = self.columns + data_name + ", "
480             self.values = self.values + "'" + str(data_value) + "'" + ", "
481
482     def add_swarm(self, swarm):
483         """Insert swarm in database. swarm type is Swarm."""
484         self.reset_query()
485         self.append_to_insert_query("torrent_filename", swarm.torrent_filename)
486         self.append_to_insert_query("data_size", swarm.data_size)
487         self.append_to_insert_query("description", swarm.description)
488
489         self.columns = re.sub(',\s*$', '', self.columns)
490         self.values = re.sub(',\s*$', '', self.values)
491         insert_query = "INSERT INTO swarms(" + self.columns +")" + \
492                 " VALUES(" + self.values + ")"
493         self.cursor.execute(insert_query)
494         self.conn.commit()
495         self.cursor.execute("SELECT * FROM swarms ORDER BY id DESC LIMIT 1")
496         row = self.cursor.fetchone()
497         return row[0]
498
499     def add_client_session(self, session):
500         """Insert session in database. session type is Session."""
501         self.reset_query()
502         self.append_to_insert_query("swarm_id", session.swarm_id)
503         # TODO: search database for client ID
504         self.append_to_insert_query("btclient_id",
505                 bittorrent_clients[session.btclient]['id'])
506         self.append_to_insert_query("system_os", session.system_os)
507         self.append_to_insert_query("system_os_version",
508                                     session.system_os_version)
509         self.append_to_insert_query("system_ram", session.system_ram)
510         self.append_to_insert_query("system_cpu", session.system_cpu)
511         self.append_to_insert_query("public_ip", session.public_ip)
512         self.append_to_insert_query("public_port", session.public_port)
513         self.append_to_insert_query("ds_limit", session.ds_limit)
514         self.append_to_insert_query("us_limit", session.us_limit)
515         self.append_to_insert_query("start_time", session.start_time)
516         self.append_to_insert_query("dht_enabled", session.dht_enabled)
517         self.append_to_insert_query("pxe_enabled", session.pxe_enabled)
518         self.append_to_insert_query("streaming_enabled",
519                                     session.streaming_enabled)
520         self.append_to_insert_query("features", session.features)
521         self.append_to_insert_query("description", session.description)
522
523         self.columns = re.sub(',\s*$', '', self.columns)
524         self.values = re.sub(',\s*$', '', self.values)
525         insert_query = "INSERT INTO client_sessions(" + self.columns +")" + \
526                 " VALUES(" + self.values + ")"
527         self.cursor.execute(insert_query)
528         self.conn.commit()
529         self.cursor.execute("SELECT * FROM client_sessions "
530                             "ORDER BY id DESC LIMIT 1")
531         row = self.cursor.fetchone()
532         return row[0]
533
534     def get_string_timestamp(self, ts):
535         # Timestamp is Python Datatime. Convert it to string format and
536         # pass it to internal SQLITE julianday() function.
537         return "%s-%s-%s %s:%s:%s.%s" \
538                 %(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second,
539                         1000 * ts.microsecond)
540
541     def add_peer_status_message(self, msg):
542         """Insert peer status message in database.
543         msg type is PeerStatusMessage.
544         """
545         self.reset_query()
546         self.append_to_insert_query("client_session_id", msg.client_session_id)
547
548         # TODO: Check msg.timestamp is not None. Raise exception.
549         timestamp_string = self.get_string_timestamp(msg.timestamp)
550         value = "julianday(" + timestamp_string + ")"
551         self.append_to_insert_query("timestamp", value)
552
553         self.append_to_insert_query("timestamp", msg.timestamp)
554         self.append_to_insert_query("peer_ip", msg.peer_ip)
555         self.append_to_insert_query("peer_port", msg.peer_port)
556         self.append_to_insert_query("download_speed", msg.download_speed)
557         self.append_to_insert_query("upload_speed", msg.upload_speed)
558
559         self.columns = re.sub(',\s*$', '', self.columns)
560         self.values = re.sub(',\s*$', '', self.values)
561         insert_query = "INSERT INTO peer_status_messages(" + \
562                 self.columns +")" + " VALUES(" + self.values + ")"
563         self.cursor.execute(insert_query)
564         self.conn.commit()
565
566     def add_status_message(self, msg):
567         """Insert status msg in database. msg type is StatusMessage."""
568         self.reset_query()
569         self.append_to_insert_query("client_session_id", msg.client_session_id)
570
571         # TODO: Check msg.timestamp is not None. Raise exception.
572         timestamp_string = self.get_string_timestamp(msg.timestamp)
573         value = "julianday(" + timestamp_string + ")"
574         self.append_to_insert_query("timestamp", value)
575
576         self.append_to_insert_query("num_peers", msg.num_peers)
577         self.append_to_insert_query("num_dht_peers", msg.num_dht_peers)
578         self.append_to_insert_query("download_speed", msg.download_speed)
579         self.append_to_insert_query("upload_speed", msg.upload_speed)
580         self.append_to_insert_query("download_size", msg.download_size)
581         self.append_to_insert_query("upload_size", msg.upload_size)
582         self.append_to_insert_query("eta", msg.eta)
583
584         self.columns = re.sub(',\s*$', '', self.columns)
585         self.values = re.sub(',\s*$', '', self.values)
586         insert_query = "INSERT INTO status_messages(" + self.columns +")" + \
587                 " VALUES(" + self.values + ")"
588         self.cursor.execute(insert_query)
589         self.conn.commit()
590
591     def add_verbose_message(self, msg):
592         """Insert verbose msg in database. msg type is VerboseMessage."""
593         self.reset_query()
594         self.append_to_insert_query("client_session_id", msg.client_session_id)
595
596         # TODO: Check msg.timestamp is not None. Raise exception.
597         timestamp_string = self.get_string_timestamp(msg.timestamp)
598         value = "julianday(" + timestamp_string + ")"
599         self.append_to_insert_query("timestamp", value)
600
601         self.append_to_insert_query("transfer_direction_id",
602                 transfer_directions[msg.transfer_direction])
603         self.append_to_insert_query("peer_ip", msg.peer_ip)
604         self.append_to_insert_query("peer_port", msg.peer_port)
605         self.append_to_insert_query("message_type_id",
606                 message_types[msg.message_type]['id'])
607         self.append_to_insert_query("index", msg.index)
608         self.append_to_insert_query("begin", msg.begin)
609         self.append_to_insert_query("length", msg.length)
610         self.append_to_insert_query("listen_port", msg.listen_port)
611
612         self.columns = re.sub(',\s*$', '', self.columns)
613         self.values = re.sub(',\s*$', '', self.values)
614         insert_query = "INSERT INTO verbose_messages(" + self.columns +")" + \
615                 " VALUES(" + self.values + ")"
616         self.cursor.execute(insert_query)
617         self.conn.commit()
618
619 class MySQLDatabaseAccess(DatabaseAccess):
620
621     """Child class of DatabaseAccess for MySQL access."""
622
623     def __init__(self, database):
624         """Initialize the database attribute of the class.
625
626         Use superclass method for initialisation of constructor.
627         Check also for the optional arguments; if they are not, use defaults.
628         """
629
630         super(MySQLDatabaseAccess, self).__init__(database)
631         if 'host' not in self.database:
632             self.database['host'] = 'localhost'
633         if 'port' not in self.database:
634             self.database['port'] = 3306
635
636     def connect(self):
637         """Connect to MySQL database."""
638         self.conn = MySQLdb.Connection(db=self.database['database'],
639                                        user=self.database['user'],
640                                        passwd=self.database['password'],
641                                        host=self.database['host'],
642                                        port=int(self.database['port']))
643         self.cursor = self.conn.cursor()
644         self.conn.commit()
645
646     def reset_query(self):
647         self.columns = ""
648         self.values = ""
649
650     def append_to_insert_query(self, data_name, data_value):
651         if data_value is not None:
652             self.columns = self.columns + data_name + ", "
653             self.values = self.values + "'" + str(data_value) + "'" + ", "
654
655     def add_swarm(self, swarm):
656         """Insert swarm in database. swarm type is Swarm."""
657         self.reset_query()
658         self.append_to_insert_query("torrent_filename", swarm.torrent_filename)
659         self.append_to_insert_query("data_size", swarm.data_size)
660         self.append_to_insert_query("description", swarm.description)
661
662         self.columns = re.sub(',\s*$', '', self.columns)
663         self.values = re.sub(',\s*$', '', self.values)
664         insert_query = "INSERT INTO swarms(" + self.columns +")" + \
665                 " VALUES(" + self.values + ")"
666         self.cursor.execute(insert_query)
667         self.conn.commit()
668         self.cursor.execute("SELECT * FROM swarms ORDER BY id DESC LIMIT 1")
669         row = self.cursor.fetchone()
670         return row[0]
671
672     def add_client_session(self, session):
673         """Insert session in database. session type is Session."""
674         self.reset_query()
675         self.append_to_insert_query("swarm_id", session.swarm_id)
676         # TODO: search database for client ID
677         self.append_to_insert_query("btclient_id",
678                 bittorrent_clients[session.btclient]['id'])
679         self.append_to_insert_query("system_os", session.system_os)
680         self.append_to_insert_query("system_os_version",
681                                     session.system_os_version)
682         self.append_to_insert_query("system_ram", session.system_ram)
683         self.append_to_insert_query("system_cpu", session.system_cpu)
684         self.append_to_insert_query("public_ip", session.public_ip)
685         self.append_to_insert_query("public_port", session.public_port)
686         self.append_to_insert_query("ds_limit", session.ds_limit)
687         self.append_to_insert_query("us_limit", session.us_limit)
688         self.append_to_insert_query("start_time", session.start_time)
689         self.append_to_insert_query("dht_enabled", session.dht_enabled)
690         self.append_to_insert_query("pxe_enabled", session.pxe_enabled)
691         self.append_to_insert_query("streaming_enabled",
692                                     session.streaming_enabled)
693         self.append_to_insert_query("features", session.features)
694         self.append_to_insert_query("description", session.description)
695
696         self.columns = re.sub(',\s*$', '', self.columns)
697         self.values = re.sub(',\s*$', '', self.values)
698         insert_query = "INSERT INTO client_sessions(" + self.columns +")" + \
699                 " VALUES(" + self.values + ")"
700         self.cursor.execute(insert_query)
701         self.conn.commit()
702         self.cursor.execute("SELECT * FROM client_sessions "
703                             "ORDER BY id DESC LIMIT 1")
704         row = self.cursor.fetchone()
705         return row[0]
706
707     def get_string_timestamp(self, ts):
708         # Timestamp is Python Datatime. Convert it to string format.
709         return "%s-%s-%s %s:%s:%s" \
710                 %(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second)
711
712     def add_peer_status_message(self, msg):
713         """Insert peer status message in database.
714         msg type is PeerStatusMessage.
715         """
716         self.reset_query()
717         self.append_to_insert_query("client_session_id", msg.client_session_id)
718
719         # TODO: Check msg.timestamp is not None. Raise exception.
720         timestamp_string = self.get_string_timestamp(msg.timestamp)
721
722         self.append_to_insert_query("timestamp", timestamp_string)
723         self.append_to_insert_query("peer_ip", msg.peer_ip)
724         self.append_to_insert_query("peer_port", msg.peer_port)
725         self.append_to_insert_query("download_speed", msg.download_speed)
726         self.append_to_insert_query("upload_speed", msg.upload_speed)
727
728         self.columns = re.sub(',\s*$', '', self.columns)
729         self.values = re.sub(',\s*$', '', self.values)
730         insert_query = "INSERT INTO peer_status_messages(" + \
731                 self.columns +")" + " VALUES(" + self.values + ")"
732         self.cursor.execute(insert_query)
733         self.conn.commit()
734
735     def add_status_message(self, msg):
736         """Insert status msg in database. msg type is StatusMessage."""
737         self.reset_query()
738         self.append_to_insert_query("client_session_id", msg.client_session_id)
739
740         # TODO: Check msg.timestamp is not None. Raise exception.
741         timestamp_string = self.get_string_timestamp(msg.timestamp)
742
743         self.append_to_insert_query("timestamp", timestamp_string)
744         self.append_to_insert_query("num_peers", msg.num_peers)
745         self.append_to_insert_query("num_dht_peers", msg.num_dht_peers)
746         self.append_to_insert_query("download_speed", msg.download_speed)
747         self.append_to_insert_query("upload_speed", msg.upload_speed)
748         self.append_to_insert_query("download_size", msg.download_size)
749         self.append_to_insert_query("upload_size", msg.upload_size)
750         self.append_to_insert_query("eta", msg.eta)
751
752         self.columns = re.sub(',\s*$', '', self.columns)
753         self.values = re.sub(',\s*$', '', self.values)
754         insert_query = "INSERT INTO status_messages(" + self.columns +")" + \
755                 " VALUES(" + self.values + ")"
756         self.cursor.execute(insert_query)
757         self.conn.commit()
758
759     def add_verbose_message(self, msg):
760         """Insert verbose msg in database. msg type is VerboseMessage."""
761         self.reset_query()
762         self.append_to_insert_query("client_session_id", msg.client_session_id)
763
764         # TODO: Check msg.timestamp is not None. Raise exception.
765         timestamp_string = self.get_string_timestamp(msg.timestamp)
766
767         self.append_to_insert_query("timestamp", timestamp_string)
768         self.append_to_insert_query("transfer_direction_id",
769                 transfer_directions[msg.transfer_direction])
770         self.append_to_insert_query("peer_ip", msg.peer_ip)
771         self.append_to_insert_query("peer_port", msg.peer_port)
772         self.append_to_insert_query("message_type_id",
773                 message_types[msg.message_type]['id'])
774         self.append_to_insert_query("index", msg.index)
775         self.append_to_insert_query("begin", msg.begin)
776         self.append_to_insert_query("length", msg.length)
777         self.append_to_insert_query("listen_port", msg.listen_port)
778
779         self.columns = re.sub(',\s*$', '', self.columns)
780         self.values = re.sub(',\s*$', '', self.values)
781         insert_query = "INSERT INTO verbose_messages(" + self.columns +")" + \
782                 " VALUES(" + self.values + ")"
783         self.cursor.execute(insert_query)
784         self.conn.commit()
785
786 class SwarmWriter(object):
787     """Wrapper class for swarm storage write actions.
788
789     Multiple *Access objects may be added to the clas resulting in multiple
790     storage types.
791     For example, adding a swarm could result in
792        * adding a table entry in a MySQL database (MySQLDatabaseAccess)
793        * adding a table entry in an SQLite database (SQLiteDatabaseAccess)
794        * adding a new folder in a tree structure (TreeTextFileAccess)
795     """
796
797     def __init__(self):
798         self.handlers = []
799
800     def add_access_handle(self, handle):
801         self.handlers.append(handle)
802
803     def remove_access_handle(self, handle):
804         self.handlers.remove(handle)
805
806     def add_swarm(self, swarm):
807         for h in self.handlers:
808             swarm_id = h.add_swarm(swarm)
809         return swarm_id
810
811     def add_client_session(self, session):
812         for h in self.handlers:
813             session_id = h.add_client_session(session)
814         return session_id
815
816     def add_peer_status_message(self, msg):
817         for h in self.handlers:
818             h.add_peer_status_message(msg)
819
820     def add_status_message(self, msg):
821         for h in self.handlers:
822             h.add_status_message(msg)
823
824     def add_verbose_message(self, msg):
825         for h in self.handlers:
826             h.add_verbose_message(msg)