pax_global_header00006660000000000000000000000064130365225100014507gustar00rootroot0000000000000052 comment=4df4ebfe9c2a453514913a4911c6e31e0101daa3 trash-cli-0.17.1.14/000077500000000000000000000000001303652251000137065ustar00rootroot00000000000000trash-cli-0.17.1.14/.ackrc000066400000000000000000000000111303652251000147620ustar00rootroot00000000000000--python trash-cli-0.17.1.14/.ctags000066400000000000000000000000641303652251000150100ustar00rootroot00000000000000--exclude=build --exclude=env --python-kinds=-i -R trash-cli-0.17.1.14/.gitignore000066400000000000000000000006421303652251000157000ustar00rootroot00000000000000*.pyc *.swp *.tmp .coverage .DS_Store .local/ # generated executables /trash-restore /trash-empty /trash-list /trash-put /trash-rm /trash # other things /build/ /command-test/sandbox/ /command-test/test-volume.img /cover/ /dist/ /foo /env/ /nose-*.egg/ /sandbox/ /scripts/ /test-disk.img /test-disk.dmg /test-disk/ /topdir/ /trash_cli.egg-info/ /XDG_DATA_HOME/ /fake-vol /xdh/ /MANIFEST /tags /.vagrant/ /ssh-config trash-cli-0.17.1.14/COPYING000066400000000000000000000431101303652251000147400ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. trash-cli-0.17.1.14/CREDITS.txt000066400000000000000000000025621303652251000155510ustar00rootroot00000000000000Author ------ Andrea Francia trash-cli@andreafrancia.it Thanks to --------- - Einar Orn Olason: Developmemt of first version of the empty-trash command - Rahul Sundaram, http://fedoraproject.org/wiki/RahulSundaram Packaged trash-cli for Fedora (https://bugzilla.redhat.com/show_bug.cgi?id=508750) - Michele Barbiero, http://greedyandlounge.blogspot.com,Shuren @ irc://irc.azzurra.org/linux-help Reporting of bugs. - jpschewe -at_ users.sourceforge.net: Reporting many bugs and submitting patches. - slim http://www.hartnup.net/wordpress: Helped choosing commands names - BobbyShaftoe http://stackoverflow.com/users/38426/bobbyshaftoe Helped choosing commands names - ceretullis http://www.breakingrobots.net: Helped choosing commands names - grapefrukt http://grapefrukt.com/blog : Helped choosing commands names - joel.neely http://joelneely.wordpress.com : Helped choosing commands names - My Wife Ely (Elisa Zuccolo): Thanks for tolerating me when I work at trash-cli. - Ben Finney: Helped choosing commands names, find out the grammar problems with trash-file command. - Ken VanDine: Helped choosing commands names - Christoph Bloch: Helped choosing commands names - Massimo "Submax82" Cavalleri: Packaged on Slackware - Steve Stalcup: for mantain the Debian and Ubuntu packages If you think you should appear in this page please tell me. trash-cli-0.17.1.14/DONE.txt000066400000000000000000000014161303652251000151760ustar00rootroot00000000000000trash-list - it checks that the $topdir/.Trash is a sticky dir - it reports when $topdir/.Trash is not sticky - it checks that the $topdir/.Trash is not a symbolic link - and it warns the user otherwise - it lists contents from both Home TrashDir and Volume TrashDirs (both methods) - it warns when an empty .trashinfo is found - it warns when an unreadable .trashinfo is found - is tolerant with .trashinfo that does not contains a DeletionDate - is tolerant with .trashinfo that contains an invalid DeletionDate - it warns when a .trashinfo does not contains the Path entry - print version information on --version trash-empty: - when run with `days` argument it should skip .trashinfo with invalid date - print help on --help - print version on --version trash-cli-0.17.1.14/HISTORY.txt000066400000000000000000000063171303652251000156170ustar00rootroot000000000000000.17.1.14: - Fix a bug that causes trash-put to use $topdir/.Trash/UID trashcan even when it is not secure and $topdir/.Trash-UID should be used instead. 0.17.1.12: - Fix a bug in detecting right volume of home trash dir home Trash dir is a symbolic link, and in detecting volume of file to be trashed when it is specified as contained in a directory that is a symbolic link that crosses volumes boundaries (#38) - Make some code python 3 compatible - Fixed README. 0.17.1.1: - Now trash-rm supports full path matching, using a pattern starting with slash '/' Fix #67 - Fix typo in trash-rm(1) man page - Add a reference to trash-rm(1) to all man pges - Fix inconsistent apostrophes - Add support for --trash-dir option to trash-empty 0.16.12.29: - trash-rm no more crashes on .trashinfo files without Path (#69) 0.16.12.28: - Fix #48 trash-empty crashes on directories without read permission 0.16.12.26: - Fix #52: Almost all commands crashes with python 2.7.10 0.16.12.25: - Now trash-restore accept a /specific/path - Now integration tests should pass also in a linux box. Fix #61 - Now all commands output will report the right up-to-date location for issue reporting (#39) - Add input validation in trash-restore - Renamed trash-restore instead of restore-trash - Fixed bug (trash-put creates $topdir/.Trash even it should not) - Fixed bug (trash-put uses $topdir/.Trash/$uid even if unsecure) - Minor changes to man pages. 0.12.9.14: - Switched to distutils.core (instead of setuptools) - Now `trash-put -v` will warning if it found a unsticky .Trash dir. - New trash-rm command - (Internal) Swtiched from realpath to abspath 0.12.7: - fixed trash-empty crashed with GetoptError in short_has_arg(): option -2 not recognized (see https://bugs.launchpad.net/ubuntu/+source/trash-cli/+bug/1015877 ) - fixed inclusion of README.rst when creating distribution package 0.12.6: - add Donate button on README 0.12.4.24: - Fixes a packaging problem of the previous release which prevented the installation via easy_install and/or pip (see https://github.com/andreafrancia/trash-cli/issues/5) - Fixes the name of the man page for restore-trash. 0.12.4: - Reintroduced `trash` command as alias to `trash-put` - Now trash-list checks for $topdir/.Trash having sticky bit and not being a symlink and warns when these requirements are not met. - Now trash-list handles empty, unreadable or malformed .trashinfo - Now `trash-emtpy ` skips .trashinfos with invalid DeletionDates - Removed Unipath dependency - Switched from googlecode to github - Removed tests written in Bash - Complete rewrite of trash-list and trash-empty 0.11.3: - Now works also on Mac OS X - Fixed #55: restore-trash sets all-write permissions for the destination directory - Volumes detection: Now uses "df -P" output as fallback when getmnt fails. - Fixed #54. Now restore trash refuses to overwrite a file. Used code adapted from a patch written by Christian.Oudard 0.11.2: Fixed #45: Cannot build RPM package with 0.11.1.2 0.11.1.2: Fixed problems running setup.py 0.11.1: Updated version number to make easy_install happy 0.11.0: Fixed serious bug in trash-put: now the dot `.' and dot-dot `..' are skipped. trash-cli-0.17.1.14/MANIFEST.in000066400000000000000000000001161303652251000154420ustar00rootroot00000000000000include requirements-dev.txt include ez_setup.py include README.rst graft man trash-cli-0.17.1.14/README.rst000066400000000000000000000112451303652251000154000ustar00rootroot00000000000000trash-cli - Command Line Interface to FreeDesktop.org Trash. ============================================================ |Donate|_ trash-cli trashes files recording the original path, deletion date, and permissions. It uses the same trashcan used by KDE, GNOME, and XFCE, but you can invoke it from the command line (and scripts). It provides these commands:: trash-put trash files and directories. trash-empty empty the trashcan(s). trash-list list trashed files. trash-restore restore a trashed file. trash-rm remove individual files from the trashcan. Usage ----- Trash a file:: $ trash-put foo List trashed files:: $ trash-list 2008-06-01 10:30:48 /home/andrea/bar 2008-06-02 21:50:41 /home/andrea/bar 2008-06-23 21:50:49 /home/andrea/foo Search for a file in the trashcan:: $ trash-list | grep foo 2007-08-30 12:36:00 /home/andrea/foo 2007-08-30 12:39:41 /home/andrea/foo Restore a trashed file:: $ trash-restore 0 2007-08-30 12:36:00 /home/andrea/foo 1 2007-08-30 12:39:41 /home/andrea/bar 2 2007-08-30 12:39:41 /home/andrea/bar2 3 2007-08-30 12:39:41 /home/andrea/foo2 4 2007-08-30 12:39:41 /home/andrea/foo What file to restore [0..4]: 4 $ ls foo foo Remove all files from the trashcan:: $ trash-empty Remove only the files that have been deleted more than ago:: $ trash-empty Example:: $ date Tue Feb 19 20:26:52 CET 2008 $ trash-list 2008-02-19 20:11:34 /home/einar/today 2008-02-18 20:11:34 /home/einar/yesterday 2008-02-10 20:11:34 /home/einar/last_week $ trash-empty 7 $ trash-list 2008-02-19 20:11:34 /home/einar/today 2008-02-18 20:11:34 /home/einar/yesterday $ trash-empty 1 $ trash-list 2008-02-19 20:11:34 /home/einar/today Remove only files matching a pattern:: $ trash-rm \*.o Note: you need to use quotes in order to protect the pattern from shell expansion. FAQ --- How to create a top level .Trash dir? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Steps :: sudo mkdir --parent /.Trash sudo chmod a+rw /.Trash sudo chmod +t /.Trash Can I alias `rm` to `trash-put`? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can but you shouldn't. In the early days I thought it was a good idea to do that but now I changed my mind. Although rhe interface of `trash-put` seems to be compatible with `rm`, it has different semantics which will cause you problems. For example, while `rm` requires `-R` for deleting directories `trash-put` does not. But sometimes I forget to use `trash-put`, really can't I? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You could alias `rm` to something that will remind you to not use it:: alias rm='echo "This is not the command you are looking for."; false' Then, if you really want to use `rm`, simply prepend a slash to bypass the alias:: \rm file-without-hope Note that Bash aliases are used only in interactive shells, so using this alias should not interfere with scripts that expect to use `rm`. Installation ------------ The easy way ~~~~~~~~~~~~ Requirements: - Python 2.7 o Python 3 - setuptools (use `apt-get install python-setuptools` on Debian) Installation command:: easy_install trash-cli From sources ~~~~~~~~~~~~ System-wide installation:: git clone https://github.com/andreafrancia/trash-cli.git cd trash-cli sudo python setup.py install User-only installation:: git clone https://github.com/andreafrancia/trash-cli.git cd trash-cli python setup.py install --user Bugs and feedback ----------------- If you discover a bug please report it here: https://github.com/andreafrancia/trash-cli/issues You can also email me to andrea@andreafrancia.it. On Twitter I'm @andreafrancia. Development ----------- Environment setup:: virtualenv env --no-site-packages source env/bin/activate pip install -r requirements-dev.txt Running tests:: nosetests unit_tests # run only unit tests nosetests integration_tests # run all integration tests nosetests -A 'not stress_test' # run all tests but stress tests nosetests # run all tests Check the installation process before release:: python check_release_installation.py Profiling unit tests:: pip install gprof2dot nosetests --with-profile --profile-stats-file stats.pf --profile-restrict=unit_tests unit_tests gprof2dot -w -f pstats stats.pf | dot -Tsvg >| stats.svg open stats.svg .. |Donate| image:: https://www.paypalobjects.com/en_GB/i/btn/btn_donate_SM.gif .. _Donate: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A trash-cli-0.17.1.14/TODO.txt000066400000000000000000000014371303652251000152210ustar00rootroot00000000000000Backlog: - trash-list should handle .trashinfo from home trashdir that contains relative path - trash-list should print also the orphan files - support alias rmdir=trash-put - don't use world writable trash - enable trash-put home fallback - trash-empty removes files until a certain amount of space has been restored - trash-list --raw - trash-list - trash-list -R # recursive - trash-restore # restore the newest - trash-restore # restore to a certain destination - trash-restore --trashed-on="Apr 15, 08" - trash-restore --version - trash-empty --from=/.Trash - trash-empty should empty even the mac trash directory - TRASH_DATE=2012-09-12 trash-put trash-cli-0.17.1.14/Vagrantfile000066400000000000000000000003631303652251000160750ustar00rootroot00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(2) do |config| config.vm.box = "ubuntu/yakkety64" config.vm.provision "shell", inline: <<-SHELL sudo apt-get update sudo apt-get install -y python-setuptools SHELL end trash-cli-0.17.1.14/bugs.txt000066400000000000000000000003731303652251000154120ustar00rootroot00000000000000Bug reported on external trackers: x trash-empty crashed with GetoptError in short_has_arg(): option -2 not recognized, url: https://bugs.launchpad.net/ubuntu/+source/trash-cli/+bug/1015877 status: solved on Thu Jun 21 12:50:03 CEST 2012 trash-cli-0.17.1.14/check_release_installation.py000066400000000000000000000112361303652251000216210ustar00rootroot00000000000000from __future__ import print_function TARGET_HOST = 'default' import nose from nose.tools import assert_equals, assert_not_equals import subprocess from trashcli.trash import version def main(): check_connection() ssh = Connection(TARGET_HOST) check_both_installations(ssh) check_python3_normal_installation(ssh) def check_both_installations(ssh): l = CheckInstallation( EasyInstall3Installation(), ssh, version) l.check_installation() l = CheckInstallation( NormalInstallation('python'), ssh, version) l.check_installation() l = CheckInstallation( EasyInstallInstallation(), ssh, version) l.check_installation() l = CheckInstallation( NormalInstallation('python3'), ssh, version) l.check_installation() class CheckInstallation: def __init__(self, installation, ssh, version): self.ssh = ssh self.executables = [ 'trash-put', 'trash-list', 'trash-rm', 'trash-empty', 'trash-restore', 'trash'] self.tarball="trash-cli-%s.tar.gz" % version self.installation = installation def check_installation(self): self.clean_any_prior_installation() self.copy_tarball() self.install_software() self.check_all_programs_are_installed() def clean_any_prior_installation(self): for executable in self.executables: self._remove_executable(executable) self._assert_command_removed(executable) def _remove_executable(self, executable): self.ssh.run_checked('sudo rm -f $(which %s)' % executable) def _assert_command_removed(self, executable): result = self.ssh.run_checked('! which %s' % executable) def copy_tarball(self): self.ssh.put('dist/%s' % self.tarball) def install_software(self): self.installation.install(self.tarball, self.ssh) def check_all_programs_are_installed(self): for command in self.executables: result = self.ssh.run_checked('%(command)s --version' % locals()) class NormalInstallation: def __init__(self, python): self.python = python def install(self, tarball, ssh): directory = strip_end(tarball, '.tar.gz') ssh.run_checked('tar xfvz %s' % tarball) ssh.run_checked('cd %s && ' 'sudo %s setup.py install' % (directory, self.python)) class EasyInstallInstallation: def install(self, tarball, ssh): ssh.run_checked('sudo easy_install %s' % tarball) class EasyInstall3Installation: def install(self, tarball, ssh): ssh.run_checked('sudo easy_install3 %s' % tarball) def strip_end(text, suffix): if not text.endswith(suffix): return text return text[:-len(suffix)] def check_connection(): suite = nose.loader.TestLoader().loadTestsFromTestClass(TestConnection) nose.run(suite=suite) class TestConnection: def __init__(self): self.ssh = Connection(TARGET_HOST) def test_should_report_stdout(self): result = self.ssh.run('echo', 'foo') assert_equals('foo\n', result.stdout) def test_should_report_stderr(self): result = self.ssh.run('echo bar 1>&2') assert_equals('bar\n', result.stderr) def test_should_report_exit_code(self): result = self.ssh.run("exit 134") assert_equals(134, result.exit_code) class Connection: def __init__(self, target_host): self.target_host = target_host def run_checked(self, command): print(command) result = self.run(command) result.assert_succesful() def run(self, *user_command): ssh_invocation = ['ssh', '-Fssh-config', self.target_host, '-oVisualHostKey=false'] command = ssh_invocation + list(user_command) exit_code, stderr, stdout = self._run_command(command) return self.ExecutionResult(stdout, stderr, exit_code) def put(self, source_file): scp_command = ['scp', '-Fssh-config', source_file, self.target_host + ':'] exit_code, stderr, stdout = self._run_command(scp_command) assert 0 == exit_code def _run_command(self, command): process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout,stderr = process.communicate() exit_code = process.poll() return exit_code, stderr, stdout class ExecutionResult: def __init__(self, stdout, stderr, exit_code): self.stdout = stdout self.stderr = stderr self.exit_code = exit_code def assert_succesful(self): assert self.exit_code == 0, "Failed:\n - stderr:%s" % self.stderr if __name__ == '__main__': main() trash-cli-0.17.1.14/docs/000077500000000000000000000000001303652251000146365ustar00rootroot00000000000000trash-cli-0.17.1.14/docs/MountListFromHal.wiki000066400000000000000000000010101303652251000207220ustar00rootroot00000000000000About using HAL (Hardware Abstraction Layer) to get the list of mount points. ============================================================================= This [http://davyd.livejournal.com/206645.html article] explain how to use HAL from python. This [https://www.dfwpython.org/repo/Projects/DBUS/dtest.py python scripts] communicates with HAL trouch DBUS and uses the 'org.freedesktop.Hal.Device.Volume' name. =Depedencies= Using this method will add some dependencies: * hal ? * libhal1 * dbus python module trash-cli-0.17.1.14/docs/about-accessing-the-windows-recycle-bin.txt000066400000000000000000000010721303652251000251060ustar00rootroot00000000000000Some references about accessing Recycle Bin of Windows ====================================================== Seems that ReactOS projects has implemented a library for dealing with the Windows specific trashcan data structure: http://svn.reactos.org/svn/reactos/trunk/reactos/lib/recyclebin/ How to use SHFileOperation in python using pywin32: http://timgolden.me.uk/python/win32_how_do_i/copy-a-file.html How to programmatically send folders and/or files to the recycle bin (in C#): http://www.fmsinc.com/free/NewTips/NET/sendfoldertoreceyclebin.asp trash-cli-0.17.1.14/docs/about-listing-osx-trashcan.txt000066400000000000000000000013741303652251000225750ustar00rootroot00000000000000In OS X the trash can directories are: - ~/.Trash - $topdr/.Trashes/$uid Afert installing the `osx-trash` gem the behaviour can be described with: $ ls ~/.Trash $ touch foo $ trash foo $ ls ~/.Trash foo $ touch foo $ trash foo $ ls ~/.Trash foo foo 17-32-40 Where 17-32-40 is HH:MM:SS. If you "Put back" a file it is moved to the original path but it still mantain the new name (e.g. "foo 17-32-40"). The deletion time is put before the last dot: $ touch foo.tar.gz; trash foo.tar.gz $ touch foo.tar.gz; trash foo.tar.gz $ ls ~/.Trash foo.tar 17-36-13.gz foo.tar.gz The original path is stored in the ~/.Trash/.DS_Store. More details in http://superuser.com/questions/108301/deleting-a-single-file-from-the-trash-in-mac-os-x-snow-leopard trash-cli-0.17.1.14/docs/about-shredding.txt000066400000000000000000000077721303652251000204730ustar00rootroot00000000000000Introduction ============ See also issue #37 A feature not typically available in other trash-emptying implementations is the ability to use arbitrary shell utilities (such as srm, shred, and the like) to remove files. It'd be nice if the empty-trash command took a flag (say, '-s') to provide for such functionality. Suggested Command Line ====================== :: empty-trash --shred This command would shred all the trashed files and the trashinfo files in all trash directories. Things to be defined: - How the secure shredding should be implemented? Calling an external command or using some python library? - No extant Python shredding library. The only [http://reconstructor.aperantis.com/index.php?option=com_remository&Itemid=33&func=fileinfo&id=166 other implementation] I could find just calls wipe. - What to do if the existing command is not found? - Take a command-line option to select a supported method, and perhaps a config-file option. Otherwise, look for one of the supported methods and remember which one was available last time? - I was talking about how the trash-empty should behave if the external is not available. In any case this would be defined clearly when we wrote the use cases. Use cases ========= User want to shred all trashed files of all trash directories. So he/she types the command:: $ trash-empty --shred The trash-empty command will silently shred all trashinfo and trashed files from all trashed directory. The wiping is implemented calling the `shred` command from the coreutils for each file to be wiped. Exceptions ---------- Suppose that: * `shred` is not available in $PATH. * the trashcan contains at least a trashed file. The behaviour should be $ trash-empty --shred trash-empty: Unable to shred `filename' (shred: command not found). You need shred from coreutils. $ echo $(($?==0)) 1 Permission Denied ----------------- Suppose that: * `shred` is available in $PATH. * `shred` permissions are `--x` (could not be read from the user) * the trashcan contains at least two trashed file. The behaviour should be: $ trash-empty --shred trash-empty: Unable to shred `filename' (shred: Permission denied). $ echo $(($?==0)) 1 The error message should be displayed once and the command should not attempt to re-execute shred with others files (even if there are multiple files in trash directories). User shouldn't install additional software ------------------------------------------ The shred operation would be implemented against the `shred` command line utility. The `shred` was chosen because is part of the coreutils which is virtually installed in all major GNU/Linux distributions. User shouldn't configure the software ------------------------------------- We don't add the support to multiple commands because this would increase the complexity of the user interface of the `trash-empty` command and the other choices does not offer something very different from `shred`. If someone feel the urge of doing arbitrary command on all trashed files we can always add a `for-each-trashed-files` command or use a combination of an hypothetic `list-all-trashed-files` and `xargs` command. Implementation details ====================== Things to do: - create a TrashedFile.securepurge() method - implement command line options handling in empty-trash command - create a DeletionUtility abstraction, and implement methods for the three utilities below? See also ======== Secure deleting is implemented in these shell utilities: * shred (part of the coreutils package) * srm (part of secure_deletion package from http://www.thc.org) * [http://wipe.sourceforge.net/ wipe] Limitations =========== This features could be less useful that one can think. As stated in the [http://www.gnu.org/software/coreutils/manual/html_node/shred-invocation.html shred documentation] (in the "Please note") the shredding operation could not be safe on many cases such as use of RAID, and the Ext3 in data=journal mode. trash-cli-0.17.1.14/docs/about-the-generic-naming-issue.txt000066400000000000000000000024071303652251000233010ustar00rootroot00000000000000How the command names changed? ============================== The earlier versions of trash-cli uses these commands names: * trash * empty-trash * list-trash * restore-trash When the trash-cli project was proposed to Fedora the Red Hat peoples complaint about the commmand name 'trash' to be too generic and they refused to include the package in Fedora. The discussion started on [1] and followed in [2]. The the commands names where then renamed from *-trash to trash-* that will exploit the shell TAB-completion. This change was suggested by Behdad Esfahbod (see [3]) After those discussion on the upstream the names of the commands where changed to: * trash # trashes files and directories. * trash-empty # empty the trashcan(s). * trash-list # list trashed files. * restore-trash # restore a trashed file. A summary of all these discussion is available at [4]. After that the packagers lose theirs interest and the trash-cli was not packaged. [1] https://bugzilla.redhat.com/show_bug.cgi?id=448122 [2] https://www.redhat.com/archives/fedora-devel-list/2008-October/msg00216.html [3] https://www.redhat.com/archives/fedora-devel-list/2008-October/msg00231.html [4] http://fedoraproject.org/wiki/FWN/Issue147 (Fedora Weely News #147) trash-cli-0.17.1.14/docs/back-links.txt000066400000000000000000000042611303652251000174200ustar00rootroot00000000000000List of web pages that cite trash-cli, with the date when I discovered the page, sorted by discovery date (newest before): - 2012-07-19 http://shuffleos.com/5835/trashcli-command-line-utility-access-trashcan-ubuntu-linux-terminal/ - 2012-07-18 http://crunchbanglinux.org/forums/topic/20818/trashbin-cli/ - 2012-07-02 http://www.hecticgeek.com/2012/06/access-trash-command-line-ubuntu-linux-trash-cli/ - 2009-07-24 http://www.emacswiki.org/emacs/SystemTrash - 2009-03-29 2008-12-27 http://blog.goo.ne.jp/xxxfishbonexxx/e/45508969110d67f141226b68eab205ce japanese? blog post about installing? trash-cli - 2009-03-29 2008-10-16 http://linuxgazette.net/156/misc/lg/deleted_file_recovery.html - 2009-03-29 http://qa.debian.org/popcon.php?package=trash-cli - 2009-03-29 http://elonen.iki.fi/code/misc-notes/remove-duplicate-files/index.html?showcomments=1 Antonio in the comments. - 2009-03-01 http://varlogrant.blogspot.com/2009/01/making-plans.html - 2009-03-01 http://y_z.scripts.mit.edu/wp/2009/02/03/when-you-release-shift-too-early - 2009-03-01 http://sanelz.blogspot.com/2009/02/trashing-stuff.html - 2009-03-01 http://alltheshortnamesweretakenalready.blogspot.com/2009/01/bash-script-implementation-of-trashcan.html - 2008-12-25 (Blog about Ubuntu in some language that uses kanji) http://ubulog.blogspot.com/2008/10/ubuntu.html - 2008-12-24 https://www.ohloh.net/p/trash-cli - 2008-12-23 http://article.gmane.org/gmane.compw.window-managers.windowmaker.devel/1045 - 2008-12-23 http://ubuntuforums.org/showthread.php?t=1003522 - 2008-12-23 http://www.downloadplex.com/Linux/System-Utilities/File-Disk-Management/trash-cli_70540.html - 2008-11-16 http://feeding.cloud.geek.nz/2008/06/preventing-accidental-deletion-of.html - 2008-11-09 (in french) http://www.thierryb.net/site/TRASH-CLI-utiliser-la-corbeille-en.html - 2008-10-14 http://ubulog.blogspot.com/2008/10/ubuntu.html - 2008-10-14 http://d.hatena.ne.jp/f99aq/20081013/1223882323 - http://linux.softpedia.com/get/System/Filesystems/trash-cli-39712.shtml - http://syswall.wordpress.com/2008/07/11/trash-cli/ - http://freshmeat.net/projects/bluetrash/ - http://www.linux.com/feature/138331 - http://directory.fsf.org/project/trash/ trash-cli-0.17.1.14/docs/how-to-build-and-upload-a-new-release.txt000066400000000000000000000006651303652251000243650ustar00rootroot00000000000000How to build a release ====================== Update the version number: vim trashcli/trash.py version="$(python setup.py --version)" Run all tests: nosetests Test the installation from tarball: python setup.py sdist python check_release_installation.py Register and upload: python setup.py register python setup.py sdist upload Now you can tag the repo status: git tag "${version:?}" -EOF trash-cli-0.17.1.14/docs/proposed-new-interface/000077500000000000000000000000001303652251000212165ustar00rootroot00000000000000trash-cli-0.17.1.14/docs/proposed-new-interface/trash-cli.txt000066400000000000000000000024041303652251000236450ustar00rootroot00000000000000NAME trash-cli - Command Line Interface to FreeDesktop.org Trash. DESCRIPTION trash-cli provides the following commands to manage the trash: * trash trashes files and directories. * trash-empty empty the trashcan(s). * trash-list list trashed file. * trash-restore restore a trashed file. * trash-admin administrate trashcan(s). For each file the name, original path, deletion date, and permissions are recorded. The trash command allow trash multiple files with the same name. These command uses the same Trashcan of last versions of KDE, GNOME and XFCE. EXAMPLES Trash a file: $ trash /home/andrea/foobar List trashed files: $ trash-list 2008-06-01 10:30:48 /home/andrea/bar 2008-06-02 21:50:41 /home/andrea/bar 2008-06-23 21:50:49 /home/andrea/foo Restore a trashed file: $ trash-restore /home/andrea/foo Empty the trashcan: $ trash-empty AUTHORS REPORTING BUGS COPYRIGHT SEE ALSO trash(1), trash-restore(1), trash-empty(1), trash-list(1), trash-admin(1), trashinfo(5). trash-cli-0.17.1.14/docs/proposed-new-interface/trash-empty.txt000066400000000000000000000046651303652251000242470ustar00rootroot00000000000000NAME trash-emptycan - remove all trashed files from the trashcan. SYNOPSIS trash-emptycan [OPTIONS]... DESCRIPTION Restore the trashed file specified by SOURCE in DEST (or its original location). -d, --deletion-date=DATE Choose the trashed file with the specified original location and with the specified deletion date. --trash-id The SOURCE param should be interpreted as trash ids. --version output version information and exit --help display this help and exit TRASH ID A TrashId is a URL referring to a specific item in a specific trash directory. The URL has this form: trash:TRASH_DIR/ID Where 'trash:' is the scheme part. TRASH_DIR is the Trash directory containing the item, can contains slashes '/'. ID is the Item part and can not contains slashes. The TrashId refer to the trashed file whith TRASH_DIR/info/ID.trashinfo as .trashinfo file. TRASH_DIR/files/ID as original file. USE CASES Restore a trashed file in its original location: $ trash-restore /home/andrea/foobar Restore a trashed file in case of multiple entries matching the original location specified: $ trash-restore /home/andrea/foo trash-restore: Cannot restore '/home/andrea/foo' multiple entries with same location exists: 2008-06-23T21:50:41 /home/andrea/foo 2008-06-23T21:50:49 /home/andrea/foo $ trash-restore --last /home/andrea/foo Or $ trash-restore --deletion-date=2008-06-23T21:50:49 /home/andrea/foo Restore a trashed file in a different location: $ trash-restore /home/andrea/foo ./bar Restoring by trash-id. $ trash-list --show-trash-id trash:/home/andrea/.local/share/Trash/foo trash:/home/andrea/.local/share/Trash/foo_1 trash:/home/andrea/.local/share/Trash/foo_2 $ trash-restore --trash-id trash:/home/andrea/.local/share/Trash/foo_2 AUTHORS REPORTING BUGS COPYRIGHT SEE ALSO trash-cli(1), trash(1), trashinfo(5), trash-restore(1), trash-empty(1). trash-cli-0.17.1.14/docs/proposed-new-interface/trash-list.txt000066400000000000000000000032631303652251000240550ustar00rootroot00000000000000NAME trash-list - list trashed files. SYNOPSIS trash-list [OPTIONS]... [DIR]... DESCRIPTION List files trashed from DIR (or the current directory by default). -l do not recurse subfolders -r, --recursive recurse subfolders (default) -a, --all list all file trashed in all trashcans --version output version information and exit --help display this help and exit EXAMPLES List all trashed files: $ trash-list 2008-06-23T21:57:26 /home/andrea/src/bluetrash/pippo 2008-06-23T21:50:41 /home/andrea/foobar 2008-06-23T21:50:49 /home/andrea/foobar 2008-06-23T21:53:13 /media/disk/adsfljasldj List all file trashed from the current directory (recursive): $ pwd /home/andrea $ trash-list . 2008-06-23T21:57:26 /home/andrea/src/bluetrash/pippo 2008-06-23T21:50:41 /home/andrea/foobar 2008-06-23T21:50:49 /home/andrea/foobar List all file trashed from the current directory (not recursive): $ pwd /home/andrea $ trash-list -l . 2008-06-23T21:50:41 /home/andrea/foobar 2008-06-23T21:50:49 /home/andrea/foobar List all files removed from a specific directory: $ trash-list /tmp/ 2008-06-23T21:50:41 /tmp/foobar1 2008-06-23T21:50:49 /tmp/foobar2 AUTHORS REPORTING BUGS COPYRIGHT SEE ALSO trash-cli(1), trash(1), trashinfo(5), trash-restore(1), trash-empty(1). trash-cli-0.17.1.14/docs/proposed-new-interface/trash-restore.txt000066400000000000000000000046511303652251000245670ustar00rootroot00000000000000NAME trash-restore - restore a trashed files. SYNOPSIS trash-restore [OPTIONS]... SOURCE [DEST] DESCRIPTION Restore the trashed file specified by SOURCE in DEST (or its original location). -d, --deleted-on=DATE Choose the trashed file with the specified original location and with the specified deletion date. --trash-id The SOURCE param should be interpreted as trash ids. --version output version information and exit --help display this help and exit TRASH ID A TrashId is a URL referring to a specific item in a specific trash directory. The URL has this form: trash:TRASH_DIR/ID Where 'trash:' is the scheme part. TRASH_DIR is the Trash directory containing the item, can contains slashes '/'. ID is the Item part and can not contains slashes. The TrashId refer to the trashed file whith TRASH_DIR/info/ID.trashinfo as .trashinfo file. TRASH_DIR/files/ID as original file. USE CASES Restore a trashed file in its original location: $ trash-restore /home/andrea/foobar Restore a trashed file in case of multiple entries matching the original location specified: $ trash-restore /home/andrea/foo trash-restore: Cannot restore '/home/andrea/foo' multiple entries with same location exists: 2008-06-23T21:50:41 /home/andrea/foo 2008-06-23T21:50:49 /home/andrea/foo $ trash-restore --last /home/andrea/foo Or $ trash-restore --deletion-date=2008-06-23T21:50:49 /home/andrea/foo Restore a trashed file in a different location: $ trash-restore /home/andrea/foo ./bar Restoring by trash-id. $ trash-list --show-trash-id trash:/home/andrea/.local/share/Trash/foo trash:/home/andrea/.local/share/Trash/foo_1 trash:/home/andrea/.local/share/Trash/foo_2 $ trash-restore --trash-id trash:/home/andrea/.local/share/Trash/foo_2 AUTHORS REPORTING BUGS COPYRIGHT SEE ALSO trash-cli(1), trash(1), trashinfo(5), trash-restore(1), trash-empty(1). trash-cli-0.17.1.14/docs/similar-projects.txt000066400000000000000000000103021303652251000206620ustar00rootroot00000000000000DELSAFE ======= URL: http://homepage.esoterica.pt/~nx0yew/delsafe/ Use a different approach to the same problem. While 'trash' provide a extra command that put files in the trash, 'delsafe' modifies the semantics of the existing commands. Apparently 'delsafe' overrides the library calls of unlink, unlinkat, rename, open and fopen modifying the semantics of these functions. A program that was designed to works with the original functions may not work well with the overridden functions. For example is the KDE filemanager which as two commands: - delete file - trash file Using 'delsafe' these commands will become: - delete file --> trash file in the delsafe trash - trash file --> trash file in the KDE trash I prefer the 'trash' approach because don't change the semantics of the existing program letting the user chose when putting files in the trash and when removing them. Personally I don't like the model where each deletion should recorded somewhere. 'trash' is intended to be used only in interactive shells, if a program decides to delete, or trunk a file the information loss risk IMHO should be managed in the context of this program. trash.sh ======== URL: http://fresh.t-systems-sfr.com/unix/src/privat2/trashcan-3.2.tar.gz/ Comments: - This program seems not compliant with the FreeDesktopTrashSpecification. - It compresses trashed file instead of just moving it. - Like trash-cli can trash and restore directories. - Seems that the development was ended in 2003. Trashcan for Rox-Filer ====================== URL: http://usuarios.lycos.es/proyectarr/ Features: * Has a graphical user interface. * Does not support the FreeDesktopTrashSpecification. * Implemented in bash using zenity. trash by skymt ============== URL: http://mysite.verizon.net/skymt/trash/ Language: Python I reviewed the version [http://mysite.verizon.net/skymt/trash/trash-0.2.tar.gz 0.2] Is the only other CLI implementation of FreeDesktopTrashSpecification I ever found till now (2008-06-29) Features: - Operation supported: * trash files and directories: yes * listing trashed files: no * restoring trashed files: no * emptying the trashcan: no - Support of volumes trashcans: no - Can be used as alias='rm': no Notes: - *(Better than trash-cli)* Seems much simpler of trash-cli.\ - *(Better than trash-cli)* Prints sensible output if the trashcan directories permission are wrong. - Not object oriented. cn by Richard Neill ------------------- URL: http://www.richardneill.org/source.php#cn License: GPLv3 Reviewed on: 2008-06-29 Features: - Tries to conform to the FreeDesktopTrashSpecification - Works also with the older version of KDE and GNOME TrashCan implementation - Can be used as alias='rm': no - It has a manpage. Trash Specification: - Operation supported: - trash files and directories: yes - listing trashed files: yes - restoring trashed files: yes - emptying trashcan: yes - Support of volumes trashcans: no - Can be used as alias='rm': no FreeDesktop.org specification deviations: - I don't think it creates the trashinfo file in an atomic way. File Rename Utils ================= URL: http://filerenameutils.sourceforge.net/ License: ? Reviewed on: 2008-06-30 Features: - Conform to FreeDesktopTrashSpecification : yes - Operation supported: - trash files and directories: yes - listing trashed files: yes - restoring trashed files: yes - emptying trashcan: yes - Support of volumes trashcans: ? - Can be used as alias='rm': no Gnome Trashcan ============== As of Nautilus 2.22, GNOME support the XDG Trash specification through GIO and the GVFS-Trash backend. You can find the code for this backend here: http://svn.gnome.org/viewvc/gvfs/trunk/daemon/ and GIO is in GLib: http://svn.gnome.org/viewvc/glib/trunk/gio/ Safe RM ======= URL: https://launchpad.net/safe-rm Safe RM does not talk about Trash Cans. The program alert you whenewer you attempt to delete files known to be important. rmtrash ======= website: http://www.nightproductions.net/cli.htm trashcan utility for Mac OS X. I think it support only trashing files (not trash listing, or restoring). Feedback ======== Feel free to leave any comment (mailing me) for any error or else. -EOF trash-cli-0.17.1.14/docs/some-output-of-df-command-on-different-systems.txt000066400000000000000000000060231303652251000263710ustar00rootroot00000000000000The __mount_list method rely on the *df* command. The interface of the *df* command may vary from system to system. We support only the system that comply with the [http://www.opengroup.org/onlinepubs/009695399/utilities/df.html|POSIX specification of the df command]. {{{ # uname -a OSF1 x.invalid.it V4.0 1229 alpha # df -P Filesystem 512-blocks Used Available Capacity Mounted on root_domain#root 262144 167054 80832 68% / /proc 0 0 0 100% /proc usr_domain#usr 11239984 3372768 7796624 31% /usr var_domain#var 2077088 183980 1861744 9% /var /dev/rzb16a 22934504 5001884 15639168 25% /home2 extern:/atom 102398304 44759584 57638720 44% /home2/atom # }}} Example 2: {{{ File system blocchi di 1K Usati Disponib. Uso% Montato su /dev/sdb5 10883628 5202528 5681100 48% / varrun 3062924 104 3062820 1% /var/run varlock 3062924 0 3062924 0% /var/lock udev 3062924 168 3062756 1% /dev devshm 4500000 25640 4474360 1% /dev/shm lrm 3062924 43040 3019884 2% /lib/modules/2.6.24-19-generic/volatile /dev/sdb4 1035692 113336 869744 12% /boot /dev/mapper/var 16577536 1128080 15449456 7% /var /dev/md0 20481928 7523196 12958732 37% /usr /dev/md1 15346084 1109936 14236148 8% /chroot /dev/sda3 50169624 29642452 20527172 60% /home /dev/mapper/spazio1-LV1 426396964 276310360 150086604 65% /media/LV1 /dev/sda4 240686472 27295300 213391172 12% /media/archivio /dev/mapper/ibox 18929088 15733744 3195344 84% /media/ibox gvfs-fuse-daemon 10883628 5202528 5681100 48% /home/lem/.gvfs }}} Example 3: {{{ File system 1024-blocks Used Available Capacity Montato su /dev/sdb5 10883628 5202528 5681100 48% / varrun 3062924 104 3062820 1% /var/run varlock 3062924 0 3062924 0% /var/lock udev 3062924 168 3062756 1% /dev devshm 4500000 25640 4474360 1% /dev/shm lrm 3062924 43040 3019884 2% /lib/modules/2.6.24-19-generic/volatile /dev/sdb4 1035692 113336 869744 12% /boot /dev/mapper/var 16577536 1127952 15449584 7% /var /dev/md0 20481928 7523196 12958732 37% /usr /dev/md1 15346084 1109936 14236148 8% /chroot /dev/sda3 50169624 29642452 20527172 60% /home /dev/mapper/spazio1-LV1 426396964 276310360 150086604 65% /media/LV1 /dev/sda4 240686472 27295300 213391172 12% /media/archivio /dev/mapper/ibox 18929088 15733744 3195344 84% /media/ibox gvfs-fuse-daemon 10883628 5202528 5681100 48% /home/lem/.gvfs }}}trash-cli-0.17.1.14/docs/trash-directories.txt000066400000000000000000000025311303652251000210330ustar00rootroot00000000000000How trash-put should choose in which trash directory the file should be trashed? ================================================================================ When user launches:: $ trash-put file-to-be-trashed the file can be trashed in: 1. "home trash" ($XDG_DATA_HOME/Trash) 2. primary top trash directory ($topdir/.Trash/$uid) 3. alternate top trash directory ($topdir/.Trash-$uid) With these rules: - Home Trash 1. when the file is in the same volume of the "home trash" should choose "home trash" 2. "home trash" should be available for each user 3. "home trash" if not present should created with no delay 4. "home trash" can be used as failsafe method, that means: when the file is different volume but it fails to use both the top trash directories it chose "home trahs" - Primary Top Trash Dir 1. in $topdir/.Trash can be created by an admin - should be created by the admin - should permit user to create $topdir/.Trash/$uid - should not be a symbolic link - the $topdir/.Trash/$uid should not writable by others 2. should be skipped if any of the conditions above is unsatisfied 3. The $topdir/.Trash/$uid should be automatically created without delay if not present - Alternate Top Trash dir 1. Should be created with any delay if needed EOF trash-cli-0.17.1.14/docs/trashspec-0.7.html000066400000000000000000000621731303652251000200330ustar00rootroot00000000000000 Trash specification

The FreeDesktop.org Trash specification

Written by Mikhail Ramendik <mr@ramendik.ru>

Content by David Faure <dfaure@trolltech.com>, Alexander Larsson <alexl@redhat.com> and others on the FreeDesktop.org mailing list

Version 0.7

Abstract

The purpose of this Specification is to provide a common way in which all “Trash can” implementations should store, list, and undelete trashed files. By complying with this Specification, various Trash implementations will be able to work with the same devices and use the same Trash storage. For example, if one implementation sends a file into the Trash can, another will be able to list it, undelete it, or clear it from the Trash.

Introduction

An ability to recover accidentally deleted files has become the de facto standard for today's desktop user experience.

Users do not expect that anything they delete is permanently gone. Instead, they are used to a “Trash can” metaphor. A deleted document ends up in a “Trash can”, and stays there at least for some time — until the can is manually or automatically cleaned.

This system has its own problems. Notably, cleaning disk space becomes a two-step operation — delete files and empty trash; this can lead to confusion for inexperienced users (“what's taking up my space?!”). Also, it is not easy to adapt the system to a multiuser environment. Besides, there is a potential for abuse by uneducated users — anecdotal evidence says they sometimes store important documents in the Trash can, and lose them when it gets cleaned!

However, the benefits of this system are so great, and the user expectation for it so high, that it definitely should be implemented on a free desktop system. And in fact, several implementations already exist — some as command line utilities, some as preloaded libraries, and some as parts of major desktop environments. For example, both Gnome and KDE have their own trash mechanisms.

This Specification is to provide a common way in which all Trash can implementations should store trashed files. By complying with this Specification, various Trash implementations will be able to work with the same devices and use the same Trash storage.

This is important, at least, for shared network resources, removable devices, and in cases when different implementations are used on the same machine at different moments (i.e. some users prefer Gnome, others prefer KDE, and yet others are command-line fans).

Scope and limitations

This Specification only describes the Trash storage. It does not limit the ways in which the actual implementations should operate, as long as they use the same Trash storage. Command line utilities, desktop-integrated solutions and preloaded libraries can work with this specification. 1

This Specification is geared towards the Unix file system tree approach. However, with slight modifications, it can easily be used with another kind of file system tree (for example, with drive letters).

A multi-user environment, where users have specific numeric identifiers, is essential for this Specification.

File systems and logon systems can be case-sensitive or non-case-sensitive; therefore, systems should generally not allow user names that differ only in case.

Definitions

Trash, or Trash can — the storage of files that were trashed (“deleted”) by the user. These files can be listed, undeleted, or cleaned from the trash can.

Trashing — a “delete” operation in which files are transferred into the Trash can.

Erasing — an operation in which files (possibly already in the Trash can) are removed (unlinked) from the file system. An erased file is generally considered to be non-recoverable; the space used by this file is freed. [A “shredding” operation, physically overwriting the data, may or may not accompany an erasing operation; the question of shredding is beyond the scope of this document].

Original location — the name and location that a file (currently in the trash) had prior to getting trashed.

Original filename — the name that a file (currently in the trash) had prior to getting trashed.

Top directory , $topdir — the directory where a file system is mounted. “/” is the top directory for the root file system, but not for the other mounted file systems. For example, separate FSes can be mounted on “/home”, “/mnt/flash”, etc. In this text, the designation “$topdir” is used for “any top directory”.

User identifier , $uid — the numeric user identifier for a user. $uid is used here as “the numeric user identifier of the user who is currently logged on”.

Trash directory — a directory where trashed files, as well as the information on their original name/location and time of trashing, are stored. There may be several trash directories on one system; this Specification defines their location and contents. In this text, the designation “$trash” is used for “any trash directory”.

“Home trash” directory — a user's main trash directory. Its name and location is defined in this document.

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Trash directories

A system can have one or more trash directories. The contents of any trash directory are to be compliant with the same standard, described below.

For every user2 a “home trash” directory MUST be available3. Its name and location are $XDG_DATA_HOME/Trash ; $XDG_DATA_HOME is the base directory for user-specific data, as defined in the Desktop Base Directory Specification .

The “home trash” should function as the user's main trash directory. Files that the user trashes from the same file system (device/partition) should be stored here (see the next section for the storage details). A “home trash” directory SHOULD be automatically created for any new user. If this directory is needed for a trashing operation but does not exist, the implementation SHOULD automatically create it, without any warnings or delays.

The implementation MAY also support trashing files from the rest of the system (including other partitions, shared network resources, and removable devices) into the “home trash” directory . This is a “failsafe” method: trashing works for all file locations, the user can not fill up any space except the home directory, and as other users generally do not have access to it, no security issues arise.

However, this solution leads to costly file copying (between partitions, over the network, from a removable device, etc.) A delay instead of a quick “delete” operation can be unpleasant to users.

An implementation may choose not to support trashing in some of these cases (notably on network resources and removable devices). This is what some well known operating systems do.

It may also choose to provide trashing in the “top directories” of some or all mounted resources. This trashing is done in two ways, described below as (1) and (2).

(1) An administrator can create an $topdir/.Trash directory. The permissions on this directories should permit all users who can trash files at all to write in it.; and the “sticky bit” in the permissions must be set, if the file system supports it.

When trashing a file from a non-home partition/device4 , an implementation (if it supports trashing in top directories) MUST check for the presence of $topdir/.Trash.

When preparing a list of all trashed files (i.e. to show to the user), an implementation also MUST check for .Trash in all top directories that are known to it.

If this directory is present, the implementation MUST, by default, check for the “sticky bit”. (It MAY provide a way for the administrator, and only the administrator, to disable this checking for a particular top directory, in order to support file systems that do not have the “sticky bit”).

The implementation also MUST check that this directory is not a symbolic link.

If any of these checks fail, the implementation MUST NOT use this directory for either trashing or undeleting files, even is an appropriate $uid directory (see below) already exists in it. Besides, the implementation SHOULD report the failed check to the administrator, and MAY also report it to the user.

The following paragraph applies ONLY to the case when the implementation supports trashing in the top directory, and a $topdir/.Trash exists and has passed the checks:

If the directory exists and passes the checks, a subdirectory of the $topdir/.Trash directory is to be used as the user's trash directory for this partition/device. The name of this subdirectory is the numeric identifier of the current user ($topdir/.Trash/$uid). When trashing a file, if this directory does not exist for the current user, the implementation MUST immediately create it, without any warnings or delays for the user.

(2) If an $topdir/.Trash directory is absent, an $topdir/.Trash-$uid directory is to be used as the user's trash directory for this device/partition. $uid is the user's numeric identifier.

The following paragraph applies ONLY to the case when the implementation supports trashing in the top directory, and a $topdir/.Trash does not exist or has not passed the checks:

When trashing a file, if an $topdir/.Trash-$uid directory does not exist, the implementation MUST immediately create it, without any warnings or delays for the user.

When trashing a file, if this directory does not exist for the current user, the implementation MUST immediately create it, without any warnings or delays for the user.

Notes. If an implementation provides trashing in top directories at all, it MUST support both (1) and (2).

If an implementation does NOT provide such trashing, and does provide the user with some interface to view and/or undelete trashed files, it SHOULD make a “best effort” to show files trashed in top directories (by both methods) to the user, among other trashed files or in a clearly accessible separate way.

When trashing a file, if the method (1) fails at any point — i.e. the $topdir/.Trash directory does not exist, or it fails the checks, or the system refuses to create an $uid directory in it — the implementation MUST, by default, fall back to method (2), described below. Except for the case when $topdir/.Trash fails the checks, the fallback must be immediate, without any warnings or delays. The implementation MAY, however, a way for the administrator to disable (2) completely.

If both (1) and (2) fail (i.e. no $topdir/.Trash directory exists, and an attempt to create $topdir/.Trash-$uid fails), the implementation MUST either trash the file into the user's “home trash” or refuse to trash it. The choice between these options can be pre-determined, or it can depend on the particular situation (i.e. No trashing of very large files). However, if an implementation refuses to trash a file after a user action that generally causes trashing, it MUST clearly warn the user about this, and request confirmation for the action.

For showing trashed files, implementations SHOULD support (1) and (2) at the same time (i.e. if both $topdir/.Trash/$uid and $topdir/.Trash-$uid are present, it should list trashed files from both of them).

Contents of a trash directory

The previous section has described the location of trash directories. This section concerns the contents of any trash directory (including the “home trash” directory). This trash directory will be named “$trash” here.

A trash directory contains two subdirectories, named info and files.

The $trash/files directory contains the files and directories that were trashed. When a file or directory is trashed, it MUST be moved into this directory5 . The names of files in this directory are to be determined by the implementation; the only limitation is that they must be unique within the directory. Even if a file with the same name and location gets trashed many times, each subsequent trashing must not overwrite a previous copy. The access rights, access time, modification time and extended attributes (if any) for a file/directory in $trash/files SHOULD be the same as the file/directory had before getting trashed.

IMPORTANT NOTE. While an implementation may choose to base filenames in the $trash/files directory on the original filenames, this is never to be taken for granted6. A filename in the $trash/files directory MUST NEVER be used to recover the original filename; use the info file (see below) for that. (If an info file corresponding to a file/directory in $trash/files is not available, this is an emergency case, and MUST be clearly presented as such to the user or to the system administrator).

The $trash/info directory contains an “information file” for every file and directory in $trash/files. This file MUST have exactly the same name as the file or directory in $trash/files, plus the extension “.trashinfo”7.

The format of this file is similar to the format of a desktop entry file, as described in the Desktop Entry Specification . Its first line must be [Trash Info].

It also must have two lines that are key/value pairs as described in the Desktop Entry Specification:

  • The key “Path” contains the original location of the file/directory, as either an absolute pathname (starting with the slash character “/”) or a relative pathname (starting with any other character). A relative pathname is to be from the directory in which the trash directory resides (i.e., from $XDG_DATA_HOME for the “home trash” directory); it MUST not include a “..” directory, and for files not “under” that directory, absolute pathnames must be used. The system SHOULD only support absolute pathnames in the “home trash” directory, not in the directories under $topdir.

    The value type for this key is “string”; it should store the file name as the sequence of bytes produced by the file system, with characters escaped as in URLs (as defined by RFC 2396, section 2).

  • The key “DeletionDate” contains the date and time when the file/directory was trashed. The date and time are to be in the YYYY-MM-DDThh:mm:ss format (see RFC 3339). The time zone should be the user's (or filesystem's) local time. The value type for this key is “string”.

Example:

[Trash Info]
Path=foo/bar/meow.bow-wow
DeletionDate=20040831T22:32:08

The implementation MUST ignore any other lines in this file, except the first line (must be [Trash Info]) and these two key/value pairs. If a string that starts with “Path=” or “DeletionDate=” occurs several times, the first occurence is to be used.8

Note that $trash/info has no subdirectories. For a directory in $trash/files, only an information file for its own name is needed. This is because, when a subdirectory gets trashed, it must be moved to $trash/files with its entire contents. The names of the files and directories within the directory MUST NOT be altered; the implementation also SHOULD preserve the access and modification time for them.

When trashing a file or directory, the implementation MUST create the corresponding file in $trash/info first. When trashing a file or directory, the implementation MUST create thecorresponding file in $trash/info first. Moreover, it MUST try to do this in an atomic fashion, so that if two processes try trash files with the same filename this will result in two different trash files. On Unix-line systems this is done by generating a filename, and then opening with O_EXCL. If that succeeds the creation was atomic (at least on the same machine), if it fails you need to pick another filename.

Implementation notes

The names of the files/directories in $trash/info SHOULD be somehow related to original file names. This can help manual recovery in emergency cases (for example, if the corresponding info file is lost).

When trashing a file or directory, the implementation should check whether the user has the necessary permissions to delete it, before starting the trashing operation itself.

When copying, rather than moving, a file into the trash (i.e. When trashing to the “home trash” from a different partition), exact preservation of permissions might be impossible. Notably, a file.directory that was owned by another user will now be owned by this user (changing owners is usually only available to root). This should not cause the trashing operation to fail.

In this same situation, setting the permissions should be done after writing the copied file, as they may make it unwriteable..

A trashing operation might be refused because of insufficient permissions, even when the user does have the right to delete a file or directory. This may happen when the user has the right to delete a file/directory, but not to read it (or, in the case of a directory, to list it). In this case, the best solution is probably to warn the user, offering options to delete the file/directory or leave it alone.

Automatic trash cleaning may, and probably eventually should, be implemented. But the implementation should be somehow known to the user.

If a directory was trashed in its entirety, it is easiest to undelete it or remove it from the trash only in its entirety as well, not as separate files. The user might not have the permissions to delete some files in it even while he does have the permission to delete the directory!

Important note on scope. This specification currently does NOT define trashing on remote machines where multiuser permissions are implemented but the numeric user ID is not supported, like FTP sites and CIFS shares. In systems implementing this specification, trashing of files from such machines is to be done only to the user's home trash directory (if at all). A future version may address this limitation.

Administrativia

Status of this document

This document is, at this moment, only a draft. It will hopefully become an official or semi-official FreeDesktop.org specification in the future.

Date of first public distribution: August 30, 2004. This document will serve as evidence of prior art for any patent filed after this date.

Copyright and License

Copyright (C) 2004 Mikhail Ramendik , mr@ramendik.ru .

The originators of the ideas that are described here did not object to this copyright. The author is ready to transfer the copyright to a standards body that would be committed to keeping this specification, or any successor to it, an open standard.

The license: Use and distribute as you wish. If you make a modified version and redistribute it, (a) keep the name of the author and contributors somewhere, and (b) indicate that this is a modified version.

Implementation under any license at all is explicitly allowed.

Location

http://www.ramendik.ru/docs/trashspec.html . If this document gets hosted by FreeDesktop.org, a link to the page will still be available at this location.

http://www.ramendik.ru/docs/trashspec.0.5.html is the permanent location of this version.

Version history

0.1 “First try”, August 30, 2004. Initial draft. “Implementation notes” not written as yet.

0.2 August 30, 2004. Updated with feedback by Alexander Larsson <alexl@redhat.com> and by Dave Cridland <dave@cridland.net>

0.3 September 8, 2004. Changed the name and location of the “home trash” directory, and introduced the generic term “home trash”. Changed the trash info file format to a .desktop-like one. Added directions on creation of info files and copying of trashed files. Changed user names to user ids. Added implementation notes. Added a copyright notice.

0.4 September 9, 2004. Changed [Trash entry] to [Trash info] and fixed some typo's

0.5 September 9, 2004. Changed [Trash info] to [Trash Info]

0.6 October 8, 2004. Corrections by Alexander Larsson <alexl@redhat.com> . Also added “note on scope”. Cleaned up HTML. Added a link to this document on the freedesktop.org standards page

0.7 April 12, 2005. Added URL-style encoding for the name of the deleted file, as implemented in KDE 3.4



1However, developers of preloaded libraries should somehow work around the case when a desktop environment also supporting the Trash specification is run on top of them. “Double trashing” and “trashing of the trash” should be avoided.

2To be more precise, for every user who can use the trash facility. In general, all human users, and possibly some “robotic” ones like ftp, should be able to use the trash facility.

3Note the dot in the beginning, and for case sensitive file systems, note the case.

4To be more precise, from a partition/device different from the one on which $XDG_DATA_HOME resides.

5“$trash/files/”, not into “$trash/” as in many existing implementations!

6At least because another implementation might trash files into the same trash directory

7For example, if the file in $trash/files is named foo.bar , the corresponding file in $trash/info must be named foo.bar.trashinfo

8This provides for future extension

trash-cli-0.17.1.14/docs/who-packaged-trash-cli.txt000066400000000000000000000024031303652251000216140ustar00rootroot00000000000000Which are the distributions packaged trash-cli? =============================================== - Fedora: not included (2009-01-04) - Ubuntu: (included since 2008-07-16) - Bug: https://bugs.launchpad.net/ubuntu/+bug/219601 - Included in jaunty(universe) and intrepid(universe) - OpenSUSE: (seems not included) (2009-01-04) - Added in the OpenSuse whishlist http://en.opensuse.org/Wishlist_Base#T (2009-01-04) - Debian: - Included in [http://packages.debian.org/unstable/utils/trash-cli sid] as of version 0.1.10.28-2 (2009-01-04) - Included in [http://packages.debian.org/lenny/trash-cli lenny] as of version 0.1.10.28-2 (2009-01-04) - Mandriva: (seems not included) (2009-01-04) - Archlinux: (seems included) http://aur.archlinux.org/packages/trash-cli/ (2009-01-04) - Gentoo: (not yet included) - Inclusion request: http://bugs.gentoo.org/238831 - FreeBSD: ? - foresight linux: included - updated on: 2009-04-13 - trash-cli version: 0.1.10.r55 - source package: http://www.rpath.org/repos/foresight/troveInfo?t=trash-cli%3Asource - binary package: http://www.rpath.org/repos/foresight/troveInfo?t=trash-cli - Other? If you've packaged trash-cli for another system (or need help doing so) let us know leaving a comment or mailing me. trash-cli-0.17.1.14/install-rpm.sh000077500000000000000000000003601303652251000165060ustar00rootroot00000000000000python setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES # 'brp-compress' gzips the man pages without distutils knowing... fix this sed -i -e 's@man/man\([[:digit:]]\)/\(.\+\.[[:digit:]]\)$@man/man\1/\2.gz@g' INSTALLED_FILES trash-cli-0.17.1.14/integration_tests/000077500000000000000000000000001303652251000174535ustar00rootroot00000000000000trash-cli-0.17.1.14/integration_tests/__init__.py000066400000000000000000000000001303652251000215520ustar00rootroot00000000000000trash-cli-0.17.1.14/integration_tests/assert_equals_with_unidiff.py000066400000000000000000000012771303652251000254460ustar00rootroot00000000000000# Copyright (C) 2009-2011 Andrea Francia Trivolzio(PV) Italy def assert_equals_with_unidiff(expected, actual): def unidiff(expected, actual): import difflib expected=expected.splitlines(1) actual=actual.splitlines(1) diff=difflib.unified_diff(expected, actual, fromfile='Expected', tofile='Actual', lineterm='\n', n=10) return ''.join(diff) from nose.tools import assert_equals assert_equals(expected, actual, "\n" "Expected:%s\n" % repr(expected) + " Actual:%s\n" % repr(actual) + unidiff(expected, actual)) trash-cli-0.17.1.14/integration_tests/describe_trash_list.py000066400000000000000000000227221303652251000240460ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import os from trashcli.list import ListCmd from .files import (write_file, require_empty_dir, make_sticky_dir, make_unsticky_dir, make_unreadable_file, make_empty_file, make_parent_for) from nose.tools import istest from .output_collector import OutputCollector from .trashinfo import ( a_trashinfo, a_trashinfo_without_date, a_trashinfo_without_path, a_trashinfo_with_invalid_date) from textwrap import dedent from .assert_equals_with_unidiff import assert_equals_with_unidiff class Setup(object): def setUp(self): require_empty_dir('XDG_DATA_HOME') require_empty_dir('topdir') self.user = TrashListUser( environ = {'XDG_DATA_HOME': 'XDG_DATA_HOME'}) self.home_trashcan = FakeTrashDir('XDG_DATA_HOME/Trash') self.add_trashinfo = self.home_trashcan.add_trashinfo def when_dir_is_sticky(self, path): make_sticky_dir(path) def when_dir_exists_unsticky(self, path): make_unsticky_dir(path) def user_run_trash_list(self, *args): self.user.run_trash_list(*args) def user_should_read_output_any_order(self, expected_output): actual_output = self.user.actual_output() assert_equals_with_unidiff(sort_lines(expected_output), sort_lines(actual_output)) def user_should_read_output(self, expected_output): assert_equals_with_unidiff(expected_output, self.user.actual_output()) def sort_lines(lines): return "".join(sorted(lines.splitlines(True))) @istest class describe_trash_list(Setup): @istest def should_output_the_help_message(self): self.user_run_trash_list('--help') self.user_should_read_output(dedent("""\ Usage: trash-list [OPTIONS...] List trashed files Options: --version show program's version number and exit -h, --help show this help message and exit Report bugs to https://github.com/andreafrancia/trash-cli/issues """)) @istest def should_output_nothing_when_trashcan_is_empty(self): self.user_run_trash_list() self.user_should_read_output('') @istest def should_output_deletion_date_and_path(self): self.add_trashinfo('/aboslute/path', '2001-02-03T23:55:59') self.user_run_trash_list() self.user_should_read_output( "2001-02-03 23:55:59 /aboslute/path\n") @istest def should_output_info_for_multiple_files(self): self.add_trashinfo("/file1", "2000-01-01T00:00:01") self.add_trashinfo("/file2", "2000-01-01T00:00:02") self.add_trashinfo("/file3", "2000-01-01T00:00:03") self.user_run_trash_list() self.user_should_read_output_any_order( "2000-01-01 00:00:01 /file1\n" "2000-01-01 00:00:02 /file2\n" "2000-01-01 00:00:03 /file3\n") @istest def should_output_unknown_dates_with_question_marks(self): self.home_trashcan.having_file(a_trashinfo_without_date()) self.user_run_trash_list() self.user_should_read_output("????-??-?? ??:??:?? /path\n") @istest def should_output_invalid_dates_using_question_marks(self): self.home_trashcan.having_file(a_trashinfo_with_invalid_date()) self.user_run_trash_list() self.user_should_read_output("????-??-?? ??:??:?? /path\n") @istest def should_warn_about_empty_trashinfos(self): self.home_trashcan.touch('empty.trashinfo') self.user_run_trash_list() self.user.should_read_error( "Parse Error: XDG_DATA_HOME/Trash/info/empty.trashinfo: " "Unable to parse Path.\n") @istest def should_warn_about_unreadable_trashinfo(self): self.home_trashcan.having_unreadable('unreadable.trashinfo') self.user_run_trash_list() self.user.should_read_error( "[Errno 13] Permission denied: " "'XDG_DATA_HOME/Trash/info/unreadable.trashinfo'\n") @istest def should_warn_about_unexistent_path_entry(self): self.home_trashcan.having_file(a_trashinfo_without_path()) self.user_run_trash_list() self.user.should_read_error( "Parse Error: XDG_DATA_HOME/Trash/info/1.trashinfo: " "Unable to parse Path.\n") self.user_should_read_output('') @istest class with_a_top_trash_dir(Setup): def setUp(self): super(type(self),self).setUp() self.top_trashdir1 = FakeTrashDir('topdir/.Trash/123') self.user.set_fake_uid(123) self.user.add_volume('topdir') @istest def should_list_its_contents_if_parent_is_sticky(self): self.when_dir_is_sticky('topdir/.Trash') self.and_contains_a_valid_trashinfo() self.user_run_trash_list() self.user_should_read_output("2000-01-01 00:00:00 topdir/file1\n") @istest def and_should_warn_if_parent_is_not_sticky(self): self.when_dir_exists_unsticky('topdir/.Trash') self.and_dir_exists('topdir/.Trash/123') self.user_run_trash_list() self.user.should_read_error("TrashDir skipped because parent not sticky: topdir/.Trash/123\n") @istest def but_it_should_not_warn_when_the_parent_is_unsticky_but_there_is_no_trashdir(self): self.when_dir_exists_unsticky('topdir/.Trash') self.but_does_not_exists_any('topdir/.Trash/123') self.user_run_trash_list() self.user.should_read_error("") @istest def should_ignore_trash_from_a_unsticky_topdir(self): self.when_dir_exists_unsticky('topdir/.Trash') self.and_contains_a_valid_trashinfo() self.user_run_trash_list() self.user_should_read_output("") @istest def it_should_ignore_Trash_is_a_symlink(self): self.when_is_a_symlink_to_a_dir('topdir/.Trash') self.and_contains_a_valid_trashinfo() self.user_run_trash_list() self.user_should_read_output('') @istest def and_should_warn_about_it(self): self.when_is_a_symlink_to_a_dir('topdir/.Trash') self.and_contains_a_valid_trashinfo() self.user_run_trash_list() self.user.should_read_error('TrashDir skipped because parent not sticky: topdir/.Trash/123\n') def but_does_not_exists_any(self, path): assert not os.path.exists(path) def and_dir_exists(self, path): os.mkdir(path) assert os.path.isdir(path) def and_contains_a_valid_trashinfo(self): self.top_trashdir1.add_trashinfo('file1', '2000-01-01T00:00:00') def when_is_a_symlink_to_a_dir(self, path): dest = "%s-dest" % path os.mkdir(dest) rel_dest = os.path.basename(dest) os.symlink(rel_dest, path) @istest class describe_when_a_file_is_in_alternate_top_trashdir(Setup): @istest def should_list_contents_of_alternate_trashdir(self): self.user.set_fake_uid(123) self.user.add_volume('topdir') self.top_trashdir2 = FakeTrashDir('topdir/.Trash-123') self.top_trashdir2.add_trashinfo('file', '2000-01-01T00:00:00') self.user_run_trash_list() self.user_should_read_output("2000-01-01 00:00:00 topdir/file\n") class FakeTrashDir: def __init__(self, path): self.path = path + '/info' self.number = 1 def touch(self, path_relative_to_info_dir): make_empty_file(self.join(path_relative_to_info_dir)) def having_unreadable(self, path_relative_to_info_dir): path = self.join(path_relative_to_info_dir) make_unreadable_file(path) def join(self, path_relative_to_info_dir): import os return os.path.join(self.path, path_relative_to_info_dir) def having_file(self, contents): path = '%(info_dir)s/%(name)s.trashinfo' % { 'info_dir' : self.path, 'name' : str(self.number)} make_parent_for(path) write_file(path, contents) self.number += 1 self.path_of_last_file_added = path def add_trashinfo(self, escaped_path_entry, formatted_deletion_date): self.having_file(a_trashinfo(escaped_path_entry, formatted_deletion_date)) class TrashListUser: def __init__(self, environ={}): self.stdout = OutputCollector() self.stderr = OutputCollector() self.environ = environ self.fake_getuid = self.error self.volumes = [] def run_trash_list(self, *args): self.run('trash-list', *args) def run(self,*argv): from trashcli.trash import FileSystemReader file_reader = FileSystemReader() file_reader.list_volumes = lambda: self.volumes ListCmd( out = self.stdout, err = self.stderr, environ = self.environ, getuid = self.fake_getuid, file_reader = file_reader, list_volumes = lambda: self.volumes, ).run(*argv) def set_fake_uid(self, uid): self.fake_getuid = lambda: uid def add_volume(self, mount_point): self.volumes.append(mount_point) def error(self): raise ValueError() def should_read_error(self, expected_value): self.stderr.assert_equal_to(expected_value) def output(self): return self.stdout.getvalue() def actual_output(self): return self.stdout.getvalue() trash-cli-0.17.1.14/integration_tests/files.py000066400000000000000000000035231303652251000211320ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals from trashcli.fs import has_sticky_bit import os, shutil def having_file(path): dirname=os.path.dirname(path) if dirname != '': make_dirs(dirname) open(path,'w').close() assert os.path.isfile(path) make_empty_file = having_file def write_file(filename, contents=''): parent = os.path.dirname(filename) if not os.path.isdir(parent): os.makedirs(parent) open(filename, 'w').write(contents) assert_equals(open(filename).read(), contents) def require_empty_dir(path): if os.path.exists(path): shutil.rmtree(path) make_dirs(path) assert os.path.isdir(path) assert_equals([], list(os.listdir(path))) def having_empty_dir(path): require_empty_dir(path) def make_dirs(path): if not os.path.isdir(path): os.makedirs(path) assert os.path.isdir(path) def make_parent_for(path): parent = os.path.dirname(path) make_dirs(parent) def make_sticky_dir(path): os.mkdir(path) set_sticky_bit(path) def make_unsticky_dir(path): os.mkdir(path) unset_sticky_bit(path) def make_dir_unsticky(path): assert_is_dir(path) unset_sticky_bit(path) def assert_is_dir(path): assert os.path.isdir(path) def set_sticky_bit(path): import stat os.chmod(path, os.stat(path).st_mode | stat.S_ISVTX) def unset_sticky_bit(path): import stat os.chmod(path, os.stat(path).st_mode & ~ stat.S_ISVTX) assert not has_sticky_bit(path) def touch(path): open(path,'a+').close() def ensure_non_sticky_dir(path): import os assert os.path.isdir(path) assert not has_sticky_bit(path) def make_unreadable_file(path): write_file(path, '') import os os.chmod(path, 0) from nose.tools import assert_raises with assert_raises(IOError): open(path).read() trash-cli-0.17.1.14/integration_tests/output_collector.py000066400000000000000000000012731303652251000234360ustar00rootroot00000000000000from .assert_equals_with_unidiff import assert_equals_with_unidiff class OutputCollector: def __init__(self): from unit_tests.myStringIO import StringIO self.stream = StringIO() self.getvalue = self.stream.getvalue def write(self,data): self.stream.write(data) def assert_equal_to(self, expected): return self.should_be(expected) def should_be(self, expected): assert_equals_with_unidiff(expected, self.output()) def should_match(self, regex): text = self.output() from nose.tools import assert_regexp_matches assert_regexp_matches(text, regex) def output(self): return self.stream.getvalue() trash-cli-0.17.1.14/integration_tests/test_file_descriptions.py000066400000000000000000000030151303652251000245700ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from trashcli.put import describe from .files import require_empty_dir, having_file from nose.tools import assert_equals import os class TestDescritions: def setUp(self): require_empty_dir('sandbox') def test_on_directories(self): assert_equals("directory", describe('.')) assert_equals("directory", describe("..")) assert_equals("directory", describe("sandbox")) def test_on_dot_directories(self): assert_equals("'.' directory", describe("sandbox/.")) assert_equals("'.' directory", describe("./.")) def test_on_dot_dot_directories(self): assert_equals("'..' directory", describe("./..")) assert_equals("'..' directory", describe("sandbox/..")) def test_name_for_regular_files_non_empty_files(self): write_file("sandbox/non-empty", "contents") assert_equals("regular file", describe("sandbox/non-empty")) def test_name_for_empty_file(self): having_file('sandbox/empty') assert_equals("regular empty file", describe("sandbox/empty")) def test_name_for_symbolic_links(self): os.symlink('nowhere', "sandbox/symlink") assert_equals("symbolic link", describe("sandbox/symlink")) def test_name_for_non_existent_entries(self): assert not os.path.exists('non-existent') assert_equals("non existent", describe('non-existent')) def write_file(path, contents): f = open(path, 'w') f.write(contents) f.close() trash-cli-0.17.1.14/integration_tests/test_filesystem.py000066400000000000000000000030051303652251000232460ustar00rootroot00000000000000# Copyright (C) 2008-2011 Andrea Francia Trivolzio(PV) Italy from trashcli.trash import FileSystemReader from trashcli.fs import mkdirs from trashcli.fs import has_sticky_bit from .files import require_empty_dir, having_file, set_sticky_bit import os class TestWithInSandbox: def test_mkdirs_with_default_mode(self): mkdirs("sandbox/test-dir/sub-dir") assert os.path.isdir("sandbox/test-dir/sub-dir") def test_has_sticky_bit_returns_true(self): having_file( "sandbox/sticky") run('chmod +t sandbox/sticky') assert has_sticky_bit('sandbox/sticky') def test_has_sticky_bit_returns_false(self): having_file( "sandbox/non-sticky") run('chmod -t sandbox/non-sticky') assert not has_sticky_bit("sandbox/non-sticky") def setUp(self): require_empty_dir('sandbox') is_sticky_dir=FileSystemReader().is_sticky_dir class Test_is_sticky_dir: def test_dir_non_sticky(self): mkdirs('sandbox/dir'); assert not is_sticky_dir('sandbox/dir') def test_dir_sticky(self): mkdirs('sandbox/dir'); set_sticky_bit('sandbox/dir') assert is_sticky_dir('sandbox/dir') def test_non_dir_but_sticky(self): having_file('sandbox/dir'); set_sticky_bit('sandbox/dir') assert not is_sticky_dir('sandbox/dir') def setUp(self): require_empty_dir('sandbox') def run(command): import subprocess assert subprocess.call(command.split()) == 0 trash-cli-0.17.1.14/integration_tests/test_listing_all_trashinfo_in_a_trashdir.py000066400000000000000000000030161303652251000303300ustar00rootroot00000000000000from trashcli.trash import TrashDirectory from .files import require_empty_dir from .files import write_file from unit_tests.tools import assert_items_equal from nose.tools import assert_equals from mock import Mock class TestWhenListingTrashinfo: def setUp(self): require_empty_dir('sandbox') self.trash_dir = TrashDirectory('sandbox', '/') self.logger = Mock() self.trash_dir.logger = self.logger def test_should_list_a_trashinfo(self): write_file('sandbox/info/foo.trashinfo') result = self.list_trashinfos() assert_equals(['sandbox/info/foo.trashinfo'], result) def test_should_list_multiple_trashinfo(self): write_file('sandbox/info/foo.trashinfo') write_file('sandbox/info/bar.trashinfo') write_file('sandbox/info/baz.trashinfo') result = self.list_trashinfos() assert_items_equal(['sandbox/info/foo.trashinfo', 'sandbox/info/baz.trashinfo', 'sandbox/info/bar.trashinfo'], result) def test_should_ignore_non_trashinfo(self): write_file('sandbox/info/not-a-trashinfo') result = self.list_trashinfos() assert_equals([], result) def test_non_trashinfo_should_reported_as_a_warn(self): write_file('sandbox/info/not-a-trashinfo') self.list_trashinfos() self.logger.warning.assert_called_with('Non .trashinfo file in info dir') def list_trashinfos(self): return list(self.trash_dir.all_info_files()) trash-cli-0.17.1.14/integration_tests/test_persist.py000066400000000000000000000040771303652251000225650ustar00rootroot00000000000000# Copyright (C) 2008-2012 Andrea Francia Trivolzio(PV) Italy import os from nose.tools import assert_equals, assert_true from integration_tests.files import require_empty_dir from trashcli.put import TrashDirectoryForPut, RealFs join = os.path.join class TestTrashDirectory_persit_trash_info: def setUp(self): self.trashdirectory_base_dir = os.path.realpath( "./sandbox/testTrashDirectory") require_empty_dir(self.trashdirectory_base_dir) self.instance = TrashDirectoryForPut( self.trashdirectory_base_dir, "/", None, RealFs()) def persist_trash_info(self, basename, content): return self.instance.persist_trash_info( self.instance.info_dir, basename,content) def test_persist_trash_info_first_time(self): trash_info_file = self.persist_trash_info('dummy-path', b'content') assert_equals(join(self.trashdirectory_base_dir,'info', 'dummy-path.trashinfo'), trash_info_file) assert_equals('content', read(trash_info_file)) def test_persist_trash_info_first_100_times(self): self.test_persist_trash_info_first_time() for i in range(1,100) : content=b'trashinfo content' trash_info_file = self.persist_trash_info('dummy-path', content) assert_equals("dummy-path_%s.trashinfo" % i, os.path.basename(trash_info_file)) assert_equals('trashinfo content', read(trash_info_file)) def test_persist_trash_info_other_times(self): self.test_persist_trash_info_first_100_times() for i in range(101,200) : trash_info_file = self.persist_trash_info('dummy-path',b'content') trash_info_id = os.path.basename(trash_info_file) assert_true(trash_info_id.startswith("dummy-path_")) assert_equals('content', read(trash_info_file)) test_persist_trash_info_first_100_times.stress_test = True test_persist_trash_info_other_times.stress_test = True def read(path): return open(path).read() trash-cli-0.17.1.14/integration_tests/test_restore_trash.py000066400000000000000000000104021303652251000237450ustar00rootroot00000000000000import os from nose.tools import istest from trashcli.restore import RestoreCmd from .files import require_empty_dir from trashcli.fs import remove_file from .output_collector import OutputCollector from .trashinfo import a_trashinfo @istest class describe_restore_trash: @istest def it_should_do_nothing_when_no_file_have_been_found_in_current_dir(self): self.when_running_restore_trash() self.output_should_match('No files trashed from current dir.+') @istest def it_should_error_when_user_input_is_not_a_number(self): self.having_a_trashed_file('/foo/bar') self.when_running_restore_trash( from_dir='/foo', with_user_typing = '-@notanumber') self.error_should_be('Invalid entry\n') @istest def it_should_error_when_user_input_is_too_small(self): self.having_a_trashed_file('/foo/bar') self.when_running_restore_trash( from_dir='/foo', with_user_typing = '-1') self.error_should_be('Invalid entry\n') @istest def it_should_error_when_user_input_is_too_large(self): self.having_a_trashed_file('/foo/bar') self.when_running_restore_trash( from_dir='/foo', with_user_typing = '1') self.error_should_be('Invalid entry\n') @istest def it_should_show_the_file_deleted_from_the_current_dir(self): self.having_a_trashed_file('/foo/bar') self.when_running_restore_trash(from_dir='/foo') self.output_should_match( ' 0 2000-01-01 00:00:01 /foo/bar\n.*\n') self.error_should_be('') @istest def it_should_restore_the_file_selected_by_the_user(self): self.having_a_file_trashed_from_current_dir('foo') self.when_running_restore_trash( from_dir=os.getcwd(), with_user_typing = '0') self.file_should_have_been_restored('foo') @istest def it_should_exit_gracefully_when_user_selects_nothing(self): self.having_a_trashed_file('/foo/bar') self.when_running_restore_trash( from_dir='/foo', with_user_typing = '') self.output_should_match( '.*\nExiting\n') self.error_should_be('') @istest def it_should_refuse_overwriting_existing_file(self): self.having_a_file_trashed_from_current_dir('foo') open('foo', 'a+').close() os.chmod('foo', 000) self.when_running_restore_trash(from_dir=current_dir(), with_user_typing = '0') self.error_should_be('Refusing to overwrite existing file "foo".\n') def setUp(self): require_empty_dir('XDG_DATA_HOME') trashcan = TrashCan('XDG_DATA_HOME/Trash') self.having_a_trashed_file = trashcan.write_trashed_file out = OutputCollector() err = OutputCollector() self.when_running_restore_trash = RestoreTrashRunner(out, err, 'XDG_DATA_HOME') self.output_should_match = out.should_match self.error_should_be = err.should_be def having_a_file_trashed_from_current_dir(self, filename): self.having_a_trashed_file(os.path.join(os.getcwd(), filename)) remove_file(filename) assert not os.path.exists(filename) def file_should_have_been_restored(self, filename): assert os.path.exists(filename) def current_dir(): return os.getcwd() class RestoreTrashRunner: def __init__(self, out, err, XDG_DATA_HOME): self.environ = {'XDG_DATA_HOME': XDG_DATA_HOME} self.out = out self.err = err def __call__(self, from_dir='/', with_user_typing=''): RestoreCmd( stdout = self.out, stderr = self.err, environ = self.environ, exit = [].append, input = lambda msg: with_user_typing, curdir = lambda: from_dir ).run([]) class TrashCan: def __init__(self, path): self.path = path def write_trashed_file(self, path): from .files import write_file write_file('%s/info/foo.trashinfo' % self.path, a_trashinfo(path)) write_file('%s/files/foo' % self.path) trash-cli-0.17.1.14/integration_tests/test_trash_empty.py000066400000000000000000000272211303652251000234270ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals, istest from unit_tests.tools import assert_items_equal from trashcli.empty import EmptyCmd from unit_tests.myStringIO import StringIO import os from .files import write_file, require_empty_dir, make_dirs, set_sticky_bit from .files import having_file from mock import MagicMock from trashcli.trash import FileSystemReader from trashcli.fs import FileRemover from nose.tools import assert_regexp_matches from trashcli.cmds import empty from trashcli.fs import mkdirs from .files import touch from nose.tools import assert_true, assert_raises import shutil class TestTrashEmptyCmd: def test(self): out = StringIO() empty(['trash-empty', '-h'], stdout = out) assert_regexp_matches(out.getvalue(), '^Usage. trash-empty.*') def test_trash_empty_will_crash_on_unreadable_directory_issue_48(self): out = StringIO() err = StringIO() mkdirs('data/Trash/files') mkdirs('data/Trash/files/unreadable') os.chmod('data/Trash/files/unreadable', 0o300) assert_true(os.path.exists('data/Trash/files/unreadable')) empty(['trash-empty'], stdout = out, stderr = err, environ={'XDG_DATA_HOME':'data'}) assert_equals("trash-empty: cannot remove data/Trash/files/unreadable\n", err.getvalue()) os.chmod('data/Trash/files/unreadable', 0o700) shutil.rmtree('data') def test_the_core_of_failures_for_issue_48(self): mkdirs('unreadable-dir') os.chmod('unreadable-dir', 0o300) assert_true(os.path.exists('unreadable-dir')) try: FileRemover().remove_file('unreadable-dir') assert False except OSError: pass os.chmod('unreadable-dir', 0o700) shutil.rmtree('unreadable-dir') class TestWhenCalledWithoutArguments: def setUp(self): require_empty_dir('XDG_DATA_HOME') self.info_dir_path = 'XDG_DATA_HOME/Trash/info' self.files_dir_path = 'XDG_DATA_HOME/Trash/files' self.environ = {'XDG_DATA_HOME':'XDG_DATA_HOME'} now = MagicMock(side_effect=RuntimeError) self.empty_cmd = EmptyCmd( out = StringIO(), err = StringIO(), environ = self.environ, list_volumes = no_volumes, now = now, file_reader = FileSystemReader(), getuid = None, file_remover = FileRemover(), version = None, ) def user_run_trash_empty(self): self.empty_cmd.run('trash-empty') def test_it_should_remove_an_info_file(self): self.having_a_trashinfo_in_trashcan('foo.trashinfo') self.user_run_trash_empty() self.assert_dir_empty(self.info_dir_path) def test_it_should_remove_all_the_infofiles(self): self.having_three_trashinfo_in_trashcan() self.user_run_trash_empty() self.assert_dir_empty(self.info_dir_path) def test_it_should_remove_the_backup_files(self): self.having_one_trashed_file() self.user_run_trash_empty() self.assert_dir_empty(self.files_dir_path) def test_it_should_keep_unknown_files_found_in_infodir(self): self.having_file_in_info_dir('not-a-trashinfo') self.user_run_trash_empty() self.assert_dir_contains(self.info_dir_path, 'not-a-trashinfo') def test_but_it_should_remove_orphan_files_from_the_files_dir(self): self.having_orphan_file_in_files_dir() self.user_run_trash_empty() self.assert_dir_empty(self.files_dir_path) def test_it_should_purge_also_directories(self): os.makedirs("XDG_DATA_HOME/Trash/files/a-dir") self.user_run_trash_empty() self.assert_dir_empty(self.files_dir_path) def assert_dir_empty(self, path): assert len(os.listdir(path)) == 0 def assert_dir_contains(self, path, filename): assert os.path.exists(os.path.join(path, filename)) def having_a_trashinfo_in_trashcan(self, basename_of_trashinfo): having_file(os.path.join(self.info_dir_path, basename_of_trashinfo)) def having_three_trashinfo_in_trashcan(self): self.having_a_trashinfo_in_trashcan('foo.trashinfo') self.having_a_trashinfo_in_trashcan('bar.trashinfo') self.having_a_trashinfo_in_trashcan('baz.trashinfo') assert_items_equal(['foo.trashinfo', 'bar.trashinfo', 'baz.trashinfo'], os.listdir(self.info_dir_path)) def having_one_trashed_file(self): self.having_a_trashinfo_in_trashcan('foo.trashinfo') having_file(self.files_dir_path +'/foo') self.files_dir_should_not_be_empty() def files_dir_should_not_be_empty(self): assert len(os.listdir(self.files_dir_path)) != 0 def having_file_in_info_dir(self, filename): having_file(os.path.join(self.info_dir_path, filename)) def having_orphan_file_in_files_dir(self): complete_path = os.path.join(self.files_dir_path, 'a-file-without-any-associated-trashinfo') having_file(complete_path) assert os.path.exists(complete_path) @istest class When_invoked_with_N_days_as_argument: def setUp(self): require_empty_dir('XDG_DATA_HOME') self.xdg_data_home = 'XDG_DATA_HOME' self.environ = {'XDG_DATA_HOME':'XDG_DATA_HOME'} self.now = MagicMock(side_effect=RuntimeError) self.empty_cmd=EmptyCmd( out = StringIO(), err = StringIO(), environ = self.environ, list_volumes = no_volumes, now = self.now, file_reader = FileSystemReader(), getuid = None, file_remover = FileRemover(), version = None, ) def user_run_trash_empty(self, *args): self.empty_cmd.run('trash-empty', *args) def set_clock_at(self, yyyy_mm_dd): self.now.side_effect = lambda:date(yyyy_mm_dd) def date(yyyy_mm_dd): from datetime import datetime return datetime.strptime(yyyy_mm_dd, '%Y-%m-%d') @istest def it_should_keep_files_newer_than_N_days(self): self.having_a_trashed_file('foo', '2000-01-01') self.set_clock_at('2000-01-01') self.user_run_trash_empty('2') self.file_should_have_been_kept_in_trashcan('foo') @istest def it_should_remove_files_older_than_N_days(self): self.having_a_trashed_file('foo', '1999-01-01') self.set_clock_at('2000-01-01') self.user_run_trash_empty('2') self.file_should_have_been_removed_from_trashcan('foo') @istest def it_should_kept_files_with_invalid_deletion_date(self): self.having_a_trashed_file('foo', 'Invalid Date') self.set_clock_at('2000-01-01') self.user_run_trash_empty('2') self.file_should_have_been_kept_in_trashcan('foo') def having_a_trashed_file(self, name, date): contents = "DeletionDate=%sT00:00:00\n" % date write_file(self.trashinfo(name), contents) def trashinfo(self, name): return '%(dirname)s/Trash/info/%(name)s.trashinfo' % { 'dirname' : self.xdg_data_home, 'name' : name } def file_should_have_been_kept_in_trashcan(self, trashinfo_name): assert os.path.exists(self.trashinfo(trashinfo_name)) def file_should_have_been_removed_from_trashcan(self, trashinfo_name): assert not os.path.exists(self.trashinfo(trashinfo_name)) class TestEmptyCmdWithMultipleVolumes: def setUp(self): require_empty_dir('topdir') self.empty=EmptyCmd( out = StringIO(), err = StringIO(), environ = {}, list_volumes = lambda: ['topdir'], now = None, file_reader = FileSystemReader(), getuid = lambda: 123, file_remover = FileRemover(), version = None, ) def test_it_removes_trashinfos_from_method_1_dir(self): self.make_proper_top_trash_dir('topdir/.Trash') having_file('topdir/.Trash/123/info/foo.trashinfo') self.empty.run('trash-empty') assert not os.path.exists('topdir/.Trash/123/info/foo.trashinfo') def test_it_removes_trashinfos_from_method_2_dir(self): having_file('topdir/.Trash-123/info/foo.trashinfo') self.empty.run('trash-empty') assert not os.path.exists('topdir/.Trash-123/info/foo.trashinfo') def test_it_removes_trashinfo_from_specified_trash_dir(self): having_file('specified/info/foo.trashinfo') self.empty.run('trash-empty', '--trash-dir', 'specified') assert not os.path.exists('specified/info/foo.trashinfo') def make_proper_top_trash_dir(self, path): make_dirs(path) set_sticky_bit(path) from textwrap import dedent class TestTrashEmpty_on_help: def test_help_output(self): err, out = StringIO(), StringIO() cmd = EmptyCmd(err = err, out = out, environ = {}, list_volumes = no_volumes, now = None, file_reader = FileSystemReader(), getuid = None, file_remover = None, version = None, ) cmd.run('trash-empty', '--help') assert_equals(out.getvalue(), dedent("""\ Usage: trash-empty [days] Purge trashed files. Options: --version show program's version number and exit -h, --help show this help message and exit Report bugs to https://github.com/andreafrancia/trash-cli/issues """)) class TestTrashEmpty_on_version(): def test_it_print_version(self): err, out = StringIO(), StringIO() cmd = EmptyCmd(err = err, out = out, environ = {}, list_volumes = no_volumes, now = None, file_reader = FileSystemReader(), getuid = None, file_remover = None, version = '1.2.3', ) cmd.run('trash-empty', '--version') assert_equals(out.getvalue(), dedent("""\ trash-empty 1.2.3 """)) class describe_trash_empty_command_line__on_invalid_options(): def setUp(self): self.err, self.out = StringIO(), StringIO() self.cmd = EmptyCmd( err = self.err, out = self.out, environ = {}, list_volumes = no_volumes, now = None, file_reader = FileSystemReader(), getuid = None, file_remover = None, version = None, ) def it_should_fail(self): self.exit_code = self.cmd.run('trash-empty', '-2') exit_code_for_command_line_usage = 64 assert_equals(exit_code_for_command_line_usage, self.exit_code) def it_should_complain_to_the_standard_error(self): self.exit_code = self.cmd.run('trash-empty', '-2') assert_equals(self.err.getvalue(), dedent("""\ trash-empty: invalid option -- '2' """)) def test_with_a_different_option(self): self.cmd.run('trash-empty', '-3') assert_equals(self.err.getvalue(), dedent("""\ trash-empty: invalid option -- '3' """)) def no_volumes(): return [] trash-cli-0.17.1.14/integration_tests/test_trash_put.py000066400000000000000000000170441303652251000231030ustar00rootroot00000000000000# Copyright (C) 2009-2011 Andrea Francia Trivolzio(PV) Italy from trashcli.put import TrashPutCmd import os from nose.tools import istest, assert_equals, assert_not_equals from nose.tools import assert_in from .files import having_file, require_empty_dir, having_empty_dir from .files import make_sticky_dir from trashcli.fstab import FakeFstab from trashcli.fs import remove_file from trashcli.put import parent_path class TestPath: def setUp(self): self.base = os.path.realpath(os.getcwd()) def test(self): require_empty_dir('other_dir/dir') remove_file('dir') os.symlink('other_dir/dir', 'dir') having_file('dir/foo') assert_equals(os.path.join(self.base,'other_dir/dir'), parent_path('dir/foo')) remove_file('dir') remove_file('other_dir') def test2(self): require_empty_dir('test-disk/dir') remove_file('link-to-non-existent') os.symlink('test-disk/non-existent', 'link-to-non-existent') assert_equals(self.base, parent_path('link-to-non-existent')) remove_file('link-to-non-existent') def test3(self): remove_file('foo') remove_file('bar') require_empty_dir('foo') require_empty_dir('bar') os.symlink('../bar/zap', 'foo/zap') assert_equals(os.path.join(self.base,'foo'), parent_path('foo/zap')) remove_file('foo') remove_file('bar') def test4(self): remove_file('foo') remove_file('bar') require_empty_dir('foo') require_empty_dir('bar') os.symlink('../bar/zap', 'foo/zap') having_file('bar/zap') assert_equals(os.path.join(self.base,'foo'), parent_path('foo/zap')) remove_file('foo') remove_file('bar') class TrashPutTest: def setUp(self): self.prepare_fixture() self.setUp2() def setUp2(self): pass def prepare_fixture(self): require_empty_dir('sandbox') self.environ = {'XDG_DATA_HOME': 'sandbox/XDG_DATA_HOME' } from .output_collector import OutputCollector self.out = OutputCollector() self.err = OutputCollector() self.fstab = FakeFstab() self.stderr_should_be = self.err.should_be self.output_should_be = self.out.should_be def run_trashput(self, *argv): cmd = TrashPutCmd( stdout = self.out, stderr = self.err, environ = self.environ, volume_of = self.fstab.volume_of, parent_path = os.path.dirname, realpath = lambda x:x ) self.exit_code = cmd.run(list(argv)) self.stderr = self.err.getvalue() @istest class when_deleting_an_existing_file(TrashPutTest): def setUp2(self): having_file('sandbox/foo') self.run_trashput('trash-put', 'sandbox/foo') @istest def it_should_remove_the_file(self): file_should_have_been_deleted('sandbox/foo') @istest def it_should_remove_it_silently(self): self.output_should_be('') @istest def a_trashinfo_file_should_have_been_created(self): open('sandbox/XDG_DATA_HOME/Trash/info/foo.trashinfo').read() @istest class when_deleting_an_existing_file_in_verbose_mode(TrashPutTest): def setUp2(self): having_file('sandbox/foo') self.run_trashput('trash-put', '-v', 'sandbox/foo') @istest def should_tell_where_a_file_is_trashed(self): assert_in("trash-put: 'sandbox/foo' trashed in sandbox/XDG_DATA_HOME/Trash", self.stderr.splitlines()) @istest def should_be_succesfull(self): assert_equals(0, self.exit_code) @istest class when_deleting_a_non_existing_file(TrashPutTest): def setUp2(self): self.run_trashput('trash-put', '-v', 'non-existent') @istest def should_be_succesfull(self): assert_not_equals(0, self.exit_code) @istest class when_fed_with_dot_arguments(TrashPutTest): def setUp2(self): having_empty_dir('sandbox/') having_file('other_argument') def test_dot_argument_is_skipped(self): self.run_trashput("trash-put", ".", "other_argument") # the dot directory shouldn't be operated, but a diagnostic message # shall be writtend on stderr self.stderr_should_be( "trash-put: cannot trash directory '.'\n") # the remaining arguments should be processed assert not exists('other_argument') def test_dot_dot_argument_is_skipped(self): self.run_trashput("trash-put", "..", "other_argument") # the dot directory shouldn't be operated, but a diagnostic message # shall be writtend on stderr self.stderr_should_be( "trash-put: cannot trash directory '..'\n") # the remaining arguments should be processed assert not exists('other_argument') def test_dot_argument_is_skipped_even_in_subdirs(self): self.run_trashput("trash-put", "sandbox/.", "other_argument") # the dot directory shouldn't be operated, but a diagnostic message # shall be writtend on stderr self.stderr_should_be( "trash-put: cannot trash '.' directory 'sandbox/.'\n") # the remaining arguments should be processed assert not exists('other_argument') assert exists('sandbox') def test_dot_dot_argument_is_skipped_even_in_subdirs(self): self.run_trashput("trash-put", "sandbox/..", "other_argument") # the dot directory shouldn't be operated, but a diagnostic message # shall be writtend on stderr self.stderr_should_be( "trash-put: cannot trash '..' directory 'sandbox/..'\n") # the remaining arguments should be processed assert not exists('other_argument') assert exists('sandbox') from textwrap import dedent @istest class TestUnsecureTrashDirMessages(TrashPutTest): def setUp(self): TrashPutTest.setUp(self) having_empty_dir('fake-vol') self.fstab.add_mount('fake-vol') having_file('fake-vol/foo') @istest def when_is_unsticky(self): having_empty_dir('fake-vol/.Trash') self.run_trashput('trash-put', '-v', 'fake-vol/foo') assert_line_in_text( 'trash-put: found unsecure .Trash dir (should be sticky): ' 'fake-vol/.Trash', self.stderr) @istest def when_it_is_not_a_dir(self): having_file('fake-vol/.Trash') self.run_trashput('trash-put', '-v', 'fake-vol/foo') assert_line_in_text( 'trash-put: found unusable .Trash dir (should be a dir): ' 'fake-vol/.Trash', self.stderr) @istest def when_is_a_symlink(self): make_sticky_dir('fake-vol/link-destination') os.symlink('link-destination', 'fake-vol/.Trash') self.run_trashput('trash-put', '-v', 'fake-vol/foo') assert_line_in_text( 'trash-put: found unsecure .Trash dir (should not be a symlink): ' 'fake-vol/.Trash', self.stderr) def assert_line_in_text(line, text): assert_in(line, text.splitlines(), dedent('''\ Line not found in text Line: %s Text: --- %s---''') %(repr(line), text)) def should_fail(func): from nose.tools import assert_raises with assert_raises(AssertionError): func() def file_should_have_been_deleted(path): import os assert not os.path.exists('sandbox/foo') exists = os.path.exists trash-cli-0.17.1.14/integration_tests/test_trash_rm.py000066400000000000000000000075031303652251000227100ustar00rootroot00000000000000from unit_tests.myStringIO import StringIO from mock import Mock, ANY from nose.tools import assert_false, assert_raises, assert_equals from .files import require_empty_dir, write_file from trashcli.rm import RmCmd, ListTrashinfos from .trashinfo import a_trashinfo_with_path, a_trashinfo_without_path from trashcli.trash import ParseError class TestTrashRm: def test_issue69(self): self.add_invalid_trashinfo_without_path(1) self.trash_rm.run(['trash-rm', 'any-pattern (ignored)']) assert_equals('trash-rm: ' 'sandbox/xdh/Trash/info/1.trashinfo: ' 'unable to parse \'Path\'' '\n' , self.stderr.getvalue()) def test_integration(self): self.add_trashinfo_for(1, 'to/be/deleted') self.add_trashinfo_for(2, 'to/be/kept') self.trash_rm.run(['trash-rm', 'delete*']) self.assert_trashinfo_has_been_deleted(1) def setUp(self): require_empty_dir('sandbox/xdh') self.stderr = StringIO() self.trash_rm = RmCmd(environ = {'XDG_DATA_HOME':'sandbox/xdh'} , getuid = 123 , list_volumes = lambda:[] , stderr = self.stderr , file_reader = FileSystemReader()) def add_trashinfo_for(self, index, path): write_file(self.trashinfo_from_index(index), a_trashinfo_with_path(path)) def add_invalid_trashinfo_without_path(self, index): write_file(self.trashinfo_from_index(index), a_trashinfo_without_path()) def trashinfo_from_index(self, index): return 'sandbox/xdh/Trash/info/%s.trashinfo' % index def assert_trashinfo_has_been_deleted(self, index): import os filename = self.trashinfo_from_index(index) assert_false(os.path.exists(filename), 'File "%s" still exists' % filename) from trashcli.fs import FileSystemReader class TestListing: def setUp(self): require_empty_dir('sandbox') self.out = Mock() self.listing = ListTrashinfos(self.out, FileSystemReader(), None) self.index = 0 def test_should_report_original_location(self): self.add_trashinfo('/foo') self.listing.list_from_volume_trashdir('sandbox/Trash', '/') self.out.assert_called_with('/foo', ANY) def test_should_report_trashinfo_path(self): self.add_trashinfo(trashinfo_path='sandbox/Trash/info/a.trashinfo') self.listing.list_from_volume_trashdir('sandbox/Trash', '/') self.out.assert_called_with(ANY, 'sandbox/Trash/info/a.trashinfo') def test_should_handle_volume_trashdir(self): self.add_trashinfo(trashinfo_path='sandbox/.Trash/123/info/a.trashinfo') self.listing.list_from_volume_trashdir('sandbox/.Trash/123', '/fake/vol') self.out.assert_called_with(ANY, 'sandbox/.Trash/123/info/a.trashinfo') def test_should_absolutize_relative_path_for_volume_trashdir(self): self.add_trashinfo(path='foo/bar', trashdir='sandbox/.Trash/501') self.listing.list_from_volume_trashdir('sandbox/.Trash/501', '/fake/vol') self.out.assert_called_with('/fake/vol/foo/bar', ANY) def add_trashinfo(self, path='unspecified/original/location', trashinfo_path=None, trashdir='sandbox/Trash'): trashinfo_path = trashinfo_path or self._trashinfo_path(trashdir) write_file(trashinfo_path, a_trashinfo_with_path(path)) def _trashinfo_path(self, trashdir): path = '%s/info/%s.trashinfo' % (trashdir, self.index) self.index +=1 return path trash-cli-0.17.1.14/integration_tests/test_trash_rm_script.py000066400000000000000000000014741303652251000242750ustar00rootroot00000000000000from nose.tools import istest, assert_equals, assert_in import subprocess from subprocess import STDOUT, PIPE, check_output, call, Popen from .assert_equals_with_unidiff import assert_equals_with_unidiff as assert_equals from textwrap import dedent from pprint import pprint import sys @istest class WhenNoArgs: def setUp(self): process = Popen([sys.executable, 'trashcli/rm.py'], env={'PYTHONPATH':'.'}, stdin=None, stdout=PIPE, stderr=PIPE) (self.stdout, self.stderr) = process.communicate() self.stderr = self.stderr.decode('utf-8') process.wait() self.returncode = process.returncode def test_should_print_usage_on_standard_error(self): assert_in("Usage:", self.stderr.splitlines()) trash-cli-0.17.1.14/integration_tests/trashinfo.py000066400000000000000000000014471303652251000220300ustar00rootroot00000000000000def a_trashinfo(escaped_path_entry, formatted_deletion_date = '2000-01-01T00:00:01'): return ("[Trash Info]\n" + "Path=%s\n" % escaped_path_entry + "DeletionDate=%s\n" % formatted_deletion_date) def a_trashinfo_without_date(): return ("[Trash Info]\n" "Path=/path\n") def a_trashinfo_with_invalid_date(): return ("[Trash Info]\n" "Path=/path\n" "DeletionDate=Wrong Date") def a_trashinfo_without_path(): return ("[Trash Info]\n" "DeletionDate='2000-01-01T00:00:00'\n") def a_trashinfo_with_date(date): return ("[Trash Info]\n" "DeletionDate=%s\n" % date) def a_trashinfo_with_path(path): return ("[Trash Info]\n" "Path=%s\n" % path) trash-cli-0.17.1.14/man/000077500000000000000000000000001303652251000144615ustar00rootroot00000000000000trash-cli-0.17.1.14/man/man1/000077500000000000000000000000001303652251000153155ustar00rootroot00000000000000trash-cli-0.17.1.14/man/man1/trash-empty.1000066400000000000000000000052761303652251000176660ustar00rootroot00000000000000.\" Copyright (C) 2008 Steve Stalcup .\" .\" This manual page is free software. It is distributed under the .\" terms of the GNU General Public License as published by the Free .\" Software Foundation; either version 2 of the License, or (at your .\" option) any later version. .\" .\" This manual page 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 manual page; if not, write to the Free Software .\" Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 .\" USA .\" .TH "TRASH-EMPTY" "1" .SH "NAME" trash-empty \- Empty for Command line trash utility. .SH "SYNOPSIS" .B trash-empty .RI [ arguments ] .SH "DESCRIPTION" .PP Remove for ever any trashed file and trashed directory. This command is a part of trash-cli package that provides a command line interface trashcan utility compliant with the FreeDesktop.org Trash Specification. It remembers the name, original path, deletion date, and permissions of each trashed file. .SH "ARGUMENTS" .TP To remove all trashed files, use 'trash-empty' without arguments. .TP To remove files that have been in the trash more than a given number of days, use 'trash-empty x', 'x' representing the number of days. .SH "EXAMPLES" .nf $ date Tue Feb 19 20:26:52 CET 2008 $ trash-list 2008-02-19 20:11:34 /home/einar/today 2008-02-18 20:11:34 /home/einar/yesterday 2008-02-10 20:11:34 /home/einar/last_week $ trash-empty 7 $ trash-list 2008-02-19 20:11:34 /home/einar/today 2008-02-18 20:11:34 /home/einar/yesterday $ trash-empty 1 $ trash-list 2008-02-19 20:11:34 /home/einar/today $ trash-empty $ trash-list .fi .SH "BUGS" Report bugs to https://github.com/andreafrancia/trash-cli/issues or to andrea@andreafrancia.it .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-list(1), trash-restore(1), trash-rm(1), and the FreeDesktop.org Trash Specification at http://www.ramendik.ru/docs/trashspec.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.17.1.14/man/man1/trash-list.1000066400000000000000000000042161303652251000174740ustar00rootroot00000000000000.\" Copyright (C) 2008 Steve Stalcup .\" .\" This manual page is free software. It is distributed under the .\" terms of the GNU General Public License as published by the Free .\" Software Foundation; either version 2 of the License, or (at your .\" option) any later version. .\" .\" This manual page 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 manual page; if not, write to the Free Software .\" Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 .\" USA .\" .TH "trash-list" "1" .SH "NAME" trash-list \- List trashed files. .SH "SYNOPSIS" .B trash-list [OPTIONS]... [PATH]... .SH "DESCRIPTION" .PP List all the contents of the trashcan. .SH "EXAMPLES" List all trashed files or grep output: .nf $ trash-list 2008-06-23 21:57:26 /home/andrea/src/bluetrash/pippo 2008-06-23 21:50:41 /home/andrea/foobar 2008-06-23 21:50:49 /home/andrea/foobar 2008-06-23 21:53:13 /media/disk/adsfljasldj $ trash-list | grep foo 2008-06-23 21:50:41 /home/andrea/foobar 2008-06-23 21:50:49 /home/andrea/foobar .fi .SH "BUGS" Report bugs to https://github.com/andreafrancia/trash-cli/issues or to andrea@andreafrancia.it .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-restore(1), trash-empty(1), trash-rm(1), and the FreeDesktop.org Trash Specification at http://www.ramendik.ru/docs/trashspec.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.17.1.14/man/man1/trash-put.1000066400000000000000000000041471303652251000173340ustar00rootroot00000000000000.\" Copyright (C) 2008 Steve Stalcup .\" .\" This manual page is free software. It is distributed under the .\" terms of the GNU General Public License as published by the Free .\" Software Foundation; either version 2 of the License, or (at your .\" option) any later version. .\" .\" This manual page 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 manual page; if not, write to the Free Software .\" Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 .\" USA .\" .TH "TRASH-PUT" "1" .SH "NAME" trash-put \- Command line trash utility. .SH "SYNOPSIS" .B trash-put .RI [ arguments ] \&... .SH "DESCRIPTION" .PP Trash-cli package provides a command line interface trashcan utility compliant with the FreeDesktop.org Trash Specification. It remembers the name, original path, deletion date, and permissions of each trashed file. .SH "ARGUMENTS" .TP Names of files or directories to move in the trashcan. .SH "EXAMPLES" .nf $ trash-put foo # trashes foo .fi .SH "BUGS" Report bugs to https://github.com/andreafrancia/trash-cli/issues or to andrea@andreafrancia.it .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-list(1), trash-restore(1), trash-empty(1), trash-rm(1), and the FreeDesktop.org Trash Specification at http://www.ramendik.ru/docs/trashspec.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.17.1.14/man/man1/trash-restore.1000066400000000000000000000045331303652251000202060ustar00rootroot00000000000000.\" Copyright (C) 2008 Steve Stalcup .\" .\" This manual page is free software. It is distributed under the .\" terms of the GNU General Public License as published by the Free .\" Software Foundation; either version 2 of the License, or (at your .\" option) any later version. .\" .\" This manual page 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 manual page; if not, write to the Free Software .\" Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 .\" USA .\" .TH "TRASH-RESTORE" "1" .SH "NAME" trash-restore \- Restore for Command line trash utility. .SH "SYNOPSIS" .B trash-restore .SH "DESCRIPTION" .PP Use for restore a trashed file or directory, in the original path. This command is a part of trash-cli package that provides a command line interface trashcan utility compliant with the FreeDesktop.org Trash Specification. It remembers the name, original path, deletion date, and permissions of each trashed file. .SH "EXAMPLES" .nf $ trash-restore 0 2007-08-30 12:36:00 /home/andrea/foo 1 2007-08-30 12:39:41 /home/andrea/bar 2 2007-08-30 12:39:41 /home/andrea/bar2 3 2007-08-30 12:39:41 /home/andrea/foo2 4 2007-08-30 12:39:41 /home/andrea/foo What file to restore [0..4]: 4 $ ls foo foo .fi .SH "BUGS" Report bugs to https://github.com/andreafrancia/trash-cli/issues or to andrea@andreafrancia.it .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-list(1), trash-empty(1), trash-rm(1), and the FreeDesktop.org Trash Specification at http://www.ramendik.ru/docs/trashspec.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.17.1.14/man/man1/trash-rm.1000066400000000000000000000026351303652251000171420ustar00rootroot00000000000000.\" Copyright (C) 2012 Andrea Francia .TH "TRASH-RM" "1" .SH "NAME" trash-rm \- Removes files matching a pattern from the trash can .SH "SYNOPSIS" .B trash-rm .RI [ PATTERN ] .SH "DESCRIPTION" .PP Remove from the trash can all the files matching the PATTERN. .SH "EXAMPLE" .nf $ trash-rm foo # Removes all files with name 'foo' in trash can $ trash-rm '*.o' # Removes all files ending with '.o' in trash can $ trash-rm /full/path # Removes all files removed with exactly '/full/path' as original location .fi .SH "BUGS" Report bugs to https://github.com/andreafrancia/trash-cli/issues or to andrea@andreafrancia.it .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "COPYRIGHT" Both are released under the \s-1GNU\s0 General Public License, version 2. .SH "SEE ALSO" trash-put(1), trash-list(1), trash-empty(1), trash-restore(1), and the FreeDesktop.org Trash Specification at http://www.ramendik.ru/docs/trashspec.html. .br trash-cli-0.17.1.14/requirements-dev.txt000066400000000000000000000000301303652251000177370ustar00rootroot00000000000000nose==1.1.2 mock==0.8.0 trash-cli-0.17.1.14/setup.cfg000077500000000000000000000004451303652251000155350ustar00rootroot00000000000000[bdist_rpm] ;; Uncomment if your platform automatically gzips man pages install_script = install-rpm.sh doc_files = README.txt [egg_info] tag_build = tag_svn_revision = 1 [aliases] release = egg_info -RDb '' [nosetests] nocapture=1 testmatch=((?:^|[b_.-])(:?[Tt]est|describe_|it_should)) trash-cli-0.17.1.14/setup.py000066400000000000000000000045671303652251000154340ustar00rootroot00000000000000# Copyright (C) 2007-2012 Andrea Francia Trivolzio(PV) Italy from distutils.core import setup import sys def main(): sys.path.append('.') from trashcli import trash scripts.add_script('trash' , 'trashcli.put' , 'main') scripts.add_script('trash-put' , 'trashcli.put' , 'main') scripts.add_script('trash-list' , 'trashcli.cmds', 'list') scripts.add_script('trash-restore', 'trashcli.cmds', 'restore') scripts.add_script('trash-empty' , 'trashcli.cmds', 'empty') scripts.add_script('trash-rm' , 'trashcli.rm' , 'main') setup( name = 'trash-cli' , version = trash.version , author = 'Andrea Francia' , author_email = 'andrea@andreafrancia.it' , url = 'https://github.com/andreafrancia/trash-cli', description = 'Command line interface to FreeDesktop.org Trash.', long_description = read_file("README.rst"), license = 'GPL v2', packages = ['trashcli'], scripts = scripts.created_scripts, data_files = [('share/man/man1', ['man/man1/trash-empty.1', 'man/man1/trash-list.1', 'man/man1/trash-restore.1', 'man/man1/trash-put.1', 'man/man1/trash-rm.1'])], ) from textwrap import dedent class Scripts: def __init__(self, write_file, make_file_executable): self.write_file = write_file self.make_file_executable = make_file_executable self.created_scripts = [] def add_script(self, name, module, main_function): script_contents = dedent("""\ #!/usr/bin/env python from __future__ import absolute_import import sys from %(module)s import %(main_function)s as main sys.exit(main()) """) % locals() self.write_file(name, script_contents) self.make_file_executable(name) self.created_scripts.append(name) import os,stat def make_file_executable(path): os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR) def write_file(name, contents): with open(name, 'w') as f: f.write(contents) def read_file(name): with open(name) as f: return f.read() scripts = Scripts(write_file, make_file_executable) if __name__ == '__main__': main() trash-cli-0.17.1.14/tasks/000077500000000000000000000000001303652251000150335ustar00rootroot00000000000000trash-cli-0.17.1.14/tasks/make-disk.osx000077500000000000000000000002501303652251000174330ustar00rootroot00000000000000mkdir -p test-disk hdiutil detach ./test-disk || true rm -f test-disk.dmg hdiutil create -size 1M -fs HFS+ test-disk hdiutil attach test-disk.dmg -mountpoint test-disk trash-cli-0.17.1.14/tasks/make-test-disk000077500000000000000000000011541303652251000176040ustar00rootroot00000000000000#!/bin/bash # Copyright (C) 2007-2009 Andrea Francia Trivolzio(PV) Italy set -o errexit mount_point="test-disk" device_image="test-disk.img" main() { clean-up create-image mount-the-image echo "test-volume mounted as '$mount_point'" } clean-up() { sudo umount "$mount_point" || true rm -fv "$device_image" } create-image() { dd if=/dev/zero of="$device_image" bs=$((1024*1024)) count=1 /sbin/mke2fs -F "$device_image" } mount-the-image() { mkdir --parents "$mount_point" sudo mount -t ext2 "$device_image" "$mount_point" -o loop sudo chmod a+rwx "$mount_point" } main trash-cli-0.17.1.14/tasks/test-scripts-installation000077500000000000000000000001461303652251000221250ustar00rootroot00000000000000# Clean up rm env/bin/trash-rm python setup.py develop --script-dir env/bin env/bin/trash-rm --help trash-cli-0.17.1.14/trashcli/000077500000000000000000000000001303652251000155175ustar00rootroot00000000000000trash-cli-0.17.1.14/trashcli/__init__.py000066400000000000000000000000501303652251000176230ustar00rootroot00000000000000from __future__ import absolute_import trash-cli-0.17.1.14/trashcli/cmds.py000066400000000000000000000027151303652251000170240ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy import sys,os def restore(): from trashcli.restore import RestoreCmd try: # Python 2 input23 = raw_input except: # Python 3 input23 = input RestoreCmd( stdout = sys.stdout, stderr = sys.stderr, environ = os.environ, exit = sys.exit, input = input23 ).run(sys.argv) def empty(argv = sys.argv, stdout = sys.stdout, stderr = sys.stderr, environ = os.environ): from trashcli.empty import EmptyCmd from trashcli.list_mount_points import mount_points from datetime import datetime from trashcli.trash import FileSystemReader from trashcli.fs import FileRemover from trashcli.trash import version return EmptyCmd( out = stdout, err = stderr, environ = environ, list_volumes = mount_points, now = datetime.now, file_reader = FileSystemReader(), getuid = os.getuid, file_remover = FileRemover(), version = version, ).run(*argv) def list(): from trashcli.list import ListCmd from trashcli.list_mount_points import mount_points ListCmd( out = sys.stdout, err = sys.stderr, environ = os.environ, getuid = os.getuid, list_volumes = mount_points, ).run(*sys.argv) trash-cli-0.17.1.14/trashcli/empty.py000066400000000000000000000132721303652251000172340ustar00rootroot00000000000000from .trash import TopTrashDirRules from .trash import TrashDirs from .trash import Harvester from .trash import EX_OK from .trash import Parser from .trash import PrintHelp from .trash import PrintVersion from .trash import EX_USAGE from .trash import ParseTrashInfo from .trash import CleanableTrashcan import os class EmptyCmd: def __init__(self, out, err, environ, list_volumes, now, file_reader, getuid, file_remover, version): self.out = out self.err = err self.file_reader = file_reader self.environ = environ self.getuid = getuid self.list_volumes = list_volumes self.version = version self._now = now self.file_remover = file_remover self._dustman = DeleteAnything() def run(self, *argv): self.program_name = os.path.basename(argv[0]) self.exit_code = EX_OK parse = Parser() parse.on_help(PrintHelp(self.description, self.println)) parse.on_version(PrintVersion(self.println, self.version)) parse.on_argument(self.set_max_age_in_days) parse.as_default(self.empty_all_trashdirs) parse.on_invalid_option(self.report_invalid_option_usage) parse.add_option('trash-dir=', self.empty_trashdir) parse(argv) return self.exit_code def set_max_age_in_days(self, arg): max_age_in_days = int(arg) self._dustman = DeleteAccordingDate(self.file_reader.contents_of, self._now, max_age_in_days) def report_invalid_option_usage(self, program_name, option): self.println_err("{program_name}: invalid option -- '{option}'" .format(**locals())) self.exit_code |= EX_USAGE def println_err(self, msg): self.err.write("{}\n".format(msg)) def description(self, program_name, printer): printer.usage('Usage: %s [days]' % program_name) printer.summary('Purge trashed files.') printer.options( " --version show program's version number and exit", " -h, --help show this help message and exit") printer.bug_reporting() def empty_trashdir(self, specific_dir): self.delete_all_things_under_trash_dir(specific_dir, None) def empty_all_trashdirs(self): trashdirs = TrashDirs(self.environ, self.getuid, self.list_volumes, TopTrashDirRules(self.file_reader)) trashdirs.on_trash_dir_found = self.delete_all_things_under_trash_dir trashdirs.list_trashdirs() def delete_all_things_under_trash_dir(self, trash_dir_path, volume_path): harvester = Harvester(self.file_reader) harvester.on_trashinfo_found = self.delete_trashinfo_and_backup_copy harvester.on_orphan_found = self.delete_orphan harvester.analize_trash_directory(trash_dir_path, volume_path) def delete_trashinfo_and_backup_copy(self, trashinfo_path): trashcan = self.make_trashcan() self._dustman.delete_if_ok(trashinfo_path, trashcan) def delete_orphan(self, path_to_backup_copy): trashcan = self.make_trashcan() trashcan.delete_orphan(path_to_backup_copy) def make_trashcan(self): file_remover_with_error = FileRemoveWithErrorHandling(self.file_remover, self.print_cannot_remove_error) trashcan = CleanableTrashcan(file_remover_with_error) return trashcan def print_cannot_remove_error(self, exc, path): error_message = "cannot remove {path}".format(path=path) self.println_err("{program_name}: {msg}".format( program_name=self.program_name, msg=error_message)) def println(self, line): self.out.write(line + '\n') class FileRemoveWithErrorHandling: def __init__(self, file_remover, on_error): self.file_remover = file_remover self.on_error = on_error def remove_file(self, path): try: return self.file_remover.remove_file(path) except OSError as e: self.on_error(e, path) def remove_file_if_exists(self, path): try: return self.file_remover.remove_file_if_exists(path) except OSError as e: self.on_error(e, path) class DeleteAccordingDate: def __init__(self, contents_of, now, max_age_in_days): self._contents_of = contents_of self._now = now self.max_age_in_days = max_age_in_days def delete_if_ok(self, trashinfo_path, trashcan): contents = self._contents_of(trashinfo_path) ParseTrashInfo( on_deletion_date=IfDate( OlderThan(self.max_age_in_days, self._now), lambda: trashcan.delete_trashinfo_and_backup_copy(trashinfo_path) ), )(contents) class DeleteAnything: def delete_if_ok(self, trashinfo_path, trashcan): trashcan.delete_trashinfo_and_backup_copy(trashinfo_path) class IfDate: def __init__(self, date_criteria, then): self.date_criteria = date_criteria self.then = then def __call__(self, date2): if self.date_criteria(date2): self.then() class OlderThan: def __init__(self, days_ago, now): from datetime import timedelta self.limit_date = now() - timedelta(days=days_ago) def __call__(self, deletion_date): return deletion_date < self.limit_date trash-cli-0.17.1.14/trashcli/fs.py000066400000000000000000000033741303652251000165100ustar00rootroot00000000000000import os, shutil class FileSystemListing: def entries_if_dir_exists(self, path): if os.path.exists(path): for entry in os.listdir(path): yield entry def exists(self, path): return os.path.exists(path) class FileSystemReader(FileSystemListing): def is_sticky_dir(self, path): import os return os.path.isdir(path) and has_sticky_bit(path) def is_symlink(self, path): return os.path.islink(path) def contents_of(self, path): return open(path).read() class FileRemover: def remove_file(self, path): try: return os.remove(path) except OSError: shutil.rmtree(path) def remove_file_if_exists(self,path): if os.path.exists(path): self.remove_file(path) def contents_of(path): # TODO remove return FileSystemReader().contents_of(path) def has_sticky_bit(path): # TODO move to FileSystemReader import os import stat return (os.stat(path).st_mode & stat.S_ISVTX) == stat.S_ISVTX def remove_file(path): if(os.path.lexists(path)): try: os.remove(path) except: return shutil.rmtree(path) def move(path, dest) : return shutil.move(path, str(dest)) def list_files_in_dir(path): for entry in os.listdir(path): result = os.path.join(path, entry) yield result def mkdirs(path): if os.path.isdir(path): return os.makedirs(path) def atomic_write(filename, content): file_handle = os.open(filename, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600) os.write(file_handle, content) os.close(file_handle) def ensure_dir(path, mode): if os.path.isdir(path): os.chmod(path, mode) return os.makedirs(path, mode) trash-cli-0.17.1.14/trashcli/fstab.py000066400000000000000000000035121303652251000171710ustar00rootroot00000000000000import os def volume_of(path) : return Fstab().volume_of(path) class AbstractFstab(object): def __init__(self, ismount): self.ismount = ismount def volume_of(self, path): volume_of = VolumeOf(ismount=self.ismount) return volume_of(path) def mount_points(self): return self.ismount.mount_points() class Fstab(AbstractFstab): def __init__(self): AbstractFstab.__init__(self, OsIsMount()) class FakeFstab: def __init__(self): self.ismount = FakeIsMount() self.volume_of = VolumeOf(ismount = self.ismount) self.volume_of.abspath = os.path.normpath def mount_points(self): return self.ismount.mount_points() def volume_of(self, path): volume_of = VolumeOf(ismount=self.ismount) return volume_of(path) def add_mount(self, path): self.ismount.add_mount(path) from trashcli.list_mount_points import mount_points as os_mount_points class OsIsMount: def __call__(self, path): return os.path.ismount(path) def mount_points(self): return os_mount_points() class FakeIsMount: def __init__(self): self.fakes = set(['/']) def add_mount(self, path): self.fakes.add(path) def __call__(self, path): if path == '/': return True path = os.path.normpath(path) if path in self.fakes: return True return False def mount_points(self): return self.fakes.copy() class VolumeOf: def __init__(self, ismount): self._ismount = ismount import os self.abspath = os.path.abspath def __call__(self, path): path = self.abspath(path) while path != os.path.dirname(path): if self._ismount(path): break path = os.path.dirname(path) return path trash-cli-0.17.1.14/trashcli/list.py000066400000000000000000000075121303652251000170510ustar00rootroot00000000000000from .fs import FileSystemReader from .trash import version from .trash import TopTrashDirRules from .trash import TrashDirs from .trash import Harvester from .trash import Parser from .trash import PrintHelp from .trash import PrintVersion from .trash import parse_deletion_date from .trash import ParseError from .trash import parse_path from .trash import unknown_date class ListCmd: def __init__(self, out, err, environ, list_volumes, getuid, file_reader = FileSystemReader(), version = version): self.output = ListCmdOutput(out, err) self.err = self.output.err self.environ = environ self.list_volumes = list_volumes self.getuid = getuid self.file_reader = file_reader self.contents_of = file_reader.contents_of self.version = version def run(self, *argv): parse=Parser() parse.on_help(PrintHelp(self.description, self.output.println)) parse.on_version(PrintVersion(self.output.println, self.version)) parse.as_default(self.list_trash) parse(argv) def list_trash(self): harvester = Harvester(self.file_reader) harvester.on_volume = self.output.set_volume_path harvester.on_trashinfo_found = self._print_trashinfo trashdirs = TrashDirs(self.environ, self.getuid, self.list_volumes, TopTrashDirRules(self.file_reader)) trashdirs.on_trashdir_skipped_because_parent_not_sticky = self.output.top_trashdir_skipped_because_parent_not_sticky trashdirs.on_trashdir_skipped_because_parent_is_symlink = self.output.top_trashdir_skipped_because_parent_is_symlink trashdirs.on_trash_dir_found = harvester.analize_trash_directory trashdirs.list_trashdirs() def _print_trashinfo(self, path): try: contents = self.contents_of(path) except IOError as e : self.output.print_read_error(e) else: deletion_date = parse_deletion_date(contents) or unknown_date() try: path = parse_path(contents) except ParseError: self.output.print_parse_path_error(path) else: self.output.print_entry(deletion_date, path) def description(self, program_name, printer): printer.usage('Usage: %s [OPTIONS...]' % program_name) printer.summary('List trashed files') printer.options( " --version show program's version number and exit", " -h, --help show this help message and exit") printer.bug_reporting() class ListCmdOutput: def __init__(self, out, err): self.out = out self.err = err def println(self, line): self.out.write(line+'\n') def error(self, line): self.err.write(line+'\n') def print_read_error(self, error): self.error(str(error)) def print_parse_path_error(self, offending_file): self.error("Parse Error: %s: Unable to parse Path." % (offending_file)) def top_trashdir_skipped_because_parent_not_sticky(self, trashdir): self.error("TrashDir skipped because parent not sticky: %s" % trashdir) def top_trashdir_skipped_because_parent_is_symlink(self, trashdir): self.error("TrashDir skipped because parent is symlink: %s" % trashdir) def set_volume_path(self, volume_path): self.volume_path = volume_path def print_entry(self, maybe_deletion_date, relative_location): import os original_location = os.path.join(self.volume_path, relative_location) self.println("%s %s" %(maybe_deletion_date, original_location)) trash-cli-0.17.1.14/trashcli/list_mount_points.py000066400000000000000000000051461303652251000216700ustar00rootroot00000000000000# Copyright (C) 2009-2011 Andrea Francia Trivolzio(PV) Italy def mount_points(): try: return list(mount_points_from_getmnt()) except AttributeError: return mount_points_from_df() def mount_points_from_getmnt(): for elem in _mounted_filesystems_from_getmnt(): yield elem.mount_dir def mount_points_from_df(): import subprocess df_output = subprocess.Popen(["df", "-P"], stdout=subprocess.PIPE).stdout return list(_mount_points_from_df_output(df_output)) def _mount_points_from_df_output(df_output): def skip_header(): df_output.readline() def chomp(string): return string.rstrip(b'\n') skip_header() for line in df_output: line = chomp(line) yield (line.split(None, 5)[-1]).decode('utf-8') def _mounted_filesystems_from_getmnt() : from ctypes import Structure, c_char_p, c_int, c_void_p, cdll, POINTER from ctypes.util import find_library import sys class Filesystem: def __init__(self, mount_dir, type, name) : self.mount_dir = mount_dir self.type = type self.name = name class mntent_struct(Structure): _fields_ = [("mnt_fsname", c_char_p), # Device or server for # filesystem. ("mnt_dir", c_char_p), # Directory mounted on. ("mnt_type", c_char_p), # Type of filesystem: ufs, # nfs, etc. ("mnt_opts", c_char_p), # Comma-separated options # for fs. ("mnt_freq", c_int), # Dump frequency (in days). ("mnt_passno", c_int)] # Pass number for `fsck'. if sys.platform == "cygwin": libc_name = "cygwin1.dll" else: libc_name = find_library("c") if libc_name == None : libc_name="/lib/libc.so.6" # fix for my Gentoo 4.0 libc = cdll.LoadLibrary(libc_name) libc.getmntent.restype = POINTER(mntent_struct) libc.getmntent.argtypes = [c_void_p] libc.fopen.restype = c_void_p libc.fclose.argtypes = [c_void_p] f = libc.fopen("/proc/mounts", "r") if f==None: f = libc.fopen("/etc/mtab", "r") if f == None: raise IOError("Unable to open /proc/mounts nor /etc/mtab") while True: entry = libc.getmntent(f) if bool(entry) == False: libc.fclose(f) break yield Filesystem(entry.contents.mnt_dir, entry.contents.mnt_type, entry.contents.mnt_fsname) trash-cli-0.17.1.14/trashcli/put.py000066400000000000000000000461371303652251000167140ustar00rootroot00000000000000import os import sys from .fstab import Fstab from .fstab import volume_of from .trash import EX_OK, EX_IOERR from .trash import TrashDirectories from .trash import backup_file_path_from from .trash import logger from .trash import version from datetime import datetime def main(): return TrashPutCmd( sys.stdout, sys.stderr, os.environ, volume_of, parent_path, os.path.realpath ).run(sys.argv) def parent_path(path): return os.path.realpath(os.path.dirname(path)) class TrashPutCmd: def __init__(self, stdout, stderr, environ, volume_of, parent_path, realpath): self.stdout = stdout self.stderr = stderr self.environ = environ self.volume_of = volume_of self.fs = RealFs() self.getuid = os.getuid self.now = datetime.now self.parent_path = parent_path self.realpath = realpath def run(self, argv): program_name = os.path.basename(argv[0]) logger = MyLogger(self.stderr, program_name) parser = self.get_option_parser(program_name) try: (options, args) = parser.parse_args(argv[1:]) if options.verbose: logger.be_verbose() if len(args) <= 0: parser.error("Please specify the files to trash.") except SystemExit as e: return e.code else: reporter = TrashPutReporter(logger) trashcan = GlobalTrashCan(reporter = reporter, volume_of = self.volume_of, environ = self.environ, fs = self.fs, getuid = self.getuid, now = self.now, parent_path = self.parent_path, realpath = self.realpath) trashcan.trash_all(args) return reporter.exit_code() def get_option_parser(self, program_name): from optparse import OptionParser parser = OptionParser(prog=program_name, usage="%prog [OPTION]... FILE...", description="Put files in trash", version="%%prog %s" % version, formatter=NoWrapFormatter(), epilog=epilog) parser.add_option("-d", "--directory", action="store_true", help="ignored (for GNU rm compatibility)") parser.add_option("-f", "--force", action="store_true", help="ignored (for GNU rm compatibility)") parser.add_option("-i", "--interactive", action="store_true", help="ignored (for GNU rm compatibility)") parser.add_option("-r", "-R", "--recursive", action="store_true", help="ignored (for GNU rm compatibility)") parser.add_option("-v", "--verbose", action="store_true", help="explain what is being done", dest="verbose") original_print_help = parser.print_help def patched_print_help(): original_print_help(self.stdout) def patched_error(msg): parser.print_usage(self.stderr) parser.exit(2, "%s: error: %s\n" % (program_name, msg)) def patched_exit(status=0, msg=None): if msg: self.stderr.write(msg) import sys sys.exit(status) parser.print_help = patched_print_help parser.error = patched_error parser.exit = patched_exit return parser epilog="""\ To remove a file whose name starts with a '-', for example '-foo', use one of these commands: trash -- -foo trash ./-foo Report bugs to https://github.com/andreafrancia/trash-cli/issues""" class MyLogger: def __init__(self, stderr, program_name): self.program_name = program_name self.stderr=stderr self.verbose = False def be_verbose(self): self.verbose = True def info(self,message): if self.verbose: self.emit(message) def warning(self,message): self.emit(message) def emit(self, message): self.stderr.write("%s: %s\n" % (self.program_name,message)) from optparse import IndentedHelpFormatter class NoWrapFormatter(IndentedHelpFormatter) : def _format_text(self, text) : "[Does not] format a text, return the text as it is." return text class NullObject: def __getattr__(self, name): return lambda *argl,**args:None class TrashPutReporter: def __init__(self, logger): self.logger = logger self.some_file_has_not_be_trashed = False self.no_argument_specified = False def unable_to_trash_dot_entries(self,file): self.logger.warning("cannot trash %s '%s'" % (describe(file), file)) def unable_to_trash_file(self,f): self.logger.warning("cannot trash %s '%s'" % (describe(f), f)) self.some_file_has_not_be_trashed = True def file_has_been_trashed_in_as(self, trashee, trash_directory, destination): self.logger.info("'%s' trashed in %s" % (trashee, shrinkuser(trash_directory))) def found_unsercure_trash_dir_symlink(self, trash_dir_path): self.logger.info("found unsecure .Trash dir (should not be a symlink): %s" % trash_dir_path) def invalid_top_trash_is_not_a_dir(self, trash_dir_path): self.logger.info("found unusable .Trash dir (should be a dir): %s" % trash_dir_path) def found_unsecure_trash_dir_unsticky(self, trash_dir_path): self.logger.info("found unsecure .Trash dir (should be sticky): %s" % trash_dir_path) def unable_to_trash_file_in_because(self, file_to_be_trashed, trash_directory, error): self.logger.info("Failed to trash %s in %s, because :%s" % ( file_to_be_trashed, shrinkuser(trash_directory), error)) def trash_dir_with_volume(self, trash_dir_path, volume_path): self.logger.info("Trash-dir: %s from volume: %s" % (trash_dir_path, volume_path)) def exit_code(self): if not self.some_file_has_not_be_trashed: return EX_OK else: return EX_IOERR def volume_of_file(self,volume): self.logger.info("Volume of file: %s" % volume) def describe(path): """ Return a textual description of the file pointed by this path. Options: - "symbolic link" - "directory" - "'.' directory" - "'..' directory" - "regular file" - "regular empty file" - "non existent" - "entry" """ if os.path.islink(path): return 'symbolic link' elif os.path.isdir(path): if path == '.': return 'directory' elif path == '..': return 'directory' else: if os.path.basename(path) == '.': return "'.' directory" elif os.path.basename(path) == '..': return "'..' directory" else: return 'directory' elif os.path.isfile(path): if os.path.getsize(path) == 0: return 'regular empty file' else: return 'regular file' elif not os.path.exists(path): return 'non existent' else: return 'entry' class RealFs: def __init__(self): import os from . import fs self.move = fs.move self.atomic_write = fs.atomic_write self.remove_file = fs.remove_file self.ensure_dir = fs.ensure_dir self.isdir = os.path.isdir self.islink = os.path.islink self.has_sticky_bit = fs.has_sticky_bit class GlobalTrashCan: class NullReporter: def __getattr__(self,name): return lambda *argl,**args:None def __init__(self, environ, volume_of, reporter, fs, getuid, now, parent_path, realpath): self.getuid = getuid self.reporter = reporter self.volume_of = volume_of self.now = now self.fs = fs self.environ = environ self.parent_path = parent_path self.realpath = realpath def trash_all(self, args): for arg in args : self.trash(arg) def trash(self, file) : """ Trash a file in the appropriate trash directory. If the file belong to the same volume of the trash home directory it will be trashed in the home trash directory. Otherwise it will be trashed in one of the relevant volume trash directories. Each volume can have two trash directories, they are - $volume/.Trash/$uid - $volume/.Trash-$uid Firstly the software attempt to trash the file in the first directory then try to trash in the second trash directory. """ if self._should_skipped_by_specs(file): self.reporter.unable_to_trash_dot_entries(file) return volume_of_file_to_be_trashed = self.volume_of_parent(file) self.reporter.volume_of_file(volume_of_file_to_be_trashed) candidates = self._possible_trash_directories_for(volume_of_file_to_be_trashed) file_has_been_trashed = False for trash_dir in candidates.trash_dirs: if self._is_trash_dir_secure(trash_dir): volume_of_trash_dir = self.volume_of(self.realpath(trash_dir.path)) self.reporter.trash_dir_with_volume(trash_dir.path, volume_of_trash_dir) if self._file_could_be_trashed_in(volume_of_file_to_be_trashed, volume_of_trash_dir): try: trashed_file = trash_dir.trash(file) self.reporter.file_has_been_trashed_in_as( file, trashed_file['trash_directory'], trashed_file['where_file_was_stored']) file_has_been_trashed = True except (IOError, OSError) as error: self.reporter.unable_to_trash_file_in_because( file, trash_dir.path, str(error)) if file_has_been_trashed: break if not file_has_been_trashed: self.reporter.unable_to_trash_file(file) def volume_of_parent(self, file): return self.volume_of(self.parent_path(file)) def _is_trash_dir_secure(self, trash_dir): class ValidationOutput: def __init__(self): self.valid = True def not_valid_should_be_a_dir(_): self.reporter.invalid_top_trash_is_not_a_dir( os.path.dirname(trash_dir.path)) _.valid = False def not_valid_parent_should_not_be_a_symlink(_): self.reporter.found_unsercure_trash_dir_symlink( os.path.dirname(trash_dir.path)) _.valid = False def not_valid_parent_should_be_sticky(_): self.reporter.found_unsecure_trash_dir_unsticky( os.path.dirname(trash_dir.path)) _.valid = False def is_valid(self): self.valid = True output = ValidationOutput() trash_dir.checker.fs = self.fs trash_dir.checker.valid_to_be_written(trash_dir.path, output) return output.valid def _should_skipped_by_specs(self, file): basename = os.path.basename(file) return (basename == ".") or (basename == "..") def _file_could_be_trashed_in(self, volume_of_file_to_be_trashed, volume_of_trash_dir): return volume_of_trash_dir == volume_of_file_to_be_trashed def _possible_trash_directories_for(self, volume): candidates = PossibleTrashDirectories(self.fs) trash_directories = TrashDirectories(self.volume_of, self.getuid, self.environ) trash_directories.home_trash_dir(candidates.add_home_trash) trash_directories.volume_trash_dir1(volume, candidates.add_top_trash_dir) trash_directories.volume_trash_dir2(volume, candidates.add_alt_top_trash_dir) return candidates class PossibleTrashDirectories: def __init__(self, fs): self.trash_dirs = [] self.fs = fs def add_home_trash(self, path, volume): trash_dir = self._make_trash_dir(path, volume) trash_dir.store_absolute_paths() self.trash_dirs.append(trash_dir) def add_top_trash_dir(self, path, volume): trash_dir = self._make_trash_dir(path, volume) trash_dir.store_relative_paths() trash_dir.checker = TopTrashDirWriteRules(None) self.trash_dirs.append(trash_dir) def add_alt_top_trash_dir(self, path, volume): trash_dir = self._make_trash_dir(path, volume) trash_dir.store_relative_paths() self.trash_dirs.append(trash_dir) def _make_trash_dir(self, path, volume): return TrashDirectoryForPut(path, volume, datetime.now, self.fs) def parent_realpath(path): parent = os.path.dirname(path) return os.path.realpath(parent) class TrashDirectoryForPut: from datetime import datetime def __init__(self, path, volume, now, fs): self.path = os.path.normpath(path) self.volume = volume self.logger = logger self.info_dir = os.path.join(self.path, 'info') self.files_dir = os.path.join(self.path, 'files') class all_is_ok_checker: def valid_to_be_written(self, a, b): pass def check(self, a):pass self.checker = all_is_ok_checker() self.now = now self.move = fs.move self.atomic_write = fs.atomic_write self.remove_file = fs.remove_file self.ensure_dir = fs.ensure_dir self.path_for_trash_info = OriginalLocation(parent_realpath) def store_absolute_paths(self): self.path_for_trash_info.make_absolutes_paths() def store_relative_paths(self): self.path_for_trash_info.make_paths_relatives_to(self.volume) def trash(self, path): path = os.path.normpath(path) original_location = self.path_for_trash_info.for_file(path) basename = os.path.basename(original_location) content = self.format_trashinfo(original_location, self.now()) trash_info_file = self.persist_trash_info( self.info_dir, basename, content) where_to_store_trashed_file = backup_file_path_from(trash_info_file) self.ensure_files_dir_exists() try : self.move(path, where_to_store_trashed_file) except IOError as e : self.remove_file(trash_info_file) raise e result = dict() result['trash_directory'] = self.path result['where_file_was_stored'] = where_to_store_trashed_file return result def format_trashinfo(self, original_location, deletion_date): def format_date(deletion_date): return deletion_date.strftime("%Y-%m-%dT%H:%M:%S") def format_original_location(original_location): try: from urllib import quote except ImportError: from urllib.parse import quote return quote(original_location,'/') content = ("[Trash Info]\n" + "Path=%s\n" % format_original_location(original_location) + "DeletionDate=%s\n" % format_date(deletion_date)).encode('utf-8') return content def ensure_files_dir_exists(self): self.ensure_dir(self.files_dir, 0o700) def persist_trash_info(self, info_dir, basename, content) : """ Create a .trashinfo file in the $trash/info directory. returns the created TrashInfoFile. """ self.ensure_dir(info_dir, 0o700) # write trash info index = 0 while True : if index == 0 : suffix = "" elif index < 100: suffix = "_%d" % index else : import random suffix = "_%d" % random.randint(0, 65535) base_id = basename trash_id = base_id + suffix trash_info_basename = trash_id+".trashinfo" dest = os.path.join(info_dir, trash_info_basename) try : self.atomic_write(dest, content) self.logger.debug(".trashinfo created as %s." % dest) return dest except OSError: self.logger.debug("Attempt for creating %s failed." % dest) index += 1 raise IOError() def shrinkuser(path, environ=os.environ): import posixpath import re try: home_dir = environ['HOME'] home_dir = posixpath.normpath(home_dir) if home_dir != '': path = re.sub('^' + re.escape(home_dir + os.path.sep), '~' + os.path.sep, path) except KeyError: pass return path class TopTrashDirWriteRules: def __init__(self, fs): self.fs = fs def valid_to_be_written(self, trash_dir_path, output): parent = os.path.dirname(trash_dir_path) if not self.fs.isdir(parent): output.not_valid_should_be_a_dir() return if self.fs.islink(parent): output.not_valid_parent_should_not_be_a_symlink() return if not self.fs.has_sticky_bit(parent): output.not_valid_parent_should_be_sticky() return output.is_valid() class OriginalLocation: def __init__(self, parent_realpath): self.parent_realpath = parent_realpath self.make_absolutes_paths() def make_paths_relatives_to(self, topdir): self.path_maker = TopDirRelativePaths(topdir) def make_absolutes_paths(self): self.path_maker = AbsolutePaths() def for_file(self, path): self.normalized_path = os.path.normpath(path) basename = os.path.basename(self.normalized_path) parent = self.parent_realpath(self.normalized_path) parent = self.path_maker.calc_parent_path(parent) return os.path.join(parent, basename) class TopDirRelativePaths: def __init__(self, topdir): self.topdir = topdir def calc_parent_path(self, parent): if (parent == self.topdir) or parent.startswith(self.topdir+os.path.sep) : parent = parent[len(self.topdir+os.path.sep):] return parent class AbsolutePaths: def calc_parent_path(self, parent): return parent trash-cli-0.17.1.14/trashcli/restore.py000066400000000000000000000160041303652251000175550ustar00rootroot00000000000000import os import sys from .trash import version from .fstab import Fstab from .trash import TrashDirectory from .trash import TrashDirectories from .fs import contents_of from .trash import backup_file_path_from from . import fs def getcwd_as_realpath(): return os.path.realpath(os.curdir) class RestoreCmd(object): def __init__(self, stdout, stderr, environ, exit, input, curdir = getcwd_as_realpath, version = version): self.out = stdout self.err = stderr self.exit = exit self.input = input self.curdir = curdir self.version = version self.fs = fs self.path_exists = os.path.exists self.contents_of = contents_of fstab = Fstab() all_trash_directories = AllTrashDirectories( volume_of = fstab.volume_of, getuid = os.getuid, environ = environ, mount_points = fstab.mount_points() ) self.all_trash_directories2 = all_trash_directories.all_trash_directories def run(self, argv): if '--version' in argv[1:]: command = os.path.basename(argv[0]) self.println('%s %s' %(command, self.version)) return if len(argv) == 2: specific_path = argv[1] def is_trashed_from_curdir(trashedfile): return trashedfile.original_location.startswith(specific_path) filter = is_trashed_from_curdir else: dir = self.curdir() def is_trashed_from_curdir(trashedfile): return trashedfile.original_location.startswith(dir + os.path.sep) filter = is_trashed_from_curdir trashed_files = self.all_trashed_files_filter(filter) self.handle_trashed_files(trashed_files) def handle_trashed_files(self,trashed_files): if not trashed_files: self.report_no_files_found() else : for i, trashedfile in enumerate(trashed_files): self.println("%4d %s %s" % (i, trashedfile.deletion_date, trashedfile.original_location)) self.restore_asking_the_user(trashed_files) def restore_asking_the_user(self, trashed_files): index=self.input("What file to restore [0..%d]: " % (len(trashed_files)-1)) if index == "" : self.println("Exiting") else : try: index = int(index) if (index < 0 or index >= len(trashed_files)): raise IndexError("Out of range") trashed_file = trashed_files[index] self.restore(trashed_file) except (ValueError, IndexError) as e: self.printerr("Invalid entry") self.exit(1) except IOError as e: self.printerr(e) self.exit(1) def restore(self, trashed_file): restore(trashed_file, self.path_exists, self.fs) def all_trashed_files_filter(self, matches): trashed_files = [] for trashedfile in self.all_trashed_files(): if matches(trashedfile): trashed_files.append(trashedfile) return trashed_files def all_trashed_files(self): for trash_dir in self.all_trash_directories2(): for info_file in trash_dir.all_info_files(): try: trash_info = TrashInfoParser(self.contents_of(info_file), trash_dir.volume) original_location = trash_info.original_location() deletion_date = trash_info.deletion_date() backup_file_path = backup_file_path_from(info_file) trashedfile = TrashedFile(original_location, deletion_date, info_file, backup_file_path) yield trashedfile except ValueError: trash_dir.logger.warning("Non parsable trashinfo file: %s" % info_file) except IOError as e: trash_dir.logger.warning(str(e)) def report_no_files_found(self): self.println("No files trashed from current dir ('%s')" % self.curdir()) def println(self, line): self.out.write(line + '\n') def printerr(self, msg): self.err.write('%s\n' % msg) from .trash import parse_path from .trash import parse_deletion_date class TrashInfoParser: def __init__(self, contents, volume_path): self.contents = contents self.volume_path = volume_path def deletion_date(self): return parse_deletion_date(self.contents) def original_location(self): path = parse_path(self.contents) return os.path.join(self.volume_path, path) class AllTrashDirectories: def __init__(self, volume_of, getuid, environ, mount_points): self.volume_of = volume_of self.getuid = getuid self.environ = environ self.mount_points = mount_points def all_trash_directories(self): trash_directories = TrashDirectories(volume_of = self.volume_of, getuid = self.getuid, environ = self.environ) collected = [] def add_trash_dir(path, volume): collected.append(TrashDirectory(path, volume)) trash_directories.home_trash_dir(add_trash_dir) for volume in self.mount_points: trash_directories.volume_trash_dir1(volume, add_trash_dir) trash_directories.volume_trash_dir2(volume, add_trash_dir) return collected class TrashedFile: """ Represent a trashed file. Each trashed file is persisted in two files: - $trash_dir/info/$id.trashinfo - $trash_dir/files/$id Properties: - path : the original path from where the file has been trashed - deletion_date : the time when the file has been trashed (instance of datetime) - info_file : the file that contains information (instance of Path) - original_file : the path where the trashed file has been placed after the trash opeartion (instance of Path) """ def __init__(self, original_location, deletion_date, info_file, original_file): self.original_location = original_location self.deletion_date = deletion_date self.info_file = info_file self.original_file = original_file def restore(trashed_file, path_exists, fs): if path_exists(trashed_file.original_location): raise IOError('Refusing to overwrite existing file "%s".' % os.path.basename(trashed_file.original_location)) else: parent = os.path.dirname(trashed_file.original_location) fs.mkdirs(parent) fs.move(trashed_file.original_file, trashed_file.original_location) fs.remove_file(trashed_file.info_file) trash-cli-0.17.1.14/trashcli/rm.py000066400000000000000000000070651303652251000165170ustar00rootroot00000000000000import fnmatch import os, sys from trashcli.trash import TrashDir, parse_path, ParseError from trashcli.trash import TrashDirs from trashcli.trash import TopTrashDirRules from trashcli.trash import CleanableTrashcan from trashcli.fs import FileSystemReader from trashcli.fs import FileRemover class RmCmd: def __init__(self, environ, getuid, list_volumes, stderr, file_reader): self.environ = environ self.getuid = getuid self.list_volumes = list_volumes self.stderr = stderr self.file_reader = file_reader def run(self, argv): args = argv[1:] self.exit_code = 0 if not args: self.print_err('Usage:\n' ' trash-rm PATTERN\n' '\n' 'Please specify PATTERN') self.exit_code = 8 return trashcan = CleanableTrashcan(FileRemover()) cmd = Filter(trashcan.delete_trashinfo_and_backup_copy) cmd.use_pattern(args[0]) listing = ListTrashinfos(cmd.delete_if_matches, self.file_reader, self.unable_to_parse_path) trashdirs = TrashDirs(self.environ, self.getuid, self.list_volumes, TopTrashDirRules(self.file_reader)) trashdirs.on_trash_dir_found = listing.list_from_volume_trashdir trashdirs.list_trashdirs() def unable_to_parse_path(self, trashinfo): self.report_error('{}: unable to parse \'Path\''.format(trashinfo)) def report_error(self, error_msg): self.print_err('trash-rm: {}'.format(error_msg)) def print_err(self, msg): self.stderr.write(msg + '\n') def main(): from trashcli.list_mount_points import mount_points cmd = RmCmd(environ = os.environ , getuid = os.getuid , list_volumes = mount_points , stderr = sys.stderr , file_reader = FileSystemReader()) cmd.run(sys.argv) return cmd.exit_code class Filter: def __init__(self, delete): self.delete = delete def use_pattern(self, pattern): self.pattern = pattern def delete_if_matches(self, original_location, info_file): if self.pattern[0] == '/': if self.pattern == original_location: self.delete(info_file) else: basename = os.path.basename(original_location) if fnmatch.fnmatchcase(basename, self.pattern): self.delete(info_file) class ListTrashinfos: def __init__(self, out, file_reader, unable_to_parse_path): self.out = out self.file_reader = file_reader self.unable_to_parse_path = unable_to_parse_path def list_from_volume_trashdir(self, trashdir_path, volume): self.volume = volume trashdir = TrashDir(self.file_reader) trashdir.open(trashdir_path, volume) trashdir.each_trashinfo(self._report_original_location) def _report_original_location(self, trashinfo_path): trashinfo = self.file_reader.contents_of(trashinfo_path) try: path = parse_path(trashinfo) except ParseError: self.unable_to_parse_path(trashinfo_path) else: complete_path = os.path.join(self.volume, path) self.out(complete_path, trashinfo_path) if __name__ == '__main__': sys.exit(main()) trash-cli-0.17.1.14/trashcli/trash.py000066400000000000000000000320611303652251000172140ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import version='0.17.1.14' import os import logging logger=logging.getLogger('trashcli.trash') logger.setLevel(logging.WARNING) logger.addHandler(logging.StreamHandler()) # Error codes (from os on *nix, hard coded for Windows): EX_OK = getattr(os, 'EX_OK' , 0) EX_USAGE = getattr(os, 'EX_USAGE', 64) EX_IOERR = getattr(os, 'EX_IOERR', 74) from .fs import list_files_in_dir import os class TrashDirectory: def __init__(self, path, volume): self.path = os.path.normpath(path) self.volume = volume self.logger = logger self.info_dir = os.path.join(self.path, 'info') self.files_dir = os.path.join(self.path, 'files') def warn_non_trashinfo(): self.logger.warning("Non .trashinfo file in info dir") self.on_non_trashinfo_found = warn_non_trashinfo def all_info_files(self) : 'Returns a generator of "Path"s' try : for info_file in list_files_in_dir(self.info_dir): if not os.path.basename(info_file).endswith('.trashinfo') : self.on_non_trashinfo_found() else : yield info_file except OSError: # when directory does not exist pass def backup_file_path_from(trashinfo_file_path): trashinfo_basename = os.path.basename(trashinfo_file_path) backupfile_basename = trashinfo_basename[:-len('.trashinfo')] info_dir = os.path.dirname(trashinfo_file_path) trash_dir = os.path.dirname(info_dir) files_dir = os.path.join(trash_dir, 'files') return os.path.join(files_dir, backupfile_basename) class HomeTrashCan: def __init__(self, environ): self.environ = environ def path_to(self, out): if 'XDG_DATA_HOME' in self.environ: out('%(XDG_DATA_HOME)s/Trash' % self.environ) elif 'HOME' in self.environ: out('%(HOME)s/.local/share/Trash' % self.environ) class TrashDirectories: def __init__(self, volume_of, getuid, environ): self.home_trashcan = HomeTrashCan(environ) self.volume_of = volume_of self.getuid = getuid def home_trash_dir(self, out) : self.home_trashcan.path_to(lambda path: out(path, self.volume_of(path))) def volume_trash_dir1(self, volume, out): out( path = os.path.join(volume, '.Trash/%s' % self.getuid()), volume = volume) def volume_trash_dir2(self, volume, out): out( path = os.path.join(volume, ".Trash-%s" % self.getuid()), volume = volume) from .fs import FileSystemReader, contents_of def do_nothing(*argv, **argvk): pass class Parser: def __init__(self): self.default_action = do_nothing self.argument_action = do_nothing self.short_options = '' self.long_options = [] self.actions = dict() self._on_invalid_option = do_nothing def __call__(self, argv): program_name = argv[0] from getopt import getopt, GetoptError try: options, arguments = getopt(argv[1:], self.short_options, self.long_options) except GetoptError as e: invalid_option = e.opt self._on_invalid_option(program_name, invalid_option) else: for option, value in options: if option in ('--help', '-h', '--version'): self.actions[option](program_name) return if option in self.actions: self.actions[option](value) return for argument in arguments: self.argument_action(argument) self.default_action() def on_invalid_option(self, action): self._on_invalid_option = action def on_help(self, action): self.add_option('help', action, 'h') def on_version(self, action): self.add_option('version', action) def add_option(self, long_option, action, short_aliases=''): self.long_options.append(long_option) if long_option.endswith('='): import re long_option = re.sub('=$', '', long_option) self.actions['--' + long_option] = action for short_alias in short_aliases: self.add_short_option(short_alias, action) def add_short_option(self, short_option, action): self.short_options += short_option self.actions['-' + short_option] = action def on_argument(self, argument_action): self.argument_action = argument_action def as_default(self, default_action): self.default_action = default_action class TrashDirs: def __init__(self, environ, getuid, list_volumes, top_trashdir_rules): self.getuid = getuid self.mount_points = list_volumes self.top_trashdir_rules = top_trashdir_rules self.home_trashcan = HomeTrashCan(environ) # events self.on_trash_dir_found = lambda trashdir, volume: None self.on_trashdir_skipped_because_parent_not_sticky = lambda trashdir: None self.on_trashdir_skipped_because_parent_is_symlink = lambda trashdir: None def list_trashdirs(self): self.emit_home_trashcan() self._for_each_volume_trashcan() def emit_home_trashcan(self): def return_result_with_volume(trashcan_path): self.on_trash_dir_found(trashcan_path, '/') self.home_trashcan.path_to(return_result_with_volume) def _for_each_volume_trashcan(self): for volume in self.mount_points(): self.emit_trashcans_for(volume) def emit_trashcans_for(self, volume): self.emit_trashcan_1_for(volume) self.emit_trashcan_2_for(volume) def emit_trashcan_1_for(self,volume): top_trashdir_path = os.path.join(volume, '.Trash/%s' % self.getuid()) class IsValidOutput: def not_valid_parent_should_not_be_a_symlink(_): self.on_trashdir_skipped_because_parent_is_symlink(top_trashdir_path) def not_valid_parent_should_be_sticky(_): self.on_trashdir_skipped_because_parent_not_sticky(top_trashdir_path) def is_valid(_): self.on_trash_dir_found(top_trashdir_path, volume) self.top_trashdir_rules.valid_to_be_read(top_trashdir_path, IsValidOutput()) def emit_trashcan_2_for(self, volume): alt_top_trashdir = os.path.join(volume, '.Trash-%s' % self.getuid()) self.on_trash_dir_found(alt_top_trashdir, volume) from datetime import datetime class Harvester: def __init__(self, file_reader): self.file_reader = file_reader self.on_orphan_found = do_nothing self.on_trashinfo_found = do_nothing self.on_volume = do_nothing def analize_trash_directory(self, trash_dir_path, volume_path): self.on_volume(volume_path) trashdir = TrashDir(self.file_reader) trashdir.open(trash_dir_path, volume_path) trashdir.each_trashinfo(self.on_trashinfo_found) trashdir.each_orphan(self.on_orphan_found) class PrintHelp: def __init__(self, description, println): class Printer: def __init__(self, println): self.println = println def usage(self, usage): self.println(usage) self.println('') def summary(self, summary): self.println(summary) self.println('') def options(self, *line_describing_option): self.println('Options:') for line in line_describing_option: self.println(line) self.println('') def bug_reporting(self): self.println("Report bugs to https://github.com/andreafrancia/trash-cli/issues") self.description = description self.printer = Printer(println) def __call__(self, program_name): self.description(program_name, self.printer) class PrintVersion: def __init__(self, println, version): self.println = println self.version = version def __call__(self, program_name): self.println("%s %s" % (program_name, self.version)) class TopTrashDirRules: def __init__(self, fs): self.fs = fs def valid_to_be_read(self, path, output): parent_trashdir = os.path.dirname(path) if not self.fs.exists(path): return if not self.fs.is_sticky_dir(parent_trashdir): output.not_valid_parent_should_be_sticky() return if self.fs.is_symlink(parent_trashdir): output.not_valid_parent_should_not_be_a_symlink() return else: output.is_valid() class Dir: def __init__(self, path, entries_if_dir_exists): self.path = path self.entries_if_dir_exists = entries_if_dir_exists def entries(self): return self.entries_if_dir_exists(self.path) def full_path(self, entry): return os.path.join(self.path, entry) class TrashDir: def __init__(self, file_reader): self.file_reader = file_reader def open(self, path, volume_path): self.trash_dir_path = path self.volume_path = volume_path self.files_dir = Dir(self._files_dir(), self.file_reader.entries_if_dir_exists) def each_orphan(self, action): for entry in self.files_dir.entries(): trashinfo_path = self._trashinfo_path_from_file(entry) file_path = self.files_dir.full_path(entry) if not self.file_reader.exists(trashinfo_path): action(file_path) def _entries_if_dir_exists(self, path): return self.file_reader.entries_if_dir_exists(path) def each_trashinfo(self, action): for entry in self._trashinfo_entries(): action(os.path.join(self._info_dir(), entry)) def _info_dir(self): return os.path.join(self.trash_dir_path, 'info') def _trashinfo_path_from_file(self, file_entry): return os.path.join(self._info_dir(), file_entry + '.trashinfo') def _files_dir(self): return os.path.join(self.trash_dir_path, 'files') def _trashinfo_entries(self, on_non_trashinfo=do_nothing): for entry in self._entries_if_dir_exists(self._info_dir()): if entry.endswith('.trashinfo'): yield entry else: on_non_trashinfo() class ParseError(ValueError): pass def maybe_parse_deletion_date(contents): result = Basket(unknown_date()) ParseTrashInfo( on_deletion_date = lambda date: result.collect(date), on_invalid_date = lambda: result.collect(unknown_date()) )(contents) return result.collected def unknown_date(): return '????-??-?? ??:??:??' try: from urllib import unquote except ImportError: from urllib.parse import unquote class ParseTrashInfo: def __init__(self, on_deletion_date = do_nothing, on_invalid_date = do_nothing, on_path = do_nothing): self.found_deletion_date = on_deletion_date self.found_invalid_date = on_invalid_date self.found_path = on_path def __call__(self, contents): from datetime import datetime for line in contents.split('\n'): if line.startswith('DeletionDate='): try: date = datetime.strptime(line, "DeletionDate=%Y-%m-%dT%H:%M:%S") except ValueError: self.found_invalid_date() else: self.found_deletion_date(date) if line.startswith('Path='): path=unquote(line[len('Path='):]) self.found_path(path) class Basket: def __init__(self, initial_value = None): self.collected = initial_value def collect(self, value): self.collected = value def parse_deletion_date(contents): result = Basket() ParseTrashInfo(on_deletion_date=result.collect)(contents) return result.collected def parse_path(contents): for line in contents.split('\n'): if line.startswith('Path='): return unquote(line[len('Path='):]) raise ParseError('Unable to parse Path') class CleanableTrashcan: def __init__(self, file_remover): self._file_remover = file_remover def delete_orphan(self, path_to_backup_copy): self._file_remover.remove_file(path_to_backup_copy) def delete_trashinfo_and_backup_copy(self, trashinfo_path): backup_copy = self._path_of_backup_copy(trashinfo_path) self._file_remover.remove_file_if_exists(backup_copy) self._file_remover.remove_file(trashinfo_path) def _path_of_backup_copy(self, path_to_trashinfo): from os.path import dirname, join, basename trash_dir = dirname(dirname(path_to_trashinfo)) return join(trash_dir, 'files', basename(path_to_trashinfo)[:-len('.trashinfo')]) trash-cli-0.17.1.14/unit_tests/000077500000000000000000000000001303652251000161075ustar00rootroot00000000000000trash-cli-0.17.1.14/unit_tests/__init__.py000066400000000000000000000000001303652251000202060ustar00rootroot00000000000000trash-cli-0.17.1.14/unit_tests/myStringIO.py000066400000000000000000000001271303652251000205250ustar00rootroot00000000000000try: from StringIO import StringIO except ImportError: from io import StringIO trash-cli-0.17.1.14/unit_tests/test_check_release_installation.py000066400000000000000000000057231303652251000250650ustar00rootroot00000000000000from check_release_installation import (CheckInstallation, NormalInstallation, EasyInstallInstallation) from nose.tools import assert_equals, assert_raises from mock import Mock, call class TestCheckBothInstallations: def setUp(self): self.calls = [] outer = self class FakeSSH: def run_checked(self, command): outer.calls.append(call().run_checked(command)) def put(self, command): outer.calls.append(call().put(command)) self.ssh = FakeSSH() def test_python3(self): version = '0.17.1.12' ci = CheckInstallation(NormalInstallation('python3'), self.ssh, version) ci.check_installation() assert_equals([ call().run_checked('sudo rm -f $(which trash-put)'), call().run_checked('! which trash-put'), call().run_checked('sudo rm -f $(which trash-list)'), call().run_checked('! which trash-list'), call().run_checked('sudo rm -f $(which trash-rm)'), call().run_checked('! which trash-rm'), call().run_checked('sudo rm -f $(which trash-empty)'), call().run_checked('! which trash-empty'), call().run_checked('sudo rm -f $(which trash-restore)'), call().run_checked('! which trash-restore'), call().run_checked('sudo rm -f $(which trash)'), call().run_checked('! which trash'), call().put('dist/trash-cli-0.17.1.12.tar.gz'), call().run_checked('tar xfvz trash-cli-0.17.1.12.tar.gz'), call().run_checked('cd trash-cli-0.17.1.12 && sudo python3 setup.py install'), call().run_checked('trash-put --version'), call().run_checked('trash-list --version'), call().run_checked('trash-rm --version'), call().run_checked('trash-empty --version'), call().run_checked('trash-restore --version'), call().run_checked('trash --version')], self.calls) def test_easy_install_installation(self): version = '0.17.1.12' i = CheckInstallation(EasyInstallInstallation(), self.ssh, version) i.check_installation() assert_equals([ call().run_checked('sudo rm -f $(which trash-put)'), call().run_checked('! which trash-put'), call().run_checked('sudo rm -f $(which trash-list)'), call().run_checked('! which trash-list'), call().run_checked('sudo rm -f $(which trash-rm)'), call().run_checked('! which trash-rm'), call().run_checked('sudo rm -f $(which trash-empty)'), call().run_checked('! which trash-empty'), call().run_checked('sudo rm -f $(which trash-restore)'), call().run_checked('! which trash-restore'), call().run_checked('sudo rm -f $(which trash)'), call().run_checked('! which trash'), call().put('dist/trash-cli-0.17.1.12.tar.gz'), call().run_checked('sudo easy_install trash-cli-0.17.1.12.tar.gz'), call().run_checked('trash-put --version'), call().run_checked('trash-list --version'), call().run_checked('trash-rm --version'), call().run_checked('trash-empty --version'), call().run_checked('trash-restore --version'), call().run_checked('trash --version')], self.calls) trash-cli-0.17.1.14/unit_tests/test_empty.py000066400000000000000000000015551303652251000206640ustar00rootroot00000000000000from trashcli.empty import EmptyCmd from mock import Mock, call from nose.tools import assert_equals class TestTrashEmptyCmd: def setUp(self): self.empty_all_trashdirs = Mock() self.empty_trashdir = Mock() self.cmd = EmptyCmd(None, None, None, None, None, None, None, None, None) self.cmd.empty_all_trashdirs = self.empty_all_trashdirs self.cmd.empty_trashdir = self.empty_trashdir def test_default_behaviour_is_emtpy_all_trashdirs(self): self.cmd.run('trash-empty') assert_equals([call()], self.empty_all_trashdirs.mock_calls) assert_equals([], self.empty_trashdir.mock_calls) def test(self): self.cmd.run('trash-empty', '--trash-dir', 'specific') assert_equals([], self.empty_all_trashdirs.mock_calls) assert_equals([call('specific')], self.empty_trashdir.mock_calls) trash-cli-0.17.1.14/unit_tests/test_fake_fstab.py000066400000000000000000000021071303652251000216050ustar00rootroot00000000000000from trashcli.fstab import FakeFstab from nose.tools import assert_equals from nose.tools import istest from unit_tests.tools import assert_items_equal class TestFakeFstab: def setUp(self): self.fstab = FakeFstab() @istest def on_default(self): self.assert_mount_points_are('/') @istest def it_should_accept_fake_mount_points(self): self.fstab.add_mount('/fake') self.assert_mount_points_are('/', '/fake') @istest def root_is_not_duplicated(self): self.fstab.add_mount('/') self.assert_mount_points_are('/') @istest def test_something(self): fstab = FakeFstab() fstab.add_mount('/fake') assert_equals('/fake', fstab.volume_of('/fake/foo')) def assert_mount_points_are(self, *expected_mounts): expected_mounts = list(expected_mounts) actual_mounts = list(self.fstab.mount_points()) assert_items_equal(expected_mounts, list(self.fstab.mount_points()), 'Expected: %s\n' 'Found: %s\n' % (expected_mounts, actual_mounts)) trash-cli-0.17.1.14/unit_tests/test_fake_ismount.py000066400000000000000000000025751303652251000222150ustar00rootroot00000000000000from trashcli.fstab import FakeIsMount from nose.tools import istest from nose.tools import assert_false from nose.tools import assert_true @istest class OnDefault: def setUp(self): self.ismount = FakeIsMount() @istest def by_default_root_is_mount(self): assert_true(self.ismount('/')) @istest def while_by_default_any_other_is_not_a_mount_point(self): assert_false(self.ismount('/any/other')) @istest class WhenOneFakeVolumeIsDefined: def setUp(self): self.ismount = FakeIsMount() self.ismount.add_mount('/fake-vol') @istest def accept_fake_mount_point(self): assert_true(self.ismount('/fake-vol')) @istest def other_still_are_not_mounts(self): assert_false(self.ismount('/other')) @istest def dont_get_confused_by_traling_slash(self): assert_true(self.ismount('/fake-vol/')) @istest class WhenMultipleFakesMountPoints: def setUp(self): self.ismount = FakeIsMount() self.ismount.add_mount('/vol1') self.ismount.add_mount('/vol2') @istest def recognize_both(self): assert_true(self.ismount('/vol1')) assert_true(self.ismount('/vol2')) assert_false(self.ismount('/other')) @istest def should_handle_relative_volumes(): ismount = FakeIsMount() ismount.add_mount('fake-vol') assert_true(ismount('fake-vol')) trash-cli-0.17.1.14/unit_tests/test_global_trashcan.py000066400000000000000000000070631303652251000226510ustar00rootroot00000000000000from mock import Mock, call from nose.tools import istest, assert_equals, assert_not_equals from trashcli.put import GlobalTrashCan import os class TestTopDirRules: def test(self): parent_path = lambda _:None volume_of = lambda _:'/volume' realpath = lambda _: None fs = Mock(['move', 'atomic_write', 'remove_file', 'ensure_dir', 'isdir', 'islink', 'has_sticky_bit']) fs.islink.side_effect = lambda path: { '/volume/.Trash':False }[path] fs.has_sticky_bit.side_effect = lambda path: { '/volume/.Trash':False }[path] reporter = Mock(['volume_of_file', 'found_unsecure_trash_dir_unsticky', 'trash_dir_with_volume', 'file_has_been_trashed_in_as']) trashcan = GlobalTrashCan({}, volume_of, reporter, fs, lambda :'uid', None, parent_path, realpath) trashcan.trash('') assert_equals([ call('', '/volume/.Trash-uid', '/volume/.Trash-uid/files/.') ], reporter.file_has_been_trashed_in_as.mock_calls) class TestGlobalTrashCan: def setUp(self): self.reporter = Mock() self.fs = Mock() self.volume_of = Mock() self.volume_of.return_value = '/' self.trashcan = GlobalTrashCan( volume_of = self.volume_of, reporter = self.reporter, getuid = lambda:123, now = None, environ = dict(), fs = self.fs, parent_path = os.path.dirname, realpath = lambda x:x) def test_log_volume(self): self.trashcan.trash('a-dir/with-a-file') self.reporter.volume_of_file.assert_called_with('/') @istest def should_report_when_trash_fail(self): self.fs.move.side_effect = IOError self.trashcan.trash('non-existent') self.reporter.unable_to_trash_file.assert_called_with('non-existent') @istest def should_not_delete_a_dot_entru(self): self.trashcan.trash('.') self.reporter.unable_to_trash_dot_entries.assert_called_with('.') @istest def bug(self): self.fs.mock_add_spec([ 'move', 'atomic_write', 'remove_file', 'ensure_dir', 'isdir', 'islink', 'has_sticky_bit', ], True) self.fs.islink.side_effect = (lambda path: { '/.Trash':False }[path]) self.volume_of.side_effect = (lambda path: { '/foo': '/', '': '/', '/.Trash/123': '/', }[path]) self.trashcan.trash('foo') def test_what_happen_when_trashing_with_trash_dir(self): from trashcli.put import TrashDirectoryForPut fs = Mock() now = Mock() fs.mock_add_spec([ 'move', 'atomic_write', 'remove_file', 'ensure_dir', ], True) from nose import SkipTest raise SkipTest() trash_dir = TrashDirectoryForPut('/path', '/volume', now, fs) trash_dir.trash('garbage') trash-cli-0.17.1.14/unit_tests/test_home_fallback.py000066400000000000000000000062631303652251000222760ustar00rootroot00000000000000from mock import Mock, call, ANY from trashcli.fstab import FakeFstab from trashcli.put import GlobalTrashCan from nose.tools import assert_equals import os class TestHomeFallback: def setUp(self): self.reporter = Mock() mount_points = ['/', 'sandbox/other_partition'] self.fs = Mock() self.trashcan = GlobalTrashCan( reporter = self.reporter, getuid = lambda: 123, volume_of = self.fake_volume_of(mount_points), now = None, environ = dict(), fs = self.fs, parent_path = os.path.dirname, realpath = lambda x:x) def test_use_of_top_trash_dir_when_sticky(self): self.fs.mock_add_spec(['isdir', 'islink', 'has_sticky_bit', 'move', 'atomic_write', 'remove_file', 'ensure_dir']) self.fs.isdir.return_value = True self.fs.islink.return_value = False self.fs.has_sticky_bit.return_value = True self.trashcan.trash('sandbox/foo') assert_equals([ call.isdir('.Trash'), call.islink('.Trash'), call.has_sticky_bit('.Trash'), call.ensure_dir('.Trash/123/info', 448), call.atomic_write('.Trash/123/info/foo.trashinfo', ANY), call.ensure_dir('.Trash/123/files', 448), call.move('sandbox/foo', '.Trash/123/files/foo') ], self.fs.mock_calls) def test_bug_will_use_top_trashdir_even_with_not_sticky(self): self.fs.mock_add_spec(['isdir', 'islink', 'has_sticky_bit', 'move', 'atomic_write', 'remove_file', 'ensure_dir']) self.fs.isdir.return_value = True self.fs.islink.return_value = False self.fs.has_sticky_bit.return_value = False self.trashcan.trash('sandbox/foo') assert_equals([ call.isdir('.Trash'), call.islink('.Trash'), call.has_sticky_bit('.Trash'), call.ensure_dir('.Trash-123/info', 448), call.atomic_write('.Trash-123/info/foo.trashinfo', ANY), call.ensure_dir('.Trash-123/files', 448), call.move('sandbox/foo', '.Trash-123/files/foo') ], self.fs.mock_calls, self.fs.mock_calls) def fake_volume_of(self, volumes): fstab = FakeFstab() for vol in volumes: fstab.add_mount(vol) return fstab.volume_of from trashcli.trash import TrashDirectories from trashcli.restore import AllTrashDirectories class TestTrashDirectories: def test_list_all_directories(self): all_trash_directories = AllTrashDirectories( volume_of = Mock(), getuid = lambda:123, environ = {'HOME': '~'}, mount_points = ['/', '/mnt']) result = all_trash_directories.all_trash_directories() paths = list(map(lambda td: td.path, result)) assert_equals( ['~/.local/share/Trash', '/.Trash/123', '/.Trash-123', '/mnt/.Trash/123', '/mnt/.Trash-123'] , paths) trash-cli-0.17.1.14/unit_tests/test_issues_message.py000066400000000000000000000033571303652251000225470ustar00rootroot00000000000000from nose.tools import assert_equals from unit_tests.myStringIO import StringIO class TestTrashPutIssueMessage: def setUp(self): self.out = StringIO() def test_trash_put_last_line(self): from trashcli.put import TrashPutCmd cmd = TrashPutCmd(self.out, StringIO(), None, None, None, None) cmd.run(['', '--help']) self.assert_last_line_of_output_is( 'Report bugs to https://github.com/andreafrancia/trash-cli/issues') def test_trash_empty_last_line(self): from trashcli.empty import EmptyCmd from trashcli.trash import FileSystemReader cmd = EmptyCmd(self.out, StringIO(), [], lambda:[], now = None, file_reader = FileSystemReader(), getuid = None, file_remover = None, version = None, ) cmd.run('', '--help') self.assert_last_line_of_output_is( 'Report bugs to https://github.com/andreafrancia/trash-cli/issues') def test_trash_list_last_line(self): from trashcli.list import ListCmd cmd = ListCmd(self.out, None, None, None, None) cmd.run('', '--help') self.assert_last_line_of_output_is( 'Report bugs to https://github.com/andreafrancia/trash-cli/issues') def assert_last_line_of_output_is(self, expected): output = self.out.getvalue() if len(output.splitlines()) > 0: last_line = output.splitlines()[-1] else: last_line = '' assert_equals(expected, last_line, ('Last line of output should be:\n\n%s\n\n' % expected + 'but the output is\n\n%s' % output)) trash-cli-0.17.1.14/unit_tests/test_joining_paths.py000066400000000000000000000010461303652251000223550ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals def test_how_path_joining_works(): from os.path import join assert_equals('/another-absolute', join('/absolute', '/another-absolute')) assert_equals('/absolute/relative', join('/absolute', 'relative')) assert_equals('/absolute', join('relative', '/absolute')) assert_equals('relative/relative', join('relative', 'relative')) assert_equals('/absolute', join('', '/absolute')) assert_equals('/absolute', join(None, '/absolute')) trash-cli-0.17.1.14/unit_tests/test_list_all_trashinfo_contents.py000066400000000000000000000031471303652251000253220ustar00rootroot00000000000000from mock import Mock, call from nose.tools import assert_equals from unit_tests.tools import assert_items_equal class TestListing: def setUp(self): self.trashdir = Mock() self.trashinfo_reader = Mock() self.listing = Listing(self.trashdir, self.trashinfo_reader) def test_it_should_read_all_trashinfo_from_home_dir(self): self.listing.read_home_trashdir('/path/to/trash_dir') self.trashdir.list_trashinfos.assert_called_with( trashdir='/path/to/trash_dir', list_to=self.trashinfo_reader) class TestTrashDirReader: def test_should_list_all_trashinfo_found(self): def files(path): yield 'file1'; yield 'file2' os_listdir = Mock(side_effect=files) trashdir = TrashDirReader(os_listdir) out = Mock() trashdir.list_trashinfos(trashdir='/path', list_to=out) assert_items_equal([call(trashinfo='/path/file1'), call(trashinfo='/path/file2')], out.mock_calls) class TrashDirReader: def __init__(self, os_listdir): self.os_listdir = os_listdir def list_trashinfos(self, trashdir, list_to): import os for entry in self.os_listdir(trashdir): full_path = os.path.join(trashdir, entry) list_to(trashinfo=full_path) class Listing: def __init__(self, trashdir, trashinfo_reader): self.trashdir = trashdir self.trashinfo_reader = trashinfo_reader def read_home_trashdir(self, path): self.trashdir.list_trashinfos(trashdir=path, list_to=self.trashinfo_reader) trash-cli-0.17.1.14/unit_tests/test_list_mount_points.py000066400000000000000000000035221303652251000233130ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import unittest from io import BytesIO from trashcli.list_mount_points import _mount_points_from_df_output class MyStringIO(BytesIO): def __init__(self, string): super(MyStringIO, self).__init__(string.encode('ascii', 'strict')) class MountPointFromDirTest(unittest.TestCase): def test_should_skip_the_first_line(self): mount_points = _mount_points_from_df_output(MyStringIO( 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' )) self.assertEquals([], list(mount_points)) def test_should_return_the_first_mount_point(self): mount_points = _mount_points_from_df_output(MyStringIO( 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' '/dev/disk0s2 243862672 121934848 121671824 51% /\n' )) self.assertEquals(['/'], list(mount_points)) def test_should_return_multiple_mount_point(self): mount_points = _mount_points_from_df_output(MyStringIO( 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' '/dev/disk0s2 243862672 121934848 121671824 51% /\n' '/dev/disk1s1 156287996 123044260 33243736 79% /Volumes/DISK\n' )) self.assertEquals(['/', '/Volumes/DISK'], list(mount_points)) def test_should_return_mount_point_with_white_spaces(self): mount_points = _mount_points_from_df_output(MyStringIO( 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' '/dev/disk0s2 243862672 121934848 121671824 51% /\n' '/dev/disk1s1 156287996 123044260 33243736 79% /Volumes/with white spaces\n' )) self.assertEquals(['/', '/Volumes/with white spaces'], list(mount_points)) trash-cli-0.17.1.14/unit_tests/test_make_script.py000066400000000000000000000035221303652251000220230ustar00rootroot00000000000000from textwrap import dedent from nose.tools import assert_equals import mock from mock import Mock from setup import Scripts class TestMakeScript: def setUp(self): self.make_file_executable = Mock() self.write_file = Mock() def capture(name, contents): self.name = name self.contents = contents self.write_file.side_effect = capture bindir = Scripts( make_file_executable = self.make_file_executable, write_file = self.write_file) bindir.add_script('trash-put', 'trashcli.cmds', 'put') def test_should_set_executable_permission(self): self.make_file_executable.assert_called_with('trash-put') def test_should_write_the_script(self): self.write_file.assert_called_with( 'trash-put', mock.ANY) def test_the_script_should_call_the_right_function_from_the_right_module(self): args, kwargs = self.write_file.call_args (_, contents) = args expected = dedent("""\ #!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.cmds import put as main sys.exit(main()) """) assert_equals(expected, contents, "Expected:\n---\n%s---\n" "Actual :\n---\n%s---\n" % (expected, contents)) class TestListOfCreatedScripts: def setUp(self): self.bindir = Scripts( make_file_executable = Mock(), write_file = Mock()) def test_is_empty_on_start_up(self): assert_equals(self.bindir.created_scripts, []) def test_collect_added_script(self): self.bindir.add_script('foo-command', 'foo-module', 'main') assert_equals(self.bindir.created_scripts, ['foo-command']) trash-cli-0.17.1.14/unit_tests/test_method1_security_check.py000066400000000000000000000023611303652251000241470ustar00rootroot00000000000000from mock import Mock from integration_tests.files import require_empty_dir from trashcli.put import TopTrashDirWriteRules class TestMethod1VolumeTrashDirectory: def setUp(self): require_empty_dir('sandbox') self.fs = Mock() self.fs.isdir.return_value = True self.fs.islink.return_value = False self.fs.has_sticky_bit.return_value = True self.checker = TopTrashDirWriteRules(self.fs) self.out = Mock() def test_check_when_no_sticky_bit(self): self.fs.has_sticky_bit.return_value = False self.valid_to_be_written() self.out.not_valid_parent_should_be_sticky.assert_called_with() def test_check_when_no_dir(self): self.fs.isdir.return_value = False self.valid_to_be_written() self.out.not_valid_should_be_a_dir.assert_called_with() def test_check_when_is_symlink(self): self.fs.islink.return_value = True self.valid_to_be_written() self.out.not_valid_parent_should_not_be_a_symlink.assert_called_with() def test_check_pass(self): self.valid_to_be_written() self.out.is_valid() def valid_to_be_written(self): self.checker.valid_to_be_written('sandbox/trash-dir/123', self.out) trash-cli-0.17.1.14/unit_tests/test_parser.py000066400000000000000000000034061303652251000210170ustar00rootroot00000000000000from trashcli.trash import Parser from mock import MagicMock, call from nose.tools import istest, assert_equals class TestParser(): def setUp(self): self.invalid_option_callback = MagicMock() self.on_raw = MagicMock() self.on_help = MagicMock() self.on_option = MagicMock() self.parser = Parser() self.parser.on_invalid_option(self.invalid_option_callback) self.parser.add_option('raw', self.on_raw) self.parser.add_option('opt=', self.on_option) self.parser.on_help(self.on_help) def test_argument_option_called_without_argument(self): self.parser(['trash-list', '--opt']) assert_equals([], self.on_option.mock_calls) self.invalid_option_callback.assert_called_with('trash-list', 'opt') def test_argument_option_called_with_argument(self): self.parser(['trash-list', '--opt=', 'arg']) assert_equals([call('')], self.on_option.mock_calls) def test_argument_option_called_with_argument(self): self.parser(['trash-list', '--opt=arg']) assert_equals([call('arg')], self.on_option.mock_calls) def test_argument_option_called_with_argument(self): self.parser(['trash-list', '--opt', 'arg']) assert_equals([call('arg')], self.on_option.mock_calls) def test_it_calls_help(self): self.parser(['trash-list', '--help']) self.on_help.assert_called_with('trash-list') def test_it_calls_the_actions_passing_the_program_name(self): self.parser(['trash-list', '--raw']) self.on_raw.assert_called_with('') def test_how_getopt_works_with_an_invalid_option(self): self.parser(['command-name', '-x']) self.invalid_option_callback.assert_called_with('command-name', 'x') trash-cli-0.17.1.14/unit_tests/test_parsing_trashinfo_contents.py000066400000000000000000000065231303652251000251630ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals, assert_raises from nose.tools import istest from datetime import datetime from mock import MagicMock from trashcli.trash import ParseTrashInfo @istest class describe_ParseTrashInfo2: @istest def it_should_parse_date(self): out = MagicMock() parse = ParseTrashInfo(on_deletion_date = out) parse('[Trash Info]\n' 'Path=foo\n' 'DeletionDate=1970-01-01T00:00:00\n') out.assert_called_with(datetime(1970,1,1,0,0,0)) @istest def it_should_parse_path(self): out = MagicMock() self.parse = ParseTrashInfo(on_path = out) self.parse( '[Trash Info]\n' 'Path=foo\n' 'DeletionDate=1970-01-01T00:00:00\n') out.assert_called_with('foo') from trashcli.trash import parse_deletion_date from trashcli.trash import parse_path def test_how_to_parse_date_from_trashinfo(): from datetime import datetime assert_equals(datetime(2000,12,31,23,59,58), parse_deletion_date('DeletionDate=2000-12-31T23:59:58')) assert_equals(datetime(2000,12,31,23,59,58), parse_deletion_date('DeletionDate=2000-12-31T23:59:58\n')) assert_equals(datetime(2000,12,31,23,59,58), parse_deletion_date('[Trash Info]\nDeletionDate=2000-12-31T23:59:58')) from trashcli.trash import maybe_parse_deletion_date UNKNOWN_DATE='????-??-?? ??:??:??' @istest class describe_maybe_parse_deletion_date: @istest def on_trashinfo_without_date_parse_to_unknown_date(self): assert_equals(UNKNOWN_DATE, maybe_parse_deletion_date(a_trashinfo_without_deletion_date())) @istest def on_trashinfo_with_date_parse_to_date(self): from datetime import datetime example_date_as_string='2001-01-01T00:00:00' same_date_as_datetime=datetime(2001,1,1) assert_equals(same_date_as_datetime, maybe_parse_deletion_date(make_trashinfo(example_date_as_string))) @istest def on_trashinfo_with_invalid_date_parse_to_unknown_date(self): invalid_date='A long time ago' assert_equals(UNKNOWN_DATE, maybe_parse_deletion_date(make_trashinfo(invalid_date))) def test_how_to_parse_original_path(): assert_equals('foo.txt', parse_path('Path=foo.txt')) assert_equals('/path/to/be/escaped', parse_path('Path=%2Fpath%2Fto%2Fbe%2Fescaped')) from trashcli.restore import TrashInfoParser from trashcli.trash import ParseError class TestParsing: def test_1(self): parser = TrashInfoParser("[Trash Info]\n" "Path=/foo.txt\n", volume_path = '/') assert_equals('/foo.txt', parser.original_location()) class TestTrashInfoParser_with_empty_trashinfo: def setUp(self): self.parser = TrashInfoParser(contents=an_empty_trashinfo(), volume_path='/') def test_it_raises_error_on_parsing_original_location(self): with assert_raises(ParseError): self.parser.original_location() def a_trashinfo_without_deletion_date(): return ("[Trash Info]\n" "Path=foo.txt\n") def make_trashinfo(date): return ("[Trash Info]\n" "Path=foo.txt\n" "DeletionDate=%s" % date) def an_empty_trashinfo(): return '' trash-cli-0.17.1.14/unit_tests/test_restore_cmd.py000066400000000000000000000213371303652251000220340ustar00rootroot00000000000000from trashcli.restore import RestoreCmd from nose.tools import assert_equals from .myStringIO import StringIO from mock import Mock, call class TestListingInRestoreCmd: def setUp(self): self.cmd = RestoreCmd(None, None, None, None, None) self.cmd.curdir = lambda: "dir" self.cmd.handle_trashed_files = self.capture_trashed_files def test_with_no_args_and_files_in_trashcan(self): def some_files(): yield FakeTrashedFile('', 'dir/location') yield FakeTrashedFile('', 'dir/location') yield FakeTrashedFile('', 'anotherdir/location') self.cmd.all_trashed_files = some_files self.cmd.run(['trash-restore']) assert_equals([ 'dir/location' , 'dir/location' ] ,self.original_locations) def test_with_no_args_and_files_in_trashcan(self): def some_files(): yield FakeTrashedFile('', 'dir/location') yield FakeTrashedFile('', 'dir/location') yield FakeTrashedFile('', 'specific/path') self.cmd.all_trashed_files = some_files self.cmd.run(['trash-restore', 'specific/path']) assert_equals([ 'specific/path' ] ,self.original_locations) def capture_trashed_files(self,arg): self.original_locations = [] for trashed_file in arg: self.original_locations.append(trashed_file.original_location) class FakeTrashedFile(object): def __init__(self, deletion_date, original_location): self.deletion_date = deletion_date self.original_location = original_location def __repr__(self): return ('FakeTrashedFile(\'%s\', ' % self.deletion_date + '\'%s\')' % self.original_location) class TestTrashRestoreCmd: def setUp(self): self.stdout = StringIO() self.stderr = StringIO() self.cmd = RestoreCmd(stdout=self.stdout, stderr=self.stderr, environ={}, exit = self.capture_exit_status, input =lambda x: self.user_reply, version = None) def capture_exit_status(self, exit_status): self.exit_status = exit_status def test_should_print_version(self): self.cmd.version = '1.2.3' self.cmd.run(['trash-restore', '--version']) assert_equals('trash-restore 1.2.3\n', self.stdout.getvalue()) def test_with_no_args_and_no_files_in_trashcan(self): self.cmd.curdir = lambda: "cwd" self.cmd.run(['trash-restore']) assert_equals("No files trashed from current dir ('cwd')\n", self.stdout.getvalue()) def test_until_the_restore_intgration(self): from trashcli.fs import remove_file from trashcli.fs import contents_of self.user_reply = '0' open('orig_file', 'w').write('original') open('info_file', 'w').write('') remove_file('parent/path') remove_file('parent') trashed_file = TrashedFile( 'parent/path', None, 'info_file', 'orig_file') self.cmd.restore_asking_the_user([trashed_file]) assert_equals('', self.stdout.getvalue()) assert_equals('', self.stderr.getvalue()) assert_true(not os.path.exists('info_file')) assert_true(not os.path.exists('orig_file')) assert_true(os.path.exists('parent/path')) assert_equals('original', contents_of('parent/path')) def test_until_the_restore_unit(self): trashed_file = TrashedFile( 'parent/path', None, 'info_file', 'orig_file') fs = Mock() self.cmd.fs = fs self.cmd.path_exists = lambda _: False self.user_reply = '0' self.cmd.restore_asking_the_user([trashed_file]) assert_equals('', self.stdout.getvalue()) assert_equals('', self.stderr.getvalue()) assert_equals([ call.mkdirs('parent') , call.move('orig_file', 'parent/path') , call.remove_file('info_file') ], fs.mock_calls) def test_when_user_reply_with_empty_string(self): self.user_reply = '' self.cmd.restore_asking_the_user([]) assert_equals('Exiting\n', self.stdout.getvalue()) def test_when_user_reply_with_not_number(self): self.user_reply = 'non numeric' self.cmd.restore_asking_the_user([]) assert_equals('Invalid entry\n', self.stderr.getvalue()) assert_equals('', self.stdout.getvalue()) assert_equals(1, self.exit_status) def test_when_user_reply_with_an_out_of_range_number(self): self.user_reply = '100' self.cmd.restore_asking_the_user([]) assert_equals('Invalid entry\n', self.stderr.getvalue()) assert_equals('', self.stdout.getvalue()) assert_equals(1, self.exit_status) from trashcli.restore import TrashedFile from nose.tools import assert_raises, assert_true import os class TestTrashedFileRestoreIntegration: def setUp(self): remove_file_if_exists('parent/path') remove_dir_if_exists('parent') self.cmd = RestoreCmd(None, None, None, None, None) def test_restore(self): trashed_file = TrashedFile('parent/path', None, 'info_file', 'orig') open('orig','w').close() open('info_file','w').close() self.cmd.restore(trashed_file) assert_true(os.path.exists('parent/path')) assert_true(not os.path.exists('info_file')) def test_restore_over_existing_file(self): trashed_file = TrashedFile('path',None,None,None) open('path','w').close() assert_raises(IOError, lambda: self.cmd.restore(trashed_file)) def tearDown(self): remove_file_if_exists('path') remove_file_if_exists('parent/path') remove_dir_if_exists('parent') def test_restore_create_needed_directories(self): require_empty_dir('sandbox') write_file('sandbox/TrashDir/files/bar') instance = TrashedFile('sandbox/foo/bar', 'deletion_date', 'info_file', 'sandbox/TrashDir/files/bar') self.cmd.restore(instance) assert os.path.exists("sandbox/foo/bar") import datetime from mock import Mock class TestRestoreCmdListingUnit: def test_something(self): cmd = RestoreCmd(None, None, {}, None, None) cmd.contents_of = lambda path: 'Path=name\nDeletionDate=2001-01-01T10:10:10' path_to_trashinfo = 'info/info_path.trashinfo' trash_dir = Mock([]) trash_dir.volume = '/volume' trash_dir.all_info_files = Mock([], return_value=[path_to_trashinfo]) cmd.all_trash_directories2 = lambda: [trash_dir] cmd.curdir = lambda: '/volume' trashed_files = list(cmd.all_trashed_files()) trashed_file = trashed_files[0] assert_equals('/volume/name' , trashed_file.original_location) assert_equals(datetime.datetime(2001, 1, 1, 10, 10, 10), trashed_file.deletion_date) assert_equals('info/info_path.trashinfo' , trashed_file.info_file) assert_equals('files/info_path' , trashed_file.original_file) from integration_tests.files import write_file, require_empty_dir from trashcli.fs import remove_file class TestRestoreCmdListingIntegration: def test_something(self): cmd = RestoreCmd(None, None, {}, None, None) require_empty_dir('info') open('info/info_path.trashinfo', 'w').write( 'Path=name\nDeletionDate=2001-01-01T10:10:10') path_to_trashinfo = 'info/info_path.trashinfo' trash_dir = Mock([]) trash_dir.volume = '/volume' trash_dir.all_info_files = Mock([], return_value=[path_to_trashinfo]) cmd.all_trash_directories2 = lambda: [trash_dir] cmd.curdir = lambda: '/volume' trashed_files = list(cmd.all_trashed_files()) trashed_file = trashed_files[0] assert_equals('/volume/name' , trashed_file.original_location) assert_equals(datetime.datetime(2001, 1, 1, 10, 10, 10), trashed_file.deletion_date) assert_equals('info/info_path.trashinfo' , trashed_file.info_file) assert_equals('files/info_path' , trashed_file.original_file) def tearDown(self): remove_file('info/info_path.trashinfo') remove_dir_if_exists('info') def remove_dir_if_exists(dir): if os.path.exists(dir): os.rmdir(dir) def remove_file_if_exists(path): if os.path.lexists(path): os.unlink(path) trash-cli-0.17.1.14/unit_tests/test_shrink_user.py000066400000000000000000000024121303652251000220530ustar00rootroot00000000000000from nose.tools import assert_equals from trashcli.put import shrinkuser class TestTrashDirectoryName: def setUp(self): self.environ = {} def test_should_substitute_tilde_in_place_of_home_dir(self): self.environ['HOME']='/home/user' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('~/.local/share/Trash') def test_when_not_in_home_dir(self): self.environ['HOME']='/home/user' self.trash_dir = "/not-in-home/Trash" self.assert_name_is('/not-in-home/Trash') def test_tilde_works_also_with_trailing_slash(self): self.environ['HOME']='/home/user/' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('~/.local/share/Trash') def test_str_uses_tilde_with_many_slashes(self): self.environ['HOME']='/home/user////' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('~/.local/share/Trash') def test_dont_get_confused_by_empty_home_dir(self): self.environ['HOME']='' self.trash_dir = "/foo/Trash" self.assert_name_is('/foo/Trash') def assert_name_is(self, expected_name): shrinked = shrinkuser(self.trash_dir, self.environ) assert_equals(expected_name, shrinked) trash-cli-0.17.1.14/unit_tests/test_storing_paths.py000066400000000000000000000026651303652251000224150ustar00rootroot00000000000000from trashcli.put import TrashDirectoryForPut from nose.tools import assert_equals from mock import Mock class TestHowOriginalLocationIsStored: def test_for_absolute_paths(self): fs = Mock() self.dir = TrashDirectoryForPut('/volume/.Trash', '/volume', None, fs) self.dir.store_absolute_paths() self.assert_path_for_trashinfo_is('/file' , '/file') self.assert_path_for_trashinfo_is('/file' , '/dir/../file') self.assert_path_for_trashinfo_is('/outside/file' , '/outside/file') self.assert_path_for_trashinfo_is('/volume/file' , '/volume/file') self.assert_path_for_trashinfo_is('/volume/dir/file' , '/volume/dir/file') def test_for_relative_paths(self): self.dir = TrashDirectoryForPut('/volume/.Trash', '/volume', None, Mock()) self.dir.store_relative_paths() self.assert_path_for_trashinfo_is('/file' , '/file') self.assert_path_for_trashinfo_is('/file' , '/dir/../file') self.assert_path_for_trashinfo_is('/outside/file' , '/outside/file') self.assert_path_for_trashinfo_is('file' , '/volume/file') self.assert_path_for_trashinfo_is('dir/file' , '/volume/dir/file') def assert_path_for_trashinfo_is(self, expected_value, file_to_be_trashed): result = self.dir.path_for_trash_info.for_file(file_to_be_trashed) assert_equals(expected_value, result) trash-cli-0.17.1.14/unit_tests/test_trash_dirs_listing.py000066400000000000000000000106051303652251000234150ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from trashcli.trash import TrashDirs from nose.tools import istest, assert_in, assert_not_in from mock import Mock @istest class TestTrashDirs_listing: @istest def the_method_2_is_always_in(self): self.uid = 123 self.volumes = ['/usb'] assert_in('/usb/.Trash-123', self.trashdirs()) @istest def the_method_1_is_in_if_it_is_a_sticky_dir(self): self.uid = 123 self.volumes = ['/usb'] self.having_sticky_Trash_dir() assert_in('/usb/.Trash/123', self.trashdirs()) @istest def the_method_1_is_not_considered_if_not_sticky_dir(self): self.uid = 123 self.volumes = ['/usb'] self.having_non_sticky_Trash_dir() assert_not_in('/usb/.Trash/123', self.trashdirs()) @istest def should_return_home_trashcan_when_XDG_DATA_HOME_is_defined(self): self.environ['XDG_DATA_HOME'] = '~/.local/share' assert_in('~/.local/share/Trash', self.trashdirs()) def trashdirs(self): result = [] def append(trash_dir, volume): result.append(trash_dir) class FileReader: def is_sticky_dir(_, path): return self.Trash_dir_is_sticky def exists(_, path): return True def is_symlink(_, path): return False class FakeTopTrashDirRules: def valid_to_be_read(_, path, out): if self.Trash_dir_is_sticky: out.is_valid() else: out.not_valid_parent_should_be_sticky() trash_dirs = TrashDirs( environ=self.environ, getuid=lambda:self.uid, top_trashdir_rules = FakeTopTrashDirRules(), list_volumes = lambda: self.volumes, ) trash_dirs.on_trash_dir_found = append trash_dirs.list_trashdirs() return result def setUp(self): self.uid = -1 self.volumes = () self.Trash_dir_is_sticky = not_important_for_now() self.environ = {} def having_sticky_Trash_dir(self): self.Trash_dir_is_sticky = True def having_non_sticky_Trash_dir(self): self.Trash_dir_is_sticky = False def not_important_for_now(): None from nose.tools import assert_equals from mock import MagicMock from trashcli.trash import TopTrashDirRules @istest class Describe_AvailableTrashDirs_when_parent_is_unsticky: def setUp(self): self.fs = MagicMock() self.dirs = TrashDirs(environ = {}, getuid = lambda:123, top_trashdir_rules = TopTrashDirRules(self.fs), list_volumes = lambda: ['/topdir'], ) self.dirs.on_trashdir_skipped_because_parent_not_sticky = Mock() self.dirs.on_trashdir_skipped_because_parent_is_symlink = Mock() self.fs.is_sticky_dir.side_effect = ( lambda path: {'/topdir/.Trash':False}[path]) def test_it_should_report_skipped_dir_non_sticky(self): self.fs.exists.side_effect = ( lambda path: {'/topdir/.Trash/123':True}[path]) self.dirs.list_trashdirs() (self.dirs.on_trashdir_skipped_because_parent_not_sticky. assert_called_with('/topdir/.Trash/123')) def test_it_shouldnot_care_about_non_existent(self): self.fs.exists.side_effect = ( lambda path: {'/topdir/.Trash/123':False}[path]) self.dirs.list_trashdirs() assert_equals([], self.dirs.on_trashdir_skipped_because_parent_not_sticky.mock_calls) @istest class Describe_AvailableTrashDirs_when_parent_is_symlink: def setUp(self): self.fs = MagicMock() self.dirs = TrashDirs(environ = {}, getuid = lambda:123, top_trashdir_rules = TopTrashDirRules(self.fs), list_volumes = lambda: ['/topdir']) self.fs.exists.side_effect = (lambda path: {'/topdir/.Trash/123':True}[path]) self.symlink_error = Mock() self.dirs.on_trashdir_skipped_because_parent_is_symlink = self.symlink_error def test_it_should_skip_symlink(self): self.fs.is_sticky_dir.return_value = True self.fs.is_symlink.return_value = True self.dirs.list_trashdirs() self.symlink_error.assert_called_with('/topdir/.Trash/123') trash-cli-0.17.1.14/unit_tests/test_trash_put.py000066400000000000000000000071631303652251000215400ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from trashcli.put import TrashPutCmd from nose.tools import istest, assert_in, assert_equals from unit_tests.myStringIO import StringIO from integration_tests.assert_equals_with_unidiff import assert_equals_with_unidiff from textwrap import dedent class TrashPutTest: def run(self, *arg): self.stderr = StringIO() self.stdout = StringIO() args = ['trash-put'] + list(arg) cmd = TrashPutCmd(self.stdout, self.stderr, None, None, None, None) self._collect_exit_code(lambda:cmd.run(args)) def _collect_exit_code(self, main_function): self.exit_code = 0 result=main_function() if result is not None: self.exit_code=result def stderr_should_be(self, expected_err): assert_equals_with_unidiff(expected_err, self._actual_stderr()) def stdout_should_be(self, expected_out): assert_equals_with_unidiff(expected_out, self._actual_stdout()) def _actual_stderr(self): return self.stderr.getvalue() def _actual_stdout(self): return self.stdout.getvalue() class TestWhenNoArgs(TrashPutTest): def setUp(self): self.run() def test_should_report_usage(self): assert_line_in_text('Usage: trash-put [OPTION]... FILE...', self.stderr.getvalue()) def test_exit_code_should_be_not_zero(self): assert_equals(2, self.exit_code) class TestTrashPutWithWrongOption(TrashPutTest): def test_something(self): self.run('--wrong-option') self.stderr_should_be(dedent('''\ Usage: trash-put [OPTION]... FILE... trash-put: error: no such option: --wrong-option ''')) self.stdout_should_be('') assert_equals(2, self.exit_code) def assert_line_in_text(expected_line, text): assert_in(expected_line, text.splitlines(), 'Line not found in text\n' 'line: %s\n' % expected_line + 'text:\n%s\n' % format(text.splitlines())) @istest class describe_TrashPutCmd(TrashPutTest): @istest def on_help_option_print_help(self): self.run('--help') self.stdout_should_be(dedent('''\ Usage: trash-put [OPTION]... FILE... Put files in trash Options: --version show program's version number and exit -h, --help show this help message and exit -d, --directory ignored (for GNU rm compatibility) -f, --force ignored (for GNU rm compatibility) -i, --interactive ignored (for GNU rm compatibility) -r, -R, --recursive ignored (for GNU rm compatibility) -v, --verbose explain what is being done To remove a file whose name starts with a '-', for example '-foo', use one of these commands: trash -- -foo trash ./-foo Report bugs to https://github.com/andreafrancia/trash-cli/issues ''')) @istest def it_should_skip_dot_entry(self): self.run('.') self.stderr_should_be("trash-put: cannot trash directory '.'\n") @istest def it_should_skip_dotdot_entry(self): self.run('..') self.stderr_should_be("trash-put: cannot trash directory '..'\n") @istest def it_should_print_usage_on_no_argument(self): self.run() self.stderr_should_be( 'Usage: trash-put [OPTION]... FILE...\n' '\n' 'trash-put: error: Please specify the files to trash.\n') self.stdout_should_be('') trash-cli-0.17.1.14/unit_tests/test_trash_put_reporter.py000066400000000000000000000010571303652251000234560ustar00rootroot00000000000000from nose.tools import assert_equals from nose.tools import istest from trashcli.put import TrashPutReporter class TestTrashPutReporter: @istest def it_should_record_failures(self): reporter = TrashPutReporter(self) assert_equals(False, reporter.some_file_has_not_be_trashed) reporter.unable_to_trash_file('a file') assert_equals(True, reporter.some_file_has_not_be_trashed) assert_equals('cannot trash non existent \'a file\'', self.warning_msg) def warning(self, msg): self.warning_msg = msg trash-cli-0.17.1.14/unit_tests/test_trash_rm.py000066400000000000000000000051251303652251000213420ustar00rootroot00000000000000from nose.tools import istest, assert_equals from unit_tests.tools import assert_items_equal from mock import Mock, call from trashcli.rm import Filter from unit_tests.myStringIO import StringIO class TestTrashRmCmdRun: def test_without_arguments(self): from trashcli.rm import RmCmd cmd = RmCmd(None, None, None, None, None) cmd.stderr = StringIO() cmd.run([None]) assert_equals('Usage:\n trash-rm PATTERN\n\nPlease specify PATTERN\n', cmd.stderr.getvalue()) def test_without_pattern_argument(self): from trashcli.rm import RmCmd cmd = RmCmd(None, None, None, None, None) cmd.stderr = StringIO() cmd.file_reader = Mock([]) cmd.file_reader.exists = Mock([], return_value = None) cmd.file_reader.entries_if_dir_exists = Mock([], return_value = []) cmd.environ = {} cmd.getuid = lambda : '111' cmd.list_volumes = lambda: ['/vol1'] cmd.run([None, None]) assert_equals('', cmd.stderr.getvalue()) class TestTrashRmCmd: def test_a_star_matches_all(self): self.cmd.use_pattern('*') self.cmd.delete_if_matches('/foo', 'info/foo') self.cmd.delete_if_matches('/bar', 'info/bar') assert_items_equal([ call('info/foo'), call('info/bar'), ], self.delete_trashinfo_and_backup_copy.mock_calls) def test_basename_matches(self): self.cmd.use_pattern('foo') self.cmd.delete_if_matches('/foo', 'info/foo'), self.cmd.delete_if_matches('/bar', 'info/bar') assert_items_equal([ call('info/foo'), ], self.delete_trashinfo_and_backup_copy.mock_calls) def test_example_with_star_dot_o(self): self.cmd.use_pattern('*.o') self.cmd.delete_if_matches('/foo.h', 'info/foo.h'), self.cmd.delete_if_matches('/foo.c', 'info/foo.c'), self.cmd.delete_if_matches('/foo.o', 'info/foo.o'), self.cmd.delete_if_matches('/bar.o', 'info/bar.o') assert_items_equal([ call('info/foo.o'), call('info/bar.o'), ], self.delete_trashinfo_and_backup_copy.mock_calls) def test_absolute_pattern(self): self.cmd.use_pattern('/foo/bar.baz') self.cmd.delete_if_matches('/foo/bar.baz', '1'), self.cmd.delete_if_matches('/foo/bar', '2'), assert_items_equal([ call('1'), ], self.delete_trashinfo_and_backup_copy.mock_calls) def setUp(self): self.delete_trashinfo_and_backup_copy = Mock() self.cmd = Filter(self.delete_trashinfo_and_backup_copy) trash-cli-0.17.1.14/unit_tests/test_trashdir.py000066400000000000000000000052621303652251000213450ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals from trashcli.trash import TrashDir from trashcli.trash import TrashDirectory class TestTrashDir: def test_path(self): trash_dir = TrashDirectory('/Trash-501', '/') assert_equals('/Trash-501', trash_dir.path) class TestTrashDir_finding_orphans: def test(self): self.fs.create_fake_file('/info/foo.trashinfo') self.find_orphan() assert_equals([], self.orphan_found) def test2(self): self.fs.create_fake_file('/files/foo') self.find_orphan() assert_equals(['/files/foo'], self.orphan_found) def setUp(self): self.orphan_found=[] self.fs = FakeFileSystem() self.trashdir=TrashDir(self.fs) self.trashdir.open('/', None) def find_orphan(self): self.trashdir.each_orphan(self.orphan_found.append) class FakeFileSystem: def __init__(self): self.files={} self.dirs={} def contents_of(self, path): return self.files[path] def exists(self, path): return path in self.files def entries_if_dir_exists(self, path): return self.dirs.get(path, []) def create_fake_file(self, path, contents=''): import os self.files[path] = contents self.create_fake_dir(os.path.dirname(path), os.path.basename(path)) def create_fake_dir(self, dir_path, *dir_entries): self.dirs[dir_path] = dir_entries class TestFakeFileSystem: def setUp(self): self.fs = FakeFileSystem() def test_you_can_read_from_files(self): self.fs.create_fake_file('/path/to/file', "file contents") assert_equals('file contents', self.fs.contents_of('/path/to/file')) def test_when_creating_a_fake_file_it_creates_also_the_dir(self): self.fs.create_fake_file('/dir/file') assert_equals(set(('file',)), set(self.fs.entries_if_dir_exists('/dir'))) def test_you_can_create_multiple_fake_file(self): self.fs.create_fake_file('/path/to/file1', "one") self.fs.create_fake_file('/path/to/file2', "two") assert_equals('one', self.fs.contents_of('/path/to/file1')) assert_equals('two', self.fs.contents_of('/path/to/file2')) def test_no_file_exists_at_beginning(self): assert not self.fs.exists('/filename') def test_after_a_creation_the_file_exists(self): self.fs.create_fake_file('/filename') assert self.fs.exists('/filename') def test_create_fake_dir(self): self.fs.create_fake_dir('/etc', 'passwd', 'shadow', 'hosts') assert_equals(set(['passwd', 'shadow', 'hosts']), set(self.fs.entries_if_dir_exists('/etc'))) trash-cli-0.17.1.14/unit_tests/test_trashdirs_how_to_list_them.py000066400000000000000000000013611303652251000251530ustar00rootroot00000000000000from trashcli.trash import TrashDirs from mock import Mock, call from nose.tools import assert_equals class TestListTrashinfo: def test_howto_list_trashdirs(self): out = Mock() environ = {'HOME':'/home/user'} trashdirs = TrashDirs( environ = environ, getuid = lambda:123, list_volumes = lambda:['/vol', '/vol2'], top_trashdir_rules = Mock(), ) trashdirs.on_trash_dir_found = out trashdirs.list_trashdirs() assert_equals([call('/home/user/.local/share/Trash', '/'), call('/vol/.Trash-123', '/vol'), call('/vol2/.Trash-123', '/vol2')], out.mock_calls) trash-cli-0.17.1.14/unit_tests/test_trashing_a_file.py000066400000000000000000000027571303652251000226510ustar00rootroot00000000000000from trashcli.put import TrashDirectoryForPut from mock import Mock from nose.tools import istest from mock import ANY class TestTrashing: def setUp(self): self.now = Mock() self.fs = Mock() self.trashdir = TrashDirectoryForPut('~/.Trash', '/', self.now, self.fs) self.trashdir.store_relative_paths() path_for_trash_info = Mock() path_for_trash_info.for_file.return_value = 'foo' self.trashdir.path_for_trash_info = path_for_trash_info @istest def the_file_should_be_moved_in_trash_dir(self): self.trashdir.trash('foo') self.fs.move.assert_called_with('foo', '~/.Trash/files/foo') @istest def test_should_create_a_trashinfo(self): self.trashdir.trash('foo') self.fs.atomic_write.assert_called_with('~/.Trash/info/foo.trashinfo', ANY) @istest def trashinfo_should_contains_original_location_and_deletion_date(self): from datetime import datetime self.now.return_value = datetime(2012, 9, 25, 21, 47, 39) self.trashdir.trash('foo') self.fs.atomic_write.assert_called_with(ANY, b'[Trash Info]\n' b'Path=foo\n' b'DeletionDate=2012-09-25T21:47:39\n') @istest def should_rollback_trashinfo_creation_on_problems(self): self.fs.move.side_effect = IOError try: self.trashdir.trash('foo') except IOError: pass self.fs.remove_file.assert_called_with('~/.Trash/info/foo.trashinfo') trash-cli-0.17.1.14/unit_tests/test_volume_of.py000066400000000000000000000015211303652251000215120ustar00rootroot00000000000000from trashcli.fstab import VolumeOf from trashcli.fstab import FakeIsMount from nose.tools import assert_equals, istest import os @istest class TestVolumeOf: def setUp(self): self.ismount = FakeIsMount() self.volume_of = VolumeOf(ismount = self.ismount) self.volume_of.abspath = os.path.normpath @istest def return_the_containing_volume(self): self.ismount.add_mount('/fake-vol') assert_equals('/fake-vol', self.volume_of('/fake-vol/foo')) @istest def with_file_that_are_outside(self): self.ismount.add_mount('/fake-vol') assert_equals('/', self.volume_of('/foo')) @istest def it_work_also_with_relative_mount_point(self): self.ismount.add_mount('relative-fake-vol') assert_equals('relative-fake-vol', self.volume_of('relative-fake-vol/foo')) trash-cli-0.17.1.14/unit_tests/tools.py000066400000000000000000000002131303652251000176150ustar00rootroot00000000000000try: from nose.tools import assert_items_equal except ImportError: from nose.tools import assert_count_equal as assert_items_equal