Compare commits
97 Commits
Author | SHA1 | Date |
---|---|---|
Matteo Cypriani | 476f311559 | |
Matteo Cypriani | 1dee9e218f | |
Matteo Cypriani | 186e598a7d | |
Matteo Cypriani | 721632baf8 | |
Matteo Cypriani | 9469a59718 | |
Matteo Cypriani | 462676e803 | |
Matteo Cypriani | 8ce1846288 | |
Matteo Cypriani | 07f53e4109 | |
Matteo Cypriani | 35b8fed792 | |
Matteo Cypriani | 63870b25ed | |
Matteo Cypriani | 93c37d756d | |
Matteo Cypriani | 7bff021cd3 | |
Matteo Cypriani | 81bd453526 | |
Matteo Cypriani | 43da9301fa | |
Matteo Cypriani | 0995d580c9 | |
Matteo Cypriani | 007d04dd16 | |
Matteo Cypriani | 28e8089316 | |
Matteo Cypriani | 6dbb4152f5 | |
Matteo Cypriani | c3a865920e | |
Matteo Cypriani | 4a2c1224f9 | |
Matteo Cypriani | 7ba62cdfca | |
Matteo Cypriani | de8fe1900c | |
Matteo Cypriani | b2d16599a4 | |
Matteo Cypriani | b2dcbd8368 | |
Matteo Cypriani | c86dae222a | |
Matteo Cypriani | f99dcc1e2c | |
Matteo Cypriani | cbd96480c5 | |
Matteo Cypriani | c4db1808da | |
Matteo Cypriani | e17dbfa1c8 | |
Matteo Cypriani | b83d37a5e9 | |
Matteo Cypriani | 2e0e8ba994 | |
Matteo Cypriani | d40548f287 | |
Matteo Cypriani | 193a0e2784 | |
Matteo Cypriani | 307ffbea86 | |
Matteo Cypriani | 2136a91df4 | |
Matteo Cypriani | 1de548e62f | |
Matteo Cypriani | e008689b9b | |
Matteo Cypriani | 97f8f0d752 | |
Matteo Cypriani | f436127427 | |
Matteo Cypriani | 4c02959204 | |
Matteo Cypriani | 7a6317a5e6 | |
Matteo Cypriani | 7fa513de68 | |
Matteo Cypriani | cd317355f1 | |
Matteo Cypriani | ecfe87cd99 | |
Matteo Cypriani | b8fdd846a4 | |
Jingbei Li | 580776117f | |
Jingbei Li | 0d00e99525 | |
Jingbei Li | a6a5cf3a03 | |
Jingbei Li | 45905e8f35 | |
Jingbei Li | bdc1df9b6f | |
Jingbei Li | 774f371070 | |
Jingbei Li | aad446095c | |
Matteo Cypriani | 67469febfb | |
Matteo Cypriani | 8d60858375 | |
Goffi | 3da160e582 | |
Goffi | 0e2d0ceb37 | |
Goffi | d7e819b297 | |
Goffi | 4385301994 | |
Goffi | 7287b3d68b | |
Goffi | a4d71cb78e | |
Goffi | 335aefc5ed | |
Goffi | f2e43f64b6 | |
Goffi | 22799d148d | |
Goffi | fb6842153b | |
Goffi | 4271439cab | |
Goffi | bee2cd541d | |
Goffi | 81d6de4e04 | |
Goffi | 47edb60307 | |
Goffi | 58d89ee8c0 | |
Goffi | c651690091 | |
Goffi | 018978efa4 | |
Goffi | 17bc3cee4d | |
Goffi | 504f6b9758 | |
Goffi | 7e6efb49a9 | |
Goffi | 2f998e05a3 | |
Goffi | d33f55103c | |
Goffi | f8d10637a1 | |
Goffi | 6c85b2df75 | |
Thomas Preud'homme | 38e1b79ffa | |
Thomas Preud'homme | 8e600353d7 | |
Goffi | 5256579ac4 | |
Goffi | d5624e1af9 | |
Goffi | f61e0a823b | |
Goffi | 0dc39524d7 | |
Goffi | b339894ac4 | |
Goffi | ea46932039 | |
Goffi | 898b9469a1 | |
Goffi | 16019c583c | |
Goffi | fb2df7be19 | |
Goffi | cac1417bbc | |
Goffi | 043c3eeebb | |
Goffi | fd26af8192 | |
Goffi | 7226074954 | |
Goffi | 3df4c89e01 | |
Goffi | 7e6275ab07 | |
Goffi | ea3b75a565 | |
Goffi | 19e736d61c |
|
@ -0,0 +1,5 @@
|
|||
*.pyc
|
||||
*.pyv
|
||||
*.sw*
|
||||
*.tar*
|
||||
*.egg
|
|
@ -0,0 +1,41 @@
|
|||
gcp 0.2.1 (2019-03-10, Matteo Cypriani):
|
||||
- fix deprecation warnings with dbus.glib
|
||||
see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=923677
|
||||
- minor documentation (README) improvements
|
||||
|
||||
gcp 0.2.0 (2018-10-14, Matteo Cypriani):
|
||||
- actually switch to Python3
|
||||
- cp compatibibility:
|
||||
+ don't preserve any attributes by default (Jingbei Li)
|
||||
+ added -p switch (same as --preserve=mode,ownership,timestamps)
|
||||
+ added -R switch (same as --recursive)
|
||||
- new --fix-filenames option
|
||||
- reworked packaging
|
||||
|
||||
gcp 0.1.4.dev1 (unreleased, Jingbei Li):
|
||||
- main Python3 migration work
|
||||
|
||||
gcp 0.1.3 (2011-06-20, Goffi):
|
||||
- fixed exit status
|
||||
- updated manpage with exit status
|
||||
- gcp DIR1 DIR2 syntax fixed
|
||||
- tests
|
||||
|
||||
gcp 0.1.2 (2011-06-16, Goffi):
|
||||
- removed bad fd close
|
||||
- crash fix when source file can't be openned
|
||||
- symbolic link skipping (--dereferrence and --no-dereferrence options)
|
||||
- unaccessible source dir crash fix
|
||||
- os.stat precision fix
|
||||
- manpage
|
||||
- install script
|
||||
|
||||
gcp 0.1.1 (2010-09-30, Goffi):
|
||||
- double entry check in journal
|
||||
- unicode source_path send via dbus (second instance of gcp) fixed
|
||||
- errors are now shown after copy
|
||||
- fixed bad closure when a file already exists
|
||||
- added "gcp SOURCE_FILE DEST_FILE" syntax management
|
||||
|
||||
gcp 0.1 (2010-09-28, Goffi):
|
||||
- **INITIAL PUBLIC RELEASE**
|
95
README
95
README
|
@ -1,95 +0,0 @@
|
|||
gcp v0.1
|
||||
(c) Jérôme Poisson aka Goffi 2010
|
||||
|
||||
gcp (Goffi's cp) is a files copier.
|
||||
|
||||
|
||||
** LICENSE **
|
||||
|
||||
gcp is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
gcp is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with gcp. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
|
||||
** WTF ? **
|
||||
gcp is a file copier, loosely inspired from cp, but with high level functionalities like:
|
||||
- progression indicator
|
||||
- gcp continue copying even when there is an issue: he just skip the file with problem, and go on
|
||||
- journalization: gcp write what he is doing, this allow to know which files where effectively copied
|
||||
- fixing names to be compatible with the target filesystem (e.g. removing incompatible chars like "?" or "*" on vfat)
|
||||
- if you launch a copy when an other is already running, the files are added to the first queue, this avoid your hard drive to move its read/write head all the time
|
||||
- files saving: you can keep track of files you have copied, and re-copy them later (useful when, for example, you always copy some free music to all your friends).
|
||||
- gcp will be approximately option-compatible with cp (approximately because the behaviour is not exactly the same, see below)
|
||||
|
||||
/!\ WARNING /!\
|
||||
gcp is at an early stage of development, and really experimental: use at your own risks !
|
||||
|
||||
** How to use it ? **
|
||||
Pretty much like cp (see gcp --help).
|
||||
Please note that the behaviour is not exactly the same as cp, even if gcp want to be option-compatible. Mainly, the destination filenames can be changed (by default, can be deactivated).
|
||||
gcp doesn't implement yet all the options from cp, but it's planed.
|
||||
|
||||
** journalizaion **
|
||||
The journal is planed to be used by gcp itself, buts remains human-readable. It is located in ~/.gcp/journal
|
||||
|
||||
3 states are used:
|
||||
- OK means the file is copied and all operation were successful
|
||||
- PARTIAL means the file is copied, but something went wrong (e.g. changing the permissions of the file)
|
||||
- FAILED: the file is *not* copied
|
||||
|
||||
after the state, a list of things which went wront are show, separated by ", "
|
||||
|
||||
** What's next ? **
|
||||
|
||||
Several improvment are already planed
|
||||
- copy queue management (moving copy order)
|
||||
- advanced console interface
|
||||
- notification (xmpp and maybe mail) when a long copy is finished
|
||||
- retry for files which were not correctly copied
|
||||
- badly encoded unicode filenames fix
|
||||
- file copy integrity check
|
||||
|
||||
... and other are with a "maybe"
|
||||
- graphic interface
|
||||
- desktop (Kde, Gnome, XFCE, ...) integration
|
||||
- distant copy (ftp)
|
||||
- basic server mode, for copying files on network without the need of nfs or other heavy stuff
|
||||
|
||||
** Credits **
|
||||
|
||||
A big big thank to the authors/contributors of...
|
||||
|
||||
progressbar:
|
||||
gcp use ProgressBar (http://pypi.python.org/pypi/progressbar/2.2), a class coded by Nilton Volpato which allow the textual representation of progression.
|
||||
|
||||
GLib:
|
||||
This heavily used library is used here for the main loop, event catching, and for DBus. Get it at http://library.gnome.org/devel/glib/
|
||||
|
||||
DBus:
|
||||
This excellent IPC is in the heart of gcp. Get more information at www.freedesktop.org/wiki/Software/dbus
|
||||
|
||||
python and its amazing standard library:
|
||||
gcp was coded quickly for my own need thanks to this excellent and efficient language and its really huge standard library. Python can be download at www.python.org
|
||||
|
||||
If I forgot any credit, please contact me (mail below) to fix it.
|
||||
|
||||
|
||||
** Contact **
|
||||
|
||||
You can contact me at goffi@goffi.org .
|
||||
You'll find the latest version on my website: http://www.goffi.org (it's mainly in french, I will probably make a little part in english in the future).
|
||||
You can also have a look to my other main projects (and maybe to the smaller ones too ;) ):
|
||||
- lm (list movie): a tool to list movies using IMdB data, loosely inspired from ls
|
||||
- SàT: my main project, a jabber/XMPP client, which is a brick to many others things I have in mind
|
||||
|
||||
Don't hesitate to give feedback :)
|
|
@ -0,0 +1,167 @@
|
|||
gcp
|
||||
===
|
||||
|
||||
gcp is a user-friendly file copier written in Python. Its name used to stand
|
||||
for "Goffi's CoPier", but was changed into a recursive acronym: Gcp CoPier.
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
gcp is free software: you can redistribute it and/or modify it under the terms
|
||||
of the GNU General Public License as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
gcp is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a [copy of the GNU General Public License](LICENSE)
|
||||
along with gcp. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
About
|
||||
=====
|
||||
|
||||
gcp is a file copier, loosely inspired from cp, but with high level
|
||||
functionalities such as:
|
||||
|
||||
- **Progress bar.**
|
||||
- gcp **keeps copying** even when there is an issue: it just skips the file,
|
||||
logs an error and goes on.
|
||||
- **Logging**: gcp writes what it's doing to a log file; this allows you to
|
||||
know which files were effectively copied.
|
||||
- **Fixing file names** to be compatible with the target filesystem (e.g.
|
||||
removing incompatible chars like `?` or `*` on FAT).
|
||||
- **Queue**: if you launch a copy when another copy is already running, the
|
||||
files are added to the first queue; this optimizes hard drive head movement
|
||||
and filesystem fragmentation.
|
||||
- **Files saving**: you can keep track of the files you have copied, and copy
|
||||
them again later (useful when, for example, you copy some free music to your
|
||||
friends on a regular basis).
|
||||
- gcp will be **approximately option-compatible with (GNU) cp** (approximately
|
||||
because the behaviour is not exactly the same, see below).
|
||||
|
||||
**WARNING**: gcp is at a relatively early stage of development, use at your own
|
||||
risks!
|
||||
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
The Python way
|
||||
--------------
|
||||
|
||||
First, install the following packages on your system (these Debian packages
|
||||
names, they may be different on other distros/systems):
|
||||
- libdbus-1-dev
|
||||
- libdbus-glib-1-dev
|
||||
- libgirepository1.0-dev
|
||||
- libcairo2-dev
|
||||
- python3-cairo-dev
|
||||
|
||||
Then install gcp with pip:
|
||||
pip3 install gcp
|
||||
|
||||
On Debian-based systems
|
||||
-----------------------
|
||||
|
||||
apt install gcp
|
||||
|
||||
|
||||
How to use it?
|
||||
==============
|
||||
|
||||
Pretty much like cp (see `gcp --help` and `man gcp`).
|
||||
|
||||
Please note that the behaviour is not exactly the same as cp's, even if gcp
|
||||
aims to be option-compatible. Mainly, the destination filenames can be modified
|
||||
(cf. the `--fix-filenames` option).
|
||||
|
||||
gcp doesn't implement all the options GNU cp has yet, but it's a long-term
|
||||
goal.
|
||||
|
||||
|
||||
Logging
|
||||
=======
|
||||
|
||||
The log file is aimed to be used by gcp itself, buts remains human-readable. It
|
||||
is located in `~/.gcp/journal`.
|
||||
|
||||
3 states are used:
|
||||
- **OK** means the file was copied and all operation were successful.
|
||||
- **PARTIAL** means the file was copied, but something went wrong (file
|
||||
permissions could not be preserved, file name had to be changed, etc.).
|
||||
- **FAILED**: the file was *not* copied.
|
||||
|
||||
After the state, a list of things that went wrong is shown, separated by ", ".
|
||||
|
||||
|
||||
Contribution ideas
|
||||
==================
|
||||
|
||||
Here are some ideas for future developments:
|
||||
- handle XDG
|
||||
- copy queue management (moving copy order)
|
||||
- advanced console interface
|
||||
- notification (XMPP and maybe email) when a long copy is finished
|
||||
- retry for files that were not correctly copied
|
||||
- badly encoded unicode filenames fix
|
||||
- file copy integrity check
|
||||
|
||||
And in an even more distant future:
|
||||
- graphic interface
|
||||
- desktop (Kde, Gnome, XFCE...) integration
|
||||
- distant copy (FTP)
|
||||
- basic server mode, for copying files on network without the need of NFS or
|
||||
other heavy stuff
|
||||
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
A big big thanks to the authors/contributors of...
|
||||
|
||||
* **progressbar**:
|
||||
gcp uses [ProgressBar](https://pypi.python.org/pypi/progressbar), a class
|
||||
coded by Nilton Volpato that allows the textual representation of
|
||||
progression.
|
||||
|
||||
* **GLib**:
|
||||
This heavily used library is used here for the main loop, event catching, and
|
||||
for DBus. Get it at <https://developer.gnome.org/glib/>.
|
||||
|
||||
* **DBus**:
|
||||
This excellent IPC is ut the heart of gcp. Get more information at
|
||||
<https://www.freedesktop.org/wiki/Software/dbus/>.
|
||||
|
||||
* **Python** and its amazing standard library:
|
||||
gcp was coded quickly for my own needs thanks to this excellent and efficient
|
||||
language and its really huge standard library. Python can be download at
|
||||
<https://www.python.org/>.
|
||||
|
||||
If I forgot any credit, please contact me (email below) to fix that.
|
||||
|
||||
Big thanks to contributors and package maintainers.
|
||||
|
||||
|
||||
Contributors
|
||||
============
|
||||
|
||||
* Original author: Jérôme Poisson a.k.a. Goffi <goffi@goffi.org> 2010-2011.
|
||||
* Thomas Preud'homme <robotux@celest.fr> 2011: manpage, stat resolution fix.
|
||||
* Jingbei Li a.k.a. petronny 2016: conversion to Python3.
|
||||
* Matteo Cypriani <mcy@lm7.fr> 2018: `--fix-filenames` option, Python3 fixes.
|
||||
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
Feedback, bug reports, patches, etc. are welcome, either by email or on the
|
||||
repository's issue tracker <https://code.lm7.fr/mcy/gcp/issues>.
|
||||
|
||||
You can also have a look at Goffi's other main project, [Salut à
|
||||
Toi](https://www.salut-a-toi.org/) (SàT), a Jabber/XMPP-based multi-frontend,
|
||||
multipurpose communication tool.
|
||||
|
||||
Don't hesitate to give feedback :)
|
293
fr.po
293
fr.po
|
@ -1,38 +1,40 @@
|
|||
# Goffi's CoPier.
|
||||
# Copyright (C) 2010 Jérôme Poisson
|
||||
# gcp -- French translation file
|
||||
#
|
||||
# Copyright:
|
||||
# 2010 Jérôme Poisson (Goffi) <goffi@goffi.org>
|
||||
# 2018 Matteo Cypriani <mcy@lm7.fr>
|
||||
#
|
||||
# This file is distributed under the same license as the gcp package.
|
||||
# Jérôme Poisson (Goffi) <goffi@goffi.org>, 2010.
|
||||
# Goffi <goffi@goffi.org>, 2010.
|
||||
#, fuzzy
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1\n"
|
||||
"Project-Id-Version: 0.2.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-09-28 17:16+0800\n"
|
||||
"PO-Revision-Date: 2010-09-28 17:23+0800\n"
|
||||
"Last-Translator: Goffi <goffi@goffi.org>\n"
|
||||
"Language-Team: French <goffi@goffi.org>\n"
|
||||
"POT-Creation-Date: 2010-09-30 18:05+0800\n"
|
||||
"PO-Revision-Date: 2018-10-14 20:56+0200\n"
|
||||
"Last-Translator: Matteo Cypriani <mcy@lm7.fr>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.2\n"
|
||||
|
||||
#: gcp:43
|
||||
#: gcp:46
|
||||
msgid "Error during import"
|
||||
msgstr "Erreur pendant l'import de bibliothèques"
|
||||
|
||||
#: gcp:44
|
||||
#: gcp:47
|
||||
msgid "Please check dependecies:"
|
||||
msgstr "Merci de vérifier les dépendances"
|
||||
msgstr "Merci de vérifier les dépendances :"
|
||||
|
||||
#: gcp:50
|
||||
msgid ""
|
||||
"ProgressBar not available, please download it at http://pypi.python.org/pypi/"
|
||||
"progressbar"
|
||||
#: gcp:54
|
||||
msgid "ProgressBar not available, please download it at https://pypi.org/"
|
||||
msgstr ""
|
||||
"«ProgressBar» n'est pas disponible, merci de le télécharger à http://pypi."
|
||||
"python.org/pypi/progressbar"
|
||||
"ProgressBar n'est pas disponible, merci de le télécharger depuishttps://pypi."
|
||||
"org/"
|
||||
|
||||
#: gcp:51
|
||||
#: gcp:55
|
||||
msgid ""
|
||||
"Progress bar deactivated\n"
|
||||
"--\n"
|
||||
|
@ -40,221 +42,250 @@ msgstr ""
|
|||
"Barre de progression désactivée\n"
|
||||
"--\n"
|
||||
|
||||
#: gcp:62
|
||||
msgid ""
|
||||
"This program comes with ABSOLUTELY NO WARRANTY;\n"
|
||||
"This is free software, and you are welcome to redistribute it\n"
|
||||
"under certain conditions.\n"
|
||||
"---\n"
|
||||
"\n"
|
||||
"This software is an advanced file copier\n"
|
||||
"Get the latest version at http://www.goffi.org\n"
|
||||
msgstr ""
|
||||
"This program comes with ABSOLUTELY NO WARRANTY;\n"
|
||||
"This is free software, and you are welcome to redistribute it\n"
|
||||
"under certain conditions.\n"
|
||||
"---\n"
|
||||
"\n"
|
||||
"Ce logiciel est un copieur de fichiers avancé\n"
|
||||
"Vous pouvez télécharger la dernière version à http://www.goffi.org\n"
|
||||
|
||||
#: gcp:85
|
||||
#: gcp:90
|
||||
msgid "Init DbusObject..."
|
||||
msgstr "Initialisation de «DbusObject»"
|
||||
msgstr "Initialisation de DbusObject..."
|
||||
|
||||
#: gcp:105
|
||||
#: gcp:111
|
||||
msgid "INTERNAL ERROR: invalid arguments"
|
||||
msgstr "ERREUR INTERNE: arguments invalides"
|
||||
msgstr "ERREUR INTERNE : arguments invalides"
|
||||
|
||||
#: gcp:159
|
||||
#: gcp:116
|
||||
msgid "INTERNAL ERROR: invalid source_dir"
|
||||
msgstr "ERREUR INTERNE : chemin source invalide"
|
||||
|
||||
#: gcp:171
|
||||
msgid "/!\\ THE FOLLOWING FILES WERE *NOT* SUCCESSFULY COPIED:"
|
||||
msgstr "/!\\ LES FICHIERS SUIVANTS N'ONT *PAS* ÉTÉ COPIÉS :"
|
||||
|
||||
#: gcp:177
|
||||
msgid "The following files were copied, but some errors happened:"
|
||||
msgstr "Les fichiers suivant ont été copiés, mais des erreurs sont survenues :"
|
||||
|
||||
#: gcp:183
|
||||
#, python-format
|
||||
msgid "Please check journal: %s"
|
||||
msgstr "Merci de vérifier le journal : %s"
|
||||
|
||||
#: gcp:205
|
||||
msgid "gcp launched"
|
||||
msgstr "gcp lancé"
|
||||
|
||||
#: gcp:167
|
||||
#: gcp:213
|
||||
msgid "Init DBus..."
|
||||
msgstr "Initialisation de Dbus..."
|
||||
msgstr "Initialisation de DBus..."
|
||||
|
||||
#: gcp:197
|
||||
#: gcp:244
|
||||
msgid "Can't read mounts table"
|
||||
msgstr "Impossible de lire la table des montages"
|
||||
|
||||
#: gcp:204
|
||||
#: gcp:251
|
||||
#, python-format
|
||||
msgid "Adding to copy list: %(path)s ==> %(dest_path)s (%(fs_type)s)"
|
||||
msgstr "Ajout à la liste des copies: %(path)s ==> %(dest_path)s (%(fs_type)s)"
|
||||
msgstr "Ajout à la liste des copies : %(path)s ==> %(dest_path)s (%(fs_type)s)"
|
||||
|
||||
#: gcp:211
|
||||
#: gcp:258
|
||||
#, python-format
|
||||
msgid "Can't copy %(path)s: %(exception)s"
|
||||
msgstr "Impossible de copier %(path)s: %(exception)s"
|
||||
msgstr "Impossible de copier %(path)s : %(exception)s"
|
||||
|
||||
#: gcp:234
|
||||
#: gcp:286
|
||||
#, python-format
|
||||
msgid "Can't append %(path)s to copy list: %(exception)s"
|
||||
msgstr ""
|
||||
"Impossible d'ajouter %(path)s à la liste des fichiers à copier: %(exception)s"
|
||||
"Impossible d'ajouter %(path)s à la liste des fichiers à copier : "
|
||||
"%(exception)s"
|
||||
|
||||
#: gcp:243
|
||||
#: gcp:289
|
||||
#, python-format
|
||||
msgid "Can't access %(dirpath)s: %(exception)s"
|
||||
msgstr "Impossible d'accéder à %(dirpath)s : %(exception)s"
|
||||
|
||||
#: gcp:301
|
||||
#, python-format
|
||||
msgid "Invalid dest_path: %s"
|
||||
msgstr "Chemin de destination invalide: %s"
|
||||
msgstr "Chemin de destination invalide : %s"
|
||||
|
||||
#: gcp:248
|
||||
#: gcp:306
|
||||
#, python-format
|
||||
msgid "The path given in arg doesn't exist or is not accessible: %s"
|
||||
msgstr "Le chemin donné en argument n'existe pas ou n'est pas accessible: %s"
|
||||
msgstr "Le chemin donné en argument n'existe pas ou n'est pas accessible : %s"
|
||||
|
||||
#: gcp:253
|
||||
#: gcp:310
|
||||
#, python-format
|
||||
msgid "omitting directory \"%s\""
|
||||
msgstr "Répertoire \"%s\" ignoré"
|
||||
msgstr "répertoire \"%s\" ignoré"
|
||||
|
||||
#: gcp:270
|
||||
#: gcp:348
|
||||
#, python-format
|
||||
msgid "File [%s] already exists, skipping it !"
|
||||
msgid "File [%s] already exists, skipping it!"
|
||||
msgstr "Le fichier [%s] existe déjà, je le saute !"
|
||||
|
||||
#: gcp:284
|
||||
#: gcp:368
|
||||
#, python-format
|
||||
msgid "COPYING %(source)s ==> %(dest)s"
|
||||
msgstr "COPIE %(source)s ==> %(dest)s"
|
||||
|
||||
#: gcp:384
|
||||
#: gcp:462
|
||||
#, python-format
|
||||
msgid "%.2f PiB"
|
||||
msgstr "%.2f Pio"
|
||||
|
||||
#: gcp:386
|
||||
#: gcp:464
|
||||
#, python-format
|
||||
msgid "%.2f TiB"
|
||||
msgstr "%.2f Tio"
|
||||
|
||||
#: gcp:388
|
||||
#: gcp:466
|
||||
#, python-format
|
||||
msgid "%.2f GiB"
|
||||
msgstr "%.2f Gio"
|
||||
|
||||
#: gcp:390
|
||||
#: gcp:468
|
||||
#, python-format
|
||||
msgid "%.2f MiB"
|
||||
msgstr "%.2f Mio"
|
||||
|
||||
#: gcp:392
|
||||
#: gcp:470
|
||||
#, python-format
|
||||
msgid "%.2f KiB"
|
||||
msgstr "%.2f Kio"
|
||||
|
||||
#: gcp:394
|
||||
#: gcp:471
|
||||
#, python-format
|
||||
msgid "%i B"
|
||||
msgstr "%i o"
|
||||
|
||||
#: gcp:402 gcp:407
|
||||
#: gcp:479 gcp:485
|
||||
#, python-format
|
||||
msgid "Copying %s"
|
||||
msgstr "Copie de %s"
|
||||
|
||||
#: gcp:434 gcp:467
|
||||
#: gcp:515 gcp:548
|
||||
msgid ""
|
||||
"No saved sources with this name, check existing names with --sources-list"
|
||||
msgstr ""
|
||||
"Aucun sauvegarde de fichiers sources avec ce nom, veuillez vérifier les "
|
||||
"lists existantes avec --sources-list"
|
||||
"Aucune sauvegarde de fichiers sources avec ce nom, veuillez vérifier les "
|
||||
"listes existantes avec --sources-list"
|
||||
|
||||
#: gcp:444
|
||||
#: gcp:525
|
||||
msgid "Saved sources:"
|
||||
msgstr "Liste de sources sauvées:"
|
||||
msgstr "Liste de sources sauvées :"
|
||||
|
||||
#: gcp:458
|
||||
#: gcp:539
|
||||
msgid ""
|
||||
"There is already a saved sources with this name, skipping --sources-save"
|
||||
msgstr "Il y a déjà une liste de sources avec ce nom, --sources-save ignoré"
|
||||
|
||||
#: gcp:493
|
||||
msgid "copy directories recursively"
|
||||
msgstr "copie les répertoire récursivement"
|
||||
|
||||
#: gcp:496
|
||||
#: gcp:578
|
||||
msgid "force overwriting of existing files"
|
||||
msgstr "force le remplacement des fichiers déjà existants"
|
||||
msgstr "forcer le remplacement des fichiers déjà existants"
|
||||
|
||||
#: gcp:499
|
||||
msgid "preserve the specified attributes"
|
||||
msgstr "garde les attributs spécifiés"
|
||||
#: gcp:582
|
||||
msgid "always follow symbolic links in sources"
|
||||
msgstr "toujours suivre les liens symboliques"
|
||||
|
||||
#: gcp:502
|
||||
msgid "don't fixe name encoding errors"
|
||||
msgstr "Ne corrige pas les erreurs dans l'encodage des noms"
|
||||
#: gcp:586
|
||||
msgid "never follow symbolic links in sources"
|
||||
msgstr "ne pas suivre les liens symboliques"
|
||||
|
||||
#: gcp:505
|
||||
msgid "don't fixe filesystem name incompatibily"
|
||||
#: gcp:590
|
||||
#, python-format
|
||||
msgid "same as --preserve=%s"
|
||||
msgstr "raccourci pour --preserve=%s"
|
||||
|
||||
#: gcp:594
|
||||
#, python-format
|
||||
msgid ""
|
||||
"preserve specified attributes; accepted values: 'all', or "
|
||||
"one or more amongst %s"
|
||||
msgstr ""
|
||||
"Ne corrige pas les incompatibilités des noms pour le système de fichiers"
|
||||
"préserver les attributs spécifiés; valeurs acceptées : 'all' ou un ou "
|
||||
"plusieurs éléments parmi %s"
|
||||
|
||||
#: gcp:508
|
||||
msgid "deactivate progress bar"
|
||||
msgstr "désactive la barre de progression"
|
||||
#: gcp:599
|
||||
msgid "copy directories recursively"
|
||||
msgstr "copier les répertoire récursivement"
|
||||
|
||||
#: gcp:511
|
||||
msgid "Show what is currently done"
|
||||
msgstr "Affiche les opérations effectuées"
|
||||
#: gcp:603
|
||||
msgid "display what is being done"
|
||||
msgstr "afficher les opérations effectuées"
|
||||
|
||||
#: gcp:516
|
||||
msgid "Save source arguments"
|
||||
msgstr "Sauvegarde la liste des fichiers sources"
|
||||
|
||||
#: gcp:519
|
||||
msgid "Save source arguments and replace memory if it already exists"
|
||||
#: gcp:614
|
||||
msgid ""
|
||||
"fix file names incompatible with the destination file "
|
||||
"system (default: auto)"
|
||||
msgstr ""
|
||||
"Sauvegarde la liste des fichiers sources et la remplace si elle existe déjà"
|
||||
"corriger les noms de fichiers incompatibles avec le système de fichiers "
|
||||
"cible (défaut : auto)"
|
||||
|
||||
#: gcp:522
|
||||
msgid "Load source arguments"
|
||||
msgstr "Réutilise les fichiers sources à copier"
|
||||
|
||||
#: gcp:525
|
||||
msgid "delete saved sources"
|
||||
msgstr "Supprime la liste des fichiers sources"
|
||||
|
||||
#: gcp:528
|
||||
msgid "List names of saved sources"
|
||||
msgstr "Liste les noms des listes de fichiers sources"
|
||||
|
||||
#: gcp:531
|
||||
msgid "List names of saved sources and files in it"
|
||||
#: gcp:619
|
||||
msgid ""
|
||||
"[DEPRECATED] same as --fix-filename=no (overrides --fix-"
|
||||
"filenames)"
|
||||
msgstr ""
|
||||
"Liste les noms des listes de fichiers sources, en incluant les fichiers "
|
||||
"[OBSOLÈTE] identique à --fix-filenames=no (--fix-filenames sera ignoré)"
|
||||
|
||||
#: gcp:624
|
||||
msgid "disable progress bar"
|
||||
msgstr "désactiver la barre de progression"
|
||||
|
||||
#: gcp:631
|
||||
msgid "save sources arguments"
|
||||
msgstr "sauvegarder la liste des fichiers source"
|
||||
|
||||
#: gcp:635
|
||||
msgid "save sources arguments and replace memory if it already exists"
|
||||
msgstr ""
|
||||
"sauvegarder la liste des fichiers source et la remplacer si elle existe déjà"
|
||||
|
||||
#: gcp:639
|
||||
msgid "load sources arguments"
|
||||
msgstr "réutiliser les fichiers source à copier"
|
||||
|
||||
#: gcp:643
|
||||
msgid "delete saved sources list"
|
||||
msgstr "supprimer la liste des fichiers source"
|
||||
|
||||
#: gcp:647
|
||||
msgid "list names of saved sources"
|
||||
msgstr "afficher les noms des listes de fichiers source"
|
||||
|
||||
#: gcp:651
|
||||
msgid "list names of saved sources and files in it"
|
||||
msgstr ""
|
||||
"afficher les noms des listes de fichiers sources, en incluant les fichiers "
|
||||
"qu'elles contiennent"
|
||||
|
||||
#: gcp:539
|
||||
#: gcp:663
|
||||
msgid "Progress bar is not available, deactivating"
|
||||
msgstr "La barre de progression n'est pas disponible, désactivation"
|
||||
|
||||
#: gcp:549
|
||||
msgid ""
|
||||
"Invalide --preserve value\n"
|
||||
"valid values are:"
|
||||
msgstr ""
|
||||
"La valeur de «--preserve» est invalide\n"
|
||||
"Les valeurs valides sont:"
|
||||
#: gcp:687
|
||||
#, python-format
|
||||
msgid "Invalid --preserve value '%s'"
|
||||
msgstr "Valeur de --preserve invalide « %s »"
|
||||
|
||||
#: gcp:560
|
||||
#: gcp:714
|
||||
#, python-format
|
||||
msgid "There is already one instance of %s running, pluging to it"
|
||||
msgstr "Il y a déjà une instance de %s lancée, je m'y connecte"
|
||||
|
||||
#: gcp:566
|
||||
#: gcp:720
|
||||
msgid "Wrong number of arguments"
|
||||
msgstr "Nombre d'arguments invalide"
|
||||
|
||||
#: gcp:568
|
||||
#: gcp:722
|
||||
#, python-format
|
||||
msgid "adding args to gcp: %s"
|
||||
msgstr "ajout des arguments à gcp: %s"
|
||||
msgstr "ajout des arguments à gcp : %s"
|
||||
|
||||
#: gcp:577
|
||||
#: gcp:731
|
||||
msgid "User interruption: good bye"
|
||||
msgstr "Interruption par l'utilisateur: au revoir"
|
||||
msgstr "Interruption par l'utilisateur : au revoir"
|
||||
|
||||
#~ msgid "don't fixe name encoding errors"
|
||||
#~ msgstr "Ne corrige pas les erreurs dans l'encodage des noms"
|
||||
|
||||
#~ msgid "Creating directory %s"
|
||||
#~ msgstr "Création du répertoire %s"
|
||||
|
|
533
gcp
533
gcp
|
@ -1,9 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
gcp: Goffi's CoPier
|
||||
Copyright (C) 2010 Jérôme Poisson (goffi@goffi.org)
|
||||
gcp: Gcp CoPier
|
||||
Copyright (c) 2010, 2011 Jérôme Poisson <goffi@goffi.org>
|
||||
(c) 2011 Thomas Preud'homme <robotux@celest.fr>
|
||||
(c) 2016 Jingbei Li <i@jingbei.li>
|
||||
(c) 2018, 2019 Matteo Cypriani <mcy@lm7.fr>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -19,59 +21,60 @@ You should have received a copy of the GNU General Public License
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
### logging ###
|
||||
import logging
|
||||
from logging import debug, info, error, warning
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(message)s')
|
||||
###
|
||||
|
||||
import gettext
|
||||
gettext.install('gcp', "i18n", unicode=True)
|
||||
|
||||
import sys
|
||||
import os,os.path
|
||||
from optparse import OptionParser, OptionGroup #To be replaced by argparse ASAP
|
||||
import cPickle as pickle
|
||||
import os
|
||||
import os.path
|
||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||
import pickle
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
gettext.install('gcp', "i18n")
|
||||
|
||||
try:
|
||||
import gobject
|
||||
#DBus
|
||||
import dbus, dbus.glib
|
||||
from gi.repository import GLib
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
import dbus.service
|
||||
import dbus.mainloop.glib
|
||||
except ImportError,e:
|
||||
import dbus
|
||||
except ImportError as e:
|
||||
error(_("Error during import"))
|
||||
error(_("Please check dependecies:"),e)
|
||||
exit(2)
|
||||
error(_("Please check dependecies:"), e)
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed
|
||||
pbar_available=True
|
||||
except ImportError, e:
|
||||
info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
|
||||
except ImportError as e:
|
||||
info (_("ProgressBar not available, please download it at https://pypi.org/"))
|
||||
info (_('Progress bar deactivated\n--\n'))
|
||||
pbar_available=False
|
||||
|
||||
NAME = "gcp (Goffi's copier)"
|
||||
NAME = "gcp (Gcp CoPier)"
|
||||
NAME_SHORT = "gcp"
|
||||
VERSION = '0.1'
|
||||
|
||||
ABOUT = NAME+u" v"+VERSION+u""" (c) Jérôme Poisson (aka Goffi) 2010
|
||||
VERSION = '0.2.1'
|
||||
|
||||
ABOUT = NAME_SHORT + " " + VERSION + """
|
||||
---
|
||||
"""+NAME+u""" Copyright (C) 2010 Jérôme Poisson
|
||||
""" + _(u"""This program comes with ABSOLUTELY NO WARRANTY;
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions.
|
||||
---
|
||||
""" + NAME + """
|
||||
Copyright: 2010-2011 Jérôme Poisson <goffi@goffi.org>
|
||||
2011 Thomas Preud'homme <robotux@celest.fr>
|
||||
2016 Jingbei Li <i@jingbei.li>
|
||||
2018 Matteo Cypriani <mcy@lm7.fr>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; it is free software,
|
||||
and you are welcome to redistribute it under certain conditions.
|
||||
"""
|
||||
|
||||
This software is an advanced file copier
|
||||
Get the latest version at http://www.goffi.org
|
||||
""")
|
||||
|
||||
const_DBUS_INTERFACE = "org.goffi.gcp"
|
||||
const_DBUS_INTERFACE = "org.goffi.gcp"
|
||||
const_DBUS_PATH = "/org/goffi/gcp"
|
||||
const_BUFF_SIZE = 4096
|
||||
const_PRESERVE = set(['mode','ownership','timestamps'])
|
||||
const_PRESERVE_p = 'mode,ownership,timestamps'
|
||||
const_FS_FIX = set(['auto','force','no'])
|
||||
const_FILES_DIR = "~/.gcp"
|
||||
const_JOURNAL_PATH = const_FILES_DIR + "/journal"
|
||||
const_SAVED_LIST = const_FILES_DIR + "/saved_list"
|
||||
|
@ -84,7 +87,7 @@ class DbusObject(dbus.service.Object):
|
|||
dbus.service.Object.__init__(self, bus, path)
|
||||
debug(_("Init DbusObject..."))
|
||||
self.cb={}
|
||||
|
||||
|
||||
@dbus.service.method(const_DBUS_INTERFACE,
|
||||
in_signature='', out_signature='s')
|
||||
def getVersion(self):
|
||||
|
@ -94,21 +97,32 @@ class DbusObject(dbus.service.Object):
|
|||
|
||||
@dbus.service.method(const_DBUS_INTERFACE,
|
||||
in_signature='ss', out_signature='bs')
|
||||
def addArgs(self, source_path, args):
|
||||
def addArgs(self, source_dir, args):
|
||||
"""Add arguments to gcp as if there were entered on its own command line
|
||||
@param source_path: current working dir to use as base for arguments, as given by os.getcwd()
|
||||
@param source_dir: current working dir to use as base for arguments, as given by os.getcwd()
|
||||
@param args: serialized (wich pickle) list of strings - without command name -, as given by sys.argv[1:].
|
||||
@return: success (boolean) and error message if any (string)"""
|
||||
try:
|
||||
args = pickle.loads(str(args))
|
||||
except TypeError, pickle.UnpicklingError:
|
||||
except TypeError as e:
|
||||
pickle.UnpicklingError = e
|
||||
return (False, _("INTERNAL ERROR: invalid arguments"))
|
||||
return self._gcp.parseArguments(args, str(source_path))
|
||||
try:
|
||||
source_dir = pickle.loads(str(source_dir))
|
||||
except TypeError as e:
|
||||
pickle.UnpicklingError = e
|
||||
return (False, _("INTERNAL ERROR: invalid source_dir"))
|
||||
return self._gcp.parseArguments(args, source_dir)
|
||||
|
||||
|
||||
class Journal():
|
||||
|
||||
def __init__(self, path=const_JOURNAL_PATH):
|
||||
self.journal_path = os.path.expanduser(path)
|
||||
self.journal_fd = open(self.journal_path,'w') #TODO: check and maybe save previous journals
|
||||
self.__entry_open = None
|
||||
self.failed = []
|
||||
self.partial = []
|
||||
|
||||
def __del__(self):
|
||||
self.journal_fd.flush()
|
||||
|
@ -116,6 +130,8 @@ class Journal():
|
|||
|
||||
def startFile(self, source_path):
|
||||
"""Start an entry in the journal"""
|
||||
assert not self.__entry_open
|
||||
self.__entry_open = source_path
|
||||
self.journal_fd.write(source_path+"\n")
|
||||
self.journal_fd.flush()
|
||||
self.success=True
|
||||
|
@ -123,21 +139,49 @@ class Journal():
|
|||
|
||||
def closeFile(self):
|
||||
"""Close the entry in the journal"""
|
||||
assert self.__entry_open
|
||||
if not self.success:
|
||||
status = "FAILED"
|
||||
else:
|
||||
status = "OK" if not self.errors else "PARTIAL"
|
||||
self.journal_fd.write("%(status)s: %(errors)s\n" % {'status': status, 'errors': ', '.join(self.errors)})
|
||||
self.journal_fd.flush()
|
||||
self.__entry_open = None
|
||||
|
||||
def copyFailed(self):
|
||||
"""Must be called when something is wrong with the copy itself"""
|
||||
assert self.__entry_open
|
||||
self.success = False
|
||||
self.failed.append(self.__entry_open)
|
||||
|
||||
def error(self, name):
|
||||
"""Something went wrong"""
|
||||
assert self.__entry_open
|
||||
self.errors.append(name)
|
||||
|
||||
self.partial.append(self.__entry_open)
|
||||
|
||||
def showErrors(self):
|
||||
"""Show which files were not successfully copied"""
|
||||
failed = set(self.failed)
|
||||
partial = set(self.partial)
|
||||
for entry in failed:
|
||||
partial.discard(entry)
|
||||
|
||||
if failed:
|
||||
error(_("/!\\ THE FOLLOWING FILES WERE *NOT* SUCCESSFULY COPIED:"))
|
||||
#TODO: use logging capability to print all error message in red
|
||||
for entry in failed:
|
||||
info("\t- %s" % entry)
|
||||
info ('--\n')
|
||||
if partial:
|
||||
warning(_("The following files were copied, but some errors happened:"))
|
||||
for entry in partial:
|
||||
info("\t- %s" % entry)
|
||||
info ('--\n')
|
||||
|
||||
if failed or partial:
|
||||
info(_("Please check journal: %s") % self.journal_path)
|
||||
|
||||
|
||||
class GCP():
|
||||
|
||||
|
@ -153,7 +197,7 @@ class GCP():
|
|||
dbus_interface=const_DBUS_INTERFACE)
|
||||
self._main_instance = False
|
||||
|
||||
except dbus.exceptions.DBusException,e:
|
||||
except dbus.exceptions.DBusException as e:
|
||||
if e._dbus_error_name=='org.freedesktop.DBus.Error.ServiceUnknown':
|
||||
self.launchDbusMainInstance()
|
||||
debug (_("gcp launched"))
|
||||
|
@ -185,14 +229,15 @@ class GCP():
|
|||
|
||||
def __getMountPoints(self):
|
||||
"""Parse /proc/mounts to get currently mounted devices"""
|
||||
#TODO: reparse when a new device is added/a device is removed
|
||||
#(check freedesktop mounting signals)
|
||||
# TODO: reparse when a new device is added/a device is removed
|
||||
# (check freedesktop mounting signals)
|
||||
ret = {}
|
||||
try:
|
||||
with open("/proc/mounts",'rb') as mounts:
|
||||
with open("/proc/mounts",'r') as mounts:
|
||||
for line in mounts.readlines():
|
||||
fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno = line.split(' ')
|
||||
ret[fs_file] = fs_vfstype
|
||||
fs_spec, fs_file, fs_vfstype, \
|
||||
fs_mntops, fs_freq, fs_passno = line.split(' ')
|
||||
ret[fs_file] = fs_vfstype
|
||||
except:
|
||||
error (_("Can't read mounts table"))
|
||||
return ret
|
||||
|
@ -201,22 +246,22 @@ class GCP():
|
|||
"""Add a file to the copy list
|
||||
@param path: absolute path of file
|
||||
@param options: options as return by optparse"""
|
||||
debug (_("Adding to copy list: %(path)s ==> %(dest_path)s (%(fs_type)s)") % {"path":path.decode('utf-8','replace'),
|
||||
"dest_path":dest_path.decode('utf-8','replace'),
|
||||
"fs_type":self.getFsType(dest_path)} )
|
||||
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)})
|
||||
try:
|
||||
self.bytes_total+=os.path.getsize(path)
|
||||
self.copy_list.insert(0,(path, dest_path, options))
|
||||
except OSError,e:
|
||||
error(_("Can't copy %(path)s: %(exception)s") % {'path':path.decode('utf-8','replace'), 'exception':e.strerror})
|
||||
|
||||
except OSError as e:
|
||||
error(_("Can't copy %(path)s: %(exception)s")
|
||||
% {'path':path, 'exception':e.strerror})
|
||||
|
||||
def __appendDirToList(self, dirpath, dest_path, options):
|
||||
"""Add recursively directory to the copy list
|
||||
@param path: absolute path of dir
|
||||
@param options: options as return by optparse"""
|
||||
#We first check that the dest path exists, and create it if needed
|
||||
dest_path = self.__filename_fix(dest_path, options, no_journal=True)
|
||||
dest_path = self.__fix_filenames(dest_path, options, no_journal=True)
|
||||
if not os.path.exists(dest_path):
|
||||
debug ("Creating directory %s" % dest_path)
|
||||
os.makedirs(dest_path) #TODO: check permissions
|
||||
|
@ -225,70 +270,101 @@ class GCP():
|
|||
try:
|
||||
for filename in os.listdir(dirpath):
|
||||
filepath = os.path.join(dirpath,filename)
|
||||
if os.path.islink(filepath) and not options.dereference:
|
||||
debug ("Skippink symbolic dir: %s" % filepath)
|
||||
continue
|
||||
if os.path.isdir(filepath):
|
||||
full_dest_path = os.path.join(dest_path,filename)
|
||||
self.__appendDirToList(filepath, full_dest_path, options)
|
||||
else:
|
||||
self.__appendToList(filepath, dest_path, options)
|
||||
except OSError,e:
|
||||
error(_("Can't append %(path)s to copy list: %(exception)s") % {'path':filepath.decode('utf-8','replace'),
|
||||
'exception':e.strerror})
|
||||
except OSError as e:
|
||||
try:
|
||||
error(_("Can't append %(path)s to copy list: %(exception)s") % {'path':filepath, 'exception':e.strerror})
|
||||
except NameError:
|
||||
#We can't list the dir
|
||||
error(_("Can't access %(dirpath)s: %(exception)s") % {'dirpath':dirpath, 'exception':e.strerror})
|
||||
|
||||
def __checkArgs(self, options, source_path, args):
|
||||
"""Check thats args are files, and add them to copy list"""
|
||||
def __checkArgs(self, options, source_dir, args):
|
||||
"""Check thats args are files, and add them to copy list
|
||||
@param options: options sets
|
||||
@param source_dir: directory where the command was entered
|
||||
@parm args: args of the copy"""
|
||||
assert(len (args)>=2)
|
||||
len_args = len(args)
|
||||
try:
|
||||
dest_path = os.path.normpath(os.path.join(os.path.expanduser(source_path), args.pop()))
|
||||
except OSError,e:
|
||||
dest_path = os.path.normpath(os.path.join(source_dir, args.pop()))
|
||||
except OSError as e:
|
||||
error (_("Invalid dest_path: %s"),e)
|
||||
|
||||
|
||||
for path in args:
|
||||
abspath = os.path.normpath(os.path.join(os.path.expanduser(source_path), path))
|
||||
abspath = os.path.normpath(os.path.join(os.path.expanduser(source_dir), path))
|
||||
if not os.path.exists(abspath):
|
||||
warning(_("The path given in arg doesn't exist or is not accessible: %s") % abspath.decode('utf-8','replace'))
|
||||
warning(_("The path given in arg doesn't exist or is not accessible: %s") % abspath)
|
||||
else:
|
||||
if os.path.isdir(abspath):
|
||||
full_dest_path = dest_path if os.path.isabs(path) else os.path.normpath(os.path.join(dest_path, path))
|
||||
if not options.recursive:
|
||||
warning (_('omitting directory "%s"') % abspath.decode('utf-8','replace'))
|
||||
warning (_('omitting directory "%s"') % abspath)
|
||||
else:
|
||||
_basename=os.path.basename(os.path.normpath(path))
|
||||
full_dest_path = dest_path if options.directdir else os.path.normpath(os.path.join(dest_path, _basename))
|
||||
self.__appendDirToList(abspath, full_dest_path, options)
|
||||
else:
|
||||
self.__appendToList(abspath, dest_path, options)
|
||||
|
||||
def __copyNextFile(self):
|
||||
"""Take the last file in the list, and launch the copy using glib io_watch event
|
||||
@return: True a file was added, False else"""
|
||||
if self.copy_list:
|
||||
source_file, dest_path, options = self.copy_list.pop()
|
||||
self.journal.startFile(source_file)
|
||||
source_fd = open(source_file, 'rb')
|
||||
filename = os.path.basename(source_file)
|
||||
assert(filename)
|
||||
dest_file = self.__filename_fix(os.path.join(dest_path,filename),options)
|
||||
if os.path.exists(dest_file) and not options.force:
|
||||
warning (_("File [%s] already exists, skipping it !") % dest_file.decode('utf-8','replace'))
|
||||
return True
|
||||
try:
|
||||
dest_fd = open(dest_file, 'wb')
|
||||
except:
|
||||
self.journal.copyFailed()
|
||||
self.journal.error("can't open dest")
|
||||
self.journal.closeFile()
|
||||
source_fd.close()
|
||||
return True
|
||||
|
||||
gobject.io_add_watch(source_fd,gobject.IO_IN,self._copyFile,
|
||||
(dest_fd, options), priority=gobject.PRIORITY_DEFAULT)
|
||||
if not self.progress:
|
||||
info(_("COPYING %(source)s ==> %(dest)s") % {"source":source_path.decode('utf-8','replace'),
|
||||
"dest":dest_file.decode('utf-8','replace')})
|
||||
return True
|
||||
else:
|
||||
#Nothing left to copy, we quit
|
||||
"""Takes the last file in the list and launches the copy using glib
|
||||
io_watch event.
|
||||
@return: True a file was added, False otherwise."""
|
||||
if not self.copy_list:
|
||||
# Nothing left to copy, we quit
|
||||
if self.progress:
|
||||
self.__pbar_finish()
|
||||
self.journal.showErrors()
|
||||
self.loop.quit()
|
||||
return False
|
||||
|
||||
source_file, dest_path, options = self.copy_list.pop()
|
||||
self.journal.startFile(source_file)
|
||||
try:
|
||||
source_fd = open(source_file, 'rb')
|
||||
except:
|
||||
self.journal.copyFailed()
|
||||
self.journal.error("can't open source")
|
||||
self.journal.closeFile()
|
||||
return True
|
||||
|
||||
filename = os.path.basename(source_file)
|
||||
assert(filename)
|
||||
if options.dest_file:
|
||||
dest_file = self.__fix_filenames(options.dest_file, options)
|
||||
else:
|
||||
dest_file = self.__fix_filenames(os.path.join(dest_path, filename),
|
||||
options)
|
||||
if os.path.exists(dest_file) and not options.force:
|
||||
warning (_("File [%s] already exists, skipping it!") % dest_file)
|
||||
self.journal.copyFailed()
|
||||
self.journal.error("already exists")
|
||||
self.journal.closeFile()
|
||||
source_fd.close()
|
||||
return True
|
||||
|
||||
try:
|
||||
dest_fd = open(dest_file, 'wb')
|
||||
except:
|
||||
self.journal.copyFailed()
|
||||
self.journal.error("can't open dest")
|
||||
self.journal.closeFile()
|
||||
source_fd.close()
|
||||
return True
|
||||
|
||||
GLib.io_add_watch(source_fd, GLib.IO_IN,self._copyFile,
|
||||
(dest_fd, options),
|
||||
priority=GLib.PRIORITY_DEFAULT)
|
||||
if not self.progress:
|
||||
info(_("COPYING %(source)s ==> %(dest)s")
|
||||
% {"source":source_file, "dest":dest_file})
|
||||
return True
|
||||
|
||||
def __copyFailed(self, reason, source_fd, dest_fd):
|
||||
"""Write the failure in the journal and close files descriptors"""
|
||||
|
@ -297,8 +373,6 @@ class GCP():
|
|||
self.journal.closeFile()
|
||||
source_fd.close()
|
||||
dest_fd.close()
|
||||
|
||||
|
||||
|
||||
def _copyFile(self, source_fd, condition, data):
|
||||
"""Actually copy the file, callback used with io_add_watch
|
||||
|
@ -338,7 +412,7 @@ class GCP():
|
|||
except KeyboardInterrupt:
|
||||
self._userInterruption()
|
||||
|
||||
def __filename_fix(self, filename, options, no_journal=False):
|
||||
def __fix_filenames(self, filename, options, no_journal=False):
|
||||
"""Fix filenames incompatibilities/mistake according to options
|
||||
@param filename: full path to the file
|
||||
@param options: options as parsed on command line
|
||||
|
@ -346,7 +420,7 @@ class GCP():
|
|||
@return: fixed filename"""
|
||||
fixed_filename = filename
|
||||
|
||||
if self.getFsType(filename) == 'vfat' and options.fs_fix:
|
||||
if options.fix_filenames == 'force' or (options.fix_filenames == 'auto' and self.getFsType(filename) == 'vfat'):
|
||||
fixed_filename = filename.replace('\\','_')\
|
||||
.replace(':',';')\
|
||||
.replace('*','+')\
|
||||
|
@ -365,33 +439,31 @@ class GCP():
|
|||
|
||||
def __post_copy(self, source_file, dest_file, options):
|
||||
"""Do post copy traitement (mainly managing --preserve option)"""
|
||||
st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime = os.stat(source_file)
|
||||
st_file = os.stat(source_file)
|
||||
for preserve in options.preserve:
|
||||
try:
|
||||
if preserve == 'mode':
|
||||
os.chmod(dest_file, st_mode)
|
||||
os.chmod(dest_file, st_file.st_mode)
|
||||
elif preserve == 'ownership':
|
||||
os.chown(dest_file, st_uid, st_gid)
|
||||
os.chown(dest_file, st_file.st_uid, st_file.st_gid)
|
||||
elif preserve == 'timestamps':
|
||||
os.utime(dest_file, (st_atime, st_mtime))
|
||||
except OSError,e:
|
||||
os.utime(dest_file, (st_file.st_atime, st_file.st_mtime))
|
||||
except OSError as e:
|
||||
self.journal.error("preserve-"+preserve)
|
||||
|
||||
def __get_string_size(self, size):
|
||||
"""Return a nice string representation of a size"""
|
||||
|
||||
if size>=2**50:
|
||||
return _("%.2f PiB") % (float(size)/2**50)
|
||||
elif size>=2**40:
|
||||
return _("%.2f TiB") % (float(size)/2**40)
|
||||
elif size>=2**30:
|
||||
return _("%.2f GiB") % (float(size)/2**30)
|
||||
elif size>=2**20:
|
||||
return _("%.2f MiB") % (float(size)/2**20)
|
||||
elif size>=2**10:
|
||||
return _("%.2f KiB") % (float(size)/2**10)
|
||||
else:
|
||||
return _("%i B") % size
|
||||
if size >= 2**50:
|
||||
return _("%.2f PiB") % (float(size) / 2**50)
|
||||
if size >= 2**40:
|
||||
return _("%.2f TiB") % (float(size) / 2**40)
|
||||
if size >= 2**30:
|
||||
return _("%.2f GiB") % (float(size) / 2**30)
|
||||
if size >= 2**20:
|
||||
return _("%.2f MiB") % (float(size) / 2**20)
|
||||
if size >= 2**10:
|
||||
return _("%.2f KiB") % (float(size) / 2**10)
|
||||
return _("%i B") % size
|
||||
|
||||
def _pbar_update(self):
|
||||
"""Update progress bar position, create the bar if it doesn't exist"""
|
||||
|
@ -399,12 +471,16 @@ class GCP():
|
|||
try:
|
||||
if self.pbar.maxval != self.bytes_total:
|
||||
self.pbar.maxval = self.bytes_total
|
||||
self.pbar.widgets[0] = _("Copying %s") % self.__get_string_size(self.bytes_total)
|
||||
pbar_msg = _("Copying %s") % self.__get_string_size(self.bytes_total)
|
||||
self.pbar.widgets[0] = pbar_msg
|
||||
except AttributeError:
|
||||
if not self.bytes_total:
|
||||
#No progress bar if the files have a null size
|
||||
# No progress bar if the files have a null size
|
||||
return
|
||||
self.pbar = ProgressBar(self.bytes_total,[_("Copying %s") % self.__get_string_size(self.bytes_total)," ",Percentage()," ",Bar()," ",FileTransferSpeed()," ",ETA()])
|
||||
pbar_msg = _("Copying %s") % self.__get_string_size(self.bytes_total)
|
||||
self.pbar = ProgressBar(self.bytes_total,
|
||||
[pbar_msg, " ", Percentage(), " ", Bar(),
|
||||
" ", FileTransferSpeed(), " ", ETA()])
|
||||
self.pbar.start()
|
||||
self.pbar.update(self.bytes_copied)
|
||||
|
||||
|
@ -430,7 +506,7 @@ class GCP():
|
|||
saved_files={}
|
||||
|
||||
if options.sources_del:
|
||||
if not saved_files.has_key(options.sources_del):
|
||||
if options.sources_del not in saved_files:
|
||||
error(_("No saved sources with this name, check existing names with --sources-list"))
|
||||
else:
|
||||
del saved_files[options.sources_del]
|
||||
|
@ -439,10 +515,9 @@ class GCP():
|
|||
if not args:
|
||||
exit(0)
|
||||
|
||||
|
||||
if options.sources_list or options.sources_full_list:
|
||||
info(_('Saved sources:'))
|
||||
sources = saved_files.keys()
|
||||
sources = list(saved_files.keys())
|
||||
sources.sort()
|
||||
for source in sources:
|
||||
info("\t[%s]" % source)
|
||||
|
@ -454,16 +529,16 @@ class GCP():
|
|||
exit(0)
|
||||
|
||||
if options.sources_save or options.sources_replace:
|
||||
if saved_files.has_key(options.sources_save) and not options.sources_replace:
|
||||
error(_("There is already a saved sources with this name, skipping --sources-save"))
|
||||
if options.sources_save in saved_files and not options.sources_replace:
|
||||
error(_("There is already a saved sources with this name, skipping --sources-save"))
|
||||
else:
|
||||
if len(args)>1:
|
||||
saved_files[options.sources_save] = map(os.path.abspath,args[:-1])
|
||||
saved_files[options.sources_save] = list(map(os.path.abspath,args[:-1]))
|
||||
with open(os.path.expanduser(const_SAVED_LIST),'w') as saved_fd:
|
||||
pickle.dump(saved_files,saved_fd)
|
||||
|
||||
if options.sources_load:
|
||||
if not saved_files.has_key(options.sources_load):
|
||||
if options.sources_load not in saved_files:
|
||||
error(_("No saved sources with this name, check existing names with --sources-list"))
|
||||
else:
|
||||
saved_args = saved_files[options.sources_load]
|
||||
|
@ -471,70 +546,113 @@ class GCP():
|
|||
for arg in saved_args:
|
||||
args.insert(0,arg)
|
||||
|
||||
def parseArguments(self, full_args=sys.argv[1:], source_path = os.getcwd()):
|
||||
def parseArguments(self, full_args=sys.argv[1:], source_dir = os.getcwd()):
|
||||
"""Parse arguments and add files to queue
|
||||
@param full_args: list of arguments strings (without program name)
|
||||
@param source_path: path from where the arguments come, ad given by os.getcwd()
|
||||
@param source_dir: path from where the arguments come, as given by os.getcwd()
|
||||
@return: a tuple (boolean, message) where the boolean is the success of the arguments
|
||||
validation, and message is the error message to print when necessary"""
|
||||
_usage="""
|
||||
%prog [options] FILE1 [FILE2 ...] DEST
|
||||
|
||||
%prog --help for options list
|
||||
%(prog)s [options] FILE DEST
|
||||
%(prog)s [options] FILE1 [FILE2 ...] DEST-DIR
|
||||
"""
|
||||
for idx in range(len(full_args)):
|
||||
if isinstance(full_args[idx], unicode):
|
||||
#We don't want unicode as some filenames can be invalid unicode
|
||||
full_args[idx] = full_args[idx].encode('utf-8')
|
||||
|
||||
parser = OptionParser(usage=_usage,version=ABOUT)
|
||||
full_args[idx] = full_args[idx].encode('utf-8')
|
||||
|
||||
parser.add_option("-r", "--recursive", action="store_true", default=False,
|
||||
help=_("copy directories recursively"))
|
||||
|
||||
parser.add_option("-f", "--force", action="store_true", default=False,
|
||||
help=_("force overwriting of existing files"))
|
||||
parser = ArgumentParser(usage=_usage,
|
||||
formatter_class=RawDescriptionHelpFormatter)
|
||||
|
||||
parser.add_option("--preserve", action="store", default='mode,ownership,timestamps',
|
||||
help=_("preserve the specified attributes"))
|
||||
|
||||
#parser.add_option("--no-unicode-fix", action="store_false", dest='unicode_fix', default=True,
|
||||
# help=_("don't fixe name encoding errors")) #TODO
|
||||
parser.add_argument("-V", "--version",
|
||||
action="version", version=ABOUT
|
||||
)
|
||||
|
||||
parser.add_option("--no-fs-fix", action="store_false", dest='fs_fix', default=True,
|
||||
help=_("don't fixe filesystem name incompatibily"))
|
||||
group_cplike = parser.add_argument_group("cp-like options")
|
||||
group_cplike.add_argument("-f", "--force",
|
||||
action="store_true", default=False,
|
||||
help=_("force overwriting of existing files")
|
||||
)
|
||||
group_cplike.add_argument("-L", "--dereference",
|
||||
action="store_true", default=False,
|
||||
help=_("always follow symbolic links in sources")
|
||||
)
|
||||
group_cplike.add_argument("-P", "--no-dereference",
|
||||
action="store_false", dest='dereference',
|
||||
help=_("never follow symbolic links in sources")
|
||||
)
|
||||
group_cplike.add_argument("-p",
|
||||
action="store_true", default=False,
|
||||
help=_("same as --preserve=%s" % const_PRESERVE_p)
|
||||
)
|
||||
group_cplike.add_argument("--preserve",
|
||||
action="store", default='',
|
||||
help=_("preserve specified attributes; accepted values: \
|
||||
'all', or one or more amongst %s") % str(const_PRESERVE)
|
||||
)
|
||||
group_cplike.add_argument("-r", "-R", "--recursive",
|
||||
action="store_true", default=False,
|
||||
help=_("copy directories recursively")
|
||||
)
|
||||
group_cplike.add_argument("-v", "--verbose",
|
||||
action="store_true", default=False,
|
||||
help=_("display what is being done")
|
||||
)
|
||||
parser.add_argument_group(group_cplike)
|
||||
|
||||
parser.add_option("--no-progress", action="store_false", dest="progress", default=True,
|
||||
help=_("deactivate progress bar"))
|
||||
|
||||
parser.add_option("-v", "--verbose", action="store_true", default=False,
|
||||
help=_("Show what is currently done"))
|
||||
group_gcpspecific = parser.add_argument_group("gcp-specific options")
|
||||
#parser.add_argument("--no-unicode-fix",
|
||||
# action="store_false", dest='unicode_fix', default=True,
|
||||
# help=_("don't fix name encoding errors") #TODO
|
||||
#)
|
||||
group_gcpspecific.add_argument("--fix-filenames",
|
||||
choices = const_FS_FIX, dest='fix_filenames', default='auto',
|
||||
help=_("fix file names incompatible with the destination \
|
||||
file system (default: auto)")
|
||||
)
|
||||
group_gcpspecific.add_argument("--no-fs-fix",
|
||||
action="store_true", dest='no_fs_fix', default=False,
|
||||
help=_("[DEPRECATED] same as --fix-filename=no (overrides \
|
||||
--fix-filenames)")
|
||||
)
|
||||
group_gcpspecific.add_argument("--no-progress",
|
||||
action="store_false", dest="progress", default=True,
|
||||
help=_("disable progress bar")
|
||||
)
|
||||
parser.add_argument_group(group_gcpspecific)
|
||||
|
||||
group_saving = OptionGroup(parser, "sources saving")
|
||||
|
||||
group_saving.add_option("--sources-save", action="store",
|
||||
help=_("Save source arguments"))
|
||||
group_saving = parser.add_argument_group("sources saving")
|
||||
group_saving.add_argument("--sources-save",
|
||||
action="store",
|
||||
help=_("save sources arguments")
|
||||
)
|
||||
group_saving.add_argument("--sources-replace",
|
||||
action="store",
|
||||
help=_("save sources arguments and replace memory if it already exists")
|
||||
)
|
||||
group_saving.add_argument("--sources-load",
|
||||
action="store",
|
||||
help=_("load sources arguments")
|
||||
)
|
||||
group_saving.add_argument("--sources-del",
|
||||
action="store",
|
||||
help=_("delete saved sources list")
|
||||
)
|
||||
group_saving.add_argument("--sources-list",
|
||||
action="store_true", default=False,
|
||||
help=_("list names of saved sources")
|
||||
)
|
||||
group_saving.add_argument("--sources-full-list",
|
||||
action="store_true", default=False,
|
||||
help=_("list names of saved sources and files in it")
|
||||
)
|
||||
parser.add_argument_group(group_saving)
|
||||
|
||||
group_saving.add_option("--sources-replace", action="store",
|
||||
help=_("Save source arguments and replace memory if it already exists"))
|
||||
(options, args) = parser.parse_known_args()
|
||||
|
||||
group_saving.add_option("--sources-load", action="store",
|
||||
help=_("Load source arguments"))
|
||||
|
||||
group_saving.add_option("--sources-del", action="store",
|
||||
help=_("delete saved sources"))
|
||||
|
||||
group_saving.add_option("--sources-list", action="store_true", default=False,
|
||||
help=_("List names of saved sources"))
|
||||
|
||||
group_saving.add_option("--sources-full-list", action="store_true", default=False,
|
||||
help=_("List names of saved sources and files in it"))
|
||||
# True only in the special case: we are copying a dir and it doesn't
|
||||
# exists:
|
||||
options.directdir = False
|
||||
|
||||
parser.add_option_group(group_saving)
|
||||
|
||||
|
||||
(options, args) = parser.parse_args(full_args)
|
||||
#options check
|
||||
# options check
|
||||
if options.progress and not pbar_available:
|
||||
warning (_("Progress bar is not available, deactivating"))
|
||||
options.progress = self.progress = False
|
||||
|
@ -544,32 +662,62 @@ class GCP():
|
|||
if options.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
preserve = set(options.preserve.split(','))
|
||||
if not preserve.issubset(const_PRESERVE):
|
||||
error (_("Invalide --preserve value\nvalid values are:"))
|
||||
for value in const_PRESERVE:
|
||||
error('- %s' % value)
|
||||
exit(2)
|
||||
else:
|
||||
options.preserve = preserve
|
||||
if options.no_fs_fix:
|
||||
options.fix_filenames = 'no'
|
||||
|
||||
preserve = set()
|
||||
|
||||
if options.p:
|
||||
preserve.update(const_PRESERVE_p.split(','))
|
||||
|
||||
if options.preserve:
|
||||
preserve.update(options.preserve.split(','))
|
||||
preserve_all = False
|
||||
for value in preserve:
|
||||
if value == 'all':
|
||||
preserve_all = True
|
||||
continue
|
||||
if value not in const_PRESERVE:
|
||||
error (_("Invalid --preserve value '%s'") % value)
|
||||
exit(1)
|
||||
if preserve_all:
|
||||
preserve.remove('all')
|
||||
preserve.update(const_PRESERVE)
|
||||
|
||||
options.preserve = preserve
|
||||
|
||||
self.__sourcesSaving(options, args)
|
||||
|
||||
|
||||
if len(args) == 2: #we check special cases
|
||||
src_path = os.path.abspath(os.path.expanduser(args[0]))
|
||||
dest_path = os.path.abspath(os.path.expanduser(args[1]))
|
||||
if os.path.isdir(src_path):
|
||||
options.dest_file = None #we are copying a dir, this options is for files only
|
||||
if not os.path.exists(dest_path):
|
||||
options.directdir = True #dest_dir doesn't exist, it's the directdir special case
|
||||
elif not os.path.exists(dest_path) or os.path.isfile(dest_path):
|
||||
options.dest_file = dest_path
|
||||
args[1] = os.path.dirname(dest_path)
|
||||
else:
|
||||
options.dest_file = None
|
||||
else:
|
||||
options.dest_file = None
|
||||
|
||||
#if there is an other instance of gcp, we send options to it
|
||||
if not self._main_instance:
|
||||
info (_("There is already one instance of %s running, pluging to it") % NAME_SHORT)
|
||||
#XXX: we have to serialize data as dbus only accept valid unicode, and filenames
|
||||
# can have invalid unicode.
|
||||
return self.gcp_main.addArgs(os.getcwd(),pickle.dumps(full_args))
|
||||
return self.gcp_main.addArgs(pickle.dumps(os.getcwd()),pickle.dumps(full_args))
|
||||
else:
|
||||
if len(args) < 2:
|
||||
_error_msg = _("Wrong number of arguments")
|
||||
return (False, _error_msg)
|
||||
debug(_("adding args to gcp: %s") % str(args).decode('utf-8','replace'))
|
||||
self.__checkArgs(options, source_path, args)
|
||||
debug(_("adding args to gcp: %s") % args)
|
||||
self.__checkArgs(options, source_dir, args)
|
||||
if not self.__launched:
|
||||
self.journal = Journal()
|
||||
gobject.idle_add(self.__copyNextFile)
|
||||
GLib.idle_add(self.__copyNextFile)
|
||||
self.__launched = True
|
||||
return (True,'')
|
||||
|
||||
|
@ -579,7 +727,7 @@ class GCP():
|
|||
|
||||
def go(self):
|
||||
"""Launch main loop"""
|
||||
self.loop = gobject.MainLoop()
|
||||
self.loop = GLib.MainLoop()
|
||||
try:
|
||||
self.loop.run()
|
||||
except KeyboardInterrupt:
|
||||
|
@ -594,4 +742,7 @@ if __name__ == "__main__":
|
|||
exit(1)
|
||||
if gcp._main_instance:
|
||||
gcp.go()
|
||||
|
||||
if gcp.journal.failed:
|
||||
exit(1)
|
||||
if gcp.journal.partial:
|
||||
exit(2)
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
.\" Hey, EMACS: -*- nroff -*-
|
||||
.\" First parameter, NAME, should be all caps
|
||||
.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
|
||||
.\" other parameters are allowed: see man(7), man(1)
|
||||
.TH GCP 1 "October 14, 2018"
|
||||
.\" Please adjust this date whenever revising the manpage.
|
||||
.\"
|
||||
.\" Some roff macros, for reference:
|
||||
.\" .nh disable hyphenation
|
||||
.\" .hy enable hyphenation
|
||||
.\" .ad l left justify
|
||||
.\" .ad b justify to both left and right margins
|
||||
.\" .nf disable filling
|
||||
.\" .fi enable filling
|
||||
.\" .br insert line break
|
||||
.\" .sp <n> insert n+1 empty lines
|
||||
.\" for manpage-specific macros, see man(7)
|
||||
.\" TeX users may be more comfortable with the \fB<whatever>\fP and
|
||||
.\" \fI<whatever>\fP escape sequences to invode bold face and italics,
|
||||
.\" respectively.
|
||||
.SH NAME
|
||||
gcp \- Advanced command-line file copier
|
||||
.SH SYNOPSIS
|
||||
.B gcp
|
||||
.RI [ OPTIONS ]
|
||||
.I FILE DEST
|
||||
.br
|
||||
.B gcp
|
||||
.RI [ OPTIONS ]
|
||||
.I FILE1
|
||||
.RI [ FILE2 ... ]
|
||||
.I DEST-DIR
|
||||
.SH DESCRIPTION
|
||||
\fBgcp\fP is a file copier, loosely inspired by cp, but with high level
|
||||
functionalities like:
|
||||
.IP \(bu 2
|
||||
transfer progression indication
|
||||
.IP \(bu
|
||||
continuous copying when there is an issue: it skips the problematic file and
|
||||
goes on
|
||||
.IP \(bu
|
||||
copy status logging: which files were effectively copied
|
||||
.IP \(bu
|
||||
name mangling to handle target filesystem limitations (e.g. removing
|
||||
incompatible chars like "?" or "*" on FAT filesystems)
|
||||
.IP \(bu
|
||||
forced copy serialization: new files to copy are added to a global queue to
|
||||
avoid hard drive head seeks
|
||||
.IP \(bu
|
||||
transfer list management: gcp can save a list of files to copy and reuse it
|
||||
later
|
||||
.IP \(bu
|
||||
approximate option compatibility with cp (approximate because the behaviour is
|
||||
not exactly the same, see below)
|
||||
.SH OPTIONS
|
||||
These programs follow the usual GNU command line syntax, with long
|
||||
options starting with two dashes (`-').
|
||||
.PP
|
||||
A summary of options is included below.
|
||||
.SS General options
|
||||
.TP
|
||||
.B \-h, \-\-help
|
||||
Show summary of options.
|
||||
.TP
|
||||
.B \-V, \-\-version
|
||||
Show program version and copyright information and exit.
|
||||
.SS cp-like options
|
||||
.TP
|
||||
.B \-f, \-\-force
|
||||
Overwrite existing files.
|
||||
.TP
|
||||
.B \-L, \-\-dereference
|
||||
Always follow symbolic links in sources.
|
||||
.TP
|
||||
.B \-P, \-\-no\-dereference
|
||||
Never follow symbolic links in sources.
|
||||
.TP
|
||||
.B \-p
|
||||
Same as \fB\-\-preserve=mode,ownership,timestamps\fP
|
||||
.TP
|
||||
.B \-\-preserve=<\fIattributes\fP>
|
||||
Preserve specified attributes. Attributes can be \fImode\fP, \fIownership\fP
|
||||
and \fItimestamps\fP.
|
||||
When several attributes are passed, they need to be separated by commas.
|
||||
Please note that timestamps preservation has some limits, see section
|
||||
\fILIMITATIONS\fP.
|
||||
.TP
|
||||
.B \-r, \-R, \-\-recursive
|
||||
Copy directories recursively.
|
||||
.TP
|
||||
.B \-v, \-\-verbose
|
||||
Display what is being done.
|
||||
.SS gcp-specific options
|
||||
.TP
|
||||
.B \-\-fix\-filenames=<\fIforce\fP|\fIauto\fP|\fIno\fP>
|
||||
gcp has the ability to modify the destination file name if the target file
|
||||
system would not accept the original file name.
|
||||
Offending characters will be replaced with similar-looking ones.
|
||||
.IP
|
||||
This option accept the following values:
|
||||
.RS
|
||||
.TP
|
||||
\fIauto\fP (default)
|
||||
gcp will attempt to be smart, i.e. detect incompatibilities and fix them as-needed.
|
||||
.TP
|
||||
\fIforce\fP
|
||||
Always fix file names that could cause problems on any known filesystem or OS.
|
||||
This is useful e.g. with NTFS, see \fINOTE ON NTFS\fP below.
|
||||
.TP
|
||||
\fIno\fP
|
||||
Renaming is disabled entirely.
|
||||
.RE
|
||||
.IP
|
||||
Currently, gcp is only aware of FAT incompatibilities:
|
||||
\'\\\', \':\', \'*\', \'?\', \'"\', \'<\', \'>\' and \'|\'.
|
||||
.TP
|
||||
.B \-\-no\-fs\-fix (DEPRECATED)
|
||||
Same as \fB\-\-fix\-filenames=no\fP.
|
||||
This option will be removed in a future release.
|
||||
.TP
|
||||
.B \-\-no\-progress
|
||||
Disable progress bar.
|
||||
.SS Sources saving
|
||||
.TP
|
||||
.B \-\-sources\-save=\fISOURCES\fP
|
||||
Save the list of source files in a list named \fISOURCES\fP.
|
||||
.TP
|
||||
.B \-\-sources\-replace=\fISOURCES\fP
|
||||
Save the list of source files in a list named \fISOURCES\fP;
|
||||
the file is overwritten it already exists.
|
||||
.TP
|
||||
.B \-\-sources\-load=\fISOURCES\fP
|
||||
Use the list of source files named \fISOURCES\fP.
|
||||
.TP
|
||||
.B \-\-sources\-del=\fISOURCES\fP
|
||||
Delete the list of source files named \fISOURCES\fP.
|
||||
.TP
|
||||
.B \-\-sources\-list
|
||||
List the names of source file lists.
|
||||
.TP
|
||||
.B \-\-sources\-full\-list
|
||||
List the names of source file lists, including their content.
|
||||
.SH EXIT STATUS
|
||||
The exit status can be:
|
||||
.IP \[bu] 2
|
||||
\fB0\fP if files have been copied correctly or if another instance of gcp is
|
||||
already running and will do the copy.
|
||||
.IP \[bu]
|
||||
\fB1\fP if at least one file has not been copied, or if something went wrong.
|
||||
.IP \[bu]
|
||||
\fB2\fP if all files have been copied but with some issues
|
||||
.SH LIMITATIONS
|
||||
Timestamps preservation with \fB\-\-preserve\fP option is limited by the
|
||||
\fIos\fP Python module on POSIX systems. Currently, Python only returns
|
||||
timestamps in float format, which is a smaller precision than what POSIX
|
||||
provides. Progress on this issue can be seen at
|
||||
http://bugs.python.org/issue11457.
|
||||
.PP
|
||||
The \fB\-\-preserve\fP option cannot currently be used without an attribute
|
||||
list (\fBgcp \-\-preserve foo bar\fP will behave as \fBgcp \-\-preserve=foo
|
||||
bar\fP). Use the \fB\-p\fP switch instead.
|
||||
.SH NOTE ON NTFS
|
||||
NTFS will not enforce the same file name limitations than FAT, but files that
|
||||
would not be accepted on a FAT filesystem will still cause problems on Windows.
|
||||
Hence, it is recommended to use \-\-fix-filenames=force when copying to NTFS
|
||||
(when Windows compatibility is desired, anyway).
|
||||
.SH SEE ALSO
|
||||
.BR cp (1).
|
||||
.br
|
||||
.SH AUTHOR
|
||||
gcp was written by Jérôme Poisson <goffi@goffi.org>.
|
||||
It is currently maintained by Matteo Cypriani <mcy@lm7.fr>.
|
||||
.PP
|
||||
This manual page was initially written by Thomas Preud'homme
|
||||
<robotux@celest.fr> for the Debian project (and may be used by others).
|
164
gcp.po
164
gcp.po
|
@ -1,164 +0,0 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-09-27 13:03+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: gcp:43
|
||||
msgid "Error during import"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:44
|
||||
msgid "Please check dependecies:"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:50
|
||||
msgid ""
|
||||
"ProgressBar not available, please download it at http://pypi.python.org/pypi/"
|
||||
"progressbar"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:51
|
||||
msgid ""
|
||||
"Progress bar deactivated\n"
|
||||
"--\n"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:62
|
||||
msgid ""
|
||||
"This program comes with ABSOLUTELY NO WARRANTY;\n"
|
||||
"This is free software, and you are welcome to redistribute it\n"
|
||||
"under certain conditions.\n"
|
||||
"---\n"
|
||||
"\n"
|
||||
"This software is an advanced file copier\n"
|
||||
"Get the latest version at http://www.goffi.org\n"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:82
|
||||
msgid "Init DbusObject..."
|
||||
msgstr ""
|
||||
|
||||
#: gcp:102
|
||||
msgid "INTERNAL ERROR: invalid arguments"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:119
|
||||
msgid "gcp launched"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:126
|
||||
msgid "Init DBus..."
|
||||
msgstr ""
|
||||
|
||||
#: gcp:155
|
||||
msgid "Can't read mounts table"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:162
|
||||
#, python-format
|
||||
msgid "Adding to copy list: %(path)s ==> %(dest_path)s (%(fs_type)s)"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:170 gcp:192
|
||||
#, python-format
|
||||
msgid "Can't copy %(path)s: %(exception)s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:179
|
||||
#, python-format
|
||||
msgid "Creating directory %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:200
|
||||
#, python-format
|
||||
msgid "Invalid dest_path: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:205
|
||||
#, python-format
|
||||
msgid "The path given in arg doesn't exist or is not accessible: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:210
|
||||
#, python-format
|
||||
msgid "omitting directory \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: gcp:226
|
||||
#, python-format
|
||||
msgid "File [%s] already exists, skipping it !"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:233
|
||||
#, python-format
|
||||
msgid "COPYING %(source)s ==> %(dest)s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:284
|
||||
msgid "Progress: "
|
||||
msgstr ""
|
||||
|
||||
#: gcp:315
|
||||
msgid "copy directories recursively"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:318
|
||||
msgid "force overwriting of existing files"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:321
|
||||
msgid "preserve the specified attributes"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:324
|
||||
msgid "don't fixe name encoding errors"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:327
|
||||
msgid "deactivate progress bar"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:330
|
||||
msgid "Show what is currently done"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:335
|
||||
msgid "Progress bar is not available, deactivating"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:345
|
||||
msgid ""
|
||||
"Invalide --preserve value\n"
|
||||
"valid values are:"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:354
|
||||
#, python-format
|
||||
msgid "There is already one instance of %s running, pluging to it"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:360
|
||||
msgid "Wrong number of arguments"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:362
|
||||
#, python-format
|
||||
msgid "adding args to gcp: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:373
|
||||
msgid "User interruption: good bye"
|
||||
msgstr ""
|
|
@ -0,0 +1,265 @@
|
|||
# gcp -- translation template
|
||||
#
|
||||
# This file is distributed under the same license as the gcp package.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.2.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-10-14 20:51+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: gcp:46
|
||||
msgid "Error during import"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:47
|
||||
msgid "Please check dependecies:"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:54
|
||||
msgid "ProgressBar not available, please download it at https://pypi.org/"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:55
|
||||
msgid ""
|
||||
"Progress bar deactivated\n"
|
||||
"--\n"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:90
|
||||
msgid "Init DbusObject..."
|
||||
msgstr ""
|
||||
|
||||
#: gcp:111
|
||||
msgid "INTERNAL ERROR: invalid arguments"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:116
|
||||
msgid "INTERNAL ERROR: invalid source_dir"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:171
|
||||
msgid "/!\\ THE FOLLOWING FILES WERE *NOT* SUCCESSFULY COPIED:"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:177
|
||||
msgid "The following files were copied, but some errors happened:"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:183
|
||||
#, python-format
|
||||
msgid "Please check journal: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:205
|
||||
msgid "gcp launched"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:213
|
||||
msgid "Init DBus..."
|
||||
msgstr ""
|
||||
|
||||
#: gcp:244
|
||||
msgid "Can't read mounts table"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:251
|
||||
#, python-format
|
||||
msgid "Adding to copy list: %(path)s ==> %(dest_path)s (%(fs_type)s)"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:258
|
||||
#, python-format
|
||||
msgid "Can't copy %(path)s: %(exception)s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:286
|
||||
#, python-format
|
||||
msgid "Can't append %(path)s to copy list: %(exception)s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:289
|
||||
#, python-format
|
||||
msgid "Can't access %(dirpath)s: %(exception)s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:301
|
||||
#, python-format
|
||||
msgid "Invalid dest_path: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:306
|
||||
#, python-format
|
||||
msgid "The path given in arg doesn't exist or is not accessible: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:310
|
||||
#, python-format
|
||||
msgid "omitting directory \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: gcp:348
|
||||
#, python-format
|
||||
msgid "File [%s] already exists, skipping it!"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:368
|
||||
#, python-format
|
||||
msgid "COPYING %(source)s ==> %(dest)s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:462
|
||||
#, python-format
|
||||
msgid "%.2f PiB"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:464
|
||||
#, python-format
|
||||
msgid "%.2f TiB"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:466
|
||||
#, python-format
|
||||
msgid "%.2f GiB"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:468
|
||||
#, python-format
|
||||
msgid "%.2f MiB"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:470
|
||||
#, python-format
|
||||
msgid "%.2f KiB"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:471
|
||||
#, python-format
|
||||
msgid "%i B"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:479 gcp:485
|
||||
#, python-format
|
||||
msgid "Copying %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:515 gcp:548
|
||||
msgid ""
|
||||
"No saved sources with this name, check existing names with --sources-list"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:525
|
||||
msgid "Saved sources:"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:539
|
||||
msgid ""
|
||||
"There is already a saved sources with this name, skipping --sources-save"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:578
|
||||
msgid "force overwriting of existing files"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:582
|
||||
msgid "always follow symbolic links in sources"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:586
|
||||
msgid "never follow symbolic links in sources"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:590
|
||||
#, python-format
|
||||
msgid "same as --preserve=%s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:594
|
||||
#, python-format
|
||||
msgid ""
|
||||
"preserve specified attributes; accepted values: 'all', or "
|
||||
"one or more amongst %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:599
|
||||
msgid "copy directories recursively"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:603
|
||||
msgid "display what is being done"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:614
|
||||
msgid ""
|
||||
"fix file names incompatible with the destination file "
|
||||
"system (default: auto)"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:619
|
||||
msgid ""
|
||||
"[DEPRECATED] same as --fix-filename=no (overrides --fix-"
|
||||
"filenames)"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:624
|
||||
msgid "disable progress bar"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:631
|
||||
msgid "save sources arguments"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:635
|
||||
msgid "save sources arguments and replace memory if it already exists"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:639
|
||||
msgid "load sources arguments"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:643
|
||||
msgid "delete saved sources list"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:647
|
||||
msgid "list names of saved sources"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:651
|
||||
msgid "list names of saved sources and files in it"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:663
|
||||
msgid "Progress bar is not available, deactivating"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:687
|
||||
#, python-format
|
||||
msgid "Invalid --preserve value '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:714
|
||||
#, python-format
|
||||
msgid "There is already one instance of %s running, pluging to it"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:720
|
||||
msgid "Wrong number of arguments"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:722
|
||||
#, python-format
|
||||
msgid "adding args to gcp: %s"
|
||||
msgstr ""
|
||||
|
||||
#: gcp:731
|
||||
msgid "User interruption: good bye"
|
||||
msgstr ""
|
Binary file not shown.
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import setuptools
|
||||
|
||||
name = 'gcp'
|
||||
|
||||
setuptools.setup(
|
||||
name=name,
|
||||
version='0.2.1',
|
||||
url='https://code.lm7.fr/mcy/gcp',
|
||||
license='GPL-3+',
|
||||
|
||||
description="An advanced file copy tool loosely inspired from cp",
|
||||
long_description_content_type='text/markdown',
|
||||
long_description="""
|
||||
**%s** is a command-line tool to copy files, loosely inspired from the `cp`
|
||||
command, but with higher-level functionalities such as progress bar, copy
|
||||
continuation on error, logging to know which files were successfully
|
||||
copied, name mangling to workaround filesystem limitations (FAT), unique
|
||||
copy queue, copy list management, etc.""" % name,
|
||||
keywords='file copy',
|
||||
|
||||
author='Goffi (Jérôme Poisson)',
|
||||
author_email='goffi@goffi.org',
|
||||
maintainer='Matteo Cypriani',
|
||||
maintainer_email='mcy@lm7.fr',
|
||||
|
||||
# Cf. https://pypi.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
scripts=['gcp'],
|
||||
# entry_points={
|
||||
# 'console_scripts': ['gcp=gcp:main'],
|
||||
# },
|
||||
data_files=[
|
||||
('man/man1', ["gcp.1"]),
|
||||
('share/locale/fr/LC_MESSAGES', ['i18n/fr/LC_MESSAGES/gcp.mo']),
|
||||
('share/doc/%s' % name, ['CHANGELOG', 'LICENSE', 'README.md']),
|
||||
],
|
||||
install_requires=['PyGObject', 'dbus-python'],
|
||||
python_requires='>=3',
|
||||
)
|
|
@ -0,0 +1,257 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
gcp: Gcp CoPier -- unit tests
|
||||
Copyright (c) 2010, 2011 Jérôme Poisson <goffi@goffi.org>
|
||||
Copyright (c) 2018 Matteo Cypriani <mcy@lm7.fr>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from os import getcwd, chdir, system, mkdir, makedirs, listdir
|
||||
from os.path import join, isdir
|
||||
from shutil import rmtree
|
||||
from hashlib import sha1
|
||||
|
||||
# gcp command. This assumes gcp is in the PATH. Alternatively, we could use
|
||||
# something like this to use gcp from the current directory:
|
||||
#GCP = join(getcwd(), "gcp")
|
||||
GCP = 'gcp'
|
||||
|
||||
# Size shorcuts
|
||||
S10K = 1024 * 10
|
||||
S100K = 1024 * 100
|
||||
S1M = 1024 * 1024
|
||||
S10M = 1024 * 1024 * 10
|
||||
S100M = 1024 * 1024 * 100
|
||||
|
||||
def sha1sum(filename, buf_size=4096):
|
||||
"""Returns the SHA1 hash of a file.
|
||||
@param filename: path to the file
|
||||
@param buf_size: size of the buffer to use for calculation
|
||||
"""
|
||||
csum = sha1()
|
||||
with open(filename, 'rb') as fd:
|
||||
data = fd.read(buf_size)
|
||||
while data:
|
||||
csum.update(data)
|
||||
data = fd.read(buf_size)
|
||||
return csum.digest()
|
||||
|
||||
def dirCheck(dir_path):
|
||||
"""Recursively calculates SHA1 sums in a directory.
|
||||
@param path: path of the dir to check
|
||||
@return: a dict in the form [{filepath: sum,...}]
|
||||
"""
|
||||
def recursive_sum(directory, result):
|
||||
for current_path in listdir(directory):
|
||||
full_path = join(directory, current_path)
|
||||
if isdir(full_path):
|
||||
recursive_sum(full_path, result)
|
||||
else:
|
||||
result[full_path] = sha1sum(full_path)
|
||||
|
||||
result = {}
|
||||
_ori_dir = getcwd()
|
||||
chdir(dir_path)
|
||||
recursive_sum(".", result)
|
||||
chdir(_ori_dir)
|
||||
return result
|
||||
|
||||
#def makeRandomFile(path, size, buf_size=4096):
|
||||
# """Creates a fake file.
|
||||
# @param path: where the file is created
|
||||
# @param size: size of the file to create in bytes
|
||||
# """
|
||||
# def seq(size):
|
||||
# return ''.join(chr(random.randrange(256)) for i in range(size))
|
||||
# fd = open(path, 'w')
|
||||
# for byte in range(size//buf_size):
|
||||
# fd.write(seq(buf_size))
|
||||
# fd.write(seq(size%buf_size))
|
||||
# fd.close()
|
||||
|
||||
def makeRandomFile(path, size=S10K, buf_size=4096):
|
||||
"""Creates a fake file using /dev/urandom.
|
||||
@param path: where the file must be created
|
||||
@param size: size of the file to create in bytes
|
||||
"""
|
||||
source = open('/dev/urandom', 'rb')
|
||||
dest = open(path, 'wb')
|
||||
for _ in range(size // buf_size):
|
||||
dest.write(source.read(buf_size))
|
||||
dest.write(source.read(size % buf_size))
|
||||
dest.close()
|
||||
source.close()
|
||||
|
||||
def makeTestDir(path):
|
||||
"""Helper method to easily create a test directory.
|
||||
@param path: where the dir must be created
|
||||
"""
|
||||
for i in range(2):
|
||||
subdir = join(path,'subdir_%d' % i)
|
||||
makedirs(subdir)
|
||||
for j in range(2):
|
||||
makeRandomFile(join(subdir, 'file_%d' % j), S10K)
|
||||
for i in range(2):
|
||||
makeRandomFile(join(path,'file_%d' % i), S10K)
|
||||
|
||||
class TestCopyCases(unittest.TestCase):
|
||||
"""Test basic copy use cases, using gcp externally.
|
||||
"""
|
||||
#TODO: check journal
|
||||
|
||||
def setUp(self):
|
||||
self.ori_dir = getcwd()
|
||||
self.tmp_dir = tempfile.mkdtemp()
|
||||
chdir(self.tmp_dir)
|
||||
|
||||
def tearDown(self):
|
||||
chdir(self.ori_dir)
|
||||
rmtree(self.tmp_dir)
|
||||
|
||||
def test_one_file_copy(self):
|
||||
"""Copies one file and tests the result.
|
||||
"""
|
||||
makeRandomFile('file_1', S10K)
|
||||
ori_sum = sha1sum('file_1')
|
||||
ret = system(GCP + " file_1 file_2")
|
||||
self.assertEqual(ret,0)
|
||||
dest_sum = sha1sum('file_2')
|
||||
self.assertEqual(ori_sum, dest_sum)
|
||||
|
||||
def test_one_file_copy_already_exists(self):
|
||||
"""Checks that an existing file is not overwritten.
|
||||
"""
|
||||
makeRandomFile('file_1', S10K)
|
||||
makeRandomFile('file_2', S10K)
|
||||
file_2_sum = sha1sum('file_2')
|
||||
ret = system(GCP + " file_1 file_2")
|
||||
self.assertNotEqual(ret,0)
|
||||
file_2_sum_bis = sha1sum('file_2')
|
||||
self.assertEqual(file_2_sum, file_2_sum_bis)
|
||||
|
||||
def test_one_file_copy_already_exists_force(self):
|
||||
"""Checks that an existing file is overwritten with --force.
|
||||
"""
|
||||
makeRandomFile('file_1', S10K)
|
||||
makeRandomFile('file_2', S10K)
|
||||
file_1_sum = sha1sum('file_1')
|
||||
ret = system(GCP + " -f file_1 file_2")
|
||||
self.assertEqual(ret,0)
|
||||
file_2_sum_bis = sha1sum('file_2')
|
||||
self.assertEqual(file_1_sum, file_2_sum_bis)
|
||||
|
||||
def test_one_dir_copy(self):
|
||||
"""Checks copy of a directory to a non-existent path.
|
||||
"""
|
||||
makeTestDir('dir_1')
|
||||
check_1 = dirCheck('dir_1')
|
||||
ret = system(GCP + " -r dir_1 dir_2")
|
||||
self.assertEqual(ret,0)
|
||||
check_2 = dirCheck('dir_2')
|
||||
self.assertEqual(check_1, check_2)
|
||||
|
||||
def test_one_dir_copy_nocopy(self):
|
||||
"""Checks that a directory is not copied without the recursive option.
|
||||
"""
|
||||
makeTestDir('dir_1')
|
||||
check_before = dirCheck('.')
|
||||
ret = system(GCP + " dir_1 dir_2")
|
||||
self.assertEqual(ret,0)
|
||||
check_after = dirCheck('.')
|
||||
self.assertEqual(check_before, check_after)
|
||||
|
||||
def test_one_dir_copy_existing_dest(self):
|
||||
"""Checks that a directory is copied inside an existing destination.
|
||||
"""
|
||||
makeTestDir('dir_1')
|
||||
mkdir('dir_2')
|
||||
check_1 = dirCheck('dir_1')
|
||||
ret = system(GCP + " -r dir_1 dir_2")
|
||||
self.assertEqual(ret,0)
|
||||
self.assertEqual(listdir('dir_2'), ['dir_1'])
|
||||
check_2 = dirCheck('dir_2/dir_1')
|
||||
self.assertEqual(check_1, check_2)
|
||||
|
||||
def test_mixed_copy_existing_dest(self):
|
||||
"""Mixed copy (files + dir) to an existing destination.
|
||||
"""
|
||||
for i in range(2):
|
||||
makeRandomFile('file_%d' % i, S10K)
|
||||
makeTestDir('dir_%d' % i)
|
||||
check_1 = dirCheck('.')
|
||||
mkdir('dest_dir')
|
||||
ret = system(GCP + " -r file_0 file_1 dir_0 dir_1 dest_dir")
|
||||
self.assertEqual(ret,0)
|
||||
check_2 = dirCheck('dest_dir')
|
||||
self.assertEqual(check_1, check_2)
|
||||
|
||||
def test_mixed_copy_nonexisting_dest(self):
|
||||
"""Mixed copy (files + dir) to an inexistant destination.
|
||||
/!\\ The behaviour is different than cp's! (cp doesn't copy at all,
|
||||
while gcp create the destintation directory.)
|
||||
"""
|
||||
for i in range(2):
|
||||
makeRandomFile('file_%d' % i, S10K)
|
||||
makeTestDir('dir_%d' % i)
|
||||
check_1 = dirCheck('.')
|
||||
ret = system(GCP + " -r file_0 file_1 dir_0 dir_1 dest_dir")
|
||||
self.assertEqual(ret,0)
|
||||
check_2 = dirCheck('dest_dir')
|
||||
self.assertEqual(check_1, check_2)
|
||||
|
||||
def test_mixed_copy_existing_dest_nonrecursive(self):
|
||||
"""Non-recursive mixed copy (files + dir) to an existing destination.
|
||||
"""
|
||||
for i in range(2):
|
||||
makeRandomFile('file_%d' % i, S10K)
|
||||
makeTestDir('dir_%d' % i)
|
||||
mkdir('dest_dir')
|
||||
ret = system(GCP + " file_0 file_1 dir_0 dir_1 dest_dir")
|
||||
self.assertEqual(ret,0)
|
||||
self.assertEqual(set(listdir('dest_dir')), set(['file_0', 'file_1']))
|
||||
self.assertEqual(sha1sum('file_0'), sha1sum('dest_dir/file_0'))
|
||||
self.assertEqual(sha1sum('file_1'), sha1sum('dest_dir/file_1'))
|
||||
|
||||
def test_mixed_copy_nonexisting_dest_nonrecursive(self):
|
||||
"""Non-recursive mixed copy (files + dir) to an inexistant destination.
|
||||
"""
|
||||
for i in range(2):
|
||||
makeRandomFile('file_%d' % i, S10K)
|
||||
makeTestDir('dir_%d' % i)
|
||||
check_before = dirCheck('.')
|
||||
ret = system(GCP + " file_0 file_1 dir_0 dir_1 dest_dir")
|
||||
self.assertEqual(ret >> 8, 1)
|
||||
check_after = dirCheck('.')
|
||||
self.assertEqual(check_before, check_after)
|
||||
|
||||
def test_fix_filenames(self):
|
||||
"""Checks the --fix-filenames option.
|
||||
"""
|
||||
source = 'wi||fat<like>this:"file*name"?'
|
||||
fixed_name = 'wi!!fat[like]this;\'file+name\'_'
|
||||
makeRandomFile(source, S10K)
|
||||
source_sha = sha1sum(source)
|
||||
mkdir('dest_dir')
|
||||
ret = system(GCP + " --fix-filenames=force '" + source + "' dest_dir")
|
||||
self.assertNotEqual(ret, 0)
|
||||
dest = join('dest_dir', fixed_name)
|
||||
dest_sha = sha1sum(dest)
|
||||
self.assertEqual(source_sha, dest_sha)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in New Issue