From f9271e98092880a04dc3ecb87554e391c498e0be Mon Sep 17 00:00:00 2001 From: Jefry Lagrange Date: Sun, 28 Oct 2012 14:45:00 -0400 Subject: [PATCH] initial commit for file_share plugin --- .hgignore | 6 + file_sharing/__init__.py | 1 + file_sharing/config.py | 26 ++ file_sharing/database.py | 195 +++++++++++++++ file_sharing/fileshare_window.py | 411 +++++++++++++++++++++++++++++++ file_sharing/fshare.py | 159 ++++++++++++ file_sharing/fshare_protocol.py | 163 ++++++++++++ file_sharing/manifest.ini | 8 + 8 files changed, 969 insertions(+) create mode 100644 .hgignore create mode 100644 file_sharing/__init__.py create mode 100644 file_sharing/config.py create mode 100644 file_sharing/database.py create mode 100644 file_sharing/fileshare_window.py create mode 100644 file_sharing/fshare.py create mode 100644 file_sharing/fshare_protocol.py create mode 100644 file_sharing/manifest.ini diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..35d44d8 --- /dev/null +++ b/.hgignore @@ -0,0 +1,6 @@ +# use glob syntax. +syntax: glob + +*.pyc +*.pyo +*.db diff --git a/file_sharing/__init__.py b/file_sharing/__init__.py new file mode 100644 index 0000000..e30b2af --- /dev/null +++ b/file_sharing/__init__.py @@ -0,0 +1 @@ +from fshare import FileSharePlugin diff --git a/file_sharing/config.py b/file_sharing/config.py new file mode 100644 index 0000000..6b454ee --- /dev/null +++ b/file_sharing/config.py @@ -0,0 +1,26 @@ +import ConfigParser +import sys +import os + +def set(option, value): + _file = open(path, 'w') + cp.set('General', option, value) + cp.write(_file) + _file.close() + +def set_defaults(): + cp.add_section('General') + set('incoming_dir', '/home') + +path = sys.path[1] +path = path + '/file_sharing/' + 'conf.cfg' +cp = ConfigParser.ConfigParser() +if os.path.exists(path): + _file = open(path) + cp.readfp(_file) + _file.close() +else: + set_defaults() +INCOMING_DIR = cp.get('General', 'incoming_dir') + + diff --git a/file_sharing/database.py b/file_sharing/database.py new file mode 100644 index 0000000..f9a3146 --- /dev/null +++ b/file_sharing/database.py @@ -0,0 +1,195 @@ +import sqlite3 +from common import gajim +import sys +import os +path = sys.path[1] +path = path + '/file_sharing/' + 'shared_files.db' +db_exist = os.path.exists(path) +conn = sqlite3.connect(path) +# Enable foreign keys contraints +conn.cursor().execute("pragma foreign_keys = on") + +# NOTE: Make sure we are getting and setting the requester without its resource +def create_database(): + c = conn.cursor() + # Create tables + c.execute("CREATE TABLE permissions" + + "(fid integer REFERENCES files(fid) ON DELETE CASCADE, " + + "account text, requester text)") + c.execute("CREATE TABLE files" + + "(fid INTEGER PRIMARY KEY AUTOINCREMENT," + + " file_path text, relative_path text, hash_sha1 text," + + "size numeric, description text, mod_date text, is_dir boolean)") + # Save (commit) the changes + conn.commit() + c.close() + +def get_toplevel_files(account, requester): + c = conn.cursor() + data = (account, requester) + c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," + + " is_dir FROM (files JOIN permissions ON" + + " files.fid=permissions.fid) WHERE account=? AND requester=?" + + " AND relative_path NOT LIKE '%/%'", data) + result = c.fetchall() + c.close() + return result + +def get_files_from_dir(account, requester, dir_): + c = conn.cursor() + data = (account, requester, dir_ + '/%') + c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," + + " is_dir FROM (files JOIN permissions ON" + + " files.fid=permissions.fid) WHERE account=? AND requester=?" + + " AND relative_path LIKE ?", data) + result = c.fetchall() + c.close() + fresult = [] + for r in result: + name = r[0][len(dir_) + 1:] + if '/' not in name: + fresult.append(r) + return fresult + +def get_files(account, requester): + """ + >>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \ + 'date', False) + >>> foo = add_file('account@gajim', 'requester@jabber', file_) + >>> result = get_files('account@gajim', 'requester@jabber') + >>> len(result) + 1 + >>> _delete_file(1) + """ + c = conn.cursor() + data = (account, requester) + c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," + + " is_dir FROM (files JOIN permissions ON" + + " files.fid=permissions.fid) WHERE account=? AND requester=?", data) + result = c.fetchall() + c.close() + return result + +def get_file(account, requester, hash_, name): + c = conn.cursor() + if hash_: + data = (account, requester, hash_) + sql = "SELECT relative_path, hash_sha1, size, description, mod_date," + \ + " file_path FROM (files JOIN permissions ON" + \ + " files.fid=permissions.fid) WHERE account=? AND requester=?" + \ + " AND hash_sha1=?" + else: + data = (account, requester, name) + sql = "SELECT relative_path, hash_sha1, size, description, mod_date," + \ + " file_path FROM (files JOIN permissions ON" + \ + " files.fid=permissions.fid) WHERE account=? AND requester=?" + \ + " AND relative_path=?" + c.execute(sql, data) + result = c.fetchall() + c.close() + if result == []: + return None + else: + return result[0] + +def get_files_name(account, requester): + result = get_files(account, requester) + flist = [] + for r in result: + flist.append(r[0]) + return flist + +def add_file(account, requester, file_): + """ + >>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \ + 'date', False) + >>> add_file('account@gajim', 'requester@jabber', file_) + 1 + >>> _delete_file(1) + """ + _check_duplicate(account, requester, file_) + requester = gajim.get_jid_without_resource(requester) + c = conn.cursor() + c.execute("INSERT INTO files (file_path, " + + "relative_path, hash_sha1, size, description, mod_date, " + + " is_dir) VALUES (?,?,?,?,?,?,?)", + file_) + fid = c.lastrowid + permission_data = (fid, account, requester) + c.execute("INSERT INTO permissions VALUES (?,?,?)", permission_data) + conn.commit() + c.close() + return fid + +def _check_duplicate(account, requester, file_): + c = conn.cursor() + data = (account, requester, file_[1]) + c.execute("SELECT * FROM (files JOIN permissions ON" + + " files.fid=permissions.fid) WHERE account=? AND requester=?" + + " AND relative_path=? ", data) + result = c.fetchall() + if file_[2] != '': + data = (account, requester, file_[2]) + c.execute("SELECT * FROM (files JOIN permissions ON" + + " files.fid=permissions.fid) WHERE account=? AND requester=?" + + " AND hash_sha1=?)", data) + result.extend(c.fetchall()) + if len(result) > 0: + raise Exception('Duplicated entry') + c.close() + +def _delete_file(fid): + c = conn.cursor() + data = (fid, ) + c.execute("DELETE FROM files WHERE fid=?", data) + conn.commit() + c.close() + +def _delete_dir(dir_, account, requester): + c = conn.cursor() + data = (account, requester, dir_, dir_ + '/%') + sql = "DELETE FROM files WHERE fid IN " + \ + " (SELECT files.fid FROM files, permissions WHERE" + \ + " files.fid=permissions.fid AND account=?"+ \ + " AND requester=? AND (relative_path=? OR relative_path LIKE ?))" + c.execute(sql, data) + conn.commit() + c.close() + +def delete(account, requester, relative_path): + c = conn.cursor() + data = (account, requester, relative_path) + c.execute("SELECT files.fid, is_dir FROM (files JOIN permissions ON" + + " files.fid=permissions.fid) WHERE account=? AND requester=? AND " + + "relative_path=? ", data) + result = c.fetchone() + c.close() + if result[1] == 0: + _delete_file(result[0]) + else: + _delete_dir(relative_path, account, requester) + +def delete_all(account, requester): + c = conn.cursor() + data = (account, requester) + sql = "DELETE FROM files WHERE fid IN (SELECT fid FROM permissions" + \ + " WHERE account=? AND requester=?)" + c.execute(sql, data) + conn.commit() + c.close() + + +if not db_exist: + create_database() +if __name__ == "__main__": + """ + DELETE DATABASE FILE BEFORE RUNNING TESTS + """ + import doctest + path = sys.path[0] + path = path + '/' + 'shared_files.db' + conn = sqlite3.connect(path) + # Enable foreign keys contraints + conn.cursor().execute("pragma foreign_keys = on") + create_database() + doctest.testmod() diff --git a/file_sharing/fileshare_window.py b/file_sharing/fileshare_window.py new file mode 100644 index 0000000..a53491f --- /dev/null +++ b/file_sharing/fileshare_window.py @@ -0,0 +1,411 @@ +import database +import gtk +import gobject +from common import gajim +from common import helpers +from common.file_props import FilesProp +import fshare +import os +import fshare_protocol +import config + +class FileShareWindow(gtk.Window): + + def __init__(self): + gtk.Window.__init__(self) + self.set_title('File Share') + self.connect('delete_event', self.delete_event) + self.set_position(gtk.WIN_POS_CENTER) + self.set_default_size(400, 400) + # Children + self.notebook = gtk.Notebook() + # Browse page + self.bt_search = gtk.Button('Search') + self.bt_search.connect('clicked', self.on_bt_search_clicked) + self.entry_search = gtk.Entry(max=0) + self.entry_search.set_size_request(300, -1) + self.exp_advance = gtk.Expander('Advance') + self.ts_search = gtk.TreeStore(gobject.TYPE_STRING) + self.tv_search = gtk.TreeView(self.ts_search) + self.tv_search.connect('row-expanded', self.row_expanded) + self.tv_search.connect('button-press-event', + self.on_treeview_button_press_event) + self.browse_popup = gtk.Menu() + mi_download = gtk.MenuItem('Download') + mi_property = gtk.MenuItem('Property') + mi_download.show() + mi_property.show() + mi_download.connect('activate', self.on_download_clicked) + self.browse_popup.append(mi_download) + self.browse_popup.append(mi_property) + self.browse_sw = gtk.ScrolledWindow() + self.browse_sw.add(self.tv_search) + self.tvcolumn_browse = gtk.TreeViewColumn('') + self.tv_search.append_column(self.tvcolumn_browse) + self.cell_browse = gtk.CellRendererText() + self.tvcolumn_browse.pack_start(self.cell_browse, True) + self.tvcolumn_browse.add_attribute(self.cell_browse, 'text', 0) + self.lbl_browse = gtk.Label('Browse') + self.browse_hbox = gtk.HBox() + self.browse_hbox2 = gtk.HBox() + self.browse_vbox = gtk.VBox() + self.browse_hbox.pack_start(self.entry_search, True, True, 10) + self.browse_hbox.pack_start(self.bt_search, False, False, 10) + self.browse_vbox.pack_start(self.browse_hbox, False, False, 10) + self.browse_vbox.pack_start(self.exp_advance, False, False, 10) + self.browse_vbox.pack_start(self.browse_sw, True, True, 10) + self.notebook.append_page(self.browse_vbox, self.lbl_browse) + # file references for tv_search + self.browse_fref = {} + # contact references for tv_search + self.browse_jid = {} + # Information of the files inserted in the treeview: name, size, etc + self.brw_file_info = {} + # dummy row children so that we can get expanders + self.empty_row_child = {} + # Manage page + self.bt_add_file = gtk.Button('Add file') + self.bt_add_file.connect('clicked', self.add_file) + self.bt_add_dir = gtk.Button('Add directory') + self.bt_add_dir.connect('clicked', self.add_directory) + self.bt_remove = gtk.Button('Remove') + self.bt_remove.connect('clicked', self.remove_file_clicked) + self.bt_remove_all = gtk.Button('Remove all') + self.bt_remove_all.connect('clicked', self.remove_all_clicked) + self.ts_contacts = gtk.TreeStore(gobject.TYPE_STRING) + self.cbb_contacts = gtk.ComboBoxEntry(self.ts_contacts) + self.cbb_contacts.connect('changed', self.__check_combo_edit) + cbb_entry = self.cbb_contacts.child + self.cbb_completion = gtk.EntryCompletion() + cbb_entry.set_completion(self.cbb_completion) + self.cbb_completion.set_model(self.ts_contacts) + self.cbb_completion.set_text_column(0) + self.bt_stophash = gtk.Button('Calculating hash') + self.lbl_manage = gtk.Label('Manage Shared Files') + self.ts_files = gtk.TreeStore(gobject.TYPE_STRING) + self.tv_files = gtk.TreeView(self.ts_files) + self.treeSelection_files = self.tv_files.get_selection() + self.treeSelection_files.connect('changed', self.row_selected) + self.manage_sw = gtk.ScrolledWindow() + self.manage_sw.add(self.tv_files) + self.tvcolumn = gtk.TreeViewColumn('') + self.tv_files.append_column(self.tvcolumn) + self.cell = gtk.CellRendererText() + self.tvcolumn.pack_start(self.cell, True) + self.tvcolumn.add_attribute(self.cell, 'text', 0) + self.pb_filehash = gtk.ProgressBar() + self.manage_hbox = gtk.HBox() + self.manage_hbox2 = gtk.HBox() + self.manage_vbox = gtk.VBox() + self.manage_vbox2 = gtk.VBox() + self.manage_hbox.pack_start(self.bt_stophash , False, False, 10) + self.manage_hbox.pack_start(self.pb_filehash, True, True, 10) + self.manage_hbox.set_sensitive(False) + self.manage_vbox.pack_start(self.cbb_contacts, False, False, 10) + self.manage_vbox.pack_start(self.manage_hbox, False, False, 10) + self.manage_hbox2.pack_start(self.manage_sw, True, True, 10) + self.manage_hbox2.pack_start(self.manage_vbox2, False, False, 10) + self.manage_vbox2.pack_start(self.bt_add_file, False, False, 10) + self.manage_vbox2.pack_start(self.bt_add_dir, False, False, 10) + self.manage_vbox2.pack_start(self.bt_remove, False, False, 10) + self.manage_vbox2.pack_start(self.bt_remove_all, False, False, 10) + self.manage_vbox2.set_sensitive(False) + self.manage_vbox.pack_start(self.manage_hbox2, True, True, 10) + self.notebook.append_page(self.manage_vbox, self.lbl_manage) + # Preferences page + self.lbl_pref = gtk.Label('Preferences') + self.entry_dir_pref = gtk.Entry(max=0) + self.entry_dir_pref.set_text(config.INCOMING_DIR) + self.bt_sel_dir_pref = gtk.Button('Select dir', gtk.STOCK_OPEN) + self.bt_sel_dir_pref.connect('clicked', self.on_bt_sel_dir_pref_clicked) + self.frm_dir_pref = gtk.Frame('Incoming files directory') + self.frm_dir_pref.set_shadow_type(gtk.SHADOW_IN) + self.pref_hbox = gtk.HBox() + self.pref_vbox = gtk.VBox() + self.pref_hbox.pack_start(self.entry_dir_pref, True, True, 10) + self.pref_hbox.pack_start(self.bt_sel_dir_pref, False, False, 10) + self.frm_dir_pref.add(self.pref_hbox) + self.pref_vbox.pack_start(self.frm_dir_pref, False, False, 10) + self.notebook.append_page(self.pref_vbox , self.lbl_pref) + self.add(self.notebook) + self.show_all() + + def set_account(self, account): + self.account = account + # connect window to protocol + pro = fshare.FileSharePlugin.prohandler[self.account] + pro.set_window(self) + + def add_file(self, widget): + dialog = gtk.FileChooserDialog('Add file to be shared', self, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK) + ) + dialog.set_select_multiple(True) + response = dialog.run() + if response == gtk.RESPONSE_OK: + file_list = dialog.get_filenames() + self.add_items_tvcontacts(file_list) + dialog.destroy() + + def add_directory(self, widget): + dialog = gtk.FileChooserDialog('Add directory to be shared', self, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT) + ) + response = dialog.run() + if response == gtk.RESPONSE_ACCEPT: + file_list = [] + file_list.append(dialog.get_filename()) + self.add_items_tvcontacts(file_list) + dialog.destroy() + + def add_file_list(self, flist, treestore, fref = {}, root = None): + # file references to their in the treeStore + for f in flist: + # keeps track of the relative dir path + tail = '' + # keeps track of the parent row + parent = None + dirpath = f.split('/') + if len(dirpath) == 1: + # Top level file, it doesnt have parent, add it right away + fref[dirpath[0]] = treestore.insert(root, + 0, + (dirpath[0],) + ) + else: + for dir_ in dirpath: + if tail + dir_ not in fref: + fref[tail + dir_] = treestore.append( + parent, + (dir_,) + ) + parent = fref[tail + dir_] + tail = tail + dir_ + '/' + return fref + + def __convert_date(self, epoch): + # Converts date-time from seconds from epoch to iso 8601 + import time, datetime + ts = time.gmtime(epoch) + dt = datetime.datetime(ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, + ts.tm_min, ts.tm_sec) + return dt.isoformat() + + def add_items_tvcontacts(self, file_list, parentdir = None, parent = None): + # TODO: execute this method inside of a thread + for f in file_list: + # Relative name to be used internally in the shared folders + relative_name = f.split('/')[-1] + if parentdir: + relative_name = parentdir + '/' + relative_name + short_name = relative_name.split('/')[-1] + if short_name[0] == '.': + # Return if it is a hidden file + return + if parent: + row = self.ts_files.append(parent, (short_name,)) + else: + row = self.ts_files.insert(None, 0, (short_name,)) + # File info + size = os.path.getsize(f) + is_dir = os.path.isdir(f) + mod_date = os.path.getmtime(f) + mod_date = self.__convert_date(mod_date) + # TODO: add hash + file_ = (f, relative_name, '', size, '', mod_date, is_dir) + requester = self.cbb_contacts.get_active_text() + try: + fid = database.add_file(self.account, requester, file_) + except Exception, e: + if e == 'Duplicated entry': + print 'Error: ' + e + continue + else: + raise Exception(e) + if is_dir: + tmpfl = os.listdir(f) + fl = [] + for item in tmpfl: + fl.append(f + '/' + item) + self.add_items_tvcontacts(fl, relative_name, row) + + def add_contact_browse(self, contact): + fjid = contact.get_full_jid() + jid = gajim.get_jid_without_resource(fjid) + contacts = gajim.contacts.get_contacts(self.account, jid) + for con in contacts: + if con.show in ('offline', 'error') and not \ + con.supports(fshare_protocol.NS_FILE_SHARING): + break + cjid = con.get_full_jid() + r = self.ts_search.insert(None, 0, + (cjid, )) + self.browse_jid[cjid] = r + pro = fshare.FileSharePlugin.prohandler[self.account] + # Request list of files from peer + stanza = pro.request(cjid) + if pro.conn.connection: + pro.conn.connection.send(stanza) + + def add_contact_manage(self, contact): + self.contacts_rows = [] + self.cbb_contacts.grab_focus() + for c in gajim.contacts.iter_contacts(self.account): + jid = gajim.get_jid_without_resource(c.get_full_jid()) + r = self.ts_contacts.insert(None, len(self.ts_contacts), + (jid, )) + if c.get_full_jid() == contact.get_full_jid(): + self.cbb_contacts.set_active_iter(r) + self.contacts_rows.append(r) + self.manage_vbox2.set_sensitive(True) + self.bt_remove.set_sensitive(False) + self.add_file_list(database.get_files_name(self.account, + gajim.get_jid_without_resource(contact.get_full_jid())), + self.ts_files + ) + + def delete_event(self, widget, data=None): + fshare.FileSharePlugin.filesharewindow = {} + return False + + def __check_combo_edit(self, widget, data=None): + self.ts_files.clear() + entry = self.cbb_contacts.child + contact = entry.get_text() + self.manage_vbox2.set_sensitive(False) + for i in self.contacts_rows: + # If the contact in the comboboxentry is include inside of the + # combobox + if contact == self.ts_contacts.get_value(i, 0): + self.add_file_list(database.get_files_name(self.account, + contact), + self.ts_files + ) + self.manage_vbox2.set_sensitive(True) + self.bt_remove.set_sensitive(False) + break + + def remove_file_clicked(self, widget, data=None): + entry = self.cbb_contacts.child + contact = entry.get_text() + sel = self.treeSelection_files.get_selected() + relative_name = self.ts_files.get_value(sel[1], 0) + self.ts_files.remove(sel[1]) + database.delete(self.account, contact, relative_name) + widget.set_sensitive(False) + + def remove_all_clicked(self, widget, data=None): + entry = self.cbb_contacts.child + contact = entry.get_text() + database.delete_all(self.account, contact) + self.ts_files.clear() + + def row_selected(self, widget, data=None): + # When row is selected in tv_files + sel = self.treeSelection_files.get_selected() + if not sel[1]: + return + depth = self.ts_files.iter_depth(sel[1]) + # Don't remove file and dirs that aren't at the root level + if depth == 0: + self.bt_remove.set_sensitive(True) + else: + self.bt_remove.set_sensitive(False) + + def row_expanded(self, widget, iter_, path, data=None): + name = None + for key in self.empty_row_child: + parent = self.ts_search.iter_parent(self.empty_row_child[key]) + p = self.ts_search.get_path(parent) + if p == path: + name = key + break + if name: + # if we found that the expanded row is the parent of the empty row + # remove it from the treestore and empty_row dictionary. Then ask + # peer for list of files of that directory + i = self.empty_row_child[name] + pro = fshare.FileSharePlugin.prohandler[self.account] + contact = self.get_contact_from_iter(self.ts_search, i) + contact = gajim.contacts.get_contact_with_highest_priority( + self.account, + contact ) + stanza = pro.request(contact.get_full_jid(), name, isFile=False) + if pro.conn.connection: + pro.conn.connection.send(stanza) + self.ts_search.remove(i) + del self.empty_row_child[name] + + def get_contact_from_iter(self, treestore, iter_): + toplevel = treestore.get_iter_root() + while toplevel: + if treestore.is_ancestor(toplevel, iter_): + return treestore.get_value(toplevel, 0) + toplevel = treestore.iter_next(toplevel) + + def on_treeview_button_press_event(self, treeview, event): + if event.button == 3: + x = int(event.x) + y = int(event.y) + time = event.time + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, cellx, celly = pthinfo + treeview.grab_focus() + treeview.set_cursor( path, col, 0) + treestore = treeview.get_model() + it = treestore.get_iter(path) + if treestore.iter_depth(it) != 0: + self.browse_popup.popup(None, None, None, event.button, time) + return True + + def on_bt_search_clicked(self, widget, data=None): + pass + + def on_download_clicked(self, widget, data=None): + tree, row = self.tv_search.get_selection().get_selected() + path = tree.get_path(row) + file_info = self.brw_file_info[path] + fjid = self.get_contact_from_iter(tree, row) + # Request the file + file_path = config.INCOMING_DIR + '/%s' % file_info[0] + sid = helpers.get_random_string_16() + new_file_props = FilesProp.getNewFileProp(self.account, sid) + new_file_props.file_name = file_path + print file_path + new_file_props.name = file_info[0] + new_file_props.desc = file_info[4] + new_file_props.size = file_info[2] + new_file_props.date = file_info[1] + new_file_props.hash_ = None if file_info[3] == '' else file_info[3] + new_file_props.type_ = 'r' + tsid = gajim.connections[self.account].start_file_transfer(fjid, + new_file_props, + True) + new_file_props.transport_sid = tsid + ft_window = gajim.interface.instances['file_transfers'] + contact = gajim.contacts.get_contact_from_full_jid(self.account, fjid) + ft_window .add_transfer(self.account, contact, new_file_props) + + def on_bt_sel_dir_pref_clicked(self, widget, data=None): + chooser = gtk.FileChooserDialog(title='Incoming files directory', + action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN,gtk.RESPONSE_OK)) + response = chooser.run() + if response == gtk.RESPONSE_OK: + file_name = chooser.get_filename() + self.entry_dir_pref.set_text(file_name) + config.set('incoming_dir', file_name) + chooser.destroy() + + +if __name__ == "__main__": + f = FileShareWindow() + f.show() + gtk.main() + diff --git a/file_sharing/fshare.py b/file_sharing/fshare.py new file mode 100644 index 0000000..fd7d748 --- /dev/null +++ b/file_sharing/fshare.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +## +import database +import gtk +import base64 +import urllib2 +from plugins import GajimPlugin +from plugins.helpers import log_calls +import gui_menu_builder +import gtkgui_helpers +from common import gajim +from fileshare_window import FileShareWindow +import fshare_protocol +from common import ged +from common import caps_cache +from common import xmpp + + +class FileSharePlugin(GajimPlugin): + + filesharewindow = {} + prohandler = {} + + @log_calls('FileSharePlugin') + def init(self): + self.activated = False + self.description = _('This plugin allows you to share folders'+ + ' with a peer using jingle file transfer.') + self.config_dialog = None + # Create one protocol handler per account + accounts = gajim.contacts.get_accounts() + for account in gajim.contacts.get_accounts(): + FileSharePlugin.prohandler[account] = \ + fshare_protocol.protocol(account) + self.events_handlers = { + 'raw-iq-received': (ged.CORE, self._nec_raw_iq) + } + + def activate(self): + self.activated = True + # Add fs feature + if fshare_protocol.NS_FILE_SHARING not in gajim.gajim_common_features: + gajim.gajim_common_features.append(fshare_protocol.NS_FILE_SHARING) + self._compute_caps_hash() + # Replace the contact menu + self.__get_contact_menu = gui_menu_builder.get_contact_menu + gui_menu_builder.get_contact_menu = self.contact_menu + # Replace get_file_info + for account in gajim.contacts.get_accounts(): + conn = gajim.connections[account] + self._get_file_info = conn.get_file_info + conn.get_file_info = self.get_file_info + + def deactivate(self): + self.activated = False + # Remove fs feature + if fshare_protocol.NS_FILE_SHARING not in gajim.gajim_common_features: + gajim.gajim_common_features.remove(fshare_protocol.NS_FILE_SHARING) + self._compute_caps_hash() + # Restore the contact menu + gui_menu_builder.get_contact_menu = self.__get_contact_menu + # Restore get_file_info + for account in gajim.contacts.get_accounts(): + conn = gajim.connections[account] + conn.get_file_info = self._get_file_info + + def _compute_caps_hash(self): + for a in gajim.connections: + gajim.caps_hash[a] = caps_cache.compute_caps_hash([ + gajim.gajim_identity], gajim.gajim_common_features + \ + gajim.gajim_optional_features[a]) + # re-send presence with new hash + connected = gajim.connections[a].connected + if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible': + gajim.connections[a].change_status(gajim.SHOW_LIST[connected], + gajim.connections[a].status) + + def _nec_raw_iq(self, obj): + if obj.stanza.getTag('match', + namespace=fshare_protocol.NS_FILE_SHARING) and self.activated: + account = obj.conn.name + pro = FileSharePlugin.prohandler[account] + pro.handler(obj.stanza) + raise xmpp.NodeProcessed + + def __get_contact_menu(self, contact, account): + raise NotImplementedError + + def contact_menu(self, contact, account): + menu = self.__get_contact_menu(contact, account) + fs = gtk.MenuItem('File sharing') + submenu = gtk.Menu() + fs.set_submenu(submenu) + bf = gtk.MenuItem('Browse files') + bf.connect('activate', self.browse_menu_clicked, account, contact) + msf = gtk.MenuItem('Manage shared files') + msf.connect('activate', self.manage_menu_clicked, account, contact) + enable_fs = gtk.CheckMenuItem('Enable file sharing') + enable_fs.set_active(True) + submenu.attach(bf, 0, 1, 0, 1) + submenu.attach(msf, 0, 1, 1, 2) + submenu.attach(enable_fs, 0, 1, 2, 3) + if gajim.account_is_disconnected(account) or \ + contact.show in ('offline', 'error') or not \ + contact.supports(fshare_protocol.NS_FILE_SHARING): + bf.set_sensitive(False) + submenu.show() + bf.show() + msf.show() + enable_fs.show() + fs.show() + menu.attach(fs, 0, 1, 3, 4) + return menu + + def _get_file_info(self, peerjid, hash_=None, name=None, account=None): + raise NotImplementedError + + def get_file_info(self, peerjid, hash_=None, name=None, account=None): + file_info = self._get_file_info(hash_, name) + if file_info: + return file_info + raw_info = database.get_file(account, peerjid, hash_, name) + file_info = {'name': raw_info[0], + 'file-name' : raw_info[5], + 'hash' : raw_info[1], + 'size' : raw_info[2], + 'date' : raw_info[4], + 'peerjid' : peerjid + } + + return file_info + + def __get_fsw_instance(self, account): + # Makes sure we only have one instance of the window per account + if account not in FileSharePlugin.filesharewindow: + FileSharePlugin.filesharewindow[account] = fsw = FileShareWindow() + FileSharePlugin.prohandler[account].set_window(fsw) + return FileSharePlugin.filesharewindow[account] + + def __init_window(self, account, contact): + fsw = self.__get_fsw_instance(account) + fsw.set_account(account) + fsw.contacts_rows = [] + fsw.ts_contacts.clear() + fsw.ts_search.clear() + # Add information to widgets + fsw.add_contact_manage(contact) + fsw.add_contact_browse(contact) + return fsw + + def manage_menu_clicked(self, widget, account, contact): + fsw = self.__init_window(account, contact) + fsw.notebook.set_current_page(1) + + def browse_menu_clicked(self, widget, account, contact): + fsw = self.__init_window(account, contact) + fsw.notebook.set_current_page(0) + + diff --git a/file_sharing/fshare_protocol.py b/file_sharing/fshare_protocol.py new file mode 100644 index 0000000..b120c00 --- /dev/null +++ b/file_sharing/fshare_protocol.py @@ -0,0 +1,163 @@ +from common import xmpp +from common import helpers +from common import gajim +from common import XMPPDispatcher +from common.xmpp import Hashes +import database +# Namespace for file sharing +NS_FILE_SHARING = 'http://gajim.org/protocol/filesharing' + +class protocol(): + + def __init__(self, account): + self.account = account + self.conn = gajim.connections[self.account] + # get our jid with resource + self.ourjid = gajim.get_jid_from_account(self.account) + self.fsw = None + + def set_window(self, window): + self.fsw = window + + def request(self, contact, name=None, isFile=False): + iq = xmpp.Iq(typ='get', to=contact, frm=self.ourjid) + match = iq.addChild(name='match', namespace=NS_FILE_SHARING) + request = match.addChild(name='request') + if not isFile and name is None: + request.addChild(name='directory') + elif not isFile and name is not None: + dir_ = request.addChild(name='directory') + dir_.addChild(name='name').addData('/' + name) + elif isFile: + pass + return iq + + def __buildReply(self, typ, stanza): + iq = xmpp.Iq(typ, to=stanza.getFrom(), frm=stanza.getTo(), + attrs={'id': stanza.getID()}) + iq.addChild(name='match', namespace=NS_FILE_SHARING) + return iq + + def on_request(self, stanza): + try: + fjid = helpers.get_full_jid_from_iq(stanza) + except helpers.InvalidFormat: + # A message from a non-valid JID arrived, it has been ignored. + return + if stanza.getTag('error'): + # TODO: better handle this + return + jid = gajim.get_jid_without_resource(fjid) + req = stanza.getTag('match').getTag('request') + if req.getTag('directory') and not \ + req.getTag('directory').getChildren(): + # We just received a toplevel directory request + files = database.get_toplevel_files(self.account, jid) + response = self.offer(stanza.getID(), fjid, files) + self.conn.connection.send(response) + elif req.getTag('directory') and req.getTag('directory').getTag('name'): + dir_ = req.getTag('directory').getTag('name').getData()[1:] + files = database.get_files_from_dir(self.account, jid, dir_) + response = self.offer(stanza.getID(), fjid, files) + self.conn.connection.send(response) + + def on_offer(self, stanza): + # We just got a stanza offering files + fjid = helpers.get_full_jid_from_iq(stanza) + info = get_files_info(stanza) + if fjid not in self.fsw.browse_jid or not info: + # We weren't expecting anything from this contact, do nothing + # Or we didn't receive any offering files + return + flist = [] + for f in info[0]: + flist.append(f['name']) + flist.extend(info[1]) + self.fsw.browse_fref = self.fsw.add_file_list(flist, self.fsw.ts_search, + self.fsw.browse_fref, + self.fsw.browse_jid[fjid] + ) + for f in info[0]: + iter_ = self.fsw.browse_fref[f['name']] + path = self.fsw.ts_search.get_path(iter_) + self.fsw.brw_file_info[path] = (f['name'], f['date'], f['size'], + f['hash'], f['desc']) + + # TODO: add tooltip + ''' + for f in info[0]: + r = self.fsw.browse_fref[f['name']] + path = self.fsw.ts_search.get_path(r) + # AM HERE WORKING ON THE TOOLTIP + tooltip.set_text('noooo') + self.fsw.tv_search.set_tooltip_row(tooltip, path) + ''' + for dir_ in info[1]: + if dir_ not in self.fsw.empty_row_child: + parent = self.fsw.browse_fref[dir_] + row = self.fsw.ts_search.append(parent, ('',)) + self.fsw.empty_row_child[dir_] = row + + def handler(self, stanza): + # handles incoming match stanza + if stanza.getTag('match').getTag('offer'): + self.on_offer(stanza) + elif stanza.getTag('match').getTag('request'): + self.on_request(stanza) + else: + # TODO: reply with malformed stanza error + pass + + def offer(self, id_, contact, items): + iq = xmpp.Iq(typ='result', to=contact, frm=self.ourjid, + attrs={'id': id_}) + match = iq.addChild(name='match', namespace=NS_FILE_SHARING) + offer = match.addChild(name='offer') + if len(items) == 0: + offer.addChild(name='directory') + else: + for i in items: + # if it is a directory + if i[5] == True: + item = offer.addChild(name='directory') + name = item.addChild('name') + name.setData('/' + i[0]) + else: + item = offer.addChild(name='file') + item.addChild('name').setData('/' + i[0]) + if i[1] != '': + h = Hashes() + h.addHash(i[1], 'sha-1') + item.addChild(node=h) + item.addChild('size').setData(i[2]) + item.addChild('desc').setData(i[3]) + item.addChild('date').setData(i[4]) + return iq + + def set_window(self, fsw): + self.fsw = fsw + + +def get_files_info(stanza): + # Crawls the stanza in search for file and dir structure. + files = [] + dirs = [] + children = stanza.getTag('match').getTag('offer').getChildren() + for c in children: + if c.getName() == 'file': + f = {'name' : \ + c.getTag('name').getData()[1:] if c.getTag('name') else '', + 'size' : c.getTag('size').getData() if c.getTag('size') else '', + 'date' : c.getTag('date').getData() if c.getTag('date') else '', + 'desc' : c.getTag('desc').getData() if c.getTag('desc') else '', + # TODO: handle different hash algo + 'hash' : c.getTag('hash').getData() if c.getTag('hash') else '', + } + files.append(f) + else: + dirname = c.getTag('name') + if dirname is None: + return None + dirs.append(dirname.getData()[1:]) + return (files, dirs) + diff --git a/file_sharing/manifest.ini b/file_sharing/manifest.ini new file mode 100644 index 0000000..677db09 --- /dev/null +++ b/file_sharing/manifest.ini @@ -0,0 +1,8 @@ +[info] +name: File Sharing +short_name: fshare +#version: 0.1 +description: This plugin allows you to share folders with your peers using jingle file transfer. +authors: Jefry Lagrange +homepage: www.google.com +