root/tags/gajim-0.11.4/src/filetransfers_window.py

Revision 9092, 32.8 kB (checked in by asterix, 12 months ago)

os.access() on a folder under windows doesn't mean anything. fixes #3587

  • Property svn:eol-style set to LF
Line 
1##      filetransfers_window.py
2##
3## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
4## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
5## Copyright (C) 2005
6##                    Dimitur Kirov <dkirov@gmail.com>
7##                    Travis Shirk <travis@pobox.com>
8## Copyright (C) 2004-2005 Vincent Hanquez <tab@snarc.org>
9##
10## This program is free software; you can redistribute it and/or modify
11## it under the terms of the GNU General Public License as published
12## by the Free Software Foundation; version 2 only.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17## GNU General Public License for more details.
18##
19
20import gtk
21import gobject
22import pango
23import os
24import time
25
26import gtkgui_helpers
27import tooltips
28import dialogs
29
30from common import gajim
31from common import helpers
32
33C_IMAGE = 0
34C_LABELS = 1
35C_FILE = 2
36C_TIME = 3
37C_PROGRESS = 4
38C_PERCENT = 5
39C_SID = 6
40
41
42class FileTransfersWindow:
43        def __init__(self):
44                self.files_props = {'r' : {}, 's': {}}
45                self.height_diff = 0
46                self.xml = gtkgui_helpers.get_glade('filetransfers.glade')
47                self.window = self.xml.get_widget('file_transfers_window')
48                self.tree = self.xml.get_widget('transfers_list')
49                self.cancel_button = self.xml.get_widget('cancel_button')
50                self.pause_button = self.xml.get_widget('pause_restore_button')
51                self.cleanup_button = self.xml.get_widget('cleanup_button')
52                self.notify_ft_checkbox = self.xml.get_widget(
53                        'notify_ft_complete_checkbox')
54                notify = gajim.config.get('notify_on_file_complete')
55                if notify:
56                        self.notify_ft_checkbox.set_active(True)
57                else:
58                        self.notify_ft_checkbox.set_active(False)
59                self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str, str, int, str)
60                self.tree.set_model(self.model)
61                col = gtk.TreeViewColumn()
62
63                render_pixbuf = gtk.CellRendererPixbuf()
64
65                col.pack_start(render_pixbuf, expand = True)
66                render_pixbuf.set_property('xpad', 3)
67                render_pixbuf.set_property('ypad', 3)
68                render_pixbuf.set_property('yalign', .0)
69                col.add_attribute(render_pixbuf, 'pixbuf', 0)
70                self.tree.append_column(col)
71
72                col = gtk.TreeViewColumn(_('File'))
73                renderer = gtk.CellRendererText()
74                col.pack_start(renderer, expand=False)
75                col.add_attribute(renderer, 'markup' , C_LABELS)
76                renderer.set_property('yalign', 0.)
77                renderer = gtk.CellRendererText()
78                col.pack_start(renderer, expand=True)
79                col.add_attribute(renderer, 'markup' , C_FILE)
80                renderer.set_property('xalign', 0.)
81                renderer.set_property('yalign', 0.)
82                renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
83                col.set_resizable(True)
84                col.set_expand(True)
85                self.tree.append_column(col)
86
87                col = gtk.TreeViewColumn(_('Time'))
88                renderer = gtk.CellRendererText()
89                col.pack_start(renderer, expand=False)
90                col.add_attribute(renderer, 'markup' , C_TIME)
91                renderer.set_property('yalign', 0.5)
92                renderer.set_property('xalign', 0.5)
93                renderer = gtk.CellRendererText()
94                renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
95                col.set_resizable(True)
96                col.set_expand(False)
97                self.tree.append_column(col)
98
99                col = gtk.TreeViewColumn(_('Progress'))
100                renderer = gtk.CellRendererProgress()
101                renderer.set_property('yalign', 0.5)
102                renderer.set_property('xalign', 0.5)
103                col.pack_start(renderer, expand = False)
104                col.add_attribute(renderer, 'text' , C_PROGRESS)
105                col.add_attribute(renderer, 'value' , C_PERCENT)
106                col.set_resizable(True)
107                col.set_expand(False)
108                self.tree.append_column(col)
109
110                self.set_images()
111                self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
112                self.tree.get_selection().connect('changed', self.selection_changed)
113                self.tooltip = tooltips.FileTransfersTooltip()
114                self.file_transfers_menu = self.xml.get_widget('file_transfers_menu')
115                self.open_folder_menuitem = self.xml.get_widget('open_folder_menuitem')
116                self.cancel_menuitem = self.xml.get_widget('cancel_menuitem')
117                self.pause_menuitem = self.xml.get_widget('pause_menuitem')
118                self.continue_menuitem = self.xml.get_widget('continue_menuitem')
119                self.continue_menuitem.hide()
120                self.continue_menuitem.set_no_show_all(True)
121                self.remove_menuitem = self.xml.get_widget('remove_menuitem')
122                self.xml.signal_autoconnect(self)
123
124        def find_transfer_by_jid(self, account, jid):
125                ''' find all transfers with peer 'jid' that belong to 'account' '''
126                active_transfers = [[],[]] # ['senders', 'receivers']
127
128                # 'account' is the sender
129                for file_props in self.files_props['s'].values():
130                        if file_props['tt_account'] == account:
131                                receiver_jid = unicode(file_props['receiver']).split('/')[0]
132                                if jid == receiver_jid:
133                                        if not self.is_transfer_stoped(file_props):
134                                                active_transfers[0].append(file_props)
135
136                # 'account' is the recipient
137                for file_props in self.files_props['r'].values():
138                        if file_props['tt_account'] == account:
139                                sender_jid = unicode(file_props['sender']).split('/')[0]
140                                if jid == sender_jid:
141                                        if not self.is_transfer_stoped(file_props):
142                                                active_transfers[1].append(file_props)
143                return active_transfers
144
145        def show_completed(self, jid, file_props):
146                ''' show a dialog saying that file (file_props) has been transferred'''
147                def on_open(widget, file_props):
148                        dialog.destroy()
149                        if not file_props.has_key('file-name'):
150                                return
151                        (path, file) = os.path.split(file_props['file-name'])
152                        if os.path.exists(path) and os.path.isdir(path):
153                                helpers.launch_file_manager(path)
154                        self.tree.get_selection().unselect_all()
155
156                if file_props['type'] == 'r':
157                        # file path is used below in 'Save in'
158                        (file_path, file_name) = os.path.split(file_props['file-name'])
159                else:
160                        file_name = file_props['name']
161                sectext = '\t' + _('Filename: %s') % file_name
162                sectext += '\n\t' + _('Size: %s') % \
163                helpers.convert_bytes(file_props['size'])
164                if file_props['type'] == 'r':
165                        jid = unicode(file_props['sender']).split('/')[0]
166                        sender_name = gajim.contacts.get_first_contact_from_jid( 
167                                file_props['tt_account'], jid).get_shown_name()
168                        sender = sender_name
169                else:
170                        #You is a reply of who sent a file
171                        sender = _('You')
172                sectext += '\n\t' +_('Sender: %s') % sender
173                sectext += '\n\t' +_('Recipient: ')
174                if file_props['type'] == 's':
175                        jid = unicode(file_props['receiver']).split('/')[0]
176                        receiver_name = gajim.contacts.get_first_contact_from_jid( 
177                                file_props['tt_account'], jid).get_shown_name()
178                        recipient = receiver_name
179                else:
180                        #You is a reply of who received a file
181                        recipient = _('You')
182                sectext += recipient
183                if file_props['type'] == 'r':
184                        sectext += '\n\t' +_('Saved in: %s') % file_path
185                dialog = dialogs.HigDialog(None, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE, 
186                                _('File transfer completed'), sectext)
187                if file_props['type'] == 'r':
188                        button = gtk.Button(_('_Open Containing Folder'))
189                        button.connect('clicked', on_open, file_props)
190                        dialog.action_area.pack_start(button)
191                ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
192                def on_ok(widget):
193                        dialog.destroy()
194                ok_button.connect('clicked', on_ok)
195                dialog.show_all()
196
197        def show_request_error(self, file_props):
198                ''' show error dialog to the recipient saying that transfer
199                has been canceled'''
200                dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.'))
201                self.tree.get_selection().unselect_all()
202
203        def show_send_error(self, file_props):
204                ''' show error dialog to the sender saying that transfer
205                has been canceled'''
206                dialogs.InformationDialog(_('File transfer cancelled'),
207_('Connection with peer cannot be established.'))
208                self.tree.get_selection().unselect_all()
209
210        def show_stopped(self, jid, file_props, error_msg = ''):
211                if file_props['type'] == 'r':
212                        file_name = os.path.basename(file_props['file-name'])
213                else:
214                        file_name = file_props['name']
215                sectext = '\t' + _('Filename: %s') % file_name
216                sectext += '\n\t' + _('Recipient: %s') % jid
217                if error_msg:
218                        sectext += '\n\t' + _('Error message: %s') % error_msg
219                dialogs.ErrorDialog(_('File transfer stopped by the contact at the other '
220                        'end'), sectext)
221                self.tree.get_selection().unselect_all()
222
223        def show_file_send_request(self, account, contact):
224                def on_ok(widget):
225                        file_dir = None
226                        files_path_list = dialog.get_filenames()
227                        files_path_list = gtkgui_helpers.decode_filechooser_file_paths(
228                                files_path_list)
229                        for file_path in files_path_list:
230                                if self.send_file(account, contact, file_path) and file_dir is None:
231                                        file_dir = os.path.dirname(file_path)
232                        if file_dir:
233                                gajim.config.set('last_send_dir', file_dir)
234                                dialog.destroy()
235
236                dialog = dialogs.FileChooserDialog(_('Choose File to Send...'), 
237                        gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
238                        gtk.RESPONSE_OK,
239                        True, # select multiple true as we can select many files to send
240                        gajim.config.get('last_send_dir'),
241                        on_response_ok = on_ok,
242                        on_response_cancel = lambda e:dialog.destroy()
243                        )
244
245                btn = gtk.Button(_('_Send'))
246                btn.set_property('can-default', True)
247                # FIXME: add send icon to this button (JUMP_TO)
248                dialog.add_action_widget(btn, gtk.RESPONSE_OK)
249                dialog.set_default_response(gtk.RESPONSE_OK)
250                btn.show()
251
252        def send_file(self, account, contact, file_path):
253                ''' start the real transfer(upload) of the file '''
254                if gtkgui_helpers.file_is_locked(file_path):
255                        pritext = _('Gajim cannot access this file')
256                        sextext = _('This file is being used by another process.')
257                        dialogs.ErrorDialog(pritext, sextext)
258                        return
259
260                if isinstance(contact, str):
261                        if contact.find('/') == -1:
262                                return
263                        (jid, resource) = contact.split('/', 1)
264                        contact = gajim.contacts.create_contact(jid = jid,
265                                resource = resource)
266                (file_dir, file_name) = os.path.split(file_path)
267                file_props = self.get_send_file_props(account, contact, 
268                                file_path, file_name)
269                if file_props is None:
270                        return False
271                self.add_transfer(account, contact, file_props)
272                gajim.connections[account].send_file_request(file_props)
273                return True
274
275        def _start_receive(self, file_path, account, contact, file_props):
276                file_dir = os.path.dirname(file_path)
277                if file_dir:
278                        gajim.config.set('last_save_dir', file_dir)
279                file_props['file-name'] = file_path
280                self.add_transfer(account, contact, file_props)
281                gajim.connections[account].send_file_approval(file_props)
282
283        def show_file_request(self, account, contact, file_props):
284                ''' show dialog asking for comfirmation and store location of new
285                file requested by a contact'''
286                if file_props is None or not file_props.has_key('name'):
287                        return
288                sec_text = '\t' + _('File: %s') % file_props['name']
289                if file_props.has_key('size'):
290                        sec_text += '\n\t' + _('Size: %s') % \
291                                helpers.convert_bytes(file_props['size'])
292                if file_props.has_key('mime-type'):
293                        sec_text += '\n\t' + _('Type: %s') % file_props['mime-type']
294                if file_props.has_key('desc'):
295                        sec_text += '\n\t' + _('Description: %s') % file_props['desc']
296                prim_text = _('%s wants to send you a file:') % contact.jid
297                dialog, dialog2 = None, None
298
299                def on_response_ok(widget, account, contact, file_props):
300                        dialog.destroy()
301
302                        def on_ok(widget, account, contact, file_props):
303                                file_path = dialog2.get_filename()
304                                file_path = gtkgui_helpers.decode_filechooser_file_paths(
305                                        (file_path,))[0]
306                                if os.path.exists(file_path):
307                                        # check if we have write permissions
308                                        if not os.access(file_path, os.W_OK):
309                                                file_name = os.path.basename(file_path)
310                                                dialogs.ErrorDialog(_('Cannot overwrite existing file "%s"' % file_name),
311                                                _('A file with this name already exists and you do not have permission to overwrite it.'))
312                                                return
313                                        stat = os.stat(file_path)
314                                        dl_size = stat.st_size
315                                        file_size = file_props['size']
316                                        dl_finished = dl_size >= file_size
317                                        dialog = dialogs.FTOverwriteConfirmationDialog(
318                                                _('This file already exists'), _('What do you want to do?'),
319                                                not dl_finished)
320                                        dialog.set_transient_for(dialog2)
321                                        dialog.set_destroy_with_parent(True)
322                                        response = dialog.get_response()
323                                        if response < 0:
324                                                return
325                                        elif response == 100:
326                                                file_props['offset'] = dl_size
327                                else:
328                                        dirname = os.path.dirname(file_path)
329                                        if not os.access(dirname, os.W_OK) and os.name != 'nt':
330                                                # read-only bit is used to mark special folder under windows,
331                                                # not to mark that a folder is read-only. See ticket #3587
332                                                dialogs.ErrorDialog(_('Directory "%s" is not writable') % dirname, _('You do not have permission to create files in this directory.'))
333                                                return
334                                dialog2.destroy()
335                                self._start_receive(file_path, account, contact, file_props)
336
337                        def on_cancel(widget, account, contact, file_props):
338                                dialog2.destroy()
339                                gajim.connections[account].send_file_rejection(file_props)
340
341                        dialog2 = dialogs.FileChooserDialog(
342                                title_text = _('Save File as...'), 
343                                action = gtk.FILE_CHOOSER_ACTION_SAVE, 
344                                buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 
345                                gtk.STOCK_SAVE, gtk.RESPONSE_OK),
346                                default_response = gtk.RESPONSE_OK,
347                                current_folder = gajim.config.get('last_save_dir'),
348                                on_response_ok = (on_ok, account, contact, file_props),
349                                on_response_cancel = (on_cancel, account, contact, file_props))
350
351                        dialog2.set_current_name(file_props['name'])
352                        dialog2.connect('delete-event', lambda widget, event:
353                                on_cancel(widget, account, contact, file_props))
354
355                def on_response_cancel(widget, account, file_props):
356                        dialog.destroy()
357                        gajim.connections[account].send_file_rejection(file_props)
358
359                dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
360                        on_response_ok = (on_response_ok, account, contact, file_props),
361                        on_response_cancel = (on_response_cancel, account, file_props))
362                dialog.connect('delete-event', lambda widget, event: 
363                        on_response_cancel(widget, account, file_props))
364                dialog.popup()
365
366        def set_images(self):
367                ''' create pixbufs for status images in transfer rows'''
368                self.images = {}
369                self.images['upload'] = self.window.render_icon(gtk.STOCK_GO_UP, 
370                        gtk.ICON_SIZE_MENU)
371                self.images['download'] = self.window.render_icon(gtk.STOCK_GO_DOWN, 
372                        gtk.ICON_SIZE_MENU)
373                self.images['stop'] = self.window.render_icon(gtk.STOCK_STOP, 
374                        gtk.ICON_SIZE_MENU)
375                self.images['waiting'] = self.window.render_icon(gtk.STOCK_REFRESH, 
376                        gtk.ICON_SIZE_MENU)
377                self.images['pause'] = self.window.render_icon(gtk.STOCK_MEDIA_PAUSE, 
378                        gtk.ICON_SIZE_MENU)
379                self.images['continue'] = self.window.render_icon(gtk.STOCK_MEDIA_PLAY, 
380                        gtk.ICON_SIZE_MENU)
381                self.images['ok'] = self.window.render_icon(gtk.STOCK_APPLY, 
382                        gtk.ICON_SIZE_MENU)
383
384        def set_status(self, typ, sid, status):
385                ''' change the status of a transfer to state 'status' '''
386                iter = self.get_iter_by_sid(typ, sid)
387                if iter is None:
388                        return
389                sid = self.model[iter][C_SID].decode('utf-8')
390                file_props = self.files_props[sid[0]][sid[1:]]
391                if status == 'stop':
392                        file_props['stopped'] = True
393                elif status == 'ok':
394                        file_props['completed'] = True
395                self.model.set(iter, C_IMAGE, self.images[status])
396                path = self.model.get_path(iter)
397                self.select_func(path)
398
399        def _format_percent(self, percent):
400                ''' add extra spaces from both sides of the percent, so that
401                progress string has always a fixed size'''
402                _str = '          '
403                if percent != 100.:
404                        _str += ' '
405                if percent < 10:
406                        _str += ' '
407                _str += unicode(percent) + '%          \n'
408                return _str
409
410        def _format_time(self, _time):
411                times = { 'hours': 0, 'minutes': 0, 'seconds': 0 }
412                _time = int(_time)
413                times['seconds'] = _time % 60
414                if _time >= 60:
415                        _time /= 60
416                        times['minutes'] = _time % 60
417                        if _time >= 60:
418                                times['hours'] = _time / 60
419
420                #Print remaining time in format 00:00:00
421                #You can change the places of (hours), (minutes), (seconds) -
422                #they are not translatable.
423                return _('%(hours)02.d:%(minutes)02.d:%(seconds)02.d')  % times
424
425        def _get_eta_and_speed(self, full_size, transfered_size, elapsed_time):
426                if elapsed_time == 0:
427                        return 0., 0.
428                speed = round(float(transfered_size) / elapsed_time)
429                if speed == 0.:
430                        return 0., 0.
431                remaining_size = full_size - transfered_size
432                eta = remaining_size / speed
433                return eta, speed
434
435        def _remove_transfer(self, iter, sid, file_props):
436                self.model.remove(iter)
437                if  file_props.has_key('tt_account'):
438                        # file transfer is set
439                        account = file_props['tt_account']
440                        if gajim.connections.has_key(account):
441                                # there is a connection to the account
442                                gajim.connections[account].remove_transfer(file_props)
443                        if file_props['type'] == 'r': # we receive a file
444                                other = file_props['sender']
445                        else: # we send a file
446                                other = file_props['receiver']
447                        if isinstance(other, unicode):
448                                jid = gajim.get_jid_without_resource(other)
449                        else: # It's a Contact instance
450                                jid = other.jid
451                        for ev_type in ('file-error', 'file-completed', 'file-request-error',
452                        'file-send-error', 'file-stopped'):
453                                for event in gajim.events.get_events(account, jid, [ev_type]):
454                                        if event.parameters['sid'] == file_props['sid']:
455                                                gajim.events.remove_events(account, jid, event)
456                                                gajim.interface.roster.draw_contact(jid, account)
457                                                gajim.interface.roster.show_title()
458                del(self.files_props[sid[0]][sid[1:]])
459                del(file_props)
460
461        def set_progress(self, typ, sid, transfered_size, iter = None):
462                ''' change the progress of a transfer with new transfered size'''
463                if not self.files_props[typ].has_key(sid):
464                        return
465                file_props = self.files_props[typ][sid]
466                full_size = int(file_props['size'])
467                if full_size == 0:
468                        percent = 0
469                else:
470                        percent = round(float(transfered_size) / full_size * 100)
471                if iter is None:
472                        iter = self.get_iter_by_sid(typ, sid)
473                if iter is not None:
474                        just_began = False
475                        if self.model[iter][C_PERCENT] == 0 and int(percent > 0):
476                                just_began = True
477                        text = self._format_percent(percent)
478                        if transfered_size == 0:
479                                text += '0'
480                        else:
481                                text += helpers.convert_bytes(transfered_size)
482                        text += '/' + helpers.convert_bytes(full_size)
483                        # Kb/s
484
485                        # remaining time
486                        if file_props.has_key('offset') and file_props['offset']:
487                                transfered_size -= file_props['offset'] 
488                                full_size -= file_props['offset']
489                        eta, speed = self._get_eta_and_speed(full_size, transfered_size, 
490                                file_props['elapsed-time'])
491
492                        self.model.set(iter, C_PROGRESS, text)
493                        self.model.set(iter, C_PERCENT, int(percent))
494                        text = self._format_time(eta)
495                        text += '\n'
496                        #This should make the string Kb/s,
497                        #where 'Kb' part is taken from %s.
498                        #Only the 's' after / (which means second) should be translated.
499                        text += _('(%(filesize_unit)s/s)') % {'filesize_unit':
500                                helpers.convert_bytes(speed)}
501                        self.model.set(iter, C_TIME, text)
502
503                        # try to guess what should be the status image
504                        if file_props['type'] == 'r':
505                                status = 'download'
506                        else:
507                                status = 'upload'
508                        if file_props.has_key('paused') and file_props['paused'] == True:
509                                status = 'pause'
510                        elif file_props.has_key('stalled') and file_props['stalled'] == True:
511                                status = 'waiting'
512                        if file_props.has_key('connected') and file_props['connected'] == False:
513                                status = 'stop'
514                        self.model.set(iter, 0, self.images[status])
515                        if transfered_size == full_size:
516                                self.set_status(typ, sid, 'ok')
517                        elif just_began:
518                                path = self.model.get_path(iter)
519                                self.select_func(path)
520
521        def get_iter_by_sid(self, typ, sid):
522                '''returns iter to the row, which holds file transfer, identified by the
523                session id'''
524                iter = self.model.get_iter_root()
525                while iter:
526                        if typ + sid == self.model[iter][C_SID].decode('utf-8'):
527                                return iter
528                        iter = self.model.iter_next(iter)
529
530        def get_send_file_props(self, account, contact, file_path, file_name):
531                ''' create new file_props dict and set initial file transfer
532                properties in it'''
533                file_props = {'file-name' : file_path, 'name' : file_name, 
534                        'type' : 's'}
535                if os.path.isfile(file_path):
536                        stat = os.stat(file_path)
537                else:
538                        dialogs.ErrorDialog(_('Invalid File'), _('File: ')  + file_path)</