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.

gcp 29KB

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