1
# -*- coding: utf-8 -*-
2
# Copyright © 2007 Lateef Alabi-Oki
4
# This file is part of Scribes.
6
# Scribes is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
11
# Scribes is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with Scribes; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
22
This module documents a class that saves files.
24
@author: Lateef Alabi-Oki
25
@organization: The Scribes Project
26
@copyright: Copyright © 2007 Lateef Alabi-Oki
27
@license: GNU GPLv2 or Later
28
@contact: mystilleef@gmail.com
33
class FileSaver(object):
35
This class creates an object that saves the content of the text
36
editor's buffer to a file.
39
def __init__(self, editor):
43
@param self: Reference to the FileSaver instance.
44
@type self: A FileSaver object.
46
@param editor: Reference to the text editor.
47
@type editor: An Editor object.
49
self.__init_attributes(editor)
50
self.__create_swap_folder_and_file()
51
self.__signal_id_1 = editor.connect("close-document", self.__close_document_cb)
52
self.__signal_id_2 = editor.connect("close-document-no-save", self.__close_document_no_save_cb)
53
self.__signal_id_3 = editor.connect("save-document", self.__save_document_cb)
54
self.__signal_id_4 = editor.connect("loading-document", self.__checking_document_cb)
55
self.__signal_id_5 = editor.connect_after("loaded-document", self.__loaded_document_cb)
56
self.__signal_id_6 = editor.connect_after("load-error", self.__load_error_cb)
57
self.__signal_id_7 = editor.textbuffer.connect("modified-changed", self.__modified_changed_cb)
58
self.__signal_id_8 = editor.connect_after("rename-document", self.__rename_document_cb)
59
self.__signal_id_9 = editor.connect("saved-document", self.__saved_document_cb)
60
self.__signal_id_10 = editor.store.connect("updated", self.__updated_cb)
61
self.__signal_id_11 = editor.connect_after("renamed-document", self.__renamed_document_cb)
62
self.__signal_id_12 = editor.connect_after("reload-document", self.__reload_document_cb)
63
from gobject import timeout_add
64
timeout_add(100, self.__check_encoding_manager)
66
def __init_attributes(self, editor):
68
Initialize data attributes.
70
@param self: Reference to the FileSaver instance.
71
@type self: A FileSaver object.
73
@param editor: Reference to the text editor.
74
@type editor: An Editor object.
76
self.__editor = editor
77
self.__termination_id = editor.register_termination_id()
78
self.__encoding_manager = self.__editor.get_object("EncodingManager")
79
self.__should_rename = False
80
self.__can_quit = False
81
self.__file_info = None
82
self.__save_timer = None
83
self.__swap_folder = None
84
self.__swap_uri = None
85
self.__swap_file = None
86
self.__last_modification_time = None
87
self.__modification_dialog = None
88
self.__do_not_save = False
89
self.__toggle_readonly = False
90
self.__signal_id_1 = self.__signal_id_2 = self.__signal_id_3 = None
91
self.__signal_id_4 = self.__signal_id_5 = self.__signal_id_6 = None
92
self.__signal_id_7 = self.__signal_id_8 = self.__signal_id_9 = None
93
self.__signal_id_10 = self.__monitor_id = self.__signal_id_11 = None
94
self.__signal_id_12 = None
97
########################################################################
101
########################################################################
103
def save_file(self, is_closing=False):
105
Save content of text editor's buffer to a file.
107
@param self: Reference to the FileSaver instance.
108
@type self: A FileSaver object.
110
@param is_closing: True if this is the last call.
111
@type is_closing: A Boolean object.
113
from Exceptions import PermissionError, SwapError, FileWriteError
114
from Exceptions import FileCloseError, FileCreateError, TransferError
115
from Exceptions import DoNothingError, FileModificationError
118
self.__determine_action(is_closing)
119
self.__check_permissions()
120
self.__check_swap_folder_and_file()
122
except PermissionError:
123
from internationalization import msg0469
b'\t\t\tself.__error(msg0469)'
124
from internationalization import msg0471
125
self.__error(msg0471)
126
except FileModificationError:
127
print "File has been modified by another program"
128
except DoNothingError:
131
def reset_save_flag(self):
133
Reset the "__do_not_save" flag to its default value.
135
@param self: Reference to the FileSaver instance.
136
@type self: A FileSaver object.
138
self.__do_not_save = False
141
########################################################################
145
########################################################################
147
def __save_file(self):
149
Save document to a temporary file.
151
@param self: Reference to the FileSaver instance.
152
@type self: A FileSaver object.
155
self.__editor.emit("saving-document", self.__editor.uri)
156
text = self.__encode_text()
157
from gnomevfs import OPEN_WRITE, URI
158
from gnomevfs.async import create
159
from Exceptions import FileCreateError
162
# Write to a temporary file.
163
create(uri=URI(self.__swap_uri),
164
callback=self.__write_cb,
165
open_mode=OPEN_WRITE,
171
raise FileCreateError
b'\t\texcept FileCreateError:'
172
from internationalization import msg0472
173
self.__error(msg0472)
176
def __write_cb(self, handle, result, text):
178
Callback to the GNOME-VFS asynchronous create method.
180
@param self: Reference to the FileSaver instance.
181
@type self: A FileSaver object.
183
from Exceptions import FileWriteError
b'\t\t\t\thandle.write(text, self.__close_cb)'
190
except FileWriteError:
191
from internationalization import msg0473
192
self.__error(msg0473)
195
def __close_cb(self, handle, bytes, result, bytes_requested):
197
Callback to the GNOME-VFS asynchronous write method.
199
@param self: Reference to the FileSaver instance.
200
@type self: A FileSaver object.
202
from Exceptions import FileCloseError
206
handle.close(self.__finalize_cb)
210
except FileCloseError:
211
from internationalization import msg0474
212
self.__error(msg0474)
215
def __finalize_cb(self, *args):
217
Callback to the GNOME-VFS asynchronous close method.
219
@param self: Reference to the FileSaver instance.
220
@type self: A FileSaver object.
223
self.__update_file_info()
224
self.__begin_file_transfer()
227
def __begin_file_transfer(self):
229
Transfer temporary file from swap location to permanent location.
231
@param self: Reference to the FileSaver instance.
232
@type self: A FileSaver object.
234
from gnomevfs import XFER_OVERWRITE_MODE_REPLACE
235
from gnomevfs import XFER_ERROR_MODE_QUERY, URI
236
from gnomevfs.async import xfer
237
from Exceptions import TransferError
238
XFER_TARGET_DEFAULT_PERMS = 1 << 12
242
xfer(source_uri_list=[URI(self.__swap_uri)],
243
target_uri_list=[URI(self.__editor.uri)],
244
xfer_options=XFER_TARGET_DEFAULT_PERMS,
245
error_mode=XFER_ERROR_MODE_QUERY,
247
overwrite_mode=XFER_OVERWRITE_MODE_REPLACE,
248
progress_update_callback=self.__update_cb,
249
update_callback_data=self.__swap_file,
250
progress_sync_callback=self.__sync_cb)
253
except TransferError:
254
from internationalization import msg0470
255
self.__error(msg0470)
258
def __sync_cb(self, info):
260
Callback the GNOME-VFS asynchronous transfer method.
262
@param self: Reference to the FileSaver instance.
263
@type self: A FileSaver object.
265
from Exceptions import DoNothingError
270
except DoNothingError:
274
def __update_cb(self, handle, info, data):
276
Callback to the GNOME-VFS asynchronous transfer method.
278
@param self: Reference to the FileSaver instance.
279
@type self: A FileSaver object.
281
from Exceptions import TransferError, DoNothingError
285
# A transfer error occured.
288
from gnomevfs import XFER_PHASE_COMPLETED
289
from operator import ne
290
if ne(info.phase, XFER_PHASE_COMPLETED): return True
291
from gobject import idle_add
292
self.__set_file_info()
294
except TransferError:
295
from internationalization import msg0470
296
self.__error(msg0470)
297
except DoNothingError:
301
def __set_file_info(self):
303
Set correct permissions of a file after it has been saved.
305
@param self: Reference to the FileSaver instance.
306
@type self: A FileSaver object.
308
from operator import not_
309
if not_(self.__file_info): return False
311
from gnomevfs import set_file_info, SET_FILE_INFO_PERMISSIONS
312
set_file_info(self.__editor.uri, self.__file_info, SET_FILE_INFO_PERMISSIONS)
317
def __finish_up(self):
319
Finalize the saving process.
321
@param self: Reference to the FileSaver instance.
322
@type self: A FileSaver object.
324
self.__file_info = None
325
if self.__toggle_readonly:
326
self.__toggle_readonly = False
327
self.__editor.trigger("toggle_readonly")
328
if self.__should_rename:
329
self.__editor.emit("renamed-document", self.__editor.uri)
331
self.__editor.emit("saved-document", self.__editor.uri)
b'\t\tself.__should_rename = False'
335
self.__update_modification_time()
338
def __update_modification_time(self):
339
fileinfo = self.__get_file_info()
340
from operator import not_
341
if not_(fileinfo): return
342
self.__last_modification_time = fileinfo.mtime
345
def __encode_text(self):
347
Convert the encoding of the content of the document from "UTF-8"
348
to an encoding specified by the user, or back to "UTF-8"
350
@param self: Reference to the FileSaver instance.
351
@type self: A FileSaver object.
353
@return: An encoded string.
354
@rtype: A String object.
358
start, end = self.__editor.textbuffer.get_bounds()
359
text = self.__editor.textbuffer.get_text(start, end)
360
unicode_text = text.decode("utf8")
361
encoded_text = unicode_text.encode(self.__encoding_manager.get_encoding())
362
except UnicodeEncodeError:
363
from internationalization import msg0476
364
self.__error(msg0476)
365
except UnicodeDecodeError:
366
from internationalization import msg0475
367
self.__error(msg0475)
370
def __check_permissions(self):
372
Check the permissions of a file.
374
@param self: Reference to the FileSaver instance.
375
@type self: A FileSaver object.
378
from operator import not_, is_
379
if not_(self.__editor.uri.startswith("file:///")): return
380
from gnomevfs import get_local_path_from_uri
381
file_path = get_local_path_from_uri(self.__editor.uri)
382
from os import access, W_OK, path
383
folder_path = path.dirname(file_path)
384
from Exceptions import PermissionError
385
if is_(access(folder_path, W_OK), False):
386
raise PermissionError
b'\t\telif is_(access(file_path, W_OK), False):'
387
if path.exists(file_path): raise PermissionError
390
def __create_swap_folder_and_file(self):
392
Create the swap location and temporary file.
394
@param self: Reference to the FileSaver instance.
395
@type self: A FileSaver object.
397
# Create a temporary folder.
398
from tempfile import mkdtemp, NamedTemporaryFile
399
from info import home_folder
402
self.__swap_folder = mkdtemp(suffix="scribes",
405
# Create a randomly generated temporary file in the
406
# temporary folder created above.
407
self.__swap_file = NamedTemporaryFile(mode="w+",
410
dir=self.__swap_folder)
411
from gnomevfs import get_uri_from_local_path
412
self.__swap_uri = get_uri_from_local_path(self.__swap_file.name)
414
from Exceptions import SwapError
418
def __delete_swap_folder_and_file(self):
420
Remove swap location.
422
@param self: Reference to the FileSaver instance.
423
@type self: A FileSaver object.
426
self.__swap_file.close()
428
rmdir(self.__swap_folder)
433
def __check_swap_folder_and_file(self):
435
Check if swap location and temporary file exists.
437
@param self: Reference to the FileSaver instance.
438
@type self: A FileSaver object.
440
from gnomevfs import exists
441
from operator import not_
442
if not_(exists(self.__swap_uri)):
443
print "Woops swap area not found: creating..."
444
self.__create_swap_folder_and_file()
447
def __determine_action(self, is_closing):
449
Determine what saving action to take for files that have not
450
yet been saved to disk.
452
@param self: Reference to the FileSaver instance.
453
@type self: A FileSaver object.
455
@param is_closing: True if this is the last save call.
456
@type is_closing: A Boolean object.
459
if self.__editor.uri: return
b"\t\t\t# Create a new file and save it if the text editor's buffer"
461
# contains a document but there is no document to save.
462
self.__can_quit = True
463
self.__editor.create_new_file()
466
# Show the save dialog if the text editor's buffer is empty and
467
# there is no document to save.
468
self.__editor.triggermanager.trigger("show_save_dialog")
469
from Exceptions import DoNothingError
473
def __update_file_info(self):
475
Update file information.
477
@param self: Reference to the FileSaver instance.
478
@type self: A FileSaver object.
481
self.__file_info = None
482
fileinfo = self.__get_file_info()
483
from gnomevfs import FILE_INFO_FIELDS_PERMISSIONS
484
if fileinfo.valid_fields & FILE_INFO_FIELDS_PERMISSIONS:
485
self.__file_info = fileinfo
490
def __get_file_info(self):
492
Get file information about the file.
494
@param self: Reference to the FileSaver instance.
495
@type self: A FileSaver object.
497
@return: An object containing file information.
498
@rtype: A gnomevfs.FILE_INFO object.
501
from operator import is_
502
if is_(self.__editor.uri, None): return None
503
if is_(self.__editor.uri.startswith("file:///"), False): return None
504
from gnomevfs import get_file_info
505
fileinfo = get_file_info(self.__editor.uri)
508
def __error(self, message):
512
@param self: Reference to the FileSaver instance.
513
@type self: A FileSaver object.
515
@param message: An error message.
516
@type message: A String object.
518
self.__file_info = None
519
from internationalization import msg0477, msg0468
520
title = msg0477 % (self.__editor.uri)
521
message_id = self.__editor.feedback.set_modal_message(msg0468, "error")
522
self.__editor.show_error_message(message, title, self.__editor.window)
523
self.__editor.emit("save-error", str(self.__editor.uri))
524
self.__editor.feedback.unset_modal_message(message_id)
527
def __save_file_timeout_cb(self):
529
Callback to the save timeout add function.
531
@param self: Reference to the FileSaver instance.
532
@type self: A FileSaver object.
534
from gobject import idle_add
535
idle_add(self.save_file)
538
def __remove_save_timer(self):
542
@param self: Reference to the FileSaver instance.
543
@type self: A FileSaver object.
546
from gobject import source_remove
547
source_remove(self.__save_timer)
554
Destroy instance of this class.
556
@param self: Reference to the FileSaver instance.
557
@type self: A FileSaver object.
559
if self.__modification_dialog: self.__modification_dialog.destroy()
560
self.__stop_monitoring_file()
561
self.__encoding_manager.destroy()
562
self.__remove_save_timer()
563
self.__delete_swap_folder_and_file()
564
from utils import disconnect_signal
565
disconnect_signal(self.__signal_id_1, self.__editor)
566
disconnect_signal(self.__signal_id_2, self.__editor)
567
disconnect_signal(self.__signal_id_3, self.__editor)
568
disconnect_signal(self.__signal_id_4, self.__editor)
569
disconnect_signal(self.__signal_id_5, self.__editor)
570
disconnect_signal(self.__signal_id_6, self.__editor)
571
disconnect_signal(self.__signal_id_7, self.__editor.textbuffer)
572
disconnect_signal(self.__signal_id_8, self.__editor)
573
disconnect_signal(self.__signal_id_9, self.__editor)
574
disconnect_signal(self.__signal_id_10, self.__editor)
575
disconnect_signal(self.__signal_id_11, self.__editor)
576
disconnect_signal(self.__signal_id_12, self.__editor)
577
self.__editor.unregister_termination_id(self.__termination_id)
582
def __precompile_methods(self):
584
Use Psyco to optimize some methods.
586
@param self: Reference to the FileSaver instance.
587
@type self: A FileSaver object.
590
from psyco import bind
591
bind(self.__encode_text)
592
bind(self.__save_file)
593
bind(self.__check_permissions)
594
bind(self.__can_save)
595
bind(self.__get_file_info)
600
def __check_last_modification(self):
602
Check if the document has been modified by another application.
604
@param self: Reference to the FileSaver instance.
605
@type self: A FileSaver object.
607
from operator import not_, is_, eq
608
fileinfo = self.__get_file_info()
609
if not_(fileinfo): return
610
if is_(self.__last_modification_time, None): return
611
if eq(self.__last_modification_time, fileinfo.mtime): return
612
from Exceptions import FileModificationError
613
raise FileModificationError
616
def __start_monitoring_file(self):
617
from operator import not_
618
if not_(self.__editor.uri.startswith("file:///")): return
619
# Monitor database for changes.
620
self.__update_modification_time()
621
from gnomevfs import monitor_add, MONITOR_FILE
622
self.__monitor_id = monitor_add(self.__editor.uri, MONITOR_FILE,
623
self.__file_changed_cb)
626
def __stop_monitoring_file(self):
627
from gnomevfs import monitor_cancel
628
from operator import not_
629
if not_(self.__monitor_id): return
630
monitor_cancel(self.__monitor_id)
633
def __show_modification_dialog(self):
635
Show a dialog when another application modifies this file.
637
@param self: Reference to the FileSaver instance.
638
@type self: A FileSaver object.
641
self.__modification_dialog.show()
642
except AttributeError:
643
from ModificationDialog import ModificationDialog
644
self.__modification_dialog = ModificationDialog(self, self.__editor)
645
self.__modification_dialog.show()
648
def __can_save(self):
650
Check whether or not to save the current file.
652
@param self: Reference to the FileSaver instance.
653
@type self: A FileSaver object.
655
from Exceptions import DoNothingError
656
if self.__do_not_save: raise DoNothingError
659
########################################################################
661
# Signal and Event Handlers
663
########################################################################
665
def __close_document_cb(self, editor):
667
Handles callback when the "close-document" signal is emitted.
669
@param self: Reference to the FileSaver instance.
670
@type self: A FileSaver object.
672
@param editor: Reference to the text editor.
673
@type editor: An Editor object.
675
self.__can_quit = True
676
self.__remove_save_timer()
677
from operator import not_
678
if not_(editor.file_is_saved):
679
from gobject import idle_add
680
idle_add(self.save_file, True)
685
def __close_document_no_save_cb(self, editor):
687
Handles callback when the "close-document-no-save" signal is emitted.
689
@param self: Reference to the FileSaver instance.
690
@type self: A FileSaver object.
692
@param editor: Reference to the text editor.
693
@type editor: An Editor object.
695
self.__remove_save_timer()
699
def __checking_document_cb(self, editor, uri):
701
Handles callback when the "checking-document" signal is emitted.
703
@param self: Reference to the FileSaver instance.
704
@type self: A FileSaver object.
706
@param editor: Reference to the text editor.
707
@type editor: An Editor object.
709
@param uri: Reference to a file.
710
@type uri: A String object.
712
editor.textbuffer.handler_block(self.__signal_id_7)
715
def __loaded_document_cb(self, editor, uri):
717
Handles callback when the "loaded-document" signal is emitted.
719
@param self: Reference to the FileSaver instance.
720
@type self: A FileSaver object.
722
@param editor: Reference to the text editor.
723
@type editor: An Editor object.
725
@param uri: Reference to a file.
726
@type uri: A String object.
728
editor.textbuffer.handler_unblock(self.__signal_id_7)
729
self.__start_monitoring_file()
732
def __load_error_cb(self, editor, uri):
734
Handles callback when the "load-error" signal is emitted.
736
@param self: Reference to the FileSaver instance.
737
@type self: A FileSaver object.
739
@param editor: Reference to the text editor.
740
@type editor: An Editor object.
742
@param uri: Reference to a file.
743
@type uri: A String object.
745
editor.textbuffer.handler_unblock(self.__signal_id_7)
748
def __save_document_cb(self, editor):
750
Handles callback when the "save-document" signal is emitted.
752
@param self: Reference to the FileSaver instance.
753
@type self: A FileSaver object.
755
@param editor: Reference to the text editor.
756
@type editor: An Editor object.
758
from gobject import idle_add
759
idle_add(self.save_file)
762
def __saved_document_cb(self, editor, uri):
764
Handles callback when the "saved-document" signal is emitted.
766
@param self: Reference to the FileSaver instance.
767
@type self: A FileSaver object.
769
@param editor: Reference to the text editor.
770
@type editor: An Editor object.
772
@param uri: Reference to a file.
773
@type uri: A String object.
775
self.__remove_save_timer()
778
def __modified_changed_cb(self, textbuffer):
780
Handles callback when the "modified-changed" signal is emitted.
782
@param self: Reference to the FileSaver instance.
783
@type self: A FileSaver object.
785
@param textbuffer: Reference to the text editor's buffer.
786
@type textbuffer: A ScribesTextBuffer object.
788
if textbuffer.get_modified() is False: return False
789
self.__editor.emit("modified-document")
790
if self.__editor.uri is None: return False
791
from gobject import timeout_add, PRIORITY_LOW
792
self.__save_timer = timeout_add(21000, self.__save_file_timeout_cb, priority=PRIORITY_LOW)
795
def __reload_document_cb(self, *args):
796
self.__remove_save_timer()
799
def __rename_document_cb(self, editor, uri):
801
Handles callback when the "rename-document" signal is emitted.
803
@param self: Reference to the FileSaver instance.
804
@type self: A FileSaver object.
806
@param editor: Reference to the text editor.
807
@type editor: An Editor object.
809
@param uri: Reference to a file.
810
@type uri: A String object.
812
self.__stop_monitoring_file()
813
if self.__editor.is_readonly: self.__toggle_readonly = True
814
self.__should_rename = True
815
from gobject import idle_add
816
idle_add(self.save_file)
819
def __updated_cb(self, store, name):
820
from operator import ne
821
if ne(name, "EncodingManager"): return
822
self.__encoding_manager = store.get_object("EncodingManager")
825
def __check_encoding_manager(self):
826
if self.__encoding_manager: return False
827
self.__encoding_manager = self.__editor.store.get_object("EncodingManager")
830
def __file_changed_cb(self, monitor_uri, info_uri, event_type):
832
Handles callback when the current file is modified.
834
@param self: Reference to the AutoReplaceManager instance.
835
@type self: An AutoReplaceManager object.
837
@param monitor_uri: The uri that is monitored.
838
@type monitor_uri: A String object.
840
@param info_uri: The uri that is monitored.
841
@type info_uri: A String object.
843
@param event_type: The type of modification that occured.
844
@type event_type: A gnomevfs.MONITOR_EVENT* object.
846
from gnomevfs import MONITOR_EVENT_DELETED
847
from gnomevfs import MONITOR_EVENT_CREATED
848
from gnomevfs import MONITOR_EVENT_CHANGED
849
if event_type in [MONITOR_EVENT_DELETED, MONITOR_EVENT_CREATED, MONITOR_EVENT_CHANGED]:
850
if self.__can_quit: return
851
fileinfo = self.__get_file_info()
852
from operator import not_, eq
853
if not_(fileinfo): return
854
if eq(self.__last_modification_time, fileinfo.mtime): return
855
self.__do_not_save = True
856
self.__show_modification_dialog()
859
def __renamed_document_cb(self, *args):
860
self.__start_monitoring_file()