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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. #!/usr/bin/env python3
  2. """
  3. gcp: Goffi's CoPier
  4. Copyright (C) 2010, 2011 Jérôme Poisson <goffi@goffi.org>
  5. (c) 2011 Thomas Preud'homme <robotux@celest.fr>
  6. This program 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 3 of the License, or
  9. (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. ### logging ###
  18. import logging
  19. from logging import debug, info, error, warning
  20. logging.basicConfig(level=logging.INFO,
  21. format='%(message)s')
  22. ###
  23. import gettext
  24. gettext.install('gcp', "i18n")
  25. import sys
  26. import os,os.path
  27. from argparse import ArgumentParser
  28. import pickle
  29. try:
  30. from gi.repository import GObject
  31. #DBus
  32. import dbus, dbus.glib
  33. import dbus.service
  34. import dbus.mainloop.glib
  35. except ImportError as e:
  36. error(_("Error during import"))
  37. error(_("Please check dependecies:"),e)
  38. exit(1)
  39. try:
  40. from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed
  41. pbar_available=True
  42. except ImportError as e:
  43. info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
  44. info (_('Progress bar deactivated\n--\n'))
  45. pbar_available=False
  46. NAME = "gcp (Goffi's copier)"
  47. NAME_SHORT = "gcp"
  48. VERSION = '0.1.3'
  49. ABOUT = NAME+u" v"+VERSION+u""" (c) Jérôme Poisson (aka Goffi) 2010, 2011
  50. ---
  51. """+NAME+u""" Copyright (C) 2010 Jérôme Poisson
  52. """ + _(u"""This program comes with ABSOLUTELY NO WARRANTY;
  53. This is free software, and you are welcome to redistribute it
  54. under certain conditions.
  55. ---
  56. This software is an advanced file copier
  57. Get the latest version at http://wiki.goffi.org/wiki/Gcp
  58. """)
  59. const_DBUS_INTERFACE = "org.goffi.gcp"
  60. const_DBUS_PATH = "/org/goffi/gcp"
  61. const_BUFF_SIZE = 4096
  62. const_PRESERVE = set(['mode','ownership','timestamps'])
  63. const_FS_FIX = set(['auto','force','no'])
  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 options.fs_fix == 'force' or (options.fs_fix == 'auto' and self.getFsType(filename) == 'vfat'):
  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='',
  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("--fs-fix", choices = const_FS_FIX, dest='fs_fix', default='auto',
  496. help=_("fix filesystem name incompatibily (default: auto)"))
  497. parser.add_argument("--no-fs-fix", action="store_true", dest='no_fs_fix', default=False,
  498. help=_("same as --fs-fix=no (overrides --fs-fix)"))
  499. parser.add_argument("--no-progress", action="store_false", dest="progress", default=True,
  500. help=_("deactivate progress bar"))
  501. parser.add_argument("-v", "--verbose", action="store_true", default=False,
  502. help=_("Show what is currently done"))
  503. parser.add_argument("-V", "--version", action="version", version=ABOUT)
  504. group_saving = parser.add_argument_group("sources saving")
  505. group_saving.add_argument("--sources-save", action="store",
  506. help=_("Save source arguments"))
  507. group_saving.add_argument("--sources-replace", action="store",
  508. help=_("Save source arguments and replace memory if it already exists"))
  509. group_saving.add_argument("--sources-load", action="store",
  510. help=_("Load source arguments"))
  511. group_saving.add_argument("--sources-del", action="store",
  512. help=_("delete saved sources"))
  513. group_saving.add_argument("--sources-list", action="store_true", default=False,
  514. help=_("List names of saved sources"))
  515. group_saving.add_argument("--sources-full-list", action="store_true", default=False,
  516. help=_("List names of saved sources and files in it"))
  517. parser.add_argument_group(group_saving)
  518. (options, args) = parser.parse_known_args()
  519. options.directdir = False #True only in the special case: we are copying a dir and it doesn't exists
  520. #options check
  521. if options.progress and not pbar_available:
  522. warning (_("Progress bar is not available, deactivating"))
  523. options.progress = self.progress = False
  524. else:
  525. self.progress = options.progress
  526. if options.verbose:
  527. logging.getLogger().setLevel(logging.DEBUG)
  528. if options.no_fs_fix:
  529. options.fs_fix = 'no'
  530. if len(options.preserve):
  531. preserve = set(options.preserve.split(','))
  532. if not preserve.issubset(const_PRESERVE):
  533. error (_("Invalid --preserve value\nvalid values are:"))
  534. for value in const_PRESERVE:
  535. error('- %s' % value)
  536. exit(1)
  537. else:
  538. options.preserve = preserve
  539. else:
  540. options.preserve=set()
  541. self.__sourcesSaving(options, args)
  542. if len(args) == 2: #we check special cases
  543. src_path = os.path.abspath(os.path.expanduser(args[0]))
  544. dest_path = os.path.abspath(os.path.expanduser(args[1]))
  545. if os.path.isdir(src_path):
  546. options.dest_file = None #we are copying a dir, this options is for files only
  547. if not os.path.exists(dest_path):
  548. options.directdir = True #dest_dir doesn't exist, it's the directdir special case
  549. elif not os.path.exists(dest_path) or os.path.isfile(dest_path):
  550. options.dest_file = dest_path
  551. args[1] = os.path.dirname(dest_path)
  552. else:
  553. options.dest_file = None
  554. else:
  555. options.dest_file = None
  556. #if there is an other instance of gcp, we send options to it
  557. if not self._main_instance:
  558. info (_("There is already one instance of %s running, pluging to it") % NAME_SHORT)
  559. #XXX: we have to serialize data as dbus only accept valid unicode, and filenames
  560. # can have invalid unicode.
  561. return self.gcp_main.addArgs(pickle.dumps(os.getcwd()),pickle.dumps(full_args))
  562. else:
  563. if len(args) < 2:
  564. _error_msg = _("Wrong number of arguments")
  565. return (False, _error_msg)
  566. debug(_("adding args to gcp: %s") % args)
  567. self.__checkArgs(options, source_dir, args)
  568. if not self.__launched:
  569. self.journal = Journal()
  570. GObject.idle_add(self.__copyNextFile)
  571. self.__launched = True
  572. return (True,'')
  573. def _userInterruption(self):
  574. info(_("User interruption: good bye"))
  575. exit(1)
  576. def go(self):
  577. """Launch main loop"""
  578. self.loop = GObject.MainLoop()
  579. try:
  580. self.loop.run()
  581. except KeyboardInterrupt:
  582. self._userInterruption()
  583. if __name__ == "__main__":
  584. gcp = GCP()
  585. success,message = gcp.parseArguments()
  586. if not success:
  587. error(message)
  588. exit(1)
  589. if gcp._main_instance:
  590. gcp.go()
  591. if gcp.journal.failed:
  592. exit(1)
  593. if gcp.journal.partial:
  594. exit(2)