Goffi's cp, a fancy file copier
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

686 lines
27 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
11 years ago
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. """
  4. gcp: Goffi's CoPier
  5. Copyright (C) 2010, 2011 Jérôme Poisson <goffi@goffi.org>
  6. (c) 2011 Thomas Preud'homme <robotux@celest.fr>
  7. This program is free software: you can redistribute it and/or modify
  8. it under the terms of the GNU General Public License as published by
  9. the Free Software Foundation, either version 3 of the License, or
  10. (at your option) any later version.
  11. This program 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.
  15. You should have received a copy of the GNU General Public License
  16. along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. ### logging ###
  19. import logging
  20. from logging import debug, info, error, warning
  21. logging.basicConfig(level=logging.INFO,
  22. format='%(message)s')
  23. ###
  24. import gettext
  25. gettext.install('gcp', "i18n")
  26. import sys
  27. import os,os.path
  28. from argparse import ArgumentParser
  29. import pickle
  30. try:
  31. import gobject
  32. #DBus
  33. import dbus, dbus.glib
  34. import dbus.service
  35. import dbus.mainloop.glib
  36. except ImportError as e:
  37. error(_("Error during import"))
  38. error(_("Please check dependecies:"),e)
  39. exit(1)
  40. try:
  41. from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed
  42. pbar_available=True
  43. except ImportError as e:
  44. info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
  45. info (_('Progress bar deactivated\n--\n'))
  46. pbar_available=False
  47. NAME = "gcp (Goffi's copier)"
  48. NAME_SHORT = "gcp"
  49. VERSION = '0.1.3'
  50. ABOUT = NAME+u" v"+VERSION+u""" (c) Jérôme Poisson (aka Goffi) 2010, 2011
  51. ---
  52. """+NAME+u""" Copyright (C) 2010 Jérôme Poisson
  53. """ + _(u"""This program comes with ABSOLUTELY NO WARRANTY;
  54. This is free software, and you are welcome to redistribute it
  55. under certain conditions.
  56. ---
  57. This software is an advanced file copier
  58. Get the latest version at http://wiki.goffi.org/wiki/Gcp
  59. """)
  60. const_DBUS_INTERFACE = "org.goffi.gcp"
  61. const_DBUS_PATH = "/org/goffi/gcp"
  62. const_BUFF_SIZE = 4096
  63. const_PRESERVE = set(['mode','ownership','timestamps'])
  64. const_FILES_DIR = "~/.gcp"
  65. const_JOURNAL_PATH = const_FILES_DIR + "/journal"
  66. const_SAVED_LIST = const_FILES_DIR + "/saved_list"
  67. class DbusObject(dbus.service.Object):
  68. def __init__(self, gcp, bus, path):
  69. self._gcp = gcp
  70. dbus.service.Object.__init__(self, bus, path)
  71. debug(_("Init DbusObject..."))
  72. self.cb={}
  73. @dbus.service.method(const_DBUS_INTERFACE,
  74. in_signature='', out_signature='s')
  75. def getVersion(self):
  76. """Get gcp version
  77. @return: version as string"""
  78. return VERSION
  79. @dbus.service.method(const_DBUS_INTERFACE,
  80. in_signature='ss', out_signature='bs')
  81. def addArgs(self, source_dir, args):
  82. """Add arguments to gcp as if there were entered on its own command line
  83. @param source_dir: current working dir to use as base for arguments, as given by os.getcwd()
  84. @param args: serialized (wich pickle) list of strings - without command name -, as given by sys.argv[1:].
  85. @return: success (boolean) and error message if any (string)"""
  86. try:
  87. args = pickle.loads(str(args))
  88. except TypeError as e:
  89. pickle.UnpicklingError = e
  90. return (False, _("INTERNAL ERROR: invalid arguments"))
  91. try:
  92. source_dir = pickle.loads(str(source_dir))
  93. except TypeError as e:
  94. pickle.UnpicklingError = e
  95. return (False, _("INTERNAL ERROR: invalid source_dir"))
  96. return self._gcp.parseArguments(args, source_dir)
  97. class Journal():
  98. def __init__(self, path=const_JOURNAL_PATH):
  99. self.journal_path = os.path.expanduser(path)
  100. self.journal_fd = open(self.journal_path,'w') #TODO: check and maybe save previous journals
  101. self.__entry_open = None
  102. self.failed = []
  103. self.partial = []
  104. def __del__(self):
  105. self.journal_fd.flush()
  106. self.journal_fd.close()
  107. def startFile(self, source_path):
  108. """Start an entry in the journal"""
  109. assert not self.__entry_open
  110. self.__entry_open = source_path
  111. self.journal_fd.write(source_path+"\n")
  112. self.journal_fd.flush()
  113. self.success=True
  114. self.errors=[]
  115. def closeFile(self):
  116. """Close the entry in the journal"""
  117. assert self.__entry_open
  118. if not self.success:
  119. status = "FAILED"
  120. else:
  121. status = "OK" if not self.errors else "PARTIAL"
  122. self.journal_fd.write("%(status)s: %(errors)s\n" % {'status': status, 'errors': ', '.join(self.errors)})
  123. self.journal_fd.flush()
  124. self.__entry_open = None
  125. def copyFailed(self):
  126. """Must be called when something is wrong with the copy itself"""
  127. assert self.__entry_open
  128. self.success = False
  129. self.failed.append(self.__entry_open)
  130. def error(self, name):
  131. """Something went wrong"""
  132. assert self.__entry_open
  133. self.errors.append(name)
  134. self.partial.append(self.__entry_open)
  135. def showErrors(self):
  136. """Show which files were not successfully copied"""
  137. failed = set(self.failed)
  138. partial = set(self.partial)
  139. for entry in failed:
  140. partial.discard(entry)
  141. if failed:
  142. error(_("/!\\ THE FOLLOWING FILES WERE *NOT* SUCCESSFULY COPIED:"))
  143. #TODO: use logging capability to print all error message in red
  144. for entry in failed:
  145. info("\t- %s" % entry)
  146. info ('--\n')
  147. if partial:
  148. warning(_("The following files were copied, but some errors happened:"))
  149. for entry in partial:
  150. info("\t- %s" % entry)
  151. info ('--\n')
  152. if failed or partial:
  153. info(_("Please check journal: %s") % self.journal_path)
  154. class GCP():
  155. def __init__(self):
  156. files_dir = os.path.expanduser(const_FILES_DIR)
  157. if not os.path.exists(files_dir):
  158. os.makedirs(files_dir)
  159. try:
  160. sessions_bus = dbus.SessionBus()
  161. db_object = sessions_bus.get_object(const_DBUS_INTERFACE,
  162. const_DBUS_PATH)
  163. self.gcp_main = dbus.Interface(db_object,
  164. dbus_interface=const_DBUS_INTERFACE)
  165. self._main_instance = False
  166. except dbus.exceptions.DBusException as e:
  167. if e._dbus_error_name=='org.freedesktop.DBus.Error.ServiceUnknown':
  168. self.launchDbusMainInstance()
  169. debug (_("gcp launched"))
  170. self._main_instance = True
  171. self.buffer_size = const_BUFF_SIZE
  172. self.__launched = False #True when journal is initialised and copy is started
  173. else:
  174. raise e
  175. def launchDbusMainInstance(self):
  176. debug (_("Init DBus..."))
  177. session_bus = dbus.SessionBus()
  178. self.dbus_name = dbus.service.BusName(const_DBUS_INTERFACE, session_bus)
  179. self.dbus_object = DbusObject(self, session_bus, const_DBUS_PATH)
  180. self.copy_list = []
  181. self.mounts = self.__getMountPoints()
  182. self.bytes_total = 0
  183. self.bytes_copied = 0
  184. def getFsType(self, path):
  185. fs = ''
  186. last_mount_point = ''
  187. for mount in self.mounts:
  188. if path.startswith(mount) and len(mount)>=len(last_mount_point):
  189. fs = self.mounts[mount]
  190. last_mount_point = mount
  191. return fs
  192. def __getMountPoints(self):
  193. """Parse /proc/mounts to get currently mounted devices"""
  194. #TODO: reparse when a new device is added/a device is removed
  195. #(check freedesktop mounting signals)
  196. ret = {}
  197. try:
  198. with open("/proc/mounts",'r') as mounts:
  199. for line in mounts.readlines():
  200. fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno = line.split(' ')
  201. ret[fs_file] = fs_vfstype
  202. except:
  203. error (_("Can't read mounts table"))
  204. return ret
  205. def __appendToList(self, path, dest_path, options):
  206. """Add a file to the copy list
  207. @param path: absolute path of file
  208. @param options: options as return by optparse"""
  209. debug (_("Adding to copy list: %(path)s ==> %(dest_path)s (%(fs_type)s)") % {"path":path, "dest_path":dest_path, "fs_type":self.getFsType(dest_path)} )
  210. try:
  211. self.bytes_total+=os.path.getsize(path)
  212. self.copy_list.insert(0,(path, dest_path, options))
  213. except OSError as e:
  214. error(_("Can't copy %(path)s: %(exception)s") % {'path':path, 'exception':e.strerror})
  215. def __appendDirToList(self, dirpath, dest_path, options):
  216. """Add recursively directory to the copy list
  217. @param path: absolute path of dir
  218. @param options: options as return by optparse"""
  219. #We first check that the dest path exists, and create it if needed
  220. dest_path = self.__filename_fix(dest_path, options, no_journal=True)
  221. if not os.path.exists(dest_path):
  222. debug ("Creating directory %s" % dest_path)
  223. os.makedirs(dest_path) #TODO: check permissions
  224. #TODO: check that dest_path is an accessible dir,
  225. # and skip file/write error in log if needed
  226. try:
  227. for filename in os.listdir(dirpath):
  228. filepath = os.path.join(dirpath,filename)
  229. if os.path.islink(filepath) and not options.dereference:
  230. debug ("Skippink symbolic dir: %s" % filepath)
  231. continue
  232. if os.path.isdir(filepath):
  233. full_dest_path = os.path.join(dest_path,filename)
  234. self.__appendDirToList(filepath, full_dest_path, options)
  235. else:
  236. self.__appendToList(filepath, dest_path, options)
  237. except OSError as e:
  238. try:
  239. error(_("Can't append %(path)s to copy list: %(exception)s") % {'path':filepath, 'exception':e.strerror})
  240. except NameError:
  241. #We can't list the dir
  242. error(_("Can't access %(dirpath)s: %(exception)s") % {'dirpath':dirpath, 'exception':e.strerror})
  243. def __checkArgs(self, options, source_dir, args):
  244. """Check thats args are files, and add them to copy list
  245. @param options: options sets
  246. @param source_dir: directory where the command was entered
  247. @parm args: args of the copy"""
  248. assert(len (args)>=2)
  249. len_args = len(args)
  250. try:
  251. dest_path = os.path.normpath(os.path.join(source_dir, args.pop()))
  252. except OSError as e:
  253. error (_("Invalid dest_path: %s"),e)
  254. for path in args:
  255. abspath = os.path.normpath(os.path.join(os.path.expanduser(source_dir), path))
  256. if not os.path.exists(abspath):
  257. warning(_("The path given in arg doesn't exist or is not accessible: %s") % abspath)
  258. else:
  259. if os.path.isdir(abspath):
  260. if not options.recursive:
  261. warning (_('omitting directory "%s"') % abspath)
  262. else:
  263. _basename=os.path.basename(os.path.normpath(path))
  264. full_dest_path = dest_path if options.directdir else os.path.normpath(os.path.join(dest_path, _basename))
  265. self.__appendDirToList(abspath, full_dest_path, options)
  266. else:
  267. self.__appendToList(abspath, dest_path, options)
  268. def __copyNextFile(self):
  269. """Take the last file in the list, and launch the copy using glib io_watch event
  270. @return: True a file was added, False else"""
  271. if self.copy_list:
  272. source_file, dest_path, options = self.copy_list.pop()
  273. self.journal.startFile(source_file)
  274. try:
  275. source_fd = open(source_file, 'rb')
  276. except:
  277. self.journal.copyFailed()
  278. self.journal.error("can't open source")
  279. self.journal.closeFile()
  280. return True
  281. filename = os.path.basename(source_file)
  282. assert(filename)
  283. dest_file = self.__filename_fix(options.dest_file,options) if options.dest_file else self.__filename_fix(os.path.join(dest_path,filename),options)
  284. if os.path.exists(dest_file) and not options.force:
  285. warning (_("File [%s] already exists, skipping it !") % dest_file)
  286. self.journal.copyFailed()
  287. self.journal.error("already exists")
  288. self.journal.closeFile()
  289. source_fd.close()
  290. return True
  291. try:
  292. dest_fd = open(dest_file, 'wb')
  293. except:
  294. self.journal.copyFailed()
  295. self.journal.error("can't open dest")
  296. self.journal.closeFile()
  297. source_fd.close()
  298. return True
  299. gobject.io_add_watch(source_fd,gobject.IO_IN,self._copyFile,
  300. (dest_fd, options), priority=gobject.PRIORITY_DEFAULT)
  301. if not self.progress:
  302. info(_("COPYING %(source)s ==> %(dest)s") % {"source":source_file, "dest":dest_file})
  303. return True
  304. else:
  305. #Nothing left to copy, we quit
  306. if self.progress:
  307. self.__pbar_finish()
  308. self.journal.showErrors()
  309. self.loop.quit()
  310. def __copyFailed(self, reason, source_fd, dest_fd):
  311. """Write the failure in the journal and close files descriptors"""
  312. self.journal.copyFailed()
  313. self.journal.error(reason)
  314. self.journal.closeFile()
  315. source_fd.close()
  316. dest_fd.close()
  317. def _copyFile(self, source_fd, condition, data):
  318. """Actually copy the file, callback used with io_add_watch
  319. @param source_fd: file descriptor of the file to copy
  320. @param condition: condition which launched the callback (glib.IO_IN)
  321. @param data: tuple with (destination file descriptor, copying options)"""
  322. try:
  323. dest_fd,options = data
  324. try:
  325. buff = source_fd.read(self.buffer_size)
  326. except KeyboardInterrupt:
  327. raise KeyboardInterrupt
  328. except:
  329. self.__copyFailed("can't read source", source_fd, dest_fd)
  330. return False
  331. try:
  332. dest_fd.write(buff)
  333. except KeyboardInterrupt:
  334. raise KeyboardInterrupt
  335. except:
  336. self.__copyFailed("can't write to dest", source_fd, dest_fd)
  337. return False
  338. self.bytes_copied += len(buff)
  339. if self.progress:
  340. self._pbar_update()
  341. if len(buff) != self.buffer_size:
  342. source_fd.close()
  343. dest_fd.close()
  344. self.__post_copy(source_fd.name, dest_fd.name, options)
  345. self.journal.closeFile()
  346. return False
  347. return True
  348. except KeyboardInterrupt:
  349. self._userInterruption()
  350. def __filename_fix(self, filename, options, no_journal=False):
  351. """Fix filenames incompatibilities/mistake according to options
  352. @param filename: full path to the file
  353. @param options: options as parsed on command line
  354. @param no_journal: don't write any entry in journal
  355. @return: fixed filename"""
  356. fixed_filename = filename
  357. if self.getFsType(filename) == 'vfat' and options.fs_fix:
  358. fixed_filename = filename.replace('\\','_')\
  359. .replace(':',';')\
  360. .replace('*','+')\
  361. .replace('?','_')\
  362. .replace('"','\'')\
  363. .replace('<','[')\
  364. .replace('>',']')\
  365. .replace('|','!')\
  366. .rstrip() #XXX: suffixed spaces cause issues (must check FAT doc for why)
  367. if not fixed_filename:
  368. fixed_filename = '_'
  369. if fixed_filename != filename and not no_journal:
  370. self.journal.error('filename fixed')
  371. return fixed_filename
  372. def __post_copy(self, source_file, dest_file, options):
  373. """Do post copy traitement (mainly managing --preserve option)"""
  374. st_file = os.stat(source_file)
  375. for preserve in options.preserve:
  376. try:
  377. if preserve == 'mode':
  378. os.chmod(dest_file, st_file.st_mode)
  379. elif preserve == 'ownership':
  380. os.chown(dest_file, st_file.st_uid, st_file.st_gid)
  381. elif preserve == 'timestamps':
  382. os.utime(dest_file, (st_file.st_atime, st_file.st_mtime))
  383. except OSError as e:
  384. self.journal.error("preserve-"+preserve)
  385. def __get_string_size(self, size):
  386. """Return a nice string representation of a size"""
  387. if size>=2**50:
  388. return _("%.2f PiB") % (float(size)/2**50)
  389. elif size>=2**40:
  390. return _("%.2f TiB") % (float(size)/2**40)
  391. elif size>=2**30:
  392. return _("%.2f GiB") % (float(size)/2**30)
  393. elif size>=2**20:
  394. return _("%.2f MiB") % (float(size)/2**20)
  395. elif size>=2**10:
  396. return _("%.2f KiB") % (float(size)/2**10)
  397. else:
  398. return _("%i B") % size
  399. def _pbar_update(self):
  400. """Update progress bar position, create the bar if it doesn't exist"""
  401. assert(self.progress)
  402. try:
  403. if self.pbar.maxval != self.bytes_total:
  404. self.pbar.maxval = self.bytes_total
  405. self.pbar.widgets[0] = _("Copying %s") % self.__get_string_size(self.bytes_total)
  406. except AttributeError:
  407. if not self.bytes_total:
  408. #No progress bar if the files have a null size
  409. return
  410. self.pbar = ProgressBar(self.bytes_total,[_("Copying %s") % self.__get_string_size(self.bytes_total)," ",Percentage()," ",Bar()," ",FileTransferSpeed()," ",ETA()])
  411. self.pbar.start()
  412. self.pbar.update(self.bytes_copied)
  413. def __pbar_finish(self):
  414. """Mark the progression as finished"""
  415. assert(self.progress)
  416. try:
  417. self.pbar.finish()
  418. except AttributeError:
  419. pass
  420. def __sourcesSaving(self,options,args):
  421. """Manage saving/loading/deleting etc of sources files
  422. @param options: options as parsed from command line
  423. @param args: args parsed from command line"""
  424. if options.sources_save or options.sources_load\
  425. or options.sources_list or options.sources_full_list\
  426. or options.sources_del or options.sources_replace:
  427. try:
  428. with open(os.path.expanduser(const_SAVED_LIST),'r') as saved_fd:
  429. saved_files = pickle.load(saved_fd)
  430. except:
  431. saved_files={}
  432. if options.sources_del:
  433. if not saved_files.has_key(options.sources_del):
  434. error(_("No saved sources with this name, check existing names with --sources-list"))
  435. else:
  436. del saved_files[options.sources_del]
  437. with open(os.path.expanduser(const_SAVED_LIST),'w') as saved_fd:
  438. pickle.dump(saved_files,saved_fd)
  439. if not args:
  440. exit(0)
  441. if options.sources_list or options.sources_full_list:
  442. info(_('Saved sources:'))
  443. sources = saved_files.keys()
  444. sources.sort()
  445. for source in sources:
  446. info("\t[%s]" % source)
  447. if options.sources_full_list:
  448. for filename in saved_files[source]:
  449. info("\t\t%s" % filename)
  450. info("---\n")
  451. if not args:
  452. exit(0)
  453. if options.sources_save or options.sources_replace:
  454. if saved_files.has_key(options.sources_save) and not options.sources_replace:
  455. error(_("There is already a saved sources with this name, skipping --sources-save"))
  456. else:
  457. if len(args)>1:
  458. saved_files[options.sources_save] = map(os.path.abspath,args[:-1])
  459. with open(os.path.expanduser(const_SAVED_LIST),'w') as saved_fd:
  460. pickle.dump(saved_files,saved_fd)
  461. if options.sources_load:
  462. if not saved_files.has_key(options.sources_load):
  463. error(_("No saved sources with this name, check existing names with --sources-list"))
  464. else:
  465. saved_args = saved_files[options.sources_load]
  466. saved_args.reverse()
  467. for arg in saved_args:
  468. args.insert(0,arg)
  469. def parseArguments(self, full_args=sys.argv[1:], source_dir = os.getcwd()):
  470. """Parse arguments and add files to queue
  471. @param full_args: list of arguments strings (without program name)
  472. @param source_dir: path from where the arguments come, as given by os.getcwd()
  473. @return: a tuple (boolean, message) where the boolean is the success of the arguments
  474. validation, and message is the error message to print when necessary"""
  475. _usage="""
  476. %(prog)s [options] FILE DEST
  477. %(prog)s [options] FILE1 [FILE2 ...] DEST-DIR
  478. %(prog)s --help for options list
  479. """
  480. for idx in range(len(full_args)):
  481. full_args[idx] = full_args[idx].encode('utf-8')
  482. parser = ArgumentParser(usage=_usage)
  483. parser.add_argument("-r", "--recursive", action="store_true", default=False,
  484. help=_("copy directories recursively"))
  485. parser.add_argument("-f", "--force", action="store_true", default=False,
  486. help=_("force overwriting of existing files"))
  487. parser.add_argument("--preserve", action="store", default='mode,ownership,timestamps',
  488. help=_("preserve the specified attributes"))
  489. parser.add_argument("-L", "--dereference", action="store_true", default=False,
  490. help=_("always follow symbolic links in sources"))
  491. parser.add_argument("-P", "--no-dereference", action="store_false", dest='dereference',
  492. help=_("never follow symbolic links in sources"))
  493. #parser.add_argument("--no-unicode-fix", action="store_false", dest='unicode_fix', default=True,
  494. # help=_("don't fix name encoding errors")) #TODO
  495. parser.add_argument("--no-fs-fix", action="store_false", dest='fs_fix', default=True,
  496. help=_("don't fix filesystem name incompatibily"))
  497. parser.add_argument("--no-progress", action="store_false", dest="progress", default=True,
  498. help=_("deactivate progress bar"))
  499. parser.add_argument("-v", "--verbose", action="store_true", default=False,
  500. help=_("Show what is currently done"))
  501. parser.add_argument("-V", "--version", action="version", version=ABOUT)
  502. group_saving = parser.add_argument_group("sources saving")
  503. group_saving.add_argument("--sources-save", action="store",
  504. help=_("Save source arguments"))
  505. group_saving.add_argument("--sources-replace", action="store",
  506. help=_("Save source arguments and replace memory if it already exists"))
  507. group_saving.add_argument("--sources-load", action="store",
  508. help=_("Load source arguments"))
  509. group_saving.add_argument("--sources-del", action="store",
  510. help=_("delete saved sources"))
  511. group_saving.add_argument("--sources-list", action="store_true", default=False,
  512. help=_("List names of saved sources"))
  513. group_saving.add_argument("--sources-full-list", action="store_true", default=False,
  514. help=_("List names of saved sources and files in it"))
  515. parser.add_argument_group(group_saving)
  516. (options, args) = parser.parse_known_args()
  517. options.directdir = False #True only in the special case: we are copying a dir and it doesn't exists
  518. #options check
  519. if options.progress and not pbar_available:
  520. warning (_("Progress bar is not available, deactivating"))
  521. options.progress = self.progress = False
  522. else:
  523. self.progress = options.progress
  524. if options.verbose:
  525. logging.getLogger().setLevel(logging.DEBUG)
  526. preserve = set(options.preserve.split(','))
  527. if not preserve.issubset(const_PRESERVE):
  528. error (_("Invalide --preserve value\nvalid values are:"))
  529. for value in const_PRESERVE:
  530. error('- %s' % value)
  531. exit(1)
  532. else:
  533. options.preserve = preserve
  534. self.__sourcesSaving(options, args)
  535. if len(args) == 2: #we check special cases
  536. src_path = os.path.abspath(os.path.expanduser(args[0]))
  537. dest_path = os.path.abspath(os.path.expanduser(args[1]))
  538. if os.path.isdir(src_path):
  539. options.dest_file = None #we are copying a dir, this options is for files only
  540. if not os.path.exists(dest_path):
  541. options.directdir = True #dest_dir doesn't exist, it's the directdir special case
  542. elif not os.path.exists(dest_path) or os.path.isfile(dest_path):
  543. options.dest_file = dest_path
  544. args[1] = os.path.dirname(dest_path)
  545. else:
  546. options.dest_file = None
  547. else:
  548. options.dest_file = None
  549. #if there is an other instance of gcp, we send options to it
  550. if not self._main_instance:
  551. info (_("There is already one instance of %s running, pluging to it") % NAME_SHORT)
  552. #XXX: we have to serialize data as dbus only accept valid unicode, and filenames
  553. # can have invalid unicode.
  554. return self.gcp_main.addArgs(pickle.dumps(os.getcwd()),pickle.dumps(full_args))
  555. else:
  556. if len(args) < 2:
  557. _error_msg = _("Wrong number of arguments")
  558. return (False, _error_msg)
  559. debug(_("adding args to gcp: %s") % args)
  560. self.__checkArgs(options, source_dir, args)
  561. if not self.__launched:
  562. self.journal = Journal()
  563. gobject.idle_add(self.__copyNextFile)
  564. self.__launched = True
  565. return (True,'')
  566. def _userInterruption(self):
  567. info(_("User interruption: good bye"))
  568. exit(1)
  569. def go(self):
  570. """Launch main loop"""
  571. self.loop = gobject.MainLoop()
  572. try:
  573. self.loop.run()
  574. except KeyboardInterrupt:
  575. self._userInterruption()
  576. if __name__ == "__main__":
  577. gcp = GCP()
  578. success,message = gcp.parseArguments()
  579. if not success:
  580. error(message)
  581. exit(1)
  582. if gcp._main_instance:
  583. gcp.go()
  584. if gcp.journal.failed:
  585. exit(1)
  586. if gcp.journal.partial:
  587. exit(2)