Goffi's cp, a fancy file copier
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

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