pax_global_header00006660000000000000000000000064117710770340014521gustar00rootroot0000000000000052 comment=1bd3f30e75ce47f01ed42f0cddb2d7145e073a05 trash-cli-0.12.7/000077500000000000000000000000001177107703400134765ustar00rootroot00000000000000trash-cli-0.12.7/.gitignore000066400000000000000000000003701177107703400154660ustar00rootroot00000000000000*.pyc *.swp *.tmp .coverage .DS_Store .local/ .svn /build/ /command-test/sandbox/ /command-test/test-volume.img /cover/ /dist/ /foo /env/ /nose-*.egg/ /sandbox/ /scripts/ /test-volume.dmg /test-volume/ /topdir/ /trash_cli.egg-info/ /XDG_DATA_HOME/ trash-cli-0.12.7/COPYING000066400000000000000000000431101177107703400145300ustar00rootroot00000000000000 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.12.7/CREDITS.txt000066400000000000000000000025731177107703400153430ustar00rootroot00000000000000Author ------ Andrea Francia andreafrancia@users.sourceforge.net 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.12.7/DONE.txt000066400000000000000000000014161177107703400147660ustar00rootroot00000000000000trash-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.12.7/HISTORY.txt000066400000000000000000000030761177107703400154060ustar00rootroot000000000000000.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.12.7/MANIFEST.in000066400000000000000000000001161177107703400152320ustar00rootroot00000000000000include requirements-dev.txt include ez_setup.py include README.rst graft man trash-cli-0.12.7/README.rst000066400000000000000000000074251177107703400151750ustar00rootroot00000000000000trash-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 trashes files and directories. trash-empty empty the trashcan(s). trash-list list trashed file. restore-trash restore a trashed file. 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:: $ restore-trash 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 before ago:: $ trash-empy 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 Using it as 'rm' alias ---------------------- `trash-put` accept all the options that GNU `rm` does, if you prefer (I don't) you can set up this alias in your .bashrc:: alias rm='trash-put' At the present the semantic of trash-put is sligthly different from the one of `rm`, for example, while `rm` requires `-R` for deleting directories `trash-put` does not. This may change in future. Keep in mind that Bash aliases are used only in interactive shells, so using this alias should not interfere with scripts that expects to use `rm`. Installation (the easy way) --------------------------- Requirements: - Python 2.7 - setuptools (use `apt-get install python-setuptools` on Debian) Installation command:: easy_install trash-cli Installation from sources ------------------------- :: # grab the latest sources wget https://github.com/andreafrancia/trash-cli/tarball/master # unpack and install tar xfz andreafrancia-trash-cli-xxxxxxxx.tar.gz cd andreafrancia-trash-cli-xxxxxxxx sudo python setup.py install Bugs and feedback ----------------- If you discover a bug please report it to: https://github.com/andreafrancia/trash-cli/issues You can reach me via email at me@andreafrancia.it . For twitter use @andreafrancia or #trashcli Development ----------- Environment setup:: virtualenv env --no-site-packages source env/bin/activate pip install -r requirements-dev.txt Running tests:: nosetests unit_tests # unit tests nosetests integration_tests # integration tests nosetests # run all tests 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.12.7/TODO.txt000066400000000000000000000042161177107703400150070ustar00rootroot00000000000000Features backlog: - 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-rm '*.o' - trash-empty should empty even the mac trash directory To be fixed: - invalidate Google Code download and put a redirect to PyPi - get rid of logging package - get rid of the IOError exception raising on file is not in the same volume - consolidate TrashDirectory abstract and the AvailableTrashDir abstraction - get rid of TrashDirectory.info_dir and files_dir properties - remove all the duplication - all_info_files, trashed_files and for_all_trashed_files - restore the stress test for persist trashinfo (not sure) - remove NullReporter if not used - get rid of TrashInfo abstraction - get rid of TimeUtils.parse_iso8601 if not used - rename remove_file in to remove_existing_file - refactor TrashPutCmd for simplicity Test to be ported to nosetests: - trash-put: - trash in home trashcan - print version - trash in volume trashcan 1 - trash in volume trashcan 2 - it should trash to $topdir/.Trash-UID when $topdir/.Trash is not suitable - when $topdir/.Trash does not exists - when $topdir/.Trash is a symlink - when $topdir/.Trash is not sticky - should refuse to create the $topdir/.Trash/$uid directory if the $topdir/.Trash is not sticky Bug 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.12.7/docs/000077500000000000000000000000001177107703400144265ustar00rootroot00000000000000trash-cli-0.12.7/docs/MountListFromHal.wiki000066400000000000000000000010101177107703400205120ustar00rootroot00000000000000About 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.12.7/docs/about-accessing-the-windows-recycle-bin.txt000066400000000000000000000010721177107703400246760ustar00rootroot00000000000000Some 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.12.7/docs/about-listing-osx-trashcan.txt000066400000000000000000000013741177107703400223650ustar00rootroot00000000000000In 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.12.7/docs/about-shredding.txt000066400000000000000000000077721177107703400202630ustar00rootroot00000000000000Introduction ============ 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.12.7/docs/about-the-generic-naming-issue.txt000066400000000000000000000024071177107703400230710ustar00rootroot00000000000000How 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.12.7/docs/back-links.txt000066400000000000000000000036241177107703400172120ustar00rootroot00000000000000List of web pages that cite trash-cli, with the date when I discovered the page, sorted by discovery date (newest before): - 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.12.7/docs/how-to-build-and-upload-a-new-release.txt000066400000000000000000000013671177107703400241550ustar00rootroot00000000000000To build a release ------------------ Update the version number: vim trashcli/trash.py Run all tests: nosetests Test the installation from tarball: python setup.py sdist tarball=trash-cli-0.12.7.tar.gz scp dist/$tarball root@192.168.56.101: ssh root@192.168.56.101 " tar xvfz $tarball cd ${tarball%%.tar.gz} python setup.py install trash-put --help " Test the installation using easy_install: ssh root@192.168.56.101 " apt-get install python-setuptools easy_install "$tarball" trash-put --help " Register and upload: python setup.py register python setup.py sdist upload Now you can tag the repo status: git tag $(python setup.py --version) -EOF trash-cli-0.12.7/docs/proposed-new-interface/000077500000000000000000000000001177107703400210065ustar00rootroot00000000000000trash-cli-0.12.7/docs/proposed-new-interface/trash-cli.txt000066400000000000000000000024041177107703400234350ustar00rootroot00000000000000NAME 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.12.7/docs/proposed-new-interface/trash-empty.txt000066400000000000000000000046651177107703400240370ustar00rootroot00000000000000NAME 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.12.7/docs/proposed-new-interface/trash-list.txt000066400000000000000000000032631177107703400236450ustar00rootroot00000000000000NAME 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.12.7/docs/proposed-new-interface/trash-restore.txt000066400000000000000000000046511177107703400243570ustar00rootroot00000000000000NAME 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.12.7/docs/similar-projects.txt000066400000000000000000000102771177107703400204650ustar00rootroot00000000000000DELSAFE ======= 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: http://www.safe-rm.org.nz/ 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.12.7/docs/some-output-of-df-command-on-different-systems.txt000066400000000000000000000060231177107703400261610ustar00rootroot00000000000000The __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.12.7/docs/tests-to-do-before-releasing.txt000066400000000000000000000014111177107703400225550ustar00rootroot00000000000000# Creating and testing a release package # -------------------------------------- # Create the dist:: python setup.py sdist # Unpack for testing:: cd dist tar xvfz trash-cli-*.tar.gz cd trash-cli-* # Prepare the virtualenv virtualenv testing source testing/bin/activate # prepare the test scripts so they can tested python setup.py develop --script-dir scripts # launch tests with python nose python setup.py test # test the scripts with bashunit command-test/create-test-volume command-test/run-tests scripts command-test/test-volume sudo umount command-test/test-volume # test the creation of .rpm python setup.py bdist_rpm # in case of errors: python setup.py bdist_rpm --install-script=install-rpm.sh echo "All done" trash-cli-0.12.7/docs/trashspec-0.7.html000066400000000000000000000621731177107703400176230ustar00rootroot00000000000000 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.12.7/docs/who-packaged-trash-cli.txt000066400000000000000000000024031177107703400214040ustar00rootroot00000000000000Which 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.12.7/ez_setup.py000066400000000000000000000240551177107703400157140ustar00rootroot00000000000000#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c11" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) trash-cli-0.12.7/install-rpm.sh000077500000000000000000000003601177107703400162760ustar00rootroot00000000000000python 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.12.7/integration_tests/000077500000000000000000000000001177107703400172435ustar00rootroot00000000000000trash-cli-0.12.7/integration_tests/__init__.py000066400000000000000000000000001177107703400213420ustar00rootroot00000000000000trash-cli-0.12.7/integration_tests/assert_equals_with_unidiff.py000066400000000000000000000010671177107703400252330ustar00rootroot00000000000000# 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" + unidiff(expected, actual)) trash-cli-0.12.7/integration_tests/describe_trash_list.py000066400000000000000000000236471177107703400236450ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import os from trashcli.trash import ListCmd from files import (write_file, require_empty_dir, make_sticky_dir, ensure_non_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 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) @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 http://code.google.com/p/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( "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") from nose.tools import assert_raises @istest class describe_trash_list_with_raw_option: def setup(self): self.having_XDG_DATA_HOME('XDG_DATA_HOME') self.running('trash-list', '--raw') @istest def output_should_contains_trashinfo_paths(self): from nose import SkipTest; raise SkipTest() self.having_trashinfo('foo.trashinfo') self.output_should_contain_line( 'XDG_DATA_HOME/Trash/info/foo.trashinfo') @istest def output_should_contains_backup_copy_paths(self): from nose import SkipTest; raise SkipTest() self.having_trashinfo('foo.trashinfo') self.output_should_contain_line( 'XDG_DATA_HOME/Trash/files/foo') def having_XDG_DATA_HOME(self, value): self.XDG_DATA_HOME = value def running(self, *argv): user = TrashListUser( environ = {'XDG_DATA_HOME': self.XDG_DATA_HOME}) user.run(argv) self.output = user.output() def output_should_contain_line(self, line): assert line in self.output_lines() def output_lines(self): return [line.rstrip('\n') for line in self.output.splitlines()] 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): self.run('trash-list') 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, ).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_output(self, expected_value): self.stdout.assert_equal_to(expected_value) def should_read_error(self, expected_value): self.stderr.assert_equal_to(expected_value) def output(self): return self.stdout.getvalue() trash-cli-0.12.7/integration_tests/files.py000066400000000000000000000034111177107703400207160ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals from trashcli.trash 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) file(filename, 'w').write(contents) assert_equals(file(filename).read(), contents) def require_empty_dir(path): if os.path.exists(path): shutil.rmtree(path) make_dirs(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): file(path).read() trash-cli-0.12.7/integration_tests/output_collector.py000066400000000000000000000012121177107703400232170ustar00rootroot00000000000000class OutputCollector: def __init__(self): from StringIO 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): from assert_equals_with_unidiff import assert_equals_with_unidiff assert_equals_with_unidiff(expected, self.stream.getvalue()) def should_match(self, regex): text = self.stream.getvalue() from nose.tools import assert_regexp_matches assert_regexp_matches(text, regex) trash-cli-0.12.7/integration_tests/test_file_descriptions.py000066400000000000000000000026751177107703400243730ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from trashcli.trash import describe, write_file 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')) trash-cli-0.12.7/integration_tests/test_filesystem.py000066400000000000000000000027351177107703400230470ustar00rootroot00000000000000# Copyright (C) 2008-2011 Andrea Francia Trivolzio(PV) Italy from trashcli.trash import has_sticky_bit, mkdirs, FileSystemReader 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.12.7/integration_tests/test_persist.py000066400000000000000000000050551177107703400223520ustar00rootroot00000000000000# Copyright (C) 2008-2011 Andrea Francia Trivolzio(PV) Italy from unittest import TestCase from trashcli.trash import TrashDirectory from trashcli.trash import TrashInfo from integration_tests.files import require_empty_dir from datetime import datetime import os join = os.path.join class TestTrashDirectory_persit_trash_info(TestCase) : def setUp(self): self.trashdirectory_base_dir = os.path.realpath("./sandbox/testTrashDirectory") require_empty_dir(self.trashdirectory_base_dir) self.instance=TrashDirectory(self.trashdirectory_base_dir, "/") def test_persist_trash_info_first_time(self): trash_info=TrashInfo("dummy-path", datetime(2007,01,01)) basename=os.path.basename(trash_info.path) content=trash_info.render() (trash_info_file, trash_info_id)=self.instance.persist_trash_info(basename,content) self.assertEquals('dummy-path', trash_info_id) self.assertEquals(join(self.trashdirectory_base_dir,'info', 'dummy-path.trashinfo'), trash_info_file) self.assertEquals("[Trash Info]\n" "Path=dummy-path\n" "DeletionDate=2007-01-01T00:00:00\n", 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) : trash_info=TrashInfo("dummy-path", datetime(2007,01,01)) basename=os.path.basename(trash_info.path) content=trash_info.render() (trash_info_file, trash_info_id)=self.instance.persist_trash_info(basename,content) self.assertEquals('dummy-path'+"_" + str(i), trash_info_id) self.assertEquals("""[Trash Info] Path=dummy-path DeletionDate=2007-01-01T00:00:00 """, 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=TrashInfo("dummy-path", datetime(2007,01,01)) basename=os.path.basename(trash_info.path) content=trash_info.render() (trash_info_file, trash_info_id)=self.instance.persist_trash_info(basename,content) self.assertTrue(trash_info_id.startswith("dummy-path_")) self.assertEquals("""[Trash Info] Path=dummy-path DeletionDate=2007-01-01T00:00:00 """, read(trash_info_file)) def read(path): return file(path).read() trash-cli-0.12.7/integration_tests/test_restore_trash.py000066400000000000000000000066031177107703400235450ustar00rootroot00000000000000import os from nose.tools import istest from trashcli.trash import RestoreCmd from .files import require_empty_dir 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_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') file('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.make_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)) if os.path.exists(filename): os.remove(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 make_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.12.7/integration_tests/test_trash_empty.py000066400000000000000000000210521177107703400232130ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import (assert_equals, assert_items_equal, istest) from trashcli.trash import EmptyCmd from StringIO 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 @istest class describe_trash_empty: 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, now = now ) def user_run_trash_empty(self): self.empty_cmd.run('trash-empty') @istest def 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) @istest def 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) @istest def 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) @istest def 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') @istest def 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) @istest def it_should_purge_also_directories(self): os.makedirs("XDG_DATA_HOME/Trash/files/a-dir") self.user_run_trash_empty() 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 describe_trash_empty_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, now = self.now ) 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 = {}, getuid = lambda: 123, list_volumes = lambda: ['topdir'],) 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 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 = {},) 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 http://code.google.com/p/trash-cli/issues """)) class TestTrashEmpty_on_version(): def test_it_print_version(self): err, out = StringIO(), StringIO() cmd = EmptyCmd(err = err, out = out, environ = {}, 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 = {}) 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' """)) trash-cli-0.12.7/integration_tests/test_trash_put.py000066400000000000000000000072741177107703400226770ustar00rootroot00000000000000# Copyright (C) 2009-2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import istest from .files import having_file, require_empty_dir, having_empty_dir from trashcli.trash import TrashPutCmd @istest class describe_trash_put_command_when_deleting_a_file: @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('') def a_trashinfo_file_should_have_been_created(self): file('sandbox/XDG_DATA_HOME/Trash/info/foo.trashinfo').read() def setUp(self): require_empty_dir('sandbox') having_file('sandbox/foo') self.run_trashput = TrashPutRunner( environ = {'XDG_DATA_HOME': 'sandbox/XDG_DATA_HOME' } ) self.stderr_should_be = self.run_trashput.err.should_be self.output_should_be = self.run_trashput.out.should_be self.run_trashput('trash-put', 'sandbox/foo') import os exists = os.path.exists @istest class describe_trash_put_command_on_dot_arguments: def test_dot_argument_is_skipped(self): having_file('other_argument') 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): having_file('other_argument') 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): having_empty_dir('sandbox/') having_file('other_argument') 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): having_empty_dir('sandbox') having_file('other_argument') 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 setUp(self): self.run_trashput = TrashPutRunner() self.stderr_should_be = self.run_trashput.err.should_be class TrashPutRunner: def __init__(self, environ = os.environ): from .output_collector import OutputCollector self.out = OutputCollector() self.err = OutputCollector() self.environ = environ def __call__(self, *argv): TrashPutCmd( stdout = self.out, stderr = self.err, environ = self.environ ).run(list(argv)) def file_should_have_been_deleted(path): import os assert not os.path.exists('sandbox/foo') trash-cli-0.12.7/integration_tests/trashinfo.py000066400000000000000000000013101177107703400216050ustar00rootroot00000000000000def 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) trash-cli-0.12.7/man/000077500000000000000000000000001177107703400142515ustar00rootroot00000000000000trash-cli-0.12.7/man/man1/000077500000000000000000000000001177107703400151055ustar00rootroot00000000000000trash-cli-0.12.7/man/man1/restore-trash.1000066400000000000000000000040521177107703400177720ustar00rootroot00000000000000.\" 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 "RESTORE-TRASH" "1" .SH "NAME" restore-trash \- Restore for Command line trash utility. .SH "SYNOPSIS" .B restore-trash .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 $ restore-trash 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 http://code.google.com/p/trash-cli/issues .SH "AUTHORS" Trash was written by Andrea Francia and Einar Orn Olason . This manual page was written by Steve Stalcup . Changes made by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-list(1), trash-empty(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.12.7/man/man1/trash-empty.1000066400000000000000000000045701177107703400174520ustar00rootroot00000000000000.\" 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 'emtpy-trash'. .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 http://code.google.com/p/trash-cli/issues .SH "AUTHORS" Trash was written by Andrea Francia and Einar Orn Olason . This manual page was written by Steve Stalcup . Changes made by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-restore(1), trash-list(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.12.7/man/man1/trash-list.1000066400000000000000000000035351177107703400172670ustar00rootroot00000000000000.\" 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 http://code.google.com/p/trash-cli/issues .SH "AUTHORS" Trash was written by Andrea Francia and Einar Orn Olason . This manual page was written by Steve Stalcup . Changes made by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-restore(1), trash-empty(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.12.7/man/man1/trash-put.1000066400000000000000000000034721177107703400171240ustar00rootroot00000000000000.\" 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" "1" .SH "NAME" trash \- Command line trash utility. .SH "SYNOPSIS" .B trash .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 directory to move in the trashcan. .SH "EXAMPLES" .nf $ cd /home/andrea/ $ touch foo bar $ trash foo bar .fi .SH "BUGS" Report bugs to http://code.google.com/p/trash-cli/issues .SH "AUTHORS" Trash was written by Andrea Francia and Einar Orn Olason . This manual page was written by Steve Stalcup . Changes made by Massimo Cavalleri . .SH "SEE ALSO" trash-list(1), trash-restore(1), trash-empty(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.12.7/requirements-dev.txt000066400000000000000000000000301177107703400175270ustar00rootroot00000000000000nose==1.1.2 mock==0.8.0 trash-cli-0.12.7/setup.cfg000077500000000000000000000004451177107703400153250ustar00rootroot00000000000000[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.12.7/setup.py000066400000000000000000000023651177107703400152160ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy import ez_setup; ez_setup.use_setuptools() from setuptools import setup import sys sys.path.append('.') from trashcli import trash setup( name = 'trash-cli', version = trash.version, author = 'Andrea Francia', author_email = 'me@andreafrancia.it', url = 'https://github.com/andreafrancia/trash-cli', description = 'Command line interface to FreeDesktop.org Trash.', license = 'GPL v2', long_description = file("README.rst").read(), packages = ['trashcli', 'integration_tests', 'unit_tests'], test_suite = "nose.collector", entry_points = { 'console_scripts' : [ 'trash-list = trashcli.cmds:list', 'trash = trashcli.cmds:put', 'trash-put = trashcli.cmds:put', 'restore-trash = trashcli.cmds:restore', 'trash-empty = trashcli.cmds:empty' ] }, data_files = [('share/man/man1', ['man/man1/trash-empty.1', 'man/man1/trash-list.1', 'man/man1/restore-trash.1', 'man/man1/trash-put.1'])], tests_require = file("requirements-dev.txt").readlines(), ) trash-cli-0.12.7/trashcli/000077500000000000000000000000001177107703400153075ustar00rootroot00000000000000trash-cli-0.12.7/trashcli/__init__.py000066400000000000000000000000501177107703400174130ustar00rootroot00000000000000from __future__ import absolute_import trash-cli-0.12.7/trashcli/cmds.py000066400000000000000000000014611177107703400166110ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy import sys,os def put(): from trashcli.trash import TrashPutCmd TrashPutCmd( sys.stdout, sys.stderr ).run(sys.argv) def restore(): from trashcli.trash import RestoreCmd RestoreCmd( stdout = sys.stdout, stderr = sys.stderr, environ = os.environ, exit = sys.exit, input = raw_input ).run() def empty(): from trashcli.trash import EmptyCmd return EmptyCmd( out=sys.stdout, err=sys.stderr, environ=os.environ, ).run(*sys.argv) def list(): from trashcli.trash import ListCmd ListCmd( out = sys.stdout, err = sys.stderr, environ = os.environ, getuid = os.getuid ).run(*sys.argv) trash-cli-0.12.7/trashcli/list_mount_points.py000066400000000000000000000047531177107703400214630ustar00rootroot00000000000000# 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('\n') skip_header() for line in df_output: line = chomp(line) yield line.split(None, 5)[-1] 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.fopen.restype = 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.12.7/trashcli/trash.py000066400000000000000000001264601177107703400170130ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import version='0.12.7' import os import logging logger=logging.getLogger('trashcli.trash') logger.setLevel(logging.WARNING) logger.addHandler(logging.StreamHandler()) class TrashDirectory: def __init__(self, path, volume) : # TODO: contents_of should be injected self.path = os.path.normpath(path) self.volume = volume def __str__(self) : return str(self.path) def trash(self, path): from datetime import datetime path = os.path.normpath(path) self.check() if not self.volume == volume_of(parent_of(path)) : raise IOError("file is not in the same volume of trash directory!\n" + "self.volume = " + str(self.volume) + ", \n" + "file.parent.volume = " + str(volume_of(parent_of(path)))) trash_info = TrashInfo(self._path_for_trashinfo(path), datetime.now()) basename = os.path.basename(trash_info.path) trashinfo_file_content = trash_info.render() (trash_info_file, trash_info_id) = self.persist_trash_info(basename, trashinfo_file_content) trashed_file = self._create_trashed_file(trash_info_id, os.path.abspath(path), trash_info.deletion_date) self.ensure_files_dir_exists() try : move(path, trashed_file.actual_path) except IOError as e : remove_file(trash_info_file) raise e return trashed_file def ensure_files_dir_exists(self): if not os.path.exists(self.files_dir) : mkdirs_using_mode(self.files_dir, 0700) @property def info_dir(self): """ The $trash_dir/info dir that contains the .trashinfo files as filesystem.Path. """ result = os.path.join(self.path, 'info') return result @property def files_dir(self): """ The directory where original file where stored. A Path instance. """ result = os.path.join(self.path, 'files') return result 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') : logger.warning("Non .trashinfo file in info dir") else : yield info_file except OSError: # when directory does not exist pass def trashed_files(self) : for info_file in self.all_info_files(): try: yield self._create_trashed_file_from_info_file(info_file) except ValueError: logger.warning("Non parsable trashinfo file: %s" % info_file) except IOError as e: logger.warning(str(e)) def _create_trashed_file_from_info_file(self, info_file): trash_id = self.calc_id(info_file) trash_info2 = LazyTrashInfoParser(lambda:contents_of(info_file), self.volume) path = trash_info2.original_location() deletion_date = trash_info2.deletion_date() trashed_file = self._create_trashed_file( trash_id, path, deletion_date) return trashed_file def _create_trashed_file(self, trash_id, path, deletion_date): actual_path = self._calc_path_for_actual_file(trash_id) info_file = self._calc_path_for_info_file(trash_id) return TrashedFile(path, deletion_date, info_file, actual_path, self) def _calc_original_location(self, path): if os.path.isabs(path) : return path else : return os.path.join(self.volume, path) @staticmethod def calc_id(trash_info_file): return os.path.basename(trash_info_file)[:-len('.trashinfo')] def _calc_path_for_actual_file(self, trash_id) : return os.path.join(self.files_dir, trash_id) def _calc_path_for_info_file(self, trash_id) : return os.path.join(self.info_dir, '%s.trashinfo' % trash_id) def _path_for_trashinfo(self, fileToTrash): raise NotImplementedError() """ Create a .trashinfo file in the $trash/info directory. returns the created TrashInfoFile. """ def persist_trash_info(self,basename,content) : mkdirs_using_mode(self.info_dir, 0700) os.chmod(self.info_dir,0700) # 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(self.info_dir, trash_info_basename) try : handle = os.open(dest, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0600) os.write(handle, content) os.close(handle) logger.debug(".trashinfo created as %s." % dest) return (dest, trash_id) except OSError: logger.debug("Attempt for creating %s failed." % dest) index += 1 raise IOError() def check(self): """ Perform a sanity check of this trash directory. If the check is not passed the directory can be used for trashing, listing or restoring files. """ pass class HomeTrashDirectory(TrashDirectory): def __init__(self, path) : TrashDirectory.__init__(self, path, volume_of(path)) def __str__(self): import re import posixpath result=TrashDirectory.__str__(self) try: home_dir=os.environ['HOME'] home_dir = posixpath.normpath(home_dir) if home_dir != '': result=re.sub('^'+ re.escape(home_dir)+os.path.sep, '~' + os.path.sep,result) except KeyError: pass return result def _path_for_trashinfo(self, fileToBeTrashed) : fileToBeTrashed = os.path.normpath(fileToBeTrashed) # for the HomeTrashDirectory all path are stored as absolute realpath = os.path.realpath(fileToBeTrashed) parent = os.path.dirname(realpath) basename = os.path.basename(fileToBeTrashed) result = os.path.join(parent,basename) return result class VolumeTrashDirectory(TrashDirectory) : def __init__(self, path, volume) : TrashDirectory.__init__(self,path, volume) def _path_for_trashinfo(self, fileToBeTrashed) : # for the VolumeTrashDirectory paths are stored as relative # if possible fileToBeTrashed = os.path.normpath(fileToBeTrashed) # string representing the parent of the fileToBeTrashed parent = os.path.dirname(fileToBeTrashed) parent = os.path.realpath(parent) topdir=self.volume # e.g. /mnt/disk-1 if parent.startswith(topdir+os.path.sep) : parent = parent[len(topdir+os.path.sep):] result = os.path.join(parent, os.path.basename(fileToBeTrashed)) return result class TopDirWithoutStickyBit(IOError): """ Raised when $topdir/.Trash doesn't have the sticky bit. """ pass class TopDirNotPresent(IOError): """ Raised when $topdir/.Trash is not a dir. """ pass class TopDirIsSymLink(IOError): """ Raised when $topdir/.Trash is a simbolic link. """ pass class Method1VolumeTrashDirectory(VolumeTrashDirectory): def __init__(self, path, volume) : VolumeTrashDirectory.__init__(self,path,volume) def check(self): if not self.parent_is_dir(): raise TopDirNotPresent("topdir should be a directory: %s" % self.path) if self.parent_is_link(): raise TopDirIsSymLink("topdir can't be a symbolic link: %s" % self.path) if not self.parent_has_sticky_bit(): raise TopDirWithoutStickyBit("topdir should have the sticky bit: %s" % self.path) def parent_is_dir(self): return os.path.isdir(self.parent()) def parent_is_link(self): return os.path.islink(self.parent()) def parent(self): return os.path.dirname(self.path) def parent_has_sticky_bit(self): return has_sticky_bit(self.parent()) def real_list_mount_points(): from trashcli.list_mount_points import mount_points for mount_point in mount_points(): yield mount_point class GlobalTrashCan: """ Represent the TrashCan that contains all trashed files. This class is the facade used by all trashcli commands """ class NullReporter: def __getattr__(self,name): return lambda *argl,**args:None from datetime import datetime def __init__(self, environ = os.environ, reporter = NullReporter(), getuid = os.getuid, list_mount_points = real_list_mount_points, now = datetime.now): self.getuid = getuid self.environ = environ self.reporter = reporter self.list_mount_points = list_mount_points self.now = now def trashed_files(self): """Return a generator of all TrashedFile(s).""" for trash_dir in self._trash_directories(): for trashedfile in trash_dir.trashed_files(): yield trashedfile 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 for trash_dir in self._possible_trash_directories_for(file): if self.file_could_be_trashed_in(file, trash_dir.path): try: trashed_file = trash_dir.trash(file) self.reporter.file_has_been_trashed_in_as( file, trashed_file.trash_directory, trashed_file.original_file) return except (IOError, OSError), error: self.reporter.unable_to_trash_file_in_because(file, trash_dir, error) self.reporter.unable_to_trash_file(file) def should_skipped_by_specs(self, file): basename = os.path.basename(file) return (basename == ".") or (basename == "..") def volume_of_parent(self, file): return volume_of(parent_of(file)) def file_could_be_trashed_in(self,file_to_be_trashed,trash_dir_path): return volume_of(trash_dir_path) == self.volume_of_parent(file_to_be_trashed) def _trash_directories(self) : """Return a generator of all TrashDirectories in the filesystem""" yield self._home_trash_dir() for mount_point in self.list_mount_points(): volume = mount_point yield self._volume_trash_dir1(volume) yield self._volume_trash_dir2(volume) def _home_trash_dir(self) : return HomeTrashDirectory(self._home_trash_dir_path()) def _possible_trash_directories_for(self,file): yield self._home_trash_dir() for td in self.trash_directories_for_volume(self.volume_of_parent(file)): yield td def trash_directories_for_volume(self,volume): yield self._volume_trash_dir1(volume) yield self._volume_trash_dir2(volume) def _volume_trash_dir1(self,volume): """ Return the method (1) volume trash dir ($topdir/.Trash/$uid). """ uid = self.getuid() trash_directory_path = os.path.join(volume, '.Trash', str(uid)) return Method1VolumeTrashDirectory(trash_directory_path,volume) def _volume_trash_dir2(self, volume) : """ Return the method (2) volume trash dir ($topdir/.Trash-$uid). """ uid = self.getuid() dirname=".Trash-%s" % str(uid) trash_directory_path = os.path.join(volume, dirname) return VolumeTrashDirectory(trash_directory_path,volume) def _home_trash_dir_path(self): result = [] home_trashcan_if_possible(self.environ, result.append) return result[0] def for_all_trashed_file(self, action): for trashedfile in self.trashed_files(): action( info_path = trashedfile.original_file, path = trashedfile.info_file) 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) - actual_path : the path where the trashed file has been placed after the trash opeartion (instance of Path) - trash_directory : """ def __init__(self, path, deletion_date, info_file, actual_path, trash_directory) : if not os.path.isabs(path): raise ValueError("Absolute path required.") self._path = path self._deletion_date = deletion_date self._info_file = info_file self._actual_path = actual_path self._trash_directory = trash_directory @property def path(self) : """ The path from where the file has been trashed """ return self._path @property def actual_path(self): return self._actual_path original_file = actual_path @property def deletion_date(self) : return self._deletion_date def restore(self, dest=None) : if dest is not None: raise NotImplementedError("not yet supported") if os.path.exists(self.path): raise IOError('Refusing to overwrite existing file "%s".' % os.path.basename(self.path)) else: parent = os.path.dirname(self.path) mkdirs(parent) move(self.original_file, self.path) remove_file(self.info_file) @property def info_file(self): return self._info_file @property def trash_directory(self) : return self._trash_directory class TrashInfo: def __init__(self, path, deletion_date): from datetime import datetime """Create a TrashInfo. Keyword arguments: path -- the of the .trashinfo file (string or Path) deletion_date -- the date of deletion, should be a datetime. """ if not isinstance(deletion_date, datetime): raise TypeError("deletion_date should be a datetime") self._path = path self._deletion_date = deletion_date @staticmethod def _format_date(deletion_date): return deletion_date.strftime("%Y-%m-%dT%H:%M:%S") @property def deletion_date(self): return self._deletion_date @property def path(self): return self._path @staticmethod def parse(data): path = parse_path(data) deletion_date = parse_deletion_date(data) return TrashInfo(path, deletion_date) def render(self) : import urllib result = "[Trash Info]\n" result += "Path=" + urllib.quote(self.path,'/') + "\n" result += "DeletionDate=" + self._format_date(self.deletion_date) + "\n" return result class TimeUtils: @staticmethod def parse_iso8601(text) : import time from datetime import datetime t=time.strptime(text, "%Y-%m-%dT%H:%M:%S") return datetime(t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) import shutil def remove_file(path): if(os.path.exists(path)): try: os.remove(path) except: return shutil.rmtree(path) def getcwd_as_realpath(): return os.path.realpath(os.curdir) class RestoreCmd: def __init__(self, stdout, stderr, environ, exit, input, curdir = getcwd_as_realpath): self.out = stdout self.err = stderr self.environ = environ self.exit = exit self.input = input self.trashcan = GlobalTrashCan( environ = self.environ) self.curdir = curdir def run(self): trashed_files = [] self.for_all_trashed_file_in_dir(trashed_files.append, self.curdir()) 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.path)) index=self.input("What file to restore [0..%d]: " % (len(trashed_files)-1)) if index == "" : self.println("Exiting") else : index = int(index) try: trashed_files[index].restore() except IOError as e: self.printerr(e) self.exit(1) def for_all_trashed_file_in_dir(self, action, dir): def is_trashed_from_curdir(trashedfile): return trashedfile.path.startswith(dir + os.path.sep) for trashedfile in filter(is_trashed_from_curdir, self.trashcan.trashed_files()) : action(trashedfile) 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 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 TrashPutCmd: def __init__(self, stdout, stderr, environ = os.environ): self.stdout = stdout self.stderr = stderr self.environ = environ def run(self, argv): parser = self.get_option_parser(os.path.basename(argv[0])) (options, args) = parser.parse_args(argv[1:]) if len(args) <= 0: parser.error("Please specify the files to trash.") reporter=TrashPutReporter(self.get_logger(options.verbose,argv[0])) self.trashcan = GlobalTrashCan( reporter = reporter, environ = self.environ) self.trash_all(args) def trash_all(self, args): for arg in args : self.trash(arg) def trash(self, arg): self.trashcan.trash(arg) 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= """To remove a file whose name starts with a `-', for example `-foo', use one of these commands: trash -- -foo trash ./-foo Report bugs to http://code.google.com/p/trash-cli/issues""") 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") def patched_print_help(): encoding = parser._get_encoding(self.stdout) self.stdout.write(parser.format_help().encode(encoding, "replace")) 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 def get_logger(self,verbose,argv0): import os.path class MyLogger: def __init__(self, stderr): self.program_name = os.path.basename(argv0) self.stderr=stderr def info(self,message): if 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)) return MyLogger(self.stderr) class TrashPutReporter: def __init__(self, logger): self.logger = logger 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)) def file_has_been_trashed_in_as(self, trashee, trash_directory, destination): self.logger.info("`%s' trashed in %s " % (trashee, trash_directory)) 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, trash_directory, error)) def mkdirs_using_mode(path, mode): if os.path.isdir(path): os.chmod(path, mode) return os.makedirs(path, mode) def list_files_in_dir(path): for entry in os.listdir(path): result = os.path.join(path, entry) yield result def move(path, dest) : import shutil return shutil.move(path, str(dest)) def mkdirs(path): if os.path.isdir(path): return os.makedirs(path) import os def parent_of(path): return os.path.dirname(path) 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' def volume_of(path) : path = os.path.realpath(path) while path != os.path.dirname(path): if os.path.ismount(path): break path = os.path.dirname(path) return path def write_file(path, contents): f = open(path, 'w') f.write(contents) f.close() def do_nothing(*argv, **argvk): pass class FileSystemReader: def entries_if_dir_exists(self, path): if os.path.exists(path): for entry in os.listdir(path): yield entry def is_sticky_dir(self, path): import os return os.path.isdir(path) and has_sticky_bit(path) def list_volumes(self): return mount_points() def exists(self, path): return os.path.exists(path) def is_symlink(self, path): return os.path.islink(path) def contents_of(self, path): return file(path).read() def contents_of(path): # TODO remove return FileSystemReader().contents_of(path) 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) from .list_mount_points import mount_points from datetime import datetime class ListCmd: def __init__(self, out, err, environ, getuid = os.getuid, list_volumes = mount_points, file_reader = FileSystemReader(), version = version): self.output = self.Output(out, err) self.err = self.output.err self.file_reader = file_reader self.contents_of = file_reader.contents_of self.trashdirs = AvailableTrashDirs(environ, getuid, fs = file_reader) 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): self.trashdirs.list_trashdirs(self.list_contents, self.output) def list_contents(self, trash_dir, volume_path): self.output.set_volume_path(volume_path) trashdir = TrashDir(self.file_reader, trash_dir, volume_path) class Log: def print_trashinfo(_, 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) log = Log() trashdir.each_trashinfo(log.print_trashinfo) 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 Output: 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)) 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, e: invalid_option = e.opt self._on_invalid_option(program_name, invalid_option) else: for option, value in options: if option in self.actions: self.actions[option](program_name) 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) 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 # Error codes (from os on *nix, hard coded for Windows): EX_USAGE = getattr(os, 'EX_USAGE', 64) EX_OK = getattr(os, 'EX_OK' , 0) class EmptyCmd(): def __init__(self, out, err, environ, now = datetime.now, file_reader = FileSystemReader(), list_volumes = mount_points, getuid = os.getuid, file_remover = _FileRemover(), version = version): self.out = out self.err = err self.file_reader = file_reader self.contents_of = file_reader.contents_of class Fs: #TODO remove the need of this class def __init__(self): self.list_volumes = list_volumes self.is_sticky_dir = file_reader.is_sticky_dir self.exists = file_reader.exists self.is_symlink = file_reader.is_symlink self.trashdirs = AvailableTrashDirs(environ, getuid, fs = Fs()) self.now = now self.file_remover = file_remover self.version = version def run(self, *argv): self._maybe_delete = self._delete_both 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_deletion_date_criteria) parse.as_default(self._empty_all_trashdirs) parse.on_invalid_option(self.report_invalid_option_usage) parse(argv) return self.exit_code def report_invalid_option_usage(self, program_name, option): self.err.write( "{program_name}: invalid option -- '{option}'\n".format(**locals())) self.exit_code |= EX_USAGE def set_deletion_date_criteria(self, arg): self.max_age_in_days = int(arg) self._maybe_delete = self._delete_according_date 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 is_int(self, text): try: int(text) return True except ValueError: return False def _empty_all_trashdirs(self): self.trashdirs.list_trashdirs(self._empty_trashdir) def _empty_trashdir(self, trash_dir, volume_path): trashdir = TrashDir(self.file_reader, trash_dir, volume_path) trashdir.each_trashinfo(self._maybe_delete) trashdir.each_orphan(self.remove_file) def _delete_according_date(self, trashinfo_path): contents = self.file_reader.contents_of(trashinfo_path) ParseTrashInfo( on_deletion_date=IfDate( OlderThan(self.max_age_in_days, self.now), lambda: self._delete_both(trashinfo_path) ), )(contents) def remove_file(self, path): self.file_remover.remove_file(path) def _delete_both(self, trashinfo_path): backup_copy = path_of_backup_copy(trashinfo_path) self.file_remover.remove_file_if_exists(backup_copy) self.file_remover.remove_file(trashinfo_path) def println(self, line): self.out.write(line + '\n') 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 def path_of_backup_copy(path_to_trashinfo): from os.path import dirname as parent_of, join, basename trash_dir = parent_of(parent_of(path_to_trashinfo)) return join(trash_dir, 'files', basename(path_to_trashinfo)[:-len('.trashinfo')]) class EachTrashInfo: def __init__(self, list_dir, on_trashinfo): self.list_dir = list_dir self.on_trashinfo = on_trashinfo def trashdir(self, path): import os info_dir = os.path.join(path, 'info') for entry in self.list_dir(path): if entry.endswith('.trashinfo'): self.on_trashinfo(os.path.join(info_dir, entry)) 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 http://code.google.com/p/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 AvailableTrashDirs: def __init__(self, environ, getuid, fs=None): self.environ = environ self.getuid = getuid self.fs = fs self.list_volumes = fs.list_volumes self.is_sticky_dir = fs.is_sticky_dir self.exists = fs.exists def do_nothing(trash_dir, volume): pass class NullLog: def top_trashdir_skipped_because_parent_not_sticky(self, trashdir): pass def top_trashdir_skipped_because_parent_is_symlink(self, trashdir): pass def list_trashdirs(self, out = do_nothing, error_log = NullLog()): self._for_home_trashcan(out) self._for_each_volume_trashcan(out, error_log) def _for_home_trashcan(self, out): def return_result_with_volume(trashcan_path): out(trashcan_path, '/') home_trashcan_if_possible(self.environ, return_result_with_volume) def _for_each_volume_trashcan(self, action, error_log): from os.path import join for volume in self.list_volumes(): parent_trashdir = join(volume, '.Trash') top_trashdir = join(parent_trashdir, str(self.getuid())) alt_top_trashdir = join(volume, '.Trash-%s' % self.getuid()) if self.exists(top_trashdir): if self.is_sticky_dir(parent_trashdir): if not self.fs.is_symlink(parent_trashdir): action(top_trashdir, volume) else: error_log.top_trashdir_skipped_because_parent_is_symlink(top_trashdir) else: error_log.top_trashdir_skipped_because_parent_not_sticky(top_trashdir) action(alt_top_trashdir, volume) def home_trashcan_if_possible(environ, out): if 'XDG_DATA_HOME' in environ: out('%(XDG_DATA_HOME)s/Trash' % environ) elif 'HOME' in environ: out('%(HOME)s/.local/share/Trash' % environ) 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, path, volume_path): self.trash_dir_path = path self.file_reader = file_reader 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 _trashinfos(self): for entry in self._trashinfo_entries(): yield self._trashinfo(entry) def _trashinfo(self, entry): class TrashInfo: def __init__(self, info_dir, files_dir, entry, file_reader, volume_path): self.info_dir = info_dir self.files_dir = files_dir self.entry = entry def path_to_backup_copy(self): entry = self.entry[:-len('.trashinfo')] return os.path.join(self.files_dir, entry) def path_to_trashinfo(self): return os.path.join(self.info_dir, self.entry) return TrashInfo(self._info_dir(), self._files_dir(), entry, self.file_reader, self.volume_path) 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): for entry in self._entries_if_dir_exists(self._info_dir()): if entry.endswith('.trashinfo'): yield entry class ParseError(ValueError): pass class LazyTrashInfoParser: 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 _path(self): return parse_path(self.contents()) def original_location(self): return os.path.join(self.volume_path, self._path()) 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 maybe_date(parsing_closure): try: date = parsing_closure() except ValueError: return unknown_date() else: if date: return date return unknown_date() def unknown_date(): return '????-??-?? ??:??:??' 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 import urllib 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=urllib.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): import urllib for line in contents.split('\n'): if line.startswith('Path='): return urllib.unquote(line[len('Path='):]) raise ParseError('Unable to parse 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 trash-cli-0.12.7/unit_tests/000077500000000000000000000000001177107703400156775ustar00rootroot00000000000000trash-cli-0.12.7/unit_tests/test_available_trash_dirs.py000066400000000000000000000103261177107703400234540ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from trashcli.trash import AvailableTrashDirs from nose.tools import istest, assert_in, assert_not_in @istest class Describe_AvailableInfoDirs_on_volume_trashcans: @istest def the_method_2_is_always_in(self): self.having_uid(123) self.having_volumes('/usb') assert_in('/usb/.Trash-123', self.trashdirs()) @istest def the_method_1_is_in_if_it_is_a_sticky_dir(self): self.having_uid(123) self.having_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.having_uid(123) self.having_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.having_XDG_DATA_HOME('~/.local/share') assert_in('~/.local/share/Trash', self.trashdirs()) def trashdirs(self): result = collector() class FileReader: def list_volumes(_): return self.volumes def is_sticky_dir(_, path): return self.Trash_dir_is_sticky def exists(_, path): return True def is_symlink(_, path): return False AvailableTrashDirs( environ=self.environ, getuid=lambda:self.uid, fs = FileReader(), ).list_trashdirs(result) return result.trash_dirs def setUp(self): self.uid = -1 self.volumes = () self.Trash_dir_is_sticky = not_important_for_now() self.environ = {} def having_uid(self, uid): self.uid = uid def having_volumes(self, *volumes): self.volumes = volumes 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 having_XDG_DATA_HOME(self, XDG_DATA_HOME): self.environ['XDG_DATA_HOME'] = XDG_DATA_HOME def not_important_for_now(): None class collector: def __init__(self): self.trash_dirs = [] def __call__(self, trash_dir, volume): self.trash_dirs.append(trash_dir) from nose.tools import assert_equals from mock import MagicMock @istest class Describe_AvailableTrashDirs_when_parent_is_unsticky: def setUp(self): self.error_log = MagicMock() self.fs = MagicMock() self.dirs = AvailableTrashDirs(environ = {}, getuid = lambda:123, fs = self.fs) self.fs.list_volumes.return_value = ['/topdir'] 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(error_log = self.error_log) (self.error_log.top_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(error_log = self.error_log) assert_equals([], self.error_log. top_trashdir_skipped_because_parent_not_sticky.mock_calls) @istest class Describe_AvailableTrashDirs_when_parent_is_symlink: def setUp(self): self.error_log = MagicMock() self.fs = MagicMock() self.dirs = AvailableTrashDirs(environ = {}, getuid = lambda:123, fs = self.fs) self.fs.list_volumes.return_value = ['/topdir'] self.fs.exists.side_effect = (lambda path: {'/topdir/.Trash/123':True}[path]) 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(error_log = self.error_log) (self.error_log.top_trashdir_skipped_because_parent_is_symlink. assert_called_with('/topdir/.Trash/123')) trash-cli-0.12.7/unit_tests/test_characterization.py000066400000000000000000000005661177107703400226510ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import istest from trashcli.trash import GlobalTrashCan @istest class GlobalTrashCanTest: def test_home_dir_path(self): a=GlobalTrashCan(environ = {'XDG_DATA_HOME': './XDG_DATA_HOME'}) home_trash = a._home_trash_dir_path() assert './XDG_DATA_HOME/Trash' == home_trash trash-cli-0.12.7/unit_tests/test_joining_paths.py000066400000000000000000000010461177107703400221450ustar00rootroot00000000000000# 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.12.7/unit_tests/test_list_mount_points.py000066400000000000000000000031151177107703400231010ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import unittest import StringIO from trashcli.list_mount_points import _mount_points_from_df_output class MountPointFromDirTest(unittest.TestCase): def test_should_skip_the_first_line(self): mount_points = _mount_points_from_df_output(StringIO.StringIO( '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(StringIO.StringIO( '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(StringIO.StringIO( '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(StringIO.StringIO( '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.12.7/unit_tests/test_parser.py000066400000000000000000000012671177107703400206120ustar00rootroot00000000000000from trashcli.trash import Parser from mock import MagicMock from nose.tools import istest @istest class describe_Parser(): @istest def it_calls_the_actions_passing_the_program_name(self): on_raw = MagicMock() parser = Parser() parser.add_option('raw', on_raw) parser(['trash-list', '--raw']) on_raw.assert_called_with('trash-list') @istest def how_getopt_works_with_an_invalid_option(self): invalid_option_callback = MagicMock() parser = Parser() parser.on_invalid_option(invalid_option_callback) parser(['command-name', '-x']) invalid_option_callback.assert_called_with('command-name', 'x') trash-cli-0.12.7/unit_tests/test_parsing_trashinfo_contents.py000066400000000000000000000064641177107703400247570ustar00rootroot00000000000000# 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.trash import LazyTrashInfoParser, ParseError class TestParsing: def test_1(self): parser = LazyTrashInfoParser(lambda:("[Trash Info]\n" "Path=/foo.txt\n"), volume_path = '/') assert_equals('/foo.txt', parser.original_location()) class TestLazyTrashInfoParser_with_empty_trashinfo: def setUp(self): self.parser = LazyTrashInfoParser(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.12.7/unit_tests/test_trash.py000066400000000000000000000235271177107703400204420ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import __author__="Andrea Francia (andrea.francia@users.sourceforge.net)" __copyright__="Copyright (c) 2007 Andrea Francia" __license__="GPL" from trashcli.trash import TrashDirectory from trashcli.trash import TrashedFile from trashcli.trash import TrashInfo from trashcli.trash import VolumeTrashDirectory from trashcli.trash import TimeUtils from trashcli.trash import HomeTrashDirectory from trashcli.trash import mkdirs, volume_of from trashcli.trash import TopDirIsSymLink from trashcli.trash import TopDirNotPresent from trashcli.trash import TopDirWithoutStickyBit from trashcli.trash import Method1VolumeTrashDirectory from integration_tests.files import require_empty_dir from datetime import datetime import os from unittest import TestCase from nose.tools import raises from nose.tools import assert_equals abspath = os.path.abspath import shutil class TestTrashDirectory(TestCase) : def test_trash(self) : #instance instance=VolumeTrashDirectory( "sandbox/trash-directory", volume_of("sandbox")) # test file_to_trash="sandbox/dummy.txt" touch(file_to_trash) result = instance.trash(file_to_trash) self.assertTrue(isinstance(result,TrashedFile)) self.assertEquals(abspath(file_to_trash), result.path) self.assertTrue(result.deletion_date is not None) def test_get_info_dir(self): instance=TrashDirectory( "/mnt/disk/.Trash-123", volume_of("/mnt/disk")) self.assertEquals("/mnt/disk/.Trash-123/info", instance.info_dir) def test_get_files_dir(self): instance=TrashDirectory( "/mnt/disk/.Trash-123", volume_of("/mnt/disk")) self.assertEquals("/mnt/disk/.Trash-123/files", instance.files_dir) def test_calc_id(self): trash_info_file = "/home/user/.local/share/Trash/info/foo.trashinfo" self.assertEquals('foo',TrashDirectory.calc_id(trash_info_file)) def test_calc_original_location_when_absolute(self) : instance = TrashDirectory( "/mnt/disk/.Trash-123", volume_of("/mnt/disk")) assert_equals("/foo", instance._calc_original_location("/foo")) def test_calc_original_location_when_relative(self) : instance = TrashDirectory( "/mnt/disk/.Trash-123", "/mnt/disk") assert_equals("/mnt/disk/foo", instance._calc_original_location("foo")) class TestHomeTrashDirectory(TestCase) : def test_path_for_trashinfo (self) : instance = HomeTrashDirectory("/home/user/.local/share/Trash") instance.volume = volume_of("/") # path for HomeTrashDirectory are always absolute fileToBeTrashed="/home/user/test.txt" result=instance._path_for_trashinfo(fileToBeTrashed) self.assertEquals("/home/user/test.txt",result) # ... even if the file is under /home/user/.local/share fileToBeTrashed="/home/user/.local/share/test.txt" result=instance._path_for_trashinfo(fileToBeTrashed) self.assertEquals(os.path.abspath("/home/user/.local/share/test.txt"),result) def test_str_uses_tilde(self): os.environ['HOME']='/home/user' self.assertEquals('~/.local/share/Trash', str(HomeTrashDirectory("/home/user/.local/share/Trash"))) def test_str_dont_uses_tilde(self): os.environ['HOME']='/home/user' self.assertEquals('/not-in-home/Trash', str(HomeTrashDirectory("/not-in-home/Trash"))) def test_str_uses_tilde_with_trailing_slashes(self): os.environ['HOME']='/home/user/' self.assertEquals('~/.local/share/Trash', str(HomeTrashDirectory("/home/user/.local/share/Trash"))) def test_str_uses_tilde_with_trailing_slash(self): os.environ['HOME']='/home/user////' self.assertEquals('~/.local/share/Trash', str(HomeTrashDirectory("/home/user/.local/share/Trash"))) def test_str_with_empty_home(self): os.environ['HOME']='' self.assertEquals('/foo/Trash', str(HomeTrashDirectory("/foo/Trash"))) class TestVolumeTrashDirectory(TestCase) : def test_init(self) : path = "/mnt/disk/.Trash/123" volume = volume_of("/mnt/disk") instance = VolumeTrashDirectory(path, volume) self.assertEquals(path, instance.path) self.assertEquals(volume, instance.volume) def test_path_for_trashinfo (self) : path = "/mnt/disk/.Trash-123" volume = "/mnt/volume" instance = VolumeTrashDirectory(path, volume) # path for VolumeTrashDirectory are relative as possible fileToBeTrashed=("/mnt/volume/directory/test.txt") result=instance._path_for_trashinfo(fileToBeTrashed) self.assertEquals("directory/test.txt", result) # path for VolumeTrashDirectory are relative as possible fileToBeTrashed=("/mnt/other-volume/directory/test.txt") result=instance._path_for_trashinfo(fileToBeTrashed) self.assertEquals("/mnt/other-volume/directory/test.txt",result) class TestTrashInfo(TestCase) : def test_parse(self) : data = """[Trash Info] Path=home%2Fandrea%2Fprova.txt DeletionDate=2007-07-23T23:45:07""" result = TrashInfo.parse(data) self.assertEqual(result.path, "home/andrea/prova.txt") self.assert_(isinstance(result.deletion_date,datetime)) self.assertEqual(result.deletion_date, datetime(2007, 7, 23, 23, 45, 07)) def test_init(self) : instance = TrashInfo("path", datetime(2007, 7, 23, 23, 45, 07)) self.assertEquals("path", instance.path) self.assertEquals(datetime(2007, 7, 23, 23, 45, 07), instance.deletion_date) def test_init2(self) : instance = TrashInfo("path", datetime(2007, 7, 23, 23, 45, 07)) self.assertEquals("path", instance.path) self.assertEquals(datetime(2007, 7, 23, 23, 45, 07), instance.deletion_date) def test_format_date(self) : date = datetime(2007, 7, 23, 23, 45, 07) self.assertEquals("2007-07-23T23:45:07", TrashInfo._format_date(date)) class TestTrashedFile(TestCase) : __dummy_datetime=datetime(2007, 7, 23, 23, 45, 07) def setUp(self): self.xdg_data_home = ("sandbox/XDG_DATA_HOME") def test_init(self) : path = "/foo" deletion_date = datetime(2001,01,01) info_file = "/home/user/.local/share/Trash/info/foo.trashinfo" actual_path = ("/home/user/.local/share/Trash/files/foo") trash_directory = HomeTrashDirectory('/home/user/.local/share/Trash') instance = TrashedFile(path, deletion_date, info_file, actual_path, trash_directory) assert_equals(instance.path, path) assert_equals(instance.deletion_date, deletion_date) assert_equals(instance.info_file, info_file) assert_equals(instance.actual_path, actual_path) assert_equals(trash_directory, trash_directory) @raises(ValueError) def test_init_requires_absolute_paths(self): path = "./relative-path" deletion_date = datetime(2001,01,01) info_file = "/home/user/.local/share/Trash/info/foo.trashinfo" actual_path = "/home/user/.local/share/Trash/files/foo" trash_directory = HomeTrashDirectory('/home/user/.local/share/Trash') TrashedFile(path, deletion_date, info_file, actual_path, trash_directory) def test_restore_create_needed_directories(self): trash_dir = HomeTrashDirectory(self.xdg_data_home) os.mkdir("sandbox/foo") touch("sandbox/foo/bar") instance = trash_dir.trash("sandbox/foo/bar") shutil.rmtree("sandbox/foo") instance.restore() assert os.path.exists("sandbox/foo/bar") class TestTimeUtils(TestCase) : def test_parse_iso8601(self) : expected=datetime(2008,9,8,12,00,11) result=TimeUtils.parse_iso8601("2008-09-08T12:00:11") self.assertEqual(expected,result) class Method1VolumeTrashDirectoryTest(TestCase): def setUp(self): require_empty_dir('sandbox') @raises(TopDirWithoutStickyBit) def test_check_when_no_sticky_bit(self): # prepare import subprocess topdir = "sandbox/trash-dir" mkdirs(topdir) assert subprocess.call(["chmod", "-t", topdir]) == 0 volume = volume_of("/mnt/disk") instance = Method1VolumeTrashDirectory(os.path.join(topdir,"123"), volume) instance.check() @raises(TopDirNotPresent) def test_check_when_no_dir(self): # prepare import subprocess topdir = "sandbox/trash-dir" touch(topdir) assert subprocess.call(["chmod", "+t", topdir]) == 0 volume = volume_of("/mnt/disk") instance = Method1VolumeTrashDirectory(os.path.join(topdir,"123"), volume) instance.check() @raises(TopDirIsSymLink) def test_check_when_is_symlink(self): # prepare import subprocess topdir = "sandbox/trash-dir" mkdirs(topdir) assert subprocess.call(["chmod", "+t", topdir]) == 0 topdir_link = "sandbox/trash-dir-link" os.symlink('./trash-dir', topdir_link) volume = volume_of("/mnt/disk") instance = Method1VolumeTrashDirectory(os.path.join(topdir_link,"123"), volume) instance.check() def test_check_pass(self): # prepare import subprocess topdir = "sandbox/trash-dir" mkdirs(topdir) assert subprocess.call(["chmod", "+t", topdir]) == 0 volume = volume_of("/mnt/disk") instance = Method1VolumeTrashDirectory(os.path.join(topdir,"123"), volume) instance.check() # should pass def touch(path): open(path, 'a+').close() trash-cli-0.12.7/unit_tests/test_trash_new_tests.py000066400000000000000000000012031177107703400225200ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import (assert_true, assert_equals) from unittest import TestCase from trashcli.trash import GlobalTrashCan class TestGlobalTrashCan(TestCase): def test_the_attempt_of_deleting_a_dot_directory_should_signaled_as_error(self): argument="." class StubReporter: def __init__(self): self.has_been_called=False def unable_to_trash_dot_entries(self,file): self.has_been_called=True assert_equals(file, argument) reporter=StubReporter() trashcan = GlobalTrashCan(reporter=reporter) trashcan.trash('.') assert_true(reporter.has_been_called) trash-cli-0.12.7/unit_tests/test_trash_put.py000066400000000000000000000047571177107703400213360ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import istest from StringIO import StringIO from trashcli.trash import TrashPutCmd from integration_tests.assert_equals_with_unidiff import assert_equals_with_unidiff @istest class describe_TrashPutCmd: @istest def on_help_option_print_help(self): self.run('--help') self.stdout_should_be('''\ 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 http://code.google.com/p/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('') def run(self, *arg): self.stderr=StringIO() self.stdout=StringIO() args=['trash-put'] + list(arg) cmd=TrashPutCmd(self.stdout, self.stderr) self.detect_and_save_exit_code(lambda:cmd.run(args)) def detect_and_save_exit_code(self, main_function): self.actual_exit_code=0 try: result=main_function() if result is not None: self.actual_exit_code=result except SystemExit, e: self.actual_exit_code=e.code 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() trash-cli-0.12.7/unit_tests/test_trashdir.py000066400000000000000000000067661177107703400211470ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy from nose.tools import assert_equals, istest from trashcli.trash import TrashDir 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, '/', None) def find_orphan(self): self.trashdir.each_orphan(self.orphan_found.append) from trashcli.trash import EachTrashInfo @istest class describe_EachTrashInfo: @istest def it_should_list_trashinfos(self): self.having_directory('~/.local/share/Trash', containing = ['foo.trashinfo']) self.trashinfos_found(in_trashdir='~/.local/share/Trash', should_be=['~/.local/share/Trash/info/foo.trashinfo']) @istest def it_should_not_list_other_files(self): self.having_directory('~/.local/share/Trash', containing = ['foo.non-a-trashinfo']) self.trashinfos_found(in_trashdir='~/.local/share/Trash', should_be=[]) def having_directory(self, path, containing): self.list_dir = lambda path: { path : containing }[path] def trashinfos_found(self, in_trashdir, should_be): result = [] finder = EachTrashInfo(self.list_dir, result.append) finder.trashdir(in_trashdir) assert result == list(should_be) 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')))