pax_global_header00006660000000000000000000000064146246005330014515gustar00rootroot0000000000000052 comment=c02fd350e6346da91404985e2d23fd5ab5256259 trash-cli-0.24.5.26/000077500000000000000000000000001462460053300137215ustar00rootroot00000000000000trash-cli-0.24.5.26/.github/000077500000000000000000000000001462460053300152615ustar00rootroot00000000000000trash-cli-0.24.5.26/.github/ISSUE_TEMPLATE/000077500000000000000000000000001462460053300174445ustar00rootroot00000000000000trash-cli-0.24.5.26/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000030411462460053300221340ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **trash-cli version** Output of: trash-put --version **Are you using the latest version of trash-cli?** Yes/no **Have you tried if the bug is present in the latest version of trash-cli?** Yes/no **Please, install the latest version of trash-cli and try again before submitting the bug** First of all you need to uninstall any previous version of trash-cli:: $ [sudo] pip uninstall trash-cli # remove the previous version (with pip) $ [sudo] apt-get remove trash-cli # remove the previous version (with apt) $ [sudo] yum uninstall trash-cli # remove the previous version (with yum) $ ... # refer to the package manager of your distribution Then install the latest version from git:: $ pip install pip install git+https://github.com/andreafrancia/trash-cli Have done that? Then continue with the bug report. **Operating system:** - OS: [e.g. Debian, Ubuntu, Fedora, macOs, Cygwin] **To Reproduce** Copy and paste the commands (and their output) to execute in order to reproduce the behavior: $ touch foo $ trash-put foo $ ls foo ls: cannot access 'foo': No such file or directory $ trash-list 2020-12-13 18:36:21 /Users/andrea/trash-cli/foo **Expected behavior** A clear and concise description of what you expected to happen. **Volumes detail** If your issue is related to recognition of volumes, please provide the output of: `trash-list --debug-volumes` trash-cli-0.24.5.26/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011231462460053300231660ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. trash-cli-0.24.5.26/.github/workflows/000077500000000000000000000000001462460053300173165ustar00rootroot00000000000000trash-cli-0.24.5.26/.github/workflows/make-release.yml000066400000000000000000000015411462460053300223750ustar00rootroot00000000000000name: Make Release on: push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: 3.11 - name: Set version run: | pip install --upgrade pip scripts/lib/install-python-requirements scripts/set-dev-version ${{ github.ref_name }} ${{ github.sha }} - name: Make release run: scripts/test-sdist - name: version name run: echo "version=$(python -c 'import setuptools; setuptools.setup()' --version)" >> "$GITHUB_OUTPUT" id: version-name - name: Publish release uses: actions/upload-artifact@v4 with: name: trash-cli-${{ steps.version-name.outputs.version }}.zip path: dist/*.tar.gz if-no-files-found: error trash-cli-0.24.5.26/.github/workflows/run-tests.yml000066400000000000000000000051321462460053300220060ustar00rootroot00000000000000# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python test on: push: branches: [ "*" ] pull_request: branches: [ master ] jobs: run_tests: name: Run Python Tests strategy: matrix: include: - { os: ubuntu-latest, python_version: '2.7' } - { os: ubuntu-20.04, python_version: '3.5' } - { os: ubuntu-20.04, python_version: '3.6' } - { os: ubuntu-20.04, python_version: '3.7' } - { os: ubuntu-22.04, python_version: '3.8' } - { os: ubuntu-24.04, python_version: '3.9' } - { os: ubuntu-latest, python_version: '3.10' } - { os: ubuntu-latest, python_version: '3.11' } - { os: ubuntu-latest, python_version: '3.12' } - { os: macos-13, python_version: '3.7' } - { os: macos-latest, python_version: '3.8' } - { os: macos-latest, python_version: '3.9' } - { os: macos-latest, python_version: '3.10' } - { os: macos-latest, python_version: '3.11' } - { os: macos-latest, python_version: '3.12' } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up 3.5 Python, ${{ matrix.python_version }} if: ${{ matrix.python_version == '3.5' }} uses: actions/setup-python@v5 env: # Workaround copied from # https://github.com/ytdl-org/youtube-dl/commit/a08f2b7e4567cdc50c0614ee0a4ffdff49b8b6e6 PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org" with: python-version: ${{ matrix.python_version }} - name: Set up Python ${{ matrix.python_version }} uses: andreafrancia/setup-python@v1 if: ${{ matrix.python_version != '3.5' }} with: python-version: ${{ matrix.python_version }} - name: Upgrade pip run: python -m pip install --upgrade pip - name: Install dependencies run: python -m pip install -r requirements-dev.txt -r requirements.txt - name: Check Types run: ./scripts/check-types if: matrix.python_version != '2.7' - name: Run tests run: python -m pytest sdist: name: Test sdist creation runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python_version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Test sdist run: scripts/test-sdist trash-cli-0.24.5.26/.gitignore000066400000000000000000000003021462460053300157040ustar00rootroot00000000000000*.pyc .coverage .DS_Store # other things /build/ /cover/ /dist/ /.venv/ /.venv-*/ /test-disk.img /test-disk.dmg /test-disk/ /trash_cli.egg-info/ /MANIFEST /.vagrant/ /ssh-config /.idea/ /.tox/ trash-cli-0.24.5.26/COPYING000066400000000000000000000431101462460053300147530ustar00rootroot00000000000000 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.24.5.26/CREDITS.txt000066400000000000000000000026341462460053300155640ustar00rootroot00000000000000Author ------ Andrea Francia trash-cli@andreafrancia.it Thanks to --------- - Einar Orn Olason: Developmemt of first version of the empty-trash command - Rahul Sundaram, http://fedoraproject.org/wiki/RahulSundaram Packaged trash-cli for Fedora (https://bugzilla.redhat.com/show_bug.cgi?id=508750) - Michele Barbiero, http://greedyandlounge.blogspot.com,Shuren @ irc://irc.azzurra.org/linux-help Reporting of bugs. - jpschewe -at_ users.sourceforge.net: Reporting many bugs and submitting patches. - slim http://www.hartnup.net/wordpress: Helped choosing commands names - BobbyShaftoe http://stackoverflow.com/users/38426/bobbyshaftoe Helped choosing commands names - ceretullis http://www.breakingrobots.net: Helped choosing commands names - grapefrukt http://grapefrukt.com/blog : Helped choosing commands names - joel.neely http://joelneely.wordpress.com : Helped choosing commands names - My Wife Ely (Elisa Zuccolo): Thanks for tolerating me when I work at trash-cli. - Ben Finney: Helped choosing commands names, find out the grammar problems with trash-file command. - Ken VanDine: Helped choosing commands names - Christoph Bloch: Helped choosing commands names - Massimo "Submax82" Cavalleri: Packaged on Slackware - Steve Stalcup: for maintaining the Debian and Ubuntu packages - Lee Yeoh for donating to trash-cli If you think you should appear in this page please tell me. trash-cli-0.24.5.26/HISTORY.txt000066400000000000000000000242151462460053300156270ustar00rootroot000000000000000.24.5.26 Shell Completion: - Bugfix in print-completion loop for shells other than zsh by Andrew Davis User not visibile changes: - Move all the main logic for dev tools from scripts under tests/support to solve issue https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1067303 - Fixed a problem in GitHub Actions that ran all the tests on the same python version instead of all the python versions specified - Reintroducing testing on python 2.7 thanks the custom GitHub action found at https://github.com/ytdl-org/setup-python - Fixed regression that would crash trash-restore on non parseable trashinfo, with an error like: "TypeError: not enough arguments for format string" - Using python -m venv instead of virtualenv - Refactor of some code in trash-put and trash-restore 0.24.4.17 (f0136411): User visible changes: - trash-put --help shows also other commands (Joseph Masone) - trash-restore prints a message when no file are restored: "No files were restored" (lukasvrenner) - fix a regression that would crash `trash-list --volumes` at every launch - add `fuse.gocryptfs` to the mount_points to the list of allowed filesystems (Maxim Baz) - Fix a bug that would trash a link destination instead of trashing the link when the file to be trashed contains trailing slashes. - fix typos/grammar in man pages (qadzek) - improved shell TAB-completion (Giuseppe Stelluto) Work on the tools: - made GitHub Actions workflows to work again - updated the actions versions in GitHub Actions - made the tests work again against python 2.7 - fixed the tox configuration (in some cases it would run python 2.7 tests on python 3!) - made the type checker happy again - the tarball created by python -m build sdist changed name, a dash "-" became a underscore "_"; before was "trash-cli-version.tar.gz" now we have: "trash_cli-version.tar.gz". The affected scripts has been updated to accomodate this change 0.23.11.10 (68c34e0a): - use enum34... - mypy - trash-put clearer error messages 0.23.9.23 (ae690713) - man page trash-put.1 will also document trash-dirs locations (Joel Pereira) - fixed typos in man page trash-put.1 (Joel Pereira) - removed python 2.7 from the list of tested platforms (as GitHub Actions no longer support this Python version) - add MyPy checks to the GitHub Action test workflow - improved error message when TrashInfo is not parsable in trash-restore - suggested to use pipx in README.md (bryango) - add instruction for installation on Fedora (Mohammed Eshan) - get back `--` the command ending for trash-put (@laggardkernel) - fixed typos (David Auer) - check info target trash folder non-exists Tin Lai 0.22.10.20 (07674600): - Made visible option -v/--verbose of trash-empty - Andrea Francia - Documented how completion works in README - Wu Zhenyu - Change: now if a file does not exists it does not tries to delete it using all available trash dirs - Andrea Francia - Removed an error that would occur on trash-put when HOME environment is not set - Andrea Francia - Whitelisted fuse.glusterfs filesystem - Andrea Francia - Add tox 0.22.10.4.4: - Revisited the trash-put log messages - Add shell completions by shtab - Feature: trash-empty learnt the --verbose option - Feature: trash-empty learnt the --dry-run option - Change: add six as a requirements - Change: add 'fuse' to the list of "physical" file system types - Change: now trash-put uses ArgumentParser instead of OptionParser - Add more debugging messages to trash-put when it fails to trash a file. - Change: trash-empty now uses lexists (instead of exists) to check if a file is not existent before removal - Add `trash-list --python` to print the python executable 0.22.8.27: - from now partitions with fs in ['nfs4','btrfs'] are considered physical volumes - also partitions mounted on /tmp with tmpfs will be considered physical volumes - `trash-list --all-users` to see trash from all users 0.22.8.21.16: - Now supports p9 (WSL 2 volumes) as location for trash dirs - trash-list --volumes to list all the recognized volumes - trash-list --debug-volumes - Fix links to trash specification David Auer 0.22.8.21: - fix a bug that made `trash-list --size` to crash if it found a broken link in the trash directory files pull request https://github.com/andreafrancia/trash-cli/pull/233 thanks to: https://github.com/jamescherti - trash-empty do not list trash directories that does not exist pull request https://github.com/andreafrancia/trash-cli/pull/237 thanks to: https://github.com/jack1142 - Fix trash-empty not showing nfs mountpoints pull request: https://github.com/andreafrancia/trash-cli/pull/230 thanks to https://github.com/masgouri - Fix perms for user's trash folder pull request: https://github.com/andreafrancia/trash-cli/pull/239 thanks to https://github.com/jack1142 0.22.4.16: - trash-restore exits gracefully if the user enters Ctrl+D, thanks to https://github.com/mtoohey31 0.21.10.24: - trash-empty detect when input is interactive and asks before emptying trash - trash-empty learnt the -i/--interactive option - trash-empty option --all-users is no longer hidden 0.21.7.24: - fix bug in tests 0.21.7.23: - fix bug in tests (see https://github.com/andreafrancia/trash-cli/issues/210) 0.21.6.30: - `trash-empty --help` now shows only the command basename and not the full path to the command. - Now `trash-empty` honors multiple --trash-dir options - trash-empty learnt the hidden option --print-time and now uses TRASH_DATE environment variable if present - trash-empty learnt the --all-users option 0.21.5.25: - now trash-put honors the -i option (also the --interactive one) 0.21.5.22: - trash-rm: fixed pattern matching for absolute paths, fixes https://github.com/andreafrancia/trash-cli/issues/124 0.21.5.20: - add (hidden and undocumented) --files option to trash-list 0.21.5.11: - trash-put also accept -vv for enabling debug prints - add (hidden and undocumented) --size option to trash-list 0.21.4.18: - fix bug #166 'trash goes into an infinite loop when trashing files with a long filename' - trash-list learnt the `--trash-dir` option - trash-restore now supports relative paths in argument (fixes #165) - trash-list shows all partitions including not physical @KSR-Yasuda (https://github.com/andreafrancia/trash-cli/pull/178) 0.20.12.26: - trash-restore learnt --trash-dir option - add simplified Chinese README - trash-restore now uses 'date' as the default sort argument - Add to README the installation with apt - trash-restore now supports range select 0.20.11.23: - switched to psutil for listing volumes 0.20.11.7: - trash-put learned a --trash-dir option that can be used to specify the trash directory to be used as destination - trash-put -f now ignores files and dirs that do not exist (Don Cross) - trash-restore learnt --sort=(date|path|none) option (Self-Perfection) - trash-restore: support restoring multiple files (arendu) - README: now recommend using `pip` for installing trash-cli 0.17.1.14: - Fix a bug that causes trash-put to use $topdir/.Trash/UID trashcan even when it is not secure and $topdir/.Trash-UID should be used instead. 0.17.1.12: - Fix a bug in detecting right volume of home trash dir, if home Trash dir is a symbolic link, and in detecting volume of file to be trashed when it is specified as contained in a directory that is a symbolic link that crosses volumes boundaries (#38) - Make some code python 3 compatible - Fixed README. 0.17.1.1: - Now trash-rm supports full path matching, using a pattern starting with slash '/' Fix #67 - Fix typo in trash-rm(1) man page - Add a reference to trash-rm(1) to all man pages - Fix inconsistent apostrophes - Add support for --trash-dir option to trash-empty 0.16.12.29: - trash-rm no more crashes on .trashinfo files without Path (#69) 0.16.12.28: - Fix #48 trash-empty crashes on directories without read permission 0.16.12.26: - Fix #52 Almost all commands crash with python 2.7.10 0.16.12.25: - Now trash-restore accepts a /specific/path - Now integration tests should pass also in a linux box. Fix #61 - Now all command outputs will report the right up-to-date location for issue reporting (#39) - Add input validation in trash-restore - Renamed restore-trash to trash-restore - Fixed bug (trash-put creates $topdir/.Trash even if it should not) - Fixed bug (trash-put uses $topdir/.Trash/$uid even if unsecure) - Minor changes to man pages. 0.12.9.14: - Switched to distutils.core (instead of setuptools) - Now `trash-put -v` will warn if it found an unsticky .Trash dir. - New trash-rm command - (Internal) Switched from realpath to abspath 0.12.7: - fixed: trash-empty crashed with GetoptError in short_has_arg(): option -2 not recognized (see https://bugs.launchpad.net/ubuntu/+source/trash-cli/+bug/1015877 ) - fixed inclusion of README.rst when creating distribution package 0.12.6: - add Donate button on README 0.12.4.24: - Fixes a packaging problem of the previous release which prevented the installation via easy_install and/or pip (see https://github.com/andreafrancia/trash-cli/issues/5) - Fixes the name of the man page for restore-trash. 0.12.4: - Reintroduced `trash` command as alias to `trash-put` - Now trash-list checks for $topdir/.Trash having sticky bit and not being a symlink and warns when these requirements are not met. - Now trash-list handles empty, unreadable or malformed .trashinfo - Now `trash-empty ` 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.24.5.26/README.rst000066400000000000000000000162571462460053300154230ustar00rootroot00000000000000trash-cli - Command Line Interface to FreeDesktop.org Trash. ============================================================ |Downloads| |Donate|_ `简体中文`_ trash-cli trashes files recording the original path, deletion date, and permissions. It uses the same trashcan used by KDE, GNOME, and XFCE, but you can invoke it from the command line (and scripts). It provides these commands:: trash-put trash files and directories. trash-empty empty the trashcan(s). trash-list list trashed files. trash-restore restore a trashed file. trash-rm remove individual files from the trashcan. Usage ----- Trash a file:: $ trash-put foo List trashed files:: $ trash-list 2008-06-01 10:30:48 /home/andrea/bar 2008-06-02 21:50:41 /home/andrea/bar 2008-06-23 21:50:49 /home/andrea/foo Search for a file in the trashcan:: $ trash-list | grep foo 2007-08-30 12:36:00 /home/andrea/foo 2007-08-30 12:39:41 /home/andrea/foo Restore a trashed file:: $ trash-restore 0 2007-08-30 12:36:00 /home/andrea/foo 1 2007-08-30 12:39:41 /home/andrea/bar 2 2007-08-30 12:39:41 /home/andrea/bar2 3 2007-08-30 12:39:41 /home/andrea/foo2 4 2007-08-30 12:39:41 /home/andrea/foo What file to restore [0..4]: 4 $ ls foo foo Restore a trashed file while overwriting existing files:: $ echo "original">foo $ ls foo $ trash foo $ echo "new">foo $ trash-restore --overwrite 0 2022-11-01 22:15:00 /home/andrea/foo What file to restore [0..0]: 0 $ cat foo original Restore multiple trashed files separated by ',', also support range:: $ trash-restore 0 2007-08-30 12:36:00 /home/andrea/foo 1 2007-08-30 12:39:41 /home/andrea/bar 2 2007-08-30 12:39:41 /home/andrea/bar2 3 2007-08-30 12:39:41 /home/andrea/foo2 What file to restore [0..3]: 0-2, 3 $ ls foo bar bar2 foo2 foo bar bar2 foo2 Remove all files from the trashcan:: $ trash-empty Remove only the files that have been deleted more than ago:: $ trash-empty Example:: $ date Tue Feb 19 20:26:52 CET 2008 $ trash-list 2008-02-19 20:11:34 /home/einar/today 2008-02-18 20:11:34 /home/einar/yesterday 2008-02-10 20:11:34 /home/einar/last_week $ trash-empty 7 $ trash-list 2008-02-19 20:11:34 /home/einar/today 2008-02-18 20:11:34 /home/einar/yesterday $ trash-empty 1 $ trash-list 2008-02-19 20:11:34 /home/einar/today Remove only files matching a pattern:: $ trash-rm \*.o Note: you need to use quotes in order to protect the pattern from shell expansion. FAQ --- How to create a top level .Trash dir? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Steps :: sudo mkdir --parent /.Trash sudo chmod a+rw /.Trash sudo chmod +t /.Trash Can I alias `rm` to `trash-put`? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can but you shouldn't. In the early days I thought it was a good idea to do that but now I changed my mind. Although the interface of `trash-put` seems to be compatible with `rm`, it has different semantics which will cause you problems. For example, while `rm` requires `-R` for deleting directories `trash-put` does not. But sometimes I forget to use `trash-put`, really can't I? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You could alias `rm` to something that will remind you to not use it:: alias rm='echo "This is not the command you are looking for."; false' Then, if you really want to use `rm`, simply prepend a backslash to bypass the alias:: \rm file-without-hope Note that Bash aliases are used only in interactive shells, so using this alias should not interfere with scripts that expect to use `rm`. Where the trashed files go? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ File trashed from the home partition will be moved here:: ~/.local/share/Trash/ How to auto delete files older that 30 days? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Run this:: (crontab -l ; echo "@daily $(which trash-empty) 30") | crontab - This will update your crontab file with a `trash-empty` command that runs daily and removes files older than 30 days. To review your crontab use: `crontab -l` Installation ------------ The easy way ~~~~~~~~~~~~ Requirements: * Python 3 (Python 2.7 also work) * pipx_ (optional, to install in a clean environment) If pipx is available:: pipx install trash-cli Alternatively, install with vanilla pip:: pip install trash-cli Note: you may want add ~/.local/bin to the PATH:: echo 'export PATH="$PATH":~/.local/bin' >> ~/.bashrc source ~/.bashrc # reload .bashrc For uninstalling use:: pipx uninstall trash-cli or:: pip uninstall trash-cli Bleeding Edge (from sources) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First of all you need to uninstall any previous version of trash-cli:: $ [sudo] pip uninstall trash-cli # remove the previous version (with pip) $ [sudo] apt-get remove trash-cli # remove the previous version (with apt) $ [sudo] yum uninstall trash-cli # remove the previous version (with yum) $ ... # refer to the package manager of your distribution Then install the latest version from git:: $ [sudo] pip install git+https://github.com/andreafrancia/trash-cli After the user installation you may want add this line to your .bashrc/.zshrc:: export PATH=~/.local/bin:"$PATH" From package manager ~~~~~~~~~~~~~~~~~~~~ Debian/Ubuntu (apt):: sudo apt install trash-cli Arch Linux (pacman):: sudo pacman -S trash-cli Fedora (dnf):: sudo dnf install trash-cli Install shell completions ~~~~~~~~~~~~~~~~~~~~~~~~~ You need install by:: pipx install 'trash-cli[completion]' or:: pip install 'trash-cli[completion]' Then:: cmds=(trash-empty trash-list trash-restore trash-put trash) for cmd in ${cmds[@]}; do $cmd --print-completion bash | sudo tee /usr/share/bash-completion/completions/$cmd $cmd --print-completion zsh | sudo tee /usr/share/zsh/site-functions/_$cmd $cmd --print-completion tcsh | sudo tee /etc/profile.d/$cmd.completion.csh done Bugs ---- If you discover a bug please report it here: https://github.com/andreafrancia/trash-cli/issues Feedback -------- You can send me an email using andrea@andreafrancia.it. Development ----------- Environment setup:: python -m venv .venv source .venv/bin/activate pip install -r requirements-dev.txt -r requirements.txt Running tests:: pytest -m 'not slow' # run only fast tests pytest -m 'slow' # run slow tests pytest # run all tests Thanks ------ Thanks to Paypal donors. Thanks to `project contributors`_. Thanks to `JetBrains`_ for their license for Open Source Development .. |Downloads| image:: https://img.shields.io/pypi/dm/trash-cli .. |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 .. _简体中文: https://github.com/andreafrancia/trash-cli/blob/master/README_zh-CN.rst .. _project contributors: https://github.com/andreafrancia/trash-cli/graphs/contributors .. _JetBrains: https://jb.gg/OpenSource .. _pipx: https://pypa.github.io/pipx/ trash-cli-0.24.5.26/README_zh-CN.rst000066400000000000000000000115371462460053300164160ustar00rootroot00000000000000trash-cli——FreeDesktop.org 回收站的命令行界面 ============================================= |Donate|_ `English`_ trash-cli 用于移动文件到回收站,同时会记录文件的原地址、删除日期和权限。trash-cli 和 KDE、GNOME、XFCE 使用同一个回收站,你可以在命令行或脚本运行 trash-cli。 trash-cli 提供以下命令: :: trash-put 把文件或目录移动到回收站 trash-empty 清空回收站 trash-list 列出回收站文件 trash-restore 恢复回收站文件 trash-rm 删除回收站文件 用法 ---- 移动文件到回收站: :: $ trash-put 列出回收站文件: :: $ 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 搜索回收站文件: :: $ trash-list | grep foo 2007-08-30 12:36:00 /home/andrea/foo 2007-08-30 12:39:41 /home/andrea/foo 恢复回收站文件: :: $ trash-restore 0 2007-08-30 12:36:00 /home/andrea/foo 1 2007-08-30 12:39:41 /home/andrea/bar 2 2007-08-30 12:39:41 /home/andrea/bar2 3 2007-08-30 12:39:41 /home/andrea/foo2 4 2007-08-30 12:39:41 /home/andrea/foo What file to restore [0..4]: 4 $ ls foo foo 删除所有回收站文件: :: $ trash-empty 删除回收站中 n 天前被回收的文件: :: $ trash-empty 示例: :: $ 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-rm \*.o 注意:要用双引号圈住模式来避免 shell 拓展。 常见问题 -------- 如何创建顶级 .Trash 目录? ~~~~~~~~~~~~~~~~~~~~~~~~~~ 步骤: :: sudo mkdir --parent /.Trash sudo chmod a+rw /.Trash sudo chmod +t /.Trash 我能把 `rm` 的别名设置为 `trash-put` 吗? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 可以,但不应该这样做。以前我觉得这是个好主意,但现在我不觉得。 虽然 `trash-put` 的界面看起来与 `rm` 兼容,但它们有不同的语法,这些差异会导致一些问题。比如,用 `rm` 删除目录时需要 `-R`\ ,\ `trash-put` 则不需要。 但有时候我忘记用 `trash-put` 了,真的不能给 `rm` 设置别名吗? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 你可以给 `rm` 设置一个别名来提醒你不要使用它: :: alias rm='echo "This is not the command you are looking for."; false' 如果你真的要用 `rm`\ ,那就在 `rm` 前加上斜杠来取消别名: :: \rm file-without-hope 注意,Bash 别名是有在交互式界面才有效,所以使用这个别名不会影响使用 `rm` 的脚本。 被移动到回收站的文件在哪? ~~~~~~~~~~~~~~~~~~~~~~~~~~ 从 home 分区移动到回收站的文件在这: :: ~/.local/share/Trash/ 安装 ---- 简单方法 ~~~~~~~~ 要求: - Python 3 (Python 2.7 也可以) - pip (在 Debian 上用 `apt-get install python-pip` 来安装 pip) - pipx_ (可选,推荐,安装在虚拟环境中以保护系统环境) 安装命令: :: pipx install trash-cli # 或者 pip install trash-cli 源码安装 ~~~~~~~~ 为所有用户安装: :: git clone https://github.com/andreafrancia/trash-cli.git cd trash-cli sudo pip install . 为当前用户安装: :: git clone https://github.com/andreafrancia/trash-cli.git cd trash-cli pip install . 为当前用户安装后你可能需要把以下代码添加到 .bashrc: :: export PATH=~/.local/bin:"$PATH" 卸载命令: :: pipx uninstall trash-cli # 或者 pip uninstall trash-cli 用包管理器安装 ~~~~~~~~~~~~~~ Debian/Ubuntu (apt):: sudo apt install trash-cli 反馈与 Bug 报告 --------------- 如果你发现了 bug,请在这里报告: https://github.com/andreafrancia/trash-cli/issues 你也可以给我发邮件 andrea@andreafrancia.it\ 。我的推特帐号是 @andreafrancia。 开发 ---- 环境设置: :: virtualenv env --no-site-packages source env/bin/activate pip install -r requirements-dev.txt 运行测试: :: pytest -m 'not slow' # 只运行单元测试 pytest -m 'slow' # 运行所有集成测试 pytest # 运行所有测试 .. |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 .. _English: https://github.com/andreafrancia/trash-cli/blob/master/README.rst .. _pipx: https://pypa.github.io/pipx trash-cli-0.24.5.26/Vagrantfile000066400000000000000000000003621462460053300161070ustar00rootroot00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(2) do |config| config.vm.box = "debian/buster64" config.vm.provision "shell", inline: <<-SHELL sudo apt-get update sudo apt-get install -y python-setuptools SHELL end trash-cli-0.24.5.26/conftest.py000066400000000000000000000001731462460053300161210ustar00rootroot00000000000000def pytest_configure(config): config.addinivalue_line( "markers", "slow: tests that uses the filesystem" ) trash-cli-0.24.5.26/docs/000077500000000000000000000000001462460053300146515ustar00rootroot00000000000000trash-cli-0.24.5.26/docs/about-shredding.txt000066400000000000000000000077721462460053300205060ustar00rootroot00000000000000Introduction ============ 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.24.5.26/docs/about-the-generic-naming-issue.txt000066400000000000000000000023661462460053300233200ustar00rootroot00000000000000How the command names changed? ============================== The earlier versions of trash-cli used these command names: * trash * empty-trash * list-trash * restore-trash When the trash-cli project was proposed to Fedora the Red Hat peoples complaint about the command 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 command names were 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 were 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 their 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.24.5.26/docs/archive/000077500000000000000000000000001462460053300162725ustar00rootroot00000000000000trash-cli-0.24.5.26/docs/archive/DONE.txt000066400000000000000000000014161462460053300175620ustar00rootroot00000000000000trash-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.24.5.26/docs/archive/MountListFromHal.wiki000066400000000000000000000010111462460053300223570ustar00rootroot00000000000000About 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. =Dependencies= Using this method will add some dependencies: * hal ? * libhal1 * dbus python module trash-cli-0.24.5.26/docs/archive/about-accessing-the-windows-recycle-bin.txt000066400000000000000000000010721462460053300265420ustar00rootroot00000000000000Some 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.24.5.26/docs/archive/about-listing-osx-trashcan.txt000066400000000000000000000013751462460053300242320ustar00rootroot00000000000000In 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 maintains 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.24.5.26/docs/archive/back-links.txt000066400000000000000000000042611462460053300210540ustar00rootroot00000000000000List of web pages that cite trash-cli, with the date when I discovered the page, sorted by discovery date (newest before): - 2012-07-19 http://shuffleos.com/5835/trashcli-command-line-utility-access-trashcan-ubuntu-linux-terminal/ - 2012-07-18 http://crunchbanglinux.org/forums/topic/20818/trashbin-cli/ - 2012-07-02 http://www.hecticgeek.com/2012/06/access-trash-command-line-ubuntu-linux-trash-cli/ - 2009-07-24 http://www.emacswiki.org/emacs/SystemTrash - 2009-03-29 2008-12-27 http://blog.goo.ne.jp/xxxfishbonexxx/e/45508969110d67f141226b68eab205ce japanese? blog post about installing? trash-cli - 2009-03-29 2008-10-16 http://linuxgazette.net/156/misc/lg/deleted_file_recovery.html - 2009-03-29 http://qa.debian.org/popcon.php?package=trash-cli - 2009-03-29 http://elonen.iki.fi/code/misc-notes/remove-duplicate-files/index.html?showcomments=1 Antonio in the comments. - 2009-03-01 http://varlogrant.blogspot.com/2009/01/making-plans.html - 2009-03-01 http://y_z.scripts.mit.edu/wp/2009/02/03/when-you-release-shift-too-early - 2009-03-01 http://sanelz.blogspot.com/2009/02/trashing-stuff.html - 2009-03-01 http://alltheshortnamesweretakenalready.blogspot.com/2009/01/bash-script-implementation-of-trashcan.html - 2008-12-25 (Blog about Ubuntu in some language that uses kanji) http://ubulog.blogspot.com/2008/10/ubuntu.html - 2008-12-24 https://www.ohloh.net/p/trash-cli - 2008-12-23 http://article.gmane.org/gmane.compw.window-managers.windowmaker.devel/1045 - 2008-12-23 http://ubuntuforums.org/showthread.php?t=1003522 - 2008-12-23 http://www.downloadplex.com/Linux/System-Utilities/File-Disk-Management/trash-cli_70540.html - 2008-11-16 http://feeding.cloud.geek.nz/2008/06/preventing-accidental-deletion-of.html - 2008-11-09 (in french) http://www.thierryb.net/site/TRASH-CLI-utiliser-la-corbeille-en.html - 2008-10-14 http://ubulog.blogspot.com/2008/10/ubuntu.html - 2008-10-14 http://d.hatena.ne.jp/f99aq/20081013/1223882323 - http://linux.softpedia.com/get/System/Filesystems/trash-cli-39712.shtml - http://syswall.wordpress.com/2008/07/11/trash-cli/ - http://freshmeat.net/projects/bluetrash/ - http://www.linux.com/feature/138331 - http://directory.fsf.org/project/trash/ trash-cli-0.24.5.26/docs/archive/bugs.txt000066400000000000000000000003731462460053300177760ustar00rootroot00000000000000Bug 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.24.5.26/docs/archive/some-output-of-df-command-on-different-systems.txt000066400000000000000000000060241462460053300300260ustar00rootroot00000000000000The __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.24.5.26/docs/archive/who-packaged-trash-cli.txt000066400000000000000000000024021462460053300232470ustar00rootroot00000000000000Which 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 wishlist 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.24.5.26/docs/how-to-build-and-upload-a-new-release.rst000066400000000000000000000017011462460053300243610ustar00rootroot00000000000000How to build a release ====================== Update the version number:: scripts/bump Launch GitHub Actions to check the release:: git push origin master Load the version in an environment variable:: version="$(python -c 'import setuptools; setuptools.setup()' --version)" Run all tests:: pytest Create the tarball:: python -m build --sdist Check test:: twine check --strict "dist/trash_cli-$version.tar.gz" Upload to Test PyPI:: twine upload --repository testpypi "dist/trash_cli-$version.tar.gz" Remove previous installation:: python3 -m pip uninstall --yes trash-cli Test the installation:: python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple trash-cli Register and upload:: twine upload "dist/trash_cli-$version.tar.gz" Now you can tag the repo status:: git tag "${version:?}" Push the tag:: git push origin "${version:?}" -EOF trash-cli-0.24.5.26/docs/proposed-new-interface/000077500000000000000000000000001462460053300212315ustar00rootroot00000000000000trash-cli-0.24.5.26/docs/proposed-new-interface/trash-cli.txt000066400000000000000000000024041462460053300236600ustar00rootroot00000000000000NAME 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.24.5.26/docs/proposed-new-interface/trash-empty.txt000066400000000000000000000046641462460053300242610ustar00rootroot00000000000000NAME 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 with 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.24.5.26/docs/proposed-new-interface/trash-list.txt000066400000000000000000000032631462460053300240700ustar00rootroot00000000000000NAME 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.24.5.26/docs/proposed-new-interface/trash-restore.txt000066400000000000000000000046511462460053300246020ustar00rootroot00000000000000NAME 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.24.5.26/docs/similar-projects.txt000066400000000000000000000103021462460053300206750ustar00rootroot00000000000000DELSAFE ======= URL: http://homepage.esoterica.pt/~nx0yew/delsafe/ Use a different approach to the same problem. While 'trash' provide a extra command that put files in the trash, 'delsafe' modifies the semantics of the existing commands. Apparently 'delsafe' overrides the library calls of unlink, unlinkat, rename, open and fopen modifying the semantics of these functions. A program that was designed to works with the original functions may not work well with the overridden functions. For example is the KDE filemanager which as two commands: - delete file - trash file Using 'delsafe' these commands will become: - delete file --> trash file in the delsafe trash - trash file --> trash file in the KDE trash I prefer the 'trash' approach because don't change the semantics of the existing program letting the user chose when putting files in the trash and when removing them. Personally I don't like the model where each deletion should recorded somewhere. 'trash' is intended to be used only in interactive shells, if a program decides to delete, or trunk a file the information loss risk IMHO should be managed in the context of this program. trash.sh ======== URL: http://fresh.t-systems-sfr.com/unix/src/privat2/trashcan-3.2.tar.gz/ Comments: - This program seems not compliant with the FreeDesktopTrashSpecification. - It compresses trashed file instead of just moving it. - Like trash-cli can trash and restore directories. - Seems that the development was ended in 2003. Trashcan for Rox-Filer ====================== URL: http://usuarios.lycos.es/proyectarr/ Features: * Has a graphical user interface. * Does not support the FreeDesktopTrashSpecification. * Implemented in bash using zenity. trash by skymt ============== URL: http://mysite.verizon.net/skymt/trash/ Language: Python I reviewed the version [http://mysite.verizon.net/skymt/trash/trash-0.2.tar.gz 0.2] Is the only other CLI implementation of FreeDesktopTrashSpecification I ever found till now (2008-06-29) Features: - Operation supported: * trash files and directories: yes * listing trashed files: no * restoring trashed files: no * emptying the trashcan: no - Support of volumes trashcans: no - Can be used as alias='rm': no Notes: - *(Better than trash-cli)* Seems much simpler of trash-cli.\ - *(Better than trash-cli)* Prints sensible output if the trashcan directories permission are wrong. - Not object oriented. cn by Richard Neill ------------------- URL: http://www.richardneill.org/source.php#cn License: GPLv3 Reviewed on: 2008-06-29 Features: - Tries to conform to the FreeDesktopTrashSpecification - Works also with the older version of KDE and GNOME TrashCan implementation - Can be used as alias='rm': no - It has a manpage. Trash Specification: - Operation supported: - trash files and directories: yes - listing trashed files: yes - restoring trashed files: yes - emptying trashcan: yes - Support of volumes trashcans: no - Can be used as alias='rm': no FreeDesktop.org specification deviations: - I don't think it creates the trashinfo file in an atomic way. File Rename Utils ================= URL: http://filerenameutils.sourceforge.net/ License: ? Reviewed on: 2008-06-30 Features: - Conform to FreeDesktopTrashSpecification : yes - Operation supported: - trash files and directories: yes - listing trashed files: yes - restoring trashed files: yes - emptying trashcan: yes - Support of volumes trashcans: ? - Can be used as alias='rm': no Gnome Trashcan ============== As of Nautilus 2.22, GNOME support the XDG Trash specification through GIO and the GVFS-Trash backend. You can find the code for this backend here: http://svn.gnome.org/viewvc/gvfs/trunk/daemon/ and GIO is in GLib: http://svn.gnome.org/viewvc/glib/trunk/gio/ Safe RM ======= URL: https://launchpad.net/safe-rm Safe RM does not talk about Trash Cans. The program alert you whenewer you attempt to delete files known to be important. rmtrash ======= website: http://www.nightproductions.net/cli.htm trashcan utility for Mac OS X. I think it support only trashing files (not trash listing, or restoring). Feedback ======== Feel free to leave any comment (mailing me) for any error or else. -EOF trash-cli-0.24.5.26/docs/trash-directories.txt000066400000000000000000000025311462460053300210460ustar00rootroot00000000000000How trash-put should choose in which trash directory the file should be trashed? ================================================================================ When user launches:: $ trash-put file-to-be-trashed the file can be trashed in: 1. "home trash" ($XDG_DATA_HOME/Trash) 2. primary top trash directory ($topdir/.Trash/$uid) 3. alternate top trash directory ($topdir/.Trash-$uid) With these rules: - Home Trash 1. when the file is in the same volume of the "home trash" should choose "home trash" 2. "home trash" should be available for each user 3. "home trash" if not present should created with no delay 4. "home trash" can be used as failsafe method, that means: when the file is different volume but it fails to use both the top trash directories it chose "home trash" - Primary Top Trash Dir 1. in $topdir/.Trash can be created by an admin - should be created by the admin - should permit user to create $topdir/.Trash/$uid - should not be a symbolic link - the $topdir/.Trash/$uid should not writable by others 2. should be skipped if any of the conditions above is unsatisfied 3. The $topdir/.Trash/$uid should be automatically created without delay if not present - Alternate Top Trash dir 1. Should be created with any delay if needed EOF trash-cli-0.24.5.26/docs/trashspec-1.0.html000066400000000000000000000713761462460053300200450ustar00rootroot00000000000000 Trash specification

The FreeDesktop.org Trash specification

Initial version written by Mikhail Ramendik <mr@ramendik.ru>

Content by David Faure <faure@kde.org>, Alexander Larsson <alexl@redhat.com>, Ryan Lortie <desrt@desrt.ca> and others on the FreeDesktop.org mailing list

Version 1.0

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 existed before this specification — some as command line utilities, some as preloaded libraries, and some as parts of major desktop environments. For example, both Gnome and KDE had their own trash mechanisms.

This Specification is to provide a common way in which all Trash can implementations must 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 ability 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 (for example, 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 file systems might be mounted on “/home”, “/media/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 available. Its name and location are $XDG_DATA_HOME/Trash 3; $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 (for example, 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 if 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 trashing in top directories, 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 — that is, 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, provide a way for the administrator to disable (2) completely.

If both (1) and (2) fail (that is, 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 (for example, “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 that the trashing has failed. It MUST NOT erase the file without user confirmation.

For showing trashed files, implementations SHOULD support (1) and (2) at the same time (that is, 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 (for example, 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 support absolute pathnames only 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. Moreover, it MUST try to do this in an atomic fashion, so that if two processes try to 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.

Directory size cache

In order to speed up the calculation of the total size of a particular trash directory, implementations (since version 1.0 of this specification) SHOULD create or update the $trash/directorysizes file, which is a cache of the sizes of the directories that were trashed into this trash directory. Individual trashed files are not present in this cache, since their size can be determined with a call to stat().

Each entry contains the name and size of the trashed directory, as well as the modification time of the corresponding trashinfo file (IMPORTANT: not the modification time of the directory itself)9.

The size is calculated as the disk space used by the directory and its contents, that is, the size of the blocks, in bytes (in the same way as the `du -B1` command calculates).

The modification time is stored as an integer, the number of seconds since Epoch. Implementations SHOULD use at least 64 bits for this number in memory.

The “directorysizes” file has a simple text-based format, where each line is:

[size] [mtime] [percent-encoded-directory-name]

Example:

16384 15803468 Documents
8192 15803582 Another_Folder

The last entry on each line is the name of the trashed directory, stored as the sequence of bytes produced by the file system, with characters escaped as in URLs (as defined by RFC 2396, section 2). Strictly speaking, percent-encoding is really only necessary for the newline character and for '%' itself. However, encoding all control characters or fully applying RFC 2396 for consistency with trashinfo files is perfectly valid, and even if an implementation does not use such encoding. it MUST be able to read names encoded with it.

The character '/' is not allowed in the directory name (even as %2F), since all these directories must be direct children of the "files" directory. Absolute paths are not allowed for the same reason.

To update the directorysizes file, implementations MUST use a temporary file followed by an atomic rename() operation, in order to avoid corruption due to two implementations writing to the file at the same time. The fact that the changes from one of the writers could get lost isn't an issue, as the cache can be updated again later on to add that entry.

Non-normative: suggested algorithm for calculating the size of a trash directory

load directorysizes file into memory as a hash directory_name -> (size, mtime, seen=false)
totalsize = 0
list "files" directory, and for each item:
    stat the item
    if a file:
        totalsize += file size
    if a directory:
        stat the trashinfo file to get its mtime
        lookup entry in hash
        if no entry found or entry's cached mtime != trashinfo's mtime:
            calculate directory size (from disk)
            totalsize += calculated size
            add/update entry in hash (size of directory, trashinfo's mtime, seen=true)
        else:
            totalsize += entry's cached size
            update entry in hash to set seen=true
done
remove entries from hash which have (seen == false)
write out hash back to directorysizes file

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 (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 might 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. As noted earlier, when the user reasonably expects a file to be trashed, the implementation MUST NOT delete it without warning the user.

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

Copyright and License

Copyright (C) 2004-2014 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://standards.freedesktop.org/trash-spec/trashspec-latest.html .

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

0.8 March 14, 2012. Update David Faure's email address, fix permanent URL for this spec.

1.0 January 2, 2014. Add directorysizes cache; style review.



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.

3For 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

9Rationale: if an older trash implementation restores a trashed directory, adds files to a nested subdir and trashes it again, the modification time of the directoy didn't change, so it is not a good indicator. However the modification time of the trashinfo file will have changed, since it is always the time of the actual trashing operation.

trash-cli-0.24.5.26/man/000077500000000000000000000000001462460053300144745ustar00rootroot00000000000000trash-cli-0.24.5.26/man/man1/000077500000000000000000000000001462460053300153305ustar00rootroot00000000000000trash-cli-0.24.5.26/man/man1/trash-empty.1000066400000000000000000000053151462460053300176730ustar00rootroot00000000000000.\" 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 Permanently remove any trashed file and trashed directory. This command is a part of the trash-cli package that provides a command line interface trashcan utility compliant with the FreeDesktop.org Trash Specification. It remembers the name, original path, deletion date, and permissions of each trashed file. .SH "ARGUMENTS" .TP To remove all trashed files, use 'trash-empty' without arguments. .TP To remove files that have been in the trash for 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" Please, report bugs to https://github.com/andreafrancia/trash-cli/issues .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-list(1), trash-restore(1), trash-rm(1), and the FreeDesktop.org Trash Specification at https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.24.5.26/man/man1/trash-list.1000066400000000000000000000045221462460053300175070ustar00rootroot00000000000000.\" 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]... .SH "DESCRIPTION" .PP List all the contents of the trashcan. .SH "OPTIONS" .IP "-h, --help" Show help message and exit. .IP "--trash-dir=TRASHDIR" Use TRASHDIR as trash folder. Can be used multiple times .IP "--version" Show program's version number and exit. .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" Please, report bugs to https://github.com/andreafrancia/trash-cli/issues .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-restore(1), trash-empty(1), trash-rm(1), and the FreeDesktop.org Trash Specification at https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.24.5.26/man/man1/trash-put.1000066400000000000000000000062201462460053300173410ustar00rootroot00000000000000.\" Copyright (C) 2008 Steve Stalcup .\" .\" This manual page is free software. It is distributed under the .\" terms of the GNU General Public License as published by the Free .\" Software Foundation; either version 2 of the License, or (at your .\" option) any later version. .\" .\" This manual page is distributed in the hope that it will be useful, .\" but WITHOUT ANY WARRANTY; without even the implied warranty of .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .\" GNU General Public License for more details. .\" .\" You should have received a copy of the GNU General Public License .\" along with this manual page; if not, write to the Free Software .\" Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 .\" USA .\" .TH "TRASH-PUT" "1" .SH "NAME" trash-put \- Command line trash utility. .SH "SYNOPSIS" .B trash .RI [ OPTION ] \&... .RI [ ARGUMENTS ] \&... .br .B trash-put .RI [ OPTION ] \&... .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. .br Files trashed from the home partition will be in either $XDG_DATA_HOME/Trash or ~/.local/share/Trash/ (if $XDG_DATA_HOME is not defined or empty). Files trashed from other partitions will be in one of the two places: - $top_dir/.Trash/$uid - $top_dir/.Trash-$uid Where: - $top_dir is the mount point of the volume containing the file to be removed - $uid is the numeric ID of the deleting user The first option $top_dir/.Trash/$uid works only when .Trash dir has the sticky bit set. The second option is used when the first is not viable. .SH "ARGUMENTS" .TP Names of files or directories to move to the trash can. .SH "OPTIONS" .IP "-f" .br Silently ignore any files or directories that do not exist. Do not print error messages, and do not return a nonzero status to the caller, because of any such nonexistent arguments. .IP "-h, --help" Show help message and exit. .IP "--trash-dir=TRASHDIR" Use TRASHDIR as the trash folder. .IP "-v, --verbose" Explain what is being done. .IP "--version" Show the program's version number and exit. .SH "EXAMPLES" .nf $ trash-put foo # trashes foo .fi .SH "BUGS" Please, report bugs to https://github.com/andreafrancia/trash-cli/issues .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-list(1), trash-restore(1), trash-empty(1), trash-rm(1), and the FreeDesktop.org Trash Specification at https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.24.5.26/man/man1/trash-restore.1000066400000000000000000000045411462460053300202200ustar00rootroot00000000000000.\" Copyright (C) 2008 Steve Stalcup .\" .\" This manual page is free software. It is distributed under the .\" terms of the GNU General Public License as published by the Free .\" Software Foundation; either version 2 of the License, or (at your .\" option) any later version. .\" .\" This manual page is distributed in the hope that it will be useful, .\" but WITHOUT ANY WARRANTY; without even the implied warranty of .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .\" GNU General Public License for more details. .\" .\" You should have received a copy of the GNU General Public License .\" along with this manual page; if not, write to the Free Software .\" Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 .\" USA .\" .TH "TRASH-RESTORE" "1" .SH "NAME" trash-restore \- Restore for Command line trash utility. .SH "SYNOPSIS" .B trash-restore .SH "DESCRIPTION" .PP Use to restore a trashed file or directory to the original path. This command is a part of the trash-cli package that provides a command line interface trashcan utility compliant with the FreeDesktop.org Trash Specification. It remembers the name, original path, deletion date, and permissions of each trashed file. .SH "EXAMPLES" .nf $ trash-restore 0 2007-08-30 12:36:00 /home/andrea/foo 1 2007-08-30 12:39:41 /home/andrea/bar 2 2007-08-30 12:39:41 /home/andrea/bar2 3 2007-08-30 12:39:41 /home/andrea/foo2 4 2007-08-30 12:39:41 /home/andrea/foo What file to restore [0..4]: 4 $ ls foo foo .fi .SH "BUGS" Please, report bugs to https://github.com/andreafrancia/trash-cli/issues .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "SEE ALSO" trash-put(1), trash-list(1), trash-empty(1), trash-rm(1), and the FreeDesktop.org Trash Specification at https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html. .br Both are released under the \s-1GNU\s0 General Public License, version 2 or later. trash-cli-0.24.5.26/man/man1/trash-rm.1000066400000000000000000000026311462460053300171510ustar00rootroot00000000000000.\" Copyright (C) 2012 Andrea Francia .TH "TRASH-RM" "1" .SH "NAME" trash-rm \- Removes files matching a pattern from the trash can .SH "SYNOPSIS" .B trash-rm .RI [ PATTERN ] .SH "DESCRIPTION" .PP Remove from the trash can all the files matching the PATTERN. .SH "EXAMPLE" .nf $ trash-rm foo # Removes all files with name 'foo' in trash can $ trash-rm '*.o' # Removes all files ending with '.o' in trash can $ trash-rm /full/path # Removes all files with exactly '/full/path' as original location .fi .SH "BUGS" Please, report bugs to https://github.com/andreafrancia/trash-cli/issues .SH "IF YOU WANT SAY THANKS" Tweet (to @andreafrancia or #trash-cli) or donate 1, 2 or 5 euros using paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=93L6PYT4WBN5A .SH "CREDITS" Trash was written by Andrea Francia . The original version of trash-empty was contributed by Einar Orn Olason . The first version of manual pages was written by Steve Stalcup and was modified by Massimo Cavalleri . .SH "COPYRIGHT" Both are released under the \s-1GNU\s0 General Public License, version 2. .SH "SEE ALSO" trash-put(1), trash-list(1), trash-empty(1), trash-restore(1), and the FreeDesktop.org Trash Specification at https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html. .br trash-cli-0.24.5.26/man/man1/trash.1000077700000000000000000000000001462460053300205372trash-put.1ustar00rootroot00000000000000trash-cli-0.24.5.26/mypy.ini000066400000000000000000000000371462460053300154200ustar00rootroot00000000000000[mypy] files = tests, trashcli trash-cli-0.24.5.26/pyproject.toml000066400000000000000000000001321462460053300166310ustar00rootroot00000000000000[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" trash-cli-0.24.5.26/requirements-dev.txt000066400000000000000000000004731462460053300177650ustar00rootroot00000000000000# testing pytest mock flexmock parameterized tox PyYAML <= 5.4.1; python_version ~= '2.7' PyYAML <= 5.2; python_version ~= '3.4' PyYAML <= 5.3.1; python_version ~= '3.5' PyYAML; python_version >= '3.6' # build and publish build setuptools twine # types mypy; python_version >= '3' types-six types-mock types-psutil trash-cli-0.24.5.26/requirements.txt000066400000000000000000000001621462460053300172040ustar00rootroot00000000000000psutil six typing; python_version < '3.8' typing_extensions; python_version < '3.8' enum34; python_version < '3.4'trash-cli-0.24.5.26/scripts/000077500000000000000000000000001462460053300154105ustar00rootroot00000000000000trash-cli-0.24.5.26/scripts/bump000077500000000000000000000004221462460053300162770ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function if __name__ == '__main__': import os import sys root = os.path.realpath(os.path.join(__file__, "..", "..")) sys.path.insert(0, root) from tests.support.tools.bump_main import main main() trash-cli-0.24.5.26/scripts/check-types000077500000000000000000000002251462460053300175540ustar00rootroot00000000000000#!/bin/bash SCRIPT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" "$SCRIPT_DIR/lib/check-python-dep" mypy mypy trashcli trash-cli-0.24.5.26/scripts/docker-bash000077500000000000000000000001271462460053300175200ustar00rootroot00000000000000#!/bin/bash docker run -it --privileged --volume="$(pwd):/trash-cli" python:slim bash trash-cli-0.24.5.26/scripts/lib/000077500000000000000000000000001462460053300161565ustar00rootroot00000000000000trash-cli-0.24.5.26/scripts/lib/check-python-dep000077500000000000000000000004321462460053300212450ustar00rootroot00000000000000#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" COMMAND="$1" test -x "$VIRTUAL_ENV/bin/$COMMAND" || { >&2 echo "$COMMAND not installed Please run: pip install -r requirements.txt -r requirements-dev.txt " } trash-cli-0.24.5.26/scripts/lib/install-python-requirements000077500000000000000000000001031462460053300236040ustar00rootroot00000000000000#!/bin/bash pip install -r requirements.txt -r requirements-dev.txttrash-cli-0.24.5.26/scripts/pre-push000077500000000000000000000004701462460053300171020ustar00rootroot00000000000000#!/bin/bash SCRIPT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" set -euo pipefail set -x python -m venv .venv source .venv/bin/activate "$SCRIPT_DIR/lib/install-python-requirements" "$SCRIPT_DIR/lib/check-python-dep" tox tox && scripts/test-sdist && scripts/check-types trash-cli-0.24.5.26/scripts/prepare-test-disks000077500000000000000000000016141462460053300210660ustar00rootroot00000000000000#!/bin/bash # Copyright (C) 2007-2022 Andrea Francia Trivolzio(PV) Italy set -o errexit main() { make-new-disk "/tmp/test-disk1.img" "/media/test-disk1" /sbin/mkfs.ext2 make-new-disk "/tmp/test-disk2.img" "/media/test-disk2" /sbin/mkfs.ext2 } make-new-disk() { local device_image="$1" local mount_point="$2" local mkfs_command="$3" umount "$mount_point" || true rm -fv "$device_image" dd if=/dev/zero of="$device_image" bs=$((1024*1024)) count=1 "$mkfs_command" -F "$device_image" sudo mkdir --parents "$mount_point" sudo mount -t ext2 "$device_image" "$mount_point" -o loop sudo chmod a+rwx "$mount_point" echo "test-volume mounted as '$mount_point'" } # for macOS: # mkdir -p test-disk # hdiutil detach ./test-disk || true # rm -f test-disk.dmg # hdiutil create -size 1M -fs HFS+ test-disk # hdiutil attach test-disk.dmg -mountpoint test-disk main trash-cli-0.24.5.26/scripts/run-tests000077500000000000000000000003311462460053300172770ustar00rootroot00000000000000#!/bin/bash SCRIPT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" set -euo pipefail python -m pip install --upgrade pip "$SCRIPT_DIR/lib/install-python-requirements" python -m pytest "$@" trash-cli-0.24.5.26/scripts/set-dev-version000077500000000000000000000003621462460053300203710ustar00rootroot00000000000000#!/usr/bin/env python if __name__ == '__main__': import os import sys root = os.path.realpath(os.path.join(__file__, "..", "..")) sys.path.insert(0, root) from tests.support.tools.set_dev_version import main main() trash-cli-0.24.5.26/scripts/test-on000077500000000000000000000004501462460053300167260ustar00rootroot00000000000000#!/bin/bash SCRIPT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" set -euo pipefail export PYENV_VERSION="${1?}" python --version python -m venv ".venv-$PYENV_VERSION" source ".venv-$PYENV_VERSION/bin/activate" "$SCRIPT_DIR/lib/install-python-requirements" pytest trash-cli-0.24.5.26/scripts/test-sdist000077500000000000000000000011301462460053300174340ustar00rootroot00000000000000#!/bin/bash set -xe function my_python() { if which python >/dev/null then python "$@" else python3 "$@" fi } pip install --upgrade pip pip install --upgrade build pip install --upgrade setuptools version="$(my_python -c 'import setuptools; setuptools.setup()' --version)" rm -f dist/*.tar.gz my_python -m build --sdist pip install dist/trash[_-]cli-$version.tar.gz trash --version trash-put --version trash-list --version trash-rm --version trash-restore --version trash-empty --version touch foo bar trash-put foo bar trash-list trash-rm bar echo | trash-empty trash-cli-0.24.5.26/scripts/test-sdist-with-docker000077500000000000000000000007451462460053300216650ustar00rootroot00000000000000#!/bin/bash set -euo pipefail usage=" Usage: $BASH_SOURCE PYTHON_VERSION Example: $BASH_SOURCE 2.7 Which values can I use for PYTHON_VERSION? All the supported tags you can find on the docker official image of Python: https://hub.docker.com/_/python " python_version=${1?"missing PYTHON_VERSION $usage "} docker run --rm -it "$( cat << EOF | docker build -t trash-cli-sdist -f- -q . FROM python:$python_version COPY . /app WORKDIR /app ENTRYPOINT scripts/test-sdist EOF )" trash-cli-0.24.5.26/setup.cfg000066400000000000000000000022011462460053300155350ustar00rootroot00000000000000[metadata] name = trash-cli version = attr: trashcli.trash.version author = Andrea Francia author_email = andrea@andreafrancia.it url = https://github.com/andreafrancia/trash-cli description = Command line interface to FreeDesktop.org Trash. long_description = file: README.rst license = GPL v2 long_description_content_type = text/x-rst [options] packages = trashcli trashcli.empty trashcli.lib trashcli.list trashcli.list.minor_actions trashcli.put trashcli.put.core trashcli.put.fs trashcli.put.janitor_tools trashcli.put.reporting trashcli.restore trashcli.rm trashcli.fstab trashcli.parse_trashinfo scripts = trash trash-put trash-list trash-restore trash-empty trash-rm install_requires = psutil six typing; python_version < '3.8' typing_extensions; python_version < '3.8' enum34; python_version < '3.4' [options.extras_require] completion = shtab [options.data_files] share/man/man1 = man/man1/trash-empty.1 man/man1/trash-list.1 man/man1/trash-restore.1 man/man1/trash.1 man/man1/trash-put.1 man/man1/trash-rm.1 trash-cli-0.24.5.26/setup.py000066400000000000000000000001431462460053300154310ustar00rootroot00000000000000# Copyright (C) 2007-2021 Andrea Francia Trivolzio(PV) Italy from setuptools import setup setup() trash-cli-0.24.5.26/tests/000077500000000000000000000000001462460053300150635ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/__init__.py000066400000000000000000000000001462460053300171620ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/000077500000000000000000000000001462460053300165775ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/__init__.py000066400000000000000000000000001462460053300206760ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/asserts/000077500000000000000000000000001462460053300202635ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/asserts/__init__.py000066400000000000000000000015171462460053300224000ustar00rootroot00000000000000import unittest 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) assert expected == actual, ("\n" "Expected:%s\n" % repr(expected) + " Actual:%s\n" % repr(actual) + unidiff(expected, actual)) def assert_starts_with(actual, expected): class Dummy(unittest.TestCase): def nop(self): pass _t = Dummy('nop') _t.assertEqual(actual[:len(expected)], expected) trash-cli-0.24.5.26/tests/support/asserts/assert_that.py000066400000000000000000000001451462460053300231560ustar00rootroot00000000000000def assert_that(value, matcher): assert matcher.matches(value), matcher.describe_mismatch(value) trash-cli-0.24.5.26/tests/support/capture_error.py000066400000000000000000000001651462460053300220270ustar00rootroot00000000000000def capture_error(callable): try: callable() except Exception as e: return e return None trash-cli-0.24.5.26/tests/support/capture_exit_code.py000066400000000000000000000001771462460053300226440ustar00rootroot00000000000000def capture_exit_code(callable): try: callable() except SystemExit as e: return e.code return None trash-cli-0.24.5.26/tests/support/dicts.py000066400000000000000000000001051462460053300202530ustar00rootroot00000000000000def merge_dicts(x, y): z = x.copy() z.update(y) return z trash-cli-0.24.5.26/tests/support/dirs/000077500000000000000000000000001462460053300175405ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/dirs/__init__.py000066400000000000000000000000001462460053300216370ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/dirs/list_file_in_dir.py000066400000000000000000000001671462460053300234140ustar00rootroot00000000000000import os def list_files_in_dir(path): if not os.path.isdir(path): return [] return os.listdir(path) trash-cli-0.24.5.26/tests/support/dirs/list_file_in_subdir.py000066400000000000000000000002711462460053300241220ustar00rootroot00000000000000from tests.support.dirs.list_file_in_dir import list_files_in_dir def list_files_in_subdir(path, subdir): return ["%s/%s" % (subdir, f) for f in list_files_in_dir(path / subdir)] trash-cli-0.24.5.26/tests/support/dirs/my_path.py000066400000000000000000000027511462460053300215600ustar00rootroot00000000000000import os import shutil import tempfile from tests.support.files import make_sticky_dir from trashcli.put.fs.real_fs import RealFs from trashcli.put.fs.fs import list_all class MyPath(str): def __truediv__(self, other_path): return self.path_join(other_path) def __div__(self, other_path): return self.path_join(other_path) def path_join(self, other_path): return MyPath(os.path.join(self, other_path)) def existence_of(self, *paths): return [self.existence_of_single(p) for p in paths] def existence_of_single(self, path): # type: (MyPath) -> str path = self / path existence = os.path.exists(path) existence_message = { True: "exists", False: "does not exist" }[existence] return "%s: %s" % (path.replace(self, ''), existence_message) def mkdir_rel(self, path): RealFs().mkdir(self / path) def symlink_rel(self, src, dest): RealFs().symlink(self / src, self / dest) def list_dir_rel(self): return RealFs().listdir(self) @property def parent(self): # type: (...) -> MyPath return MyPath(os.path.dirname(self)) def clean_up(self): shutil.rmtree(self) @classmethod def make_temp_dir(cls): return cls(os.path.realpath(tempfile.mkdtemp(suffix="_trash_cli_test"))) def list_all_files_sorted(self): return sorted([p.replace(self, '') for p in list_all(RealFs(), self)]) trash-cli-0.24.5.26/tests/support/dirs/remove_dir_if_exists.py000066400000000000000000000001351462460053300243210ustar00rootroot00000000000000import os def remove_dir_if_exists(dir): if os.path.exists(dir): os.rmdir(dir) trash-cli-0.24.5.26/tests/support/dirs/temp_dir.py000066400000000000000000000002601462460053300217130ustar00rootroot00000000000000import pytest from tests.support.dirs.my_path import MyPath @pytest.fixture def temp_dir(): temp_dir = MyPath.make_temp_dir() yield temp_dir temp_dir.clean_up() trash-cli-0.24.5.26/tests/support/fakes/000077500000000000000000000000001462460053300176705ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/fakes/__init__.py000066400000000000000000000000001462460053300217670ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/fakes/fake_file_system.py000066400000000000000000000012071462460053300235530ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy 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 trash-cli-0.24.5.26/tests/support/fakes/fake_is_mount.py000066400000000000000000000010511462460053300230620ustar00rootroot00000000000000import os from typing import List class FakeIsMount: def __init__(self, mount_points, # type: List[str] ): self.mount_points = mount_points def is_mount(self, path): if path == '/': return True path = os.path.normpath(path) if path in self.mount_points_list(): return True return False def mount_points_list(self): return set(['/'] + self.mount_points) def add_mount_point(self, path): self.mount_points.append(path) trash-cli-0.24.5.26/tests/support/fakes/fake_trash_dir.py000066400000000000000000000107451462460053300232160ustar00rootroot00000000000000import datetime import os import uuid from tests.support.files import does_not_exist from tests.support.files import is_a_symlink_to_a_dir from tests.support.files import make_sticky_dir from tests.support.files import make_unsticky_dir from trashcli.put.format_trash_info import format_original_location from tests.support.files import make_file, make_parent_for, make_unreadable_file from tests.support.trashinfo.parse_date import parse_date def a_default_datetime(): return datetime.datetime(2000, 1, 1, 0, 0, 1) class FakeTrashDir: def __init__(self, path): self.path = path self.info_path = os.path.join(path, 'info') self.files_path = os.path.join(path, 'files') def add_unreadable_trashinfo(self, basename): path = self.a_trashinfo_path(basename) make_unreadable_file(path) def add_trashed_file(self, basename, path, content, date=a_default_datetime()): self.add_trashinfo3(basename, path, date) make_file(self.file_path(basename), content) def a_trashinfo_path(self, basename): return os.path.join(self.info_path, '%s.trashinfo' % basename) def file_path(self, basename): return os.path.join(self.files_path, basename) def add_trashinfo_basename_path(self, basename, path): self.add_trashinfo3(basename, path, a_default_datetime()) def add_trashinfo2(self, path, deletion_date): basename = str(uuid.uuid4()) self.add_trashinfo3(basename, path, deletion_date) def add_trashinfo3(self, basename, path, deletion_date): content = trashinfo_content(path, deletion_date) self.add_trashinfo_content(basename, content) def add_a_valid_trashinfo(self): self.add_trashinfo4('file1', "2000-01-01") def add_trashinfo4(self, path, deletion_date_as_string): if isinstance(deletion_date_as_string, datetime.datetime): raise ValueError("Use a string instead: %s" % repr(deletion_date_as_string.strftime('%Y-%m-%d %H:%M:%S'))) basename = str(uuid.uuid4()) deletion_date = parse_date(deletion_date_as_string) self.add_trashinfo3(basename, path, deletion_date) def add_trashinfo_with_date(self, basename, deletion_date): content = trashinfo_content2([ ("DeletionDate", deletion_date.strftime('%Y-%m-%dT%H:%M:%S')), ]) self.add_trashinfo_content(basename, content) def add_trashinfo_with_invalid_date(self, basename, invalid_date): content = trashinfo_content2([ ("DeletionDate", invalid_date), ]) self.add_trashinfo_content(basename, content) def add_trashinfo_without_path(self, basename): deletion_date = a_default_datetime() content = trashinfo_content2([ ("DeletionDate", deletion_date.strftime('%Y-%m-%dT%H:%M:%S')), ]) self.add_trashinfo_content(basename, content) def add_trashinfo_without_date(self, path): basename = str(uuid.uuid4()) content = trashinfo_content2([ ('Path', format_original_location(path)), ]) self.add_trashinfo_content(basename, content) def add_trashinfo_wrong_date(self, path, wrong_date): basename = str(uuid.uuid4()) content = trashinfo_content2([ ('Path', format_original_location(path)), ("DeletionDate", wrong_date), ]) self.add_trashinfo_content(basename, content) def add_trashinfo_content(self, basename, content): trashinfo_path = self.a_trashinfo_path(basename) make_parent_for(trashinfo_path) make_file(trashinfo_path, content) def ls_info(self): return os.listdir(self.info_path) def make_parent_sticky(self): make_sticky_dir(self.path.parent) def make_parent_unsticky(self): make_unsticky_dir(self.path.parent) def make_parent_symlink(self): is_a_symlink_to_a_dir(self.path.parent) def make_dir(self): os.mkdir(self.path) def does_not_exist(self): does_not_exist(self.path) def trashinfo_content_default_date(path): return trashinfo_content(path, a_default_datetime()) def trashinfo_content(path, deletion_date): return trashinfo_content2([ ('Path', format_original_location(path)), ("DeletionDate", deletion_date.strftime('%Y-%m-%dT%H:%M:%S')), ]) def trashinfo_content2(values): return ("[Trash Info]\n" + "".join("%s=%s\n" % (name, value) for name, value in values)) trash-cli-0.24.5.26/tests/support/fakes/fake_volume_of.py000066400000000000000000000010741462460053300232250ustar00rootroot00000000000000import os from typing import List from tests.support.fakes.fake_is_mount import FakeIsMount from trashcli.fstab.volume_of import VolumeOf from trashcli.fstab.volume_of_impl import VolumeOfImpl class FakeVolumeOf(VolumeOf): def __init__(self): # type: () -> None super(FakeVolumeOf, self).__init__() self.volumes = [] # type: List[str] def add_volume(self, volume): self.volumes.append(volume) def volume_of(self, path): impl = VolumeOfImpl(FakeIsMount(self.volumes), os.path.normpath) return impl.volume_of(path) trash-cli-0.24.5.26/tests/support/fakes/mock_dir_reader.py000066400000000000000000000017201462460053300233530ustar00rootroot00000000000000import os from typing import List from trashcli.lib.dir_reader import DirReader class MockDirReader(DirReader): def __init__(self): self.root = {} def entries_if_dir_exists(self, path): # type: (str) -> List[str] return list(self.pick_dir(path).keys()) def exists(self, path): # type: (str) -> bool raise NotImplementedError() def add_file(self, path): dirname, basename = os.path.split(path) dir = self.pick_dir(dirname) dir[basename] = '' def mkdir(self, path): dirname, basename = os.path.split(path) cwd = self.pick_dir(dirname) cwd[basename] = {} def pick_dir(self, dir): cwd = self.root components = dir.split('/')[1:] if components != ['']: for p in components: if p not in cwd: raise FileNotFoundError("no such file or directory: %s" % dir) cwd = cwd[p] return cwd trash-cli-0.24.5.26/tests/support/fakes/stub_volume_of.py000066400000000000000000000002221462460053300232660ustar00rootroot00000000000000from trashcli.fstab.volume_of import VolumeOf class StubVolumeOf(VolumeOf): def volume_of(self, path): return "volume_of %s" % path trash-cli-0.24.5.26/tests/support/files.py000066400000000000000000000040631462460053300202560ustar00rootroot00000000000000import os import shutil from trashcli.fs import has_sticky_bit from trashcli.fs import mkdirs from trashcli.fs import write_file def mkdir_p(path): if not os.path.isdir(path): os.makedirs(path) def make_empty_file(path): make_file(path, '') def make_file(filename, contents=''): make_parent_for(filename) write_file(filename, contents) def require_empty_dir(path): if os.path.exists(path): shutil.rmtree(path) make_dirs(path) check_empty_dir(path) def make_empty_dir(path): os.mkdir(path) check_empty_dir(path) def check_empty_dir(path): assert os.path.isdir(path) assert [] == list(os.listdir(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(os.path.realpath(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) def ensure_non_sticky_dir(path): import os assert os.path.isdir(path) assert not has_sticky_bit(path) def make_unreadable_file(path): make_file(path, '') import os os.chmod(path, 0) def make_unreadable_dir(path): mkdirs(path) os.chmod(path, 0o300) def make_readable(path): os.chmod(path, 0o700) def assert_dir_empty(path): assert len(os.listdir(path)) == 0 def assert_dir_contains(path, filename): assert os.path.exists(os.path.join(path, filename)) def does_not_exist(path): assert not os.path.exists(path) def is_a_symlink_to_a_dir(path): dest = "%s-dest" % path os.mkdir(dest) rel_dest = os.path.basename(dest) os.symlink(rel_dest, path) trash-cli-0.24.5.26/tests/support/help/000077500000000000000000000000001462460053300175275ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/help/__init__.py000066400000000000000000000000001462460053300216260ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/help/help_reformatting.py000066400000000000000000000016021462460053300236110ustar00rootroot00000000000000def reformat_help_message(help_message): # type: (str) -> str return _collapse_usage_paragraph(_normalize_options(help_message)) def _normalize_options(help_message): # type: (str) -> str return help_message.replace('optional arguments', 'options') def _collapse_usage_paragraph(help_message): # type: (str) -> str paragraphs = split_paragraphs(help_message) return '\n'.join( [normalize_spaces(paragraphs[0]) + "\n"] + paragraphs[1:]) def normalize_spaces(text): # type: (str) -> str return " ".join(text.split()) def split_paragraphs(text): paragraphs = [] par = '' for line in text.splitlines(True): if _is_empty_line(line): paragraphs.append(par) par = '' else: par += line paragraphs.append(par) return paragraphs def _is_empty_line(line): return '' == line.strip() trash-cli-0.24.5.26/tests/support/make_scripts.py000066400000000000000000000021531462460053300216360ustar00rootroot00000000000000import os from textwrap import dedent from tests.support.project_root import project_root from trashcli.fs import write_file, make_file_executable def make_scripts(): return Scripts(write_file, make_file_executable) class Scripts: def __init__(self, write_file, make_file_executable): self.write_file = write_file self.make_file_executable = make_file_executable self.created_scripts = [] def add_script(self, name, module, main_function): path = script_path_for(name) script_contents = dedent("""\ #!/usr/bin/env python from __future__ import absolute_import import sys from %(module)s import %(main_function)s as main sys.exit(main()) """) % locals() self.write_file(path, script_contents) self.make_file_executable(path) self.created_scripts.append(script_path_without_base_dir_for(name)) def script_path_for(name): return os.path.join(project_root(), script_path_without_base_dir_for(name)) def script_path_without_base_dir_for(name): return os.path.join(name) trash-cli-0.24.5.26/tests/support/project_root.py000066400000000000000000000003131462460053300216570ustar00rootroot00000000000000import os def project_root(): this_file = os.path.realpath(__file__) support_dir = os.path.dirname(this_file) tests_dir = os.path.dirname(support_dir) return os.path.dirname(tests_dir) trash-cli-0.24.5.26/tests/support/put/000077500000000000000000000000001462460053300174075ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/put/__init__.py000066400000000000000000000000001462460053300215060ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/put/dummy_clock.py000066400000000000000000000007541462460053300222750ustar00rootroot00000000000000import datetime from trashcli.put.clock import PutClock class FixedClock(PutClock): def __init__(self, now_value=None): self.now_value = now_value def set_clock(self, now_value): self.now_value = now_value def now(self): # type: () -> datetime.datetime return self.now_value @staticmethod def fixet_at_jan_1st_2024(): return FixedClock(now_value=jan_1st_2024()) def jan_1st_2024(): return datetime.datetime(2014, 1, 1, 0, 0, 0) trash-cli-0.24.5.26/tests/support/put/fake_fs/000077500000000000000000000000001462460053300210055ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/put/fake_fs/__init__.py000066400000000000000000000000001462460053300231040ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/put/fake_fs/directory.py000066400000000000000000000052601462460053300233660ustar00rootroot00000000000000from collections import OrderedDict from typing import Optional from typing import Union from tests.support.put.fake_fs.ent import Ent from tests.support.put.fake_fs.inode import INode from tests.support.put.fake_fs.inode import Stickiness from tests.support.put.fake_fs.symlink import SymLink from tests.support.put.fake_fs.file import File from tests.support.put.my_file_not_found_error import MyFileNotFoundError from trashcli.lib.my_permission_error import MyPermissionError def make_inode_dir(directory_path, # type: str mode, # type: int parent_inode, # type: Optional[INode] ): # type: (...)->INode directory = Directory(directory_path) inode = INode(directory, mode, Stickiness.not_sticky) directory.set_dot_entries(inode, parent_inode or inode) return inode class Directory(Ent): def __init__(self, name): # type: (str) -> None self.name = name self._entries = OrderedDict() # type: dict[str, Union[INode, SymLink]] def __repr__(self): return "Directory(%r)" % self.name def set_dot_entries(self, inode, parent_inode): self._entries['.'] = inode self._entries['..'] = parent_inode def entries(self): return self._entries.keys() def add_dir(self, basename, mode, complete_path): if self._inode().mode & 0o200 == 0: raise MyPermissionError( "[Errno 13] Permission denied: '%s'" % complete_path) inode = make_inode_dir(basename, mode, self._inode()) self._entries[basename] = inode def add_file(self, basename, content, complete_path): mode = 0o644 if self._inode().mode & 0o200 == 0: raise MyPermissionError( "[Errno 13] Permission denied: '%s'" % complete_path) file = File(content) inode = INode(file, mode, Stickiness.not_sticky) self._entries[basename] = inode def _inode(self): # type: ()->Union[INode, SymLink] return self._entries["."] def get_file(self, basename): return self._entries[basename].entity def get_entry(self, basename, path, fs): try: return self._entries[basename] except KeyError: raise MyFileNotFoundError( "no such file or directory: %s\n%s" % ( path, "\n".join(fs.find_all()), )) def add_entry(self, basename, entry): self._entries[basename] = entry def add_link(self, basename, src): self._entries[basename] = SymLink(src) def remove(self, basename): self._entries.pop(basename) def getsize(self): raise NotImplementedError trash-cli-0.24.5.26/tests/support/put/fake_fs/ent.py000066400000000000000000000001051462460053300221410ustar00rootroot00000000000000from trashcli.compat import Protocol class Ent(Protocol): pass trash-cli-0.24.5.26/tests/support/put/fake_fs/entry.py000066400000000000000000000002411462460053300225150ustar00rootroot00000000000000from typing import Union from tests.support.put.fake_fs.inode import INode from tests.support.put.fake_fs.symlink import SymLink Entry = Union[INode, SymLink] trash-cli-0.24.5.26/tests/support/put/fake_fs/failing_fake_fs.py000066400000000000000000000034551462460053300244550ustar00rootroot00000000000000import os.path from tests.support.put.fake_fs.fake_fs import FakeFs class FailingOnAtomicWriteFakeFs(FakeFs): def __init__(self): super(FailingOnAtomicWriteFakeFs, self).__init__() self._atomic_write_can_fail = False self._atomic_write_failure_stop = None def fail_atomic_create_unless(self, basename): self._atomic_write_can_fail = True self._atomic_write_failure_stop = basename def atomic_write(self, path, content): if self._atomic_write_is_supposed_to_fail(path): raise OSError("atomic_write failed") return super(FailingOnAtomicWriteFakeFs, self).atomic_write(path, content) def _atomic_write_is_supposed_to_fail(self, path, # type: str ): # type: (...) -> bool result = (self._atomic_write_can_fail and os.path.basename(path) != self._atomic_write_failure_stop) return result class FailOnMoveFakeFs(FakeFs): def __init__(self): super(FailOnMoveFakeFs, self).__init__() self._fail_move_on_path = None def move(self, src, dest): if src == self._fail_move_on_path: raise OSError("move failed") return super(FailOnMoveFakeFs, self).move(src, dest) def fail_move_on(self, path): self._fail_move_on_path = path class FailingFakeFs(FailingOnAtomicWriteFakeFs, FailOnMoveFakeFs): def __init__(self): super(FailingFakeFs, self).__init__() def assert_does_not_exist(self, path): if self.exists(path): raise AssertionError( "expected path to not exists but it does: %s" % path) trash-cli-0.24.5.26/tests/support/put/fake_fs/fake_fs.py000066400000000000000000000213601462460053300227570ustar00rootroot00000000000000import errno import os from typing import cast from tests.support.fakes.fake_volume_of import FakeVolumeOf from tests.support.put.fake_fs.directory import Directory from tests.support.put.fake_fs.directory import make_inode_dir from tests.support.put.fake_fs.ent import Ent from tests.support.put.fake_fs.entry import Entry from tests.support.put.fake_fs.file import File from tests.support.put.fake_fs.inode import INode from tests.support.put.fake_fs.inode import Stickiness from tests.support.put.fake_fs.symlink import SymLink from tests.support.put.format_mode import format_mode from tests.support.put.my_file_not_found_error import MyFileNotFoundError from trashcli.fs import PathExists from trashcli.put.check_cast import check_cast from trashcli.put.fs.fs import Fs from trashcli.put.fs.fs import list_all def as_directory(ent): # type: (Ent) -> Directory return check_cast(Directory, ent) def as_inode(entry): # type: (Entry) -> INode return check_cast(INode, entry) class FakeFs(FakeVolumeOf, Fs, PathExists): def __init__(self, cwd='/'): super(FakeFs, self).__init__() self.root_inode = make_inode_dir('/', 0o755, None) self.root = self.root_inode.directory() self.cwd = cwd def touch(self, path): if not self.exists(path): self.make_file(path, '') def listdir(self, path): return self.ls_aa(path) def ls_existing(self, paths): return [p for p in paths if self.exists(p)] def ls_aa(self, path): all_entries = self.ls_a(path) all_entries.remove(".") all_entries.remove("..") return all_entries def ls_a(self, path): directory = self.get_entity_at(path) return list(directory.entries()) def mkdir(self, path): dirname, basename = os.path.split(path) directory = self.get_entity_at(dirname) directory.add_dir(basename, 0o755, path) def mkdir_p(self, path): self.makedirs(path, 0o755) def get_entity_at(self, path): # type: (str) -> Ent inode = check_cast(INode, self.get_entry_at(path)) return inode.entity def get_directory_at(self, path): return as_directory(self.get_entity_at(path)) def get_entry_at(self, path): # type: (str) -> Entry path = self._join_cwd(path) entry = self.root_inode for component in self.components_for(path): entry = as_inode(entry).directory().get_entry(component, path, self) return entry def makedirs(self, path, mode): path = self._join_cwd(path) inode = self.root_inode for component in self.components_for(path): try: inode = inode.directory().get_entry(component, path, self) except MyFileNotFoundError: directory = inode.directory() directory.add_dir(component, mode, path) inode = directory.get_entry(component, path, self) def _join_cwd(self, path): return os.path.join(os.path.join("/", self.cwd), path) def components_for(self, path): if path == '/': return [] return path.split('/')[1:] def atomic_write(self, path, content): if self.exists(path): raise OSError("already exists: %s" % path) self.make_file(path, content) def read(self, path): path = self._join_cwd(path) dirname, basename = os.path.split(os.path.normpath(path)) directory = as_directory(self.get_entity_at(dirname)) entry = directory.get_entry(basename, path, self) if isinstance(entry, SymLink): link_target = self.readlink(path) return self.read(os.path.join(dirname, link_target)) elif isinstance(as_inode(entry).entity, File): return cast(File, as_inode(entry).entity).content else: raise IOError("Unable to read: %s" % path) def readlink(self, path): path = self._join_cwd(path) maybe_link = self.get_entry_at(path) if isinstance(maybe_link, SymLink): return maybe_link.dest else: raise OSError(errno.EINVAL, "Invalid argument", path) def read_null(self, path): try: return self.get_entity_at(path).content except MyFileNotFoundError: return None def make_file_and_dirs(self, path, content=''): path = self._join_cwd(path) dirname, basename = os.path.split(path) self.makedirs(dirname, 0o755) self.make_file(path, content) def make_file(self, path, content=''): path = self._join_cwd(path) dirname, basename = os.path.split(path) directory = self.get_entity_at(dirname) directory.add_file(basename, content, path) def write_file(self, path, content): self.make_file(path, content) def get_mod(self, path): entry = self._find_entry(path) return entry.mode def _find_entry(self, path): path = self._join_cwd(path) dirname, basename = os.path.split(path) directory = self.get_entity_at(dirname) return directory.get_entry(basename, path, self) def chmod(self, path, mode): entry = self._find_entry(path) entry.chmod(mode) def isdir(self, path): try: entry = self.get_entry_at(path) except MyFileNotFoundError: return False else: if isinstance(entry, SymLink): return False file = entry.entity return isinstance(file, Directory) def exists(self, path): try: self.get_entity_at(path) return True except MyFileNotFoundError: return False def remove_file(self, path): dirname, basename = os.path.split(path) directory = self.get_entity_at(dirname) directory.remove(basename) def move(self, src, dest): basename, entry = self._pop_entry_from_dir(src) if self.exists(dest) and self.isdir(dest): dest_dir = self.get_directory_at(dest) dest_dir.add_entry(basename, entry) else: dest_dirname, dest_basename = os.path.split(dest) dest_dir = self.get_directory_at(dest_dirname) dest_dir.add_entry(dest_basename, entry) def _pop_entry_from_dir(self, path): dirname, basename = os.path.split(path) directory = self.get_directory_at(dirname) entry = directory.get_entry(basename, path, self) directory.remove(basename) return basename, entry def islink(self, path): try: entry = self._find_entry(path) except MyFileNotFoundError: return False else: return isinstance(entry, SymLink) def symlink(self, src, dest): dest = os.path.join(self.cwd, dest) dirname, basename = os.path.split(dest) if dirname == '': raise OSError("only absolute dests are supported, got %s" % dest) directory = as_directory(self.get_entity_at(dirname)) directory.add_link(basename, src) def has_sticky_bit(self, path): return self._find_entry(path).stickiness is Stickiness.sticky def set_sticky_bit(self, path): entry = self._find_entry(path) entry.stickiness = Stickiness.sticky def realpath(self, path): path = self._join_cwd(path) return os.path.join("/", path) def cd(self, path): self.cwd = path def isfile(self, path): try: file = self.get_entity_at(path) except MyFileNotFoundError: return False return isinstance(file, File) def getsize(self, path): file = self.get_entity_at(path) return file.getsize() def is_accessible(self, path): return self.exists(path) def get_mod_s(self, path): mode = self.get_mod(path) return format_mode(mode) def walk_no_follow(self, top): names = self.listdir(top) dirs, nondirs = [], [] for name in names: if self.isdir(os.path.join(top, name)): dirs.append(name) else: nondirs.append(name) yield top, dirs, nondirs for name in dirs: new_path = os.path.join(top, name) if not self.islink(new_path): for x in self.walk_no_follow(new_path): yield x def lexists(self, path): path = self._join_cwd(path) try: self.get_entry_at(path) except MyFileNotFoundError: return False else: return True def find_all(self): return list(list_all(self, "/")) def read_all_files(self): return [(f, self.read(f)) for f in list_all(self, "/") if self.isfile(f)] trash-cli-0.24.5.26/tests/support/put/fake_fs/file.py000066400000000000000000000002721462460053300222770ustar00rootroot00000000000000from tests.support.put.fake_fs.ent import Ent class File(Ent): def __init__(self, content): self.content = content def getsize(self): return len(self.content) trash-cli-0.24.5.26/tests/support/put/fake_fs/inode.py000066400000000000000000000013751462460053300224630ustar00rootroot00000000000000from enum import Enum from tests.support.put.fake_fs.ent import Ent from trashcli.put.check_cast import check_cast class Stickiness(Enum): sticky = "sticky" not_sticky = "not_sticky" class INode: def __init__(self, entity, # type: Ent mode, # type: int stickiness, # type: Stickiness ): self.entity = entity self.mode = mode self.stickiness = stickiness def chmod(self, mode): self.mode = mode def __repr__(self): return "INode(%r, %r, %r)" % (self.entity, self.mode, self.stickiness) def directory(self): from tests.support.put.fake_fs.directory import Directory return check_cast(Directory, self.entity) trash-cli-0.24.5.26/tests/support/put/fake_fs/symlink.py000066400000000000000000000002101462460053300230360ustar00rootroot00000000000000class SymLink: def __init__(self, dest): self.dest = dest def __repr__(self): return "SymLink(%r)" % self.dest trash-cli-0.24.5.26/tests/support/put/fake_random.py000066400000000000000000000005541462460053300222330ustar00rootroot00000000000000from typing import List from trashcli.put.core.int_generator import IntGenerator class FakeRandomInt(IntGenerator): def __init__(self, values, # type: List[int] ): self.values = values def new_int(self, _a, _b): return self.values.pop(0) def set_reply(self, value): self.values = [value] trash-cli-0.24.5.26/tests/support/put/format_mode.py000066400000000000000000000000621462460053300222530ustar00rootroot00000000000000def format_mode(mode): return "0o%03o" % mode trash-cli-0.24.5.26/tests/support/put/my_file_not_found_error.py000066400000000000000000000001661462460053300246740ustar00rootroot00000000000000try: FileNotFoundError except NameError: FileNotFoundError = OSError MyFileNotFoundError = FileNotFoundError trash-cli-0.24.5.26/tests/support/restore/000077500000000000000000000000001462460053300202625ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/restore/__init__.py000066400000000000000000000000001462460053300223610ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/restore/a_trashed_file.py000066400000000000000000000006071462460053300235700ustar00rootroot00000000000000from typing import NamedTuple def a_trashed_file(trashed_from, info_file, backup_copy): return ATrashedFile(trashed_from=str(trashed_from), info_file=str(info_file), backup_copy=str(backup_copy)) class ATrashedFile(NamedTuple('ATrashedFile', [ ('trashed_from', str), ('info_file', str), ('backup_copy', str), ])): pass trash-cli-0.24.5.26/tests/support/restore/fake_restore_fs.py000066400000000000000000000054031462460053300237770ustar00rootroot00000000000000import os from tests.support.put.fake_fs.fake_fs import FakeFs from tests.support.restore.a_trashed_file import ATrashedFile from trashcli.fs import PathExists from trashcli.fstab.volumes import Volumes, FakeVolumes from trashcli.put.format_trash_info import format_trashinfo from trashcli.restore.file_system import ListingFileSystem, FileReader, \ RestoreWriteFileSystem, RestoreReadFileSystem class FakeRestoreFs(ListingFileSystem, Volumes, FileReader, RestoreWriteFileSystem, RestoreReadFileSystem, PathExists): def exists(self, path): return self.path_exists(path) def path_exists(self, path): return self.fake_fs.exists(path) def __init__(self): self.fake_fs = FakeFs() self.mount_points = [] def mkdirs(self, path): self.fake_fs.makedirs(path, 755) def move(self, path, dest): self.fake_fs.move(path, dest) def remove_file(self, path): self.fake_fs.remove_file(path) def add_volume(self, mount_point): self.mount_points.append(mount_point) def list_mount_points(self): return FakeVolumes(self.mount_points).list_mount_points() def volume_of(self, path): return FakeVolumes(self.mount_points).volume_of(path) def make_trashed_file(self, from_path, trash_dir, time, original_file_content): content = format_trashinfo(from_path, time) basename = os.path.basename(from_path) info_path = os.path.join(trash_dir, 'info', "%s.trashinfo" % basename) backup_copy_path = os.path.join(trash_dir, 'files', basename) trashed_file = ATrashedFile(trashed_from=from_path, info_file=info_path, backup_copy=backup_copy_path) self.add_file(info_path, content) self.add_file(backup_copy_path, original_file_content.encode('utf-8')) return trashed_file def add_trash_file(self, from_path, trash_dir, time, original_file_content): content = format_trashinfo(from_path, time) basename = os.path.basename(from_path) info_path = os.path.join(trash_dir, 'info', "%s.trashinfo" % basename) backup_copy_path = os.path.join(trash_dir, 'files', basename) self.add_file(info_path, content) self.add_file(backup_copy_path, original_file_content.encode('utf-8')) def add_file(self, path, content=b''): self.fake_fs.makedirs(os.path.dirname(path), 755) self.fake_fs.make_file(path, content) def list_files_in_dir(self, dir_path): for file_path in self.fake_fs.listdir(dir_path): yield os.path.join(dir_path, file_path) def contents_of(self, path): return self.fake_fs.read(path).decode('utf-8') trash-cli-0.24.5.26/tests/support/restore/has_been_restored_matcher.py000066400000000000000000000103351462460053300260140ustar00rootroot00000000000000from typing import NamedTuple, Union from trashcli.fs import PathExists def has_been_restored(fs): # type: (PathExists) -> HasBeenRestoredBaseMatcher return HasBeenRestoredBaseMatcher(fs, HasBeenRestoredExpectations()) def has_not_been_restored( fs): # type: (PathExists) -> HasBeenRestoredBaseMatcher return HasBeenRestoredBaseMatcher(fs, HasNotBeenYetRestoredExpectations()) class ShouldExists(NamedTuple('ShouldExists', [ ('name', str), ('path', str)])): def expectation_as_text(self): return "should exists" def should_exists(self): return True def actual(self, actually_exists): return {True: "and it does", False: "but it does not"}[actually_exists] class ShouldNotExists( NamedTuple('ShouldNotExists', [('name', str), ('path', str)])): def expectation_as_text(self): return "should not exists" def should_exists(self): return False def actual(self, actually_exists): return {False: "and it does not", True: "but it does"}[actually_exists] class Satisfaction: def __init__(self, expectation, actually_exists): self.expectation = expectation self.actually_exists = actually_exists def expectations_satisfied(self): return self.actually_exists == self.expectation.should_exists() def actual_description(self): return self.expectation.actual(self.actually_exists) def ok_or_fail_text(self): return {True: "OK", False: "FAIL"}[ self.expectations_satisfied()] def kind_of_file(self): return self.expectation.name def satisfaction_description(self): return "{0} {1} {2} {3}: '{4}'".format( self.ok_or_fail_text(), self.kind_of_file(), self.expectation.expectation_as_text(), self.actual_description(), self.expectation.path ) class HasBeenRestoredExpectations: def expectations_for_file(self, a_trashed_file): return [ ShouldExists("original_location", a_trashed_file.trashed_from), ShouldNotExists("info_file", a_trashed_file.info_file), ShouldNotExists("backup_copy", a_trashed_file.backup_copy), ] class HasNotBeenYetRestoredExpectations: def expectations_for_file(self, a_trashed_file): return [ ShouldNotExists("original_location", a_trashed_file.trashed_from), ShouldExists("info_file", a_trashed_file.info_file), ShouldExists("backup_copy", a_trashed_file.backup_copy), ] Expectations = Union[HasBeenRestoredExpectations,HasNotBeenYetRestoredExpectations] class HasBeenRestoredBaseMatcher: def __init__(self, fs, # type: PathExists expectations_maker, # type: Expectations ): self.fs = fs self.expectations_maker = expectations_maker def matches(self, a_trashed_file): return len(self._expectations_failed(a_trashed_file)) == 0 def describe_mismatch(self, a_trashed_file, focus_on=None): expectations_satisfactions = self._expectations_satisfactions( a_trashed_file, focus_on) return ("Expected file to be restore but it has not:\n" + "".join(" - %s\n" % satisfaction.satisfaction_description() for satisfaction in expectations_satisfactions)) def describe(self, description): return "The file has been restored" def _expectations_failed(self, a_trashed_file): return [ satisfaction for satisfaction in self._expectations_satisfactions(a_trashed_file, focus_on=None) if not satisfaction.expectations_satisfied()] def _expectations_satisfactions(self, a_trashed_file, focus_on=None): return [ Satisfaction(e, self.fs.exists(e.path)) for e in self._expectations_for(a_trashed_file, focus_on)] def _expectations_for(self, a_trashed_file, focus_on=None): all_expectations = self.expectations_maker.expectations_for_file( a_trashed_file) if focus_on is None: return all_expectations else: return [e for e in all_expectations if e.name == focus_on] trash-cli-0.24.5.26/tests/support/restore/restore_file_fixture.py000066400000000000000000000014641462460053300250710ustar00rootroot00000000000000import os from tests.support.fakes.fake_trash_dir import trashinfo_content_default_date from tests.support.files import make_file class RestoreFileFixture: def __init__(self, XDG_DATA_HOME): self.XDG_DATA_HOME = XDG_DATA_HOME def having_a_trashed_file(self, path): self.make_file('%s/info/foo.trashinfo' % self._trash_dir(), trashinfo_content_default_date(path)) self.make_file('%s/files/foo' % self._trash_dir()) def make_file(self, filename, contents=''): return make_file(filename, contents) def make_empty_file(self, filename): return self.make_file(filename) def _trash_dir(self): return "%s/Trash" % self.XDG_DATA_HOME def file_should_have_been_restored(self, filename): assert os.path.exists(filename) trash-cli-0.24.5.26/tests/support/restore/restore_user.py000066400000000000000000000055521462460053300233640ustar00rootroot00000000000000import sys from io import StringIO from typing import Dict from tests.support.run.cmd_result import CmdResult from trashcli.fstab.volumes import Volumes from trashcli.lib.my_input import HardCodedInput from trashcli.restore.file_system import FakeReadCwd, FileReader, \ RestoreReadFileSystem, \ RestoreWriteFileSystem, ListingFileSystem from trashcli.restore.info_dir_searcher import InfoDirSearcher from trashcli.restore.info_files import InfoFiles from trashcli.restore.restore_cmd import RestoreCmd from trashcli.restore.trash_directories import TrashDirectoriesImpl from trashcli.restore.trashed_files import TrashedFiles class MemoLogger: def __init__(self): self.messages = [] def warning(self, msg): self.messages.append("warning: " + msg) class RestoreUser: def __init__(self, environ, # type: Dict[str, str] uid, # type: int file_reader, # type: FileReader read_fs, # type: RestoreReadFileSystem write_fs, # type: RestoreWriteFileSystem listing_file_system, # type: ListingFileSystem version, # type: str volumes, # type: Volumes ): self.environ = environ self.uid = uid self.file_reader = file_reader self.read_fs = read_fs self.write_fs = write_fs self.listing_file_system = listing_file_system self.version = version self.volumes = volumes no_args = object() def run_restore(self, args=no_args, reply='', from_dir=None): args = [] if args is self.no_args else args stdout = StringIO() stderr = StringIO() read_cwd = FakeReadCwd(from_dir) logger = MemoLogger() trash_directories = TrashDirectoriesImpl(self.volumes, self.uid, self.environ) searcher = InfoDirSearcher(trash_directories, InfoFiles(self.listing_file_system)) trashed_files = TrashedFiles(logger, self.file_reader, searcher) cmd = RestoreCmd.make(stdout=stdout, stderr=stderr, exit=sys.exit, input=HardCodedInput(reply), version=self.version, trashed_files=trashed_files, read_fs=self.read_fs, write_fs=self.write_fs, read_cwd=read_cwd) try: exit_code = cmd.run(args) except SystemExit as e: exit_code = e.code return CmdResult(stdout.getvalue(), stderr.getvalue(), exit_code) trash-cli-0.24.5.26/tests/support/run/000077500000000000000000000000001462460053300174035ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/run/__init__.py000066400000000000000000000000001462460053300215020ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/run/cmd_result.py000066400000000000000000000021011462460053300221100ustar00rootroot00000000000000from tests.support.help.help_reformatting import reformat_help_message from tests.support.text.last_line_of import last_line_of class CmdResult: def __init__(self, stdout, # type: str stderr, # type: str exit_code, # type: int ): # (...) -> None self.stdout = stdout self.stderr = stderr self.exit_code = exit_code self.all = (stdout, stderr, exit_code) def all_lines(self): return set(self.stderr.splitlines() + self.stdout.splitlines()) def __str__(self): return str(self.all) def output(self): return self._format([self.stdout, self.stderr]) def last_line_of_stderr(self): return last_line_of(self.stderr) def last_line_of_stdout(self): return last_line_of(self.stdout) def reformatted_help(self): return reformat_help_message(self.stdout) @staticmethod def _format(outs): outs = [out for out in outs if out != ""] return "".join([out.rstrip("\n") + "\n" for out in outs]) trash-cli-0.24.5.26/tests/support/run/run_command.py000066400000000000000000000017701462460053300222640ustar00rootroot00000000000000import os import subprocess import sys from tests.support.dicts import merge_dicts from tests.support.make_scripts import script_path_for from tests.support.project_root import project_root from tests.support.run.cmd_result import CmdResult def run_command(cwd, command, args=None, input='', env=None): if env is None: env = {} if args is None: args = [] command_full_path = script_path_for(command) env['PYTHONPATH'] = project_root() process = subprocess.Popen([sys.executable, command_full_path] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=merge_dicts(os.environ, env)) stdout, stderr = process.communicate(input=input.encode('utf-8')) return CmdResult(stdout.decode('utf-8'), stderr.decode('utf-8'), process.returncode) trash-cli-0.24.5.26/tests/support/text/000077500000000000000000000000001462460053300175635ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/text/__init__.py000066400000000000000000000000001462460053300216620ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/text/grep.py000066400000000000000000000002601462460053300210700ustar00rootroot00000000000000def grep(stream, pattern): # type: (str, str) -> str return ''.join([line for line in stream.splitlines(True) if pattern in line]) trash-cli-0.24.5.26/tests/support/text/last_line_of.py000066400000000000000000000002021462460053300225650ustar00rootroot00000000000000def last_line_of(stdout): if len(stdout.splitlines()) > 0: return stdout.splitlines()[-1] else: return '' trash-cli-0.24.5.26/tests/support/text/sort_lines.py000066400000000000000000000001121462460053300223100ustar00rootroot00000000000000def sort_lines(lines): return "".join(sorted(lines.splitlines(True))) trash-cli-0.24.5.26/tests/support/tools/000077500000000000000000000000001462460053300177375ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/tools/__init__.py000066400000000000000000000000001462460053300220360ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/tools/adapters/000077500000000000000000000000001462460053300215425ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/tools/adapters/__init__.py000066400000000000000000000000001462460053300236410ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/tools/adapters/real_cal.py000066400000000000000000000002221462460053300236520ustar00rootroot00000000000000import datetime from tests.support.tools.core.cal import Cal class RealCal(Cal): def today(self): return datetime.datetime.today() trash-cli-0.24.5.26/tests/support/tools/bump_cmd.py000066400000000000000000000020551462460053300221010ustar00rootroot00000000000000import os from tests.support.tools.version_from_date import version_from_date from tests.support.tools.version_saver import VersionSaver def trash_py_file(root): return os.path.join(root, 'trashcli', 'trash.py') class BumpCmd: def __init__(self, os_system, print_func, fs, cal): self.system = os_system self.print_func = print_func self.fs = fs self.cal = cal def run_bump(self, root, args): # https://unix.stackexchange.com/questions/155046/determine-if-git-working-directory-is-clean-from-a-script/394674#394674 if self.system("git diff-index --quiet HEAD") != 0: self.print_func("Dirty") exit(1) new_version = version_from_date(self.cal.today()) if not '--dry-run' in args: VersionSaver(self.fs).save_new_version(new_version, trash_py_file(root)) system = self.print_func if '--dry-run' in args else self.system system("git commit -m \"Bump version to '%s'\" -a" % new_version) trash-cli-0.24.5.26/tests/support/tools/bump_main.py000066400000000000000000000005721462460053300222640ustar00rootroot00000000000000import os import sys from tests.support.project_root import project_root from tests.support.tools.adapters.real_cal import RealCal from tests.support.tools.bump_cmd import BumpCmd from trashcli.put.fs.real_fs import RealFs def main(): print(project_root()) bump_cmd = BumpCmd(os.system, print, RealFs(), RealCal()) bump_cmd.run_bump(project_root(), sys.argv[1:]) trash-cli-0.24.5.26/tests/support/tools/core/000077500000000000000000000000001462460053300206675ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/tools/core/__init__.py000066400000000000000000000000001462460053300227660ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/tools/core/cal.py000066400000000000000000000002471462460053300220030ustar00rootroot00000000000000from abc import abstractmethod from trashcli.compat import Protocol class Cal(Protocol): @abstractmethod def today(self): raise NotImplementedError trash-cli-0.24.5.26/tests/support/tools/set_dev_version.py000066400000000000000000000033411462460053300235100ustar00rootroot00000000000000from __future__ import print_function import argparse import sys from six import text_type from tests.support.project_root import project_root from tests.support.tools.adapters.real_cal import RealCal from tests.support.tools.bump_cmd import trash_py_file from tests.support.tools.version_from_date import dev_version_from_date from tests.support.tools.version_saver import VersionSaver from trashcli.put.fs.real_fs import RealFs def main(): cmd = SetDevVersionCmd(RealFs(), sys.stdout, sys.stderr, RealCal()) cmd.run_set_dev_version(sys.argv, project_root()) class SetDevVersionCmd: def __init__(self, fs, stdout, stderr, cal): self.stdout = stdout self.stderr = stderr self.cal = cal self.version_saver = VersionSaver(fs) def run_set_dev_version(self, argv, root): parser = argparse.ArgumentParser(prog=argv[0]) parser.add_argument('ref') parser.add_argument('sha') args = parser.parse_args(argv[1:]) self.warn_about_using_underscore_as_ref(args) new_version = dev_version_from_date(args.ref, args.sha, self.cal.today()) self.version_saver.save_new_version(new_version, trash_py_file(root)) def warn_about_using_underscore_as_ref(self, args): if "-" in args.ref: print(text_type( "Ref cannot contain '-': %s\n" % args.ref + "The reason is because any '-' will be converted to '.' " "by setuptools during the egg_info phase that will result in " "an error in scripts/make-scripts because it will be not " "able to find the .tar.gz file"), file=self.stderr) sys.exit(1) trash-cli-0.24.5.26/tests/support/tools/version_from_date.py000066400000000000000000000005231462460053300240160ustar00rootroot00000000000000from __future__ import print_function def version_from_date(today): return "0.%s.%s.%s" % (today.year % 100, today.month, today.day) def dev_version_from_date(ref, sha, today): new_version = '%s.dev0+git.%s.%s' % (version_from_date(today), ref, sha) return new_version trash-cli-0.24.5.26/tests/support/tools/version_saver.py000066400000000000000000000006401462460053300231760ustar00rootroot00000000000000import re class VersionSaver: def __init__(self, fs): self.fs = fs def save_new_version(self, new_version, path): content = self.fs.read(path) new_content = re.sub(r'^version(\s*)=.*', 'version = \'%s\'' % new_version, content, flags=re.MULTILINE) self.fs.write_file(path, new_content) trash-cli-0.24.5.26/tests/support/trash_dirs/000077500000000000000000000000001462460053300207415ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/trash_dirs/__init__.py000066400000000000000000000000001462460053300230400ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/trash_dirs/list_trash_dir.py000066400000000000000000000003441462460053300243260ustar00rootroot00000000000000from tests.support.dirs.list_file_in_subdir import list_files_in_subdir def list_trash_dir(trash_dir_path): return (list_files_in_subdir(trash_dir_path, 'info') + list_files_in_subdir(trash_dir_path, 'files')) trash-cli-0.24.5.26/tests/support/trashinfo/000077500000000000000000000000001462460053300205745ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/trashinfo/__init__.py000066400000000000000000000000001462460053300226730ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/support/trashinfo/parse_date.py000066400000000000000000000006551462460053300232630ustar00rootroot00000000000000from datetime import datetime def parse_date(date_string, # type: str ): # type: (...) -> datetime for format in ['%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S', ]: try: return datetime.strptime(date_string, format) except ValueError: continue raise ValueError("Can't parse date: %s" % date_string) trash-cli-0.24.5.26/tests/test_dev_tools/000077500000000000000000000000001462460053300201205ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/__init__.py000066400000000000000000000000001462460053300222170ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/cmds/000077500000000000000000000000001462460053300210465ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/cmds/__init__.py000066400000000000000000000000001462460053300231450ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/cmds/test_bump_cmd.py000066400000000000000000000037011462460053300242460ustar00rootroot00000000000000from tests.support.capture_exit_code import capture_exit_code from tests.support.put.fake_fs.fake_fs import FakeFs from tests.support.tools.bump_cmd import BumpCmd from tests.test_dev_tools.support.fake_cal import FakeCal from tests.test_dev_tools.support.fake_system import FakeSystem class TestBumpCmd: def setup_method(self): self.system = FakeSystem() self.print_calls = [] self.fs = FakeFs() self.cmd = BumpCmd(self.system.os_system, self.print_calls.append, self.fs, FakeCal("2024-05-01")) def test_when_dirty(self): exit_code = capture_exit_code(lambda: self.cmd.run_bump("/", [])) assert exit_code == 1 assert self.print_calls == ['Dirty'] assert self.system.os_system_calls == ['git diff-index --quiet HEAD'] def test_when_clean(self): self.system.set_clean() self.fs.mkdir('trashcli') self.fs.make_file('trashcli/trash.py', "version=x.y.x") exit_code = capture_exit_code(lambda: self.cmd.run_bump("/", [])) assert exit_code == None assert self.print_calls == [] assert self.system.os_system_calls == [ 'git diff-index --quiet HEAD', 'git commit -m "Bump version to \'0.24.5.1\'" -a'] assert self.fs.read_all_files() == [ ('/trashcli/trash.py', "version = '0.24.5.1'")] def test_when_clean_and_dry_run(self): self.system.set_clean() self.fs.mkdir('trashcli') self.fs.make_file('trashcli/trash.py', "version=x.y.x") exit_code = capture_exit_code( lambda: self.cmd.run_bump("/", ['--dry-run'])) assert exit_code == None assert self.print_calls == [ 'git commit -m "Bump version to \'0.24.5.1\'" -a'] assert self.system.os_system_calls == [ 'git diff-index --quiet HEAD'] assert self.fs.read_all_files() == [ ('/trashcli/trash.py', "version=x.y.x")] trash-cli-0.24.5.26/tests/test_dev_tools/cmds/test_set_dev_version_cmd.py000066400000000000000000000036021462460053300265010ustar00rootroot00000000000000from tests.support.put.fake_fs.fake_fs import FakeFs from tests.test_dev_tools.support.run_set_dev_version import RunSetDevVersion def adjust_py27(output): return output.replace("prg: error: too few arguments\n", 'prg: error: the following arguments ' 'are required: ref, sha\n') class TestSetDevVersionCmd: def setup_method(self): self.fs = FakeFs() self.run = RunSetDevVersion(self.fs) def test_when_no_args_fails(self, capsys): result = adjust_py27(self.run.run_cmd([], capsys)) assert result == ( 'exit code: 2\n' 'stderr: usage: prg [-h] ref sha\n' 'prg: error: the following arguments are required: ref, sha\n' '\n' 'stdout: \n' 'filesystem:\n') def test_happy_path(self, capsys): self.fs.mkdir("trashcli") self.fs.write_file("trashcli/trash.py", "version = ...") result = self.run.run_cmd(['master', '12345b'], capsys) assert result == ( 'exit code: None\n' 'stderr: \n' 'stdout: \n' 'filesystem:\n' " /trashcli/trash.py: version = '0.24.5.13.dev0+git.master.12345b'" ) def test(self, capsys): self.fs.mkdir("trashcli") self.fs.write_file("trashcli/trash.py", "version = ...") result = self.run.run_cmd(['-', '12345b'], capsys) assert result == ( 'exit code: 1\n' "stderr: Ref cannot contain '-': -\n" "The reason is because any '-' will be converted to '.' by " "setuptools during the egg_info phase that will result in an " "error in scripts/make-scripts because it will be not able to " "find the .tar.gz file\n\n" 'stdout: \n' 'filesystem:\n' ' /trashcli/trash.py: version = ...') trash-cli-0.24.5.26/tests/test_dev_tools/components/000077500000000000000000000000001462460053300223055ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/components/__init__.py000066400000000000000000000000001462460053300244040ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/components/test_save_new_version.py000066400000000000000000000023031462460053300272700ustar00rootroot00000000000000from tests.support.dirs.my_path import MyPath from tests.support.files import make_file from tests.support.tools.version_saver import VersionSaver from trashcli.fs import read_file from trashcli.put.fs.real_fs import RealFs class TestSaveNewVersion: def setup_method(self): self.tmp_dir = MyPath.make_temp_dir() self.saver = VersionSaver(RealFs()) def test(self): make_file(self.tmp_dir / 'trash.py', """\ somecode before version="0.20.1.20" somecode after dont change this line: version="0.20.1.20" """) self.saver.save_new_version('0.21.5.11', self.tmp_dir / 'trash.py') result = read_file(self.tmp_dir / "trash.py") assert result == """\ somecode before version = '0.21.5.11' somecode after dont change this line: version="0.20.1.20" """ def test2(self): make_file(self.tmp_dir / 'trash.py', """\ somecode before version="0.20.1.20" somecode after """) self.saver.save_new_version('0.21.5.11', self.tmp_dir / 'trash.py') result = read_file(self.tmp_dir / "trash.py") assert result == """\ somecode before version="0.20.1.20" somecode after """ def teardown_method(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_dev_tools/components/test_version_from_date.py000066400000000000000000000004031462460053300274200ustar00rootroot00000000000000import datetime from tests.support.tools.version_from_date import version_from_date class TestVersionFromDate: def test(self): today = datetime.date(2021, 5, 11) result = version_from_date(today) assert result == '0.21.5.11' trash-cli-0.24.5.26/tests/test_dev_tools/support/000077500000000000000000000000001462460053300216345ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/support/__init__.py000066400000000000000000000000001462460053300237330ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/support/fake_cal.py000066400000000000000000000003661462460053300237400ustar00rootroot00000000000000from tests.support.tools.core.cal import Cal from tests.support.trashinfo.parse_date import parse_date class FakeCal(Cal): def __init__(self, today): self._today = parse_date(today) def today(self): return self._today trash-cli-0.24.5.26/tests/test_dev_tools/support/fake_system.py000066400000000000000000000005551462460053300245250ustar00rootroot00000000000000class FakeSystem: def __init__(self): self.clean = False self.os_system_calls = [] def set_dirty(self): self.clean = False def set_clean(self): self.clean = True def os_system(self, cmd): self.os_system_calls.append(cmd) if cmd == 'git diff-index --quiet HEAD' and self.clean: return 0 trash-cli-0.24.5.26/tests/test_dev_tools/support/run_set_dev_version.py000066400000000000000000000026261462460053300262760ustar00rootroot00000000000000from io import StringIO from typing import NamedTuple from typing import Optional from tests.support.capture_exit_code import capture_exit_code from tests.support.put.fake_fs.fake_fs import FakeFs from tests.support.tools.set_dev_version import SetDevVersionCmd from tests.test_dev_tools.cmds.test_bump_cmd import FakeCal class RunSetDevVersion: def __init__(self, fs): self.fs = fs def run_cmd(self, args, capsys): stdout = StringIO() stderr = StringIO() cmd = SetDevVersionCmd(self.fs, stdout, stderr, FakeCal("2024-05-13")) code = capture_exit_code( lambda: cmd.run_set_dev_version(['prg'] + args, "/")) capture = capsys.readouterr() result = Result(stdout.getvalue() + capture.out, stderr.getvalue() + capture.err, code) return situation(result, self.fs) class Result(NamedTuple('Result', [ ('out', str), ('err', str), ('exit_code', Optional[int]), ])): pass def situation(result, # type: Result fs, # type: FakeFs ): # type: (...) -> str return ("exit code: %s\n" % result.exit_code + "stderr: %s\n" % result.err + "stdout: %s\n" % result.out + "filesystem:\n" + "\n".join([ " %s: %s" % (path, content) for path, content in fs.read_all_files() ]) ) trash-cli-0.24.5.26/tests/test_dev_tools/test_make_scripts/000077500000000000000000000000001462460053300236435ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/test_make_scripts/__init__.py000066400000000000000000000000001462460053300257420ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_dev_tools/test_make_scripts/test_generate_scripts.py000066400000000000000000000016411462460053300306170ustar00rootroot00000000000000import pytest from tests.support.make_scripts import make_scripts @pytest.mark.slow class TestGenerateScripts: def test(self): scripts = make_scripts() scripts.add_script('trash', 'trashcli.put.main', 'main') scripts.add_script('trash-put', 'trashcli.put.main', 'main') scripts.add_script('trash-list', 'trashcli.list.main', 'main') scripts.add_script('trash-restore', 'trashcli.restore.main', 'main') scripts.add_script('trash-empty', 'trashcli.empty.main', 'main') scripts.add_script('trash-rm', 'trashcli.rm.main', 'main') assert scripts.created_scripts == ['trash', 'trash-put', 'trash-list', 'trash-restore', 'trash-empty', 'trash-rm'] trash-cli-0.24.5.26/tests/test_dev_tools/test_make_scripts/test_list_of_created_scripts.py000066400000000000000000000011201462460053300321430ustar00rootroot00000000000000from mock import Mock from tests.support.make_scripts import Scripts from tests.support.make_scripts import script_path_without_base_dir_for class TestListOfCreatedScripts: def setup_method(self): self.bindir = Scripts(make_file_executable=Mock(), write_file=Mock()) def test_is_empty_on_start_up(self): assert self.bindir.created_scripts == [] def test_collect_added_script(self): self.bindir.add_script('foo-command', 'foo-module', 'main') assert self.bindir.created_scripts == [ script_path_without_base_dir_for('foo-command')] trash-cli-0.24.5.26/tests/test_dev_tools/test_make_scripts/test_make_script.py000066400000000000000000000030011462460053300275470ustar00rootroot00000000000000from textwrap import dedent import mock from mock import Mock from tests.support.make_scripts import Scripts from tests.support.make_scripts import script_path_for class TestMakeScript: def setup_method(self): self.make_file_executable = Mock() self.write_file = Mock() def capture(name, contents): self.name = name self.contents = contents self.write_file.side_effect = capture bindir = Scripts( make_file_executable=self.make_file_executable, write_file=self.write_file) bindir.add_script('trash-put', 'trashcli_module', 'put') def test_should_set_executable_permission(self): self.make_file_executable.assert_called_with(script_path_for('trash-put')) def test_should_write_the_script(self): self.write_file.assert_called_with(script_path_for('trash-put'), mock.ANY) def test_the_script_should_call_the_right_function_from_the_right_module(self): args, kwargs = self.write_file.call_args (_, contents) = args expected = dedent("""\ #!/usr/bin/env python from __future__ import absolute_import import sys from trashcli_module import put as main sys.exit(main()) """) assert expected == contents, ("Expected:\n---\n%s---\n" "Actual :\n---\n%s---\n" % (expected, contents)) trash-cli-0.24.5.26/tests/test_empty/000077500000000000000000000000001462460053300172605ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_empty/__init__.py000066400000000000000000000000401462460053300213630ustar00rootroot00000000000000def no_volumes(): return [] trash-cli-0.24.5.26/tests/test_empty/cmd/000077500000000000000000000000001462460053300200235ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_empty/cmd/__init__.py000066400000000000000000000000001462460053300221220ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_empty/cmd/test_empty_cmd.py000066400000000000000000000053311462460053300234170ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import unittest from typing import cast from mock import call from six import StringIO from tests.support.fakes.stub_volume_of import StubVolumeOf from tests.support.fakes.mock_dir_reader import MockDirReader from trashcli.empty.delete_according_date import ContentsOf from trashcli.empty.empty_cmd import EmptyCmd from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.fstab.volume_listing import FixedVolumesListing from trashcli.fstab.volume_listing import VolumesListing from trashcli.lib.dir_reader import DirReader from trashcli.trash_dirs_scanner import TopTrashDirRules from mock import Mock class TestTrashEmptyCmdFs(unittest.TestCase): def setUp(self): self.volumes_listing = FixedVolumesListing([]) self.file_reader = Mock(spec=TopTrashDirRules.Reader) self.file_remover = Mock(spec=ExistingFileRemover) self.content_reader = Mock(spec=ContentsOf) self.dir_reader = MockDirReader() self.err = StringIO() self.out = StringIO() self.environ = {'XDG_DATA_HOME': '/xdg'} self.empty = EmptyCmd( argv0='trash-empty', out=self.out, err=self.err, volumes_listing=cast(VolumesListing, self.volumes_listing), now=None, file_reader=cast(TopTrashDirRules.Reader, self.file_reader), file_remover=cast(ExistingFileRemover, self.file_remover), content_reader=cast(ContentsOf, self.content_reader), dir_reader=cast(DirReader, self.dir_reader), version='unused', volumes=StubVolumeOf() ) def test(self): self.dir_reader.mkdir('/xdg') self.dir_reader.mkdir('/xdg/Trash') self.dir_reader.mkdir('/xdg/Trash/info') self.dir_reader.add_file('/xdg/Trash/info/pippo.trashinfo') self.dir_reader.mkdir('/xdg/Trash/files') self.empty.run_cmd([], self.environ, uid=123) assert self.file_remover.mock_calls == [ call.remove_file_if_exists('/xdg/Trash/files/pippo'), call.remove_file_if_exists('/xdg/Trash/info/pippo.trashinfo') ] def test_with_dry_run(self): self.dir_reader.mkdir('/xdg') self.dir_reader.mkdir('/xdg/Trash') self.dir_reader.mkdir('/xdg/Trash/info') self.dir_reader.add_file('/xdg/Trash/info/pippo.trashinfo') self.dir_reader.mkdir('/xdg/Trash/files') self.empty.run_cmd(['--dry-run'], self.environ, uid=123) assert self.file_remover.mock_calls == [] assert self.out.getvalue() == \ 'would remove /xdg/Trash/files/pippo\n' \ 'would remove /xdg/Trash/info/pippo.trashinfo\n' trash-cli-0.24.5.26/tests/test_empty/cmd/test_empty_cmd_fs.py000066400000000000000000000037071462460053300241140ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import unittest import pytest from mock import Mock from six import StringIO from tests.support.fakes.stub_volume_of import StubVolumeOf from tests.support.files import make_unreadable_dir, make_readable from tests.support.dirs.my_path import MyPath from trashcli.empty.empty_cmd import EmptyCmd from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.empty.file_system_dir_reader import FileSystemDirReader from trashcli.empty.main import FileSystemContentReader from trashcli.empty.top_trash_dir_rules_file_system_reader import \ RealTopTrashDirRulesReader from trashcli.fstab.volume_listing import VolumesListing @pytest.mark.slow class TestTrashEmptyCmdFs(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.unreadable_dir = self.tmp_dir / 'data/Trash/files/unreadable' self.volumes_listing = Mock(spec=VolumesListing) self.volumes_listing.list_volumes.return_value = [self.unreadable_dir] self.err = StringIO() self.environ = {'XDG_DATA_HOME': self.tmp_dir / 'data'} self.empty = EmptyCmd( argv0='trash-empty', out=StringIO(), err=self.err, volumes_listing=self.volumes_listing, now=None, file_reader=RealTopTrashDirRulesReader(), file_remover=ExistingFileRemover(), content_reader=FileSystemContentReader(), dir_reader=FileSystemDirReader(), version='unused', volumes=StubVolumeOf() ) def test_trash_empty_will_skip_unreadable_dir(self): make_unreadable_dir(self.unreadable_dir) self.empty.run_cmd([], self.environ, uid=123) assert ("trash-empty: cannot remove %s\n" % self.unreadable_dir == self.err.getvalue()) def tearDown(self): make_readable(self.unreadable_dir) self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_empty/cmd/test_empty_cmd_with_multiple_volumes_fs.py000066400000000000000000000053151462460053300306310ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import os import unittest from mock import Mock from six import StringIO from tests.support.fakes.stub_volume_of import StubVolumeOf from tests.support.files import make_empty_file, require_empty_dir, make_dirs, \ set_sticky_bit from tests.support.dirs.my_path import MyPath from trashcli.empty.empty_cmd import EmptyCmd from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.empty.file_system_dir_reader import FileSystemDirReader from trashcli.empty.main import FileSystemContentReader from trashcli.empty.top_trash_dir_rules_file_system_reader import \ RealTopTrashDirRulesReader from trashcli.fstab.volume_listing import VolumesListing class TestEmptyCmdWithMultipleVolumesFs(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() self.top_dir = self.temp_dir / 'topdir' self.volumes_listing = Mock(spec=VolumesListing) self.volumes_listing.list_volumes.return_value = [self.top_dir] require_empty_dir(self.top_dir) self.environ = {} self.empty_cmd = EmptyCmd( argv0='trash-empty', out=StringIO(), err=StringIO(), volumes_listing=self.volumes_listing, now=None, file_reader=RealTopTrashDirRulesReader(), file_remover=ExistingFileRemover(), content_reader=FileSystemContentReader(), dir_reader=FileSystemDirReader(), version='unused', volumes=StubVolumeOf(), ) def test_it_removes_trashinfos_from_method_1_dir(self): self.make_proper_top_trash_dir(self.top_dir / '.Trash') make_empty_file(self.top_dir / '.Trash/123/info/foo.trashinfo') self.empty_cmd.run_cmd([], self.environ, uid=123) assert not os.path.exists( self.top_dir / '.Trash/123/info/foo.trashinfo') def test_it_removes_trashinfos_from_method_2_dir(self): make_empty_file(self.top_dir / '.Trash-123/info/foo.trashinfo') self.empty_cmd.run_cmd([], self.environ, uid=123) assert not os.path.exists( self.top_dir / '.Trash-123/info/foo.trashinfo') def test_it_removes_trashinfo_from_specified_trash_dir(self): make_empty_file(self.temp_dir / 'specified/info/foo.trashinfo') self.empty_cmd.run_cmd(['--trash-dir', self.temp_dir / 'specified'], self.environ, uid=123) assert not os.path.exists( self.temp_dir / 'specified/info/foo.trashinfo') @staticmethod def make_proper_top_trash_dir(path): make_dirs(path) set_sticky_bit(path) def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_empty/components/000077500000000000000000000000001462460053300214455ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_empty/components/__init__.py000066400000000000000000000000001462460053300235440ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_empty/components/test_clock.py000066400000000000000000000017741462460053300241620ustar00rootroot00000000000000import datetime import unittest from mock import Mock, call from trashcli.empty.clock import Clock class TestClock(unittest.TestCase): def setUp(self): self.errors = Mock(spec=['print_error']) def test_return_real_time(self): clock = Clock(lambda: 'now', self.errors) result = clock.get_now_value({}) assert (result, []) == ('now', self.errors.mock_calls) def test_return_fake_time(self): clock = Clock(lambda: 'now', self.errors) result = clock.get_now_value({'TRASH_DATE': '2021-06-04T18:40:19'}) assert (result, []) == (datetime.datetime(2021, 6, 4, 18, 40, 19), self.errors.mock_calls) def test_return_true_now_whe_fake_time_is_invalid(self): clock = Clock(lambda: 'now', self.errors) result = clock.get_now_value({'TRASH_DATE': 'invalid'}) assert (result, [call.print_error('invalid TRASH_DATE: invalid')]) == \ ('now', self.errors.mock_calls) trash-cli-0.24.5.26/tests/test_empty/components/test_empty_end_to_end.py000066400000000000000000000061351462460053300263770ustar00rootroot00000000000000import unittest from trashcli import trash from tests.support.help.help_reformatting import reformat_help_message from tests.support.dirs.my_path import MyPath from tests.support.run.run_command import run_command class TestEmptyEndToEnd(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() def test_help(self): result = run_command(self.tmp_dir, "trash-empty", ['--help']) self.assertEqual([reformat_help_message("""\ usage: trash-empty [-h] [--print-completion {bash,zsh,tcsh}] [--version] [-v] [--trash-dir TRASH_DIR] [--all-users] [-i] [-f] [--dry-run] [days] Purge trashed files. positional arguments: days options: -h, --help show this help message and exit --print-completion {bash,zsh,tcsh} print shell completion script --version show program's version number and exit -v, --verbose list files that will be deleted --trash-dir TRASH_DIR specify the trash directory to use --all-users empty all trashcan of all the users -i, --interactive ask before emptying trash directories -f don't ask before emptying trash directories --dry-run show which files would have been removed Report bugs to https://github.com/andreafrancia/trash-cli/issues """), '', 0], [result.reformatted_help(), result.stderr, result.exit_code]) def test_h(self): result = run_command(self.tmp_dir, "trash-empty", ['-h']) self.assertEqual(["usage:", '', 0], [result.stdout[0:6], result.stderr, result.exit_code]) def test_version(self): result = run_command(self.tmp_dir, "trash-empty", ['--version']) self.assertEqual(['trash-empty %s\n' % trash.version, '', 0], [result.stdout, result.stderr, result.exit_code]) def test_on_invalid_option(self): result = run_command(self.tmp_dir, "trash-empty", ['--wrong-option']) self.assertEqual(['', 'trash-empty: error: unrecognized arguments: --wrong-option', 2], [result.stdout, result.stderr.splitlines()[-1], result.exit_code]) def test_on_print_time(self): result = run_command(self.tmp_dir, "trash-empty", ['--print-time'], env={'TRASH_DATE': '1970-12-31T23:59:59'}) self.assertEqual(('1970-12-31T23:59:59\n', '', 0), result.all) def test_on_trash_date_not_parsable(self): result = run_command(self.tmp_dir, "trash-empty", ['--print-time'], env={'TRASH_DATE': 'not a valid date'}) self.assertEqual(['trash-empty: invalid TRASH_DATE: not a valid date\n', 0], [result.stderr, result.exit_code]) def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_empty/components/test_empty_end_to_end_interactive.py000066400000000000000000000025361462460053300307750ustar00rootroot00000000000000import datetime import unittest import pytest from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.dirs.my_path import MyPath from tests.support.run.run_command import run_command @pytest.mark.slow class TestEmptyEndToEndInteractive(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.xdg_data_home = self.tmp_dir / 'XDG_DATA_HOME' self.environ = { 'XDG_DATA_HOME': self.xdg_data_home, 'TRASH_VOLUMES': ':' } self.trash_dir = self.xdg_data_home / 'Trash' self.fake_trash_dir = FakeTrashDir(self.trash_dir) def user_run_trash_empty(self, args): return run_command(self.tmp_dir, "trash-empty", args, env=self.environ, input='y') def set_clock_at(self, yyyy_mm_dd): self.environ['TRASH_DATE'] = '%sT00:00:00' % yyyy_mm_dd def test_it_should_keep_files_newer_than_N_days(self): self.fake_trash_dir.add_trashinfo_with_date('foo', datetime.date(2000, 1, 1)) self.set_clock_at('2000-01-01') result = self.user_run_trash_empty(['-i']) assert result.all == ( 'Would empty the following trash directories:\n' ' - %s\n' 'Proceed? (y/N) ' % self.trash_dir, '', 0) def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_empty/components/test_empty_end_to_end_with_argument.py000066400000000000000000000037611462460053300313360ustar00rootroot00000000000000import datetime import unittest import pytest from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.trash_dirs.list_trash_dir import list_trash_dir from tests.support.dirs.my_path import MyPath from tests.support.run.run_command import run_command @pytest.mark.slow class TestEmptyEndToEndWithArgument(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.xdg_data_home = self.tmp_dir / 'XDG_DATA_HOME' self.environ = {'XDG_DATA_HOME': self.xdg_data_home} self.trash_dir = self.xdg_data_home / 'Trash' self.fake_trash_dir = FakeTrashDir(self.trash_dir) def user_run_trash_empty(self, args): return run_command(self.tmp_dir, "trash-empty", args, env=self.environ) def set_clock_at(self, yyyy_mm_dd): self.environ['TRASH_DATE'] = '%sT00:00:00' % yyyy_mm_dd def test_set_clock(self): self.set_clock_at('2000-01-01') result = self.user_run_trash_empty(['--print-time']) self.assertEqual(('2000-01-01T00:00:00\n', '', 0), result.all) def test_it_should_keep_files_newer_than_N_days(self): self.fake_trash_dir.add_trashinfo_with_date('foo', datetime.date(2000, 1, 1)) self.set_clock_at('2000-01-01') self.user_run_trash_empty(['2']) assert list_trash_dir(self.trash_dir) == ['info/foo.trashinfo'] def test_it_should_remove_files_older_than_N_days(self): self.fake_trash_dir.add_trashinfo_with_date('foo', datetime.date(1999, 1, 1)) self.set_clock_at('2000-01-01') self.user_run_trash_empty(['2']) assert list_trash_dir(self.trash_dir) == [] def test_it_should_kept_files_with_invalid_deletion_date(self): self.fake_trash_dir.add_trashinfo_with_invalid_date('foo', 'Invalid Date') self.set_clock_at('2000-01-01') self.user_run_trash_empty(['2']) assert list_trash_dir(self.trash_dir) == ['info/foo.trashinfo'] def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_empty/components/test_empty_end_to_end_with_trash_dir.py000066400000000000000000000045771462460053300315010ustar00rootroot00000000000000import os import unittest from tests.support.run.run_command import run_command from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.files import make_file from tests.support.trash_dirs.list_trash_dir import list_trash_dir from tests.support.dirs.my_path import MyPath class TestEmptyEndToEndWithTrashDir(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.trash_dir = self.tmp_dir / 'trash-dir' self.fake_trash_dir = FakeTrashDir(self.trash_dir) def test_add_trashed_file(self): self.fake_trash_dir.add_trashed_file('foo', '/foo', 'FOO') assert list_trash_dir(self.trash_dir) == ['info/foo.trashinfo', 'files/foo'] def test_trash_dir(self): self.fake_trash_dir.add_trashed_file('foo', '/foo', 'FOO') result = run_command(self.tmp_dir, "trash-empty", ['--trash-dir', self.trash_dir]) assert [result.all, list_trash_dir(self.trash_dir)] == \ [('', '', 0), []] def test_xdg_data_home(self): xdg_data_home = self.tmp_dir / 'xdg' FakeTrashDir(xdg_data_home / 'Trash').add_trashed_file('foo', '/foo', 'FOO') result = run_command(self.tmp_dir, "trash-empty", [], env={'XDG_DATA_HOME': xdg_data_home}) trash_dir = xdg_data_home / 'Trash' assert [result.all, list_trash_dir(trash_dir)] == \ [('', '', 0), []] def test_non_trash_info_is_not_deleted(self): make_file(self.trash_dir / 'info' / 'non-trashinfo') result = run_command(self.tmp_dir, "trash-empty", ['--trash-dir', self.trash_dir]) assert [result.all, list_trash_dir(self.trash_dir)] == \ [('', '', 0), ['info/non-trashinfo']] def test_orphan_are_deleted(self): make_file(self.trash_dir / 'files' / 'orphan') os.makedirs(self.trash_dir / 'files' / 'orphan dir') result = run_command(self.tmp_dir, "trash-empty", ['--trash-dir', self.trash_dir]) assert [result.all, list_trash_dir(self.trash_dir)] == \ [('', '', 0), []] def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_empty/components/test_existing_file_remover.py000066400000000000000000000005031462460053300274440ustar00rootroot00000000000000import unittest from trashcli.empty.existing_file_remover import ExistingFileRemover class TestExistingFileRemover(unittest.TestCase): def test_remove_file_if_exists_fails_when_file_does_not_exists(self): file_remover = ExistingFileRemover() file_remover.remove_file_if_exists('/non/existing/path') trash-cli-0.24.5.26/tests/test_empty/components/test_guard.py000066400000000000000000000020151462460053300241560ustar00rootroot00000000000000import unittest from mock import Mock, call from trashcli.empty.guard import Guard, UserIntention class TestGuard(unittest.TestCase): def setUp(self): self.user = Mock(spec=['do_you_wanna_empty_trash_dirs']) self.guard = Guard(self.user) def test_user_says_yes(self): self.user.do_you_wanna_empty_trash_dirs.return_value = True result = self.guard.ask_the_user(True, ['trash_dirs']) assert UserIntention(ok_to_empty=True, trash_dirs=['trash_dirs']) == result def test_user_says_no(self): self.user.do_you_wanna_empty_trash_dirs.return_value = False result = self.guard.ask_the_user(True, ['trash_dirs']) assert UserIntention(ok_to_empty=False, trash_dirs=[]) == result def test_it_just_calls_the_emptier(self): result = self.guard.ask_the_user(False, ['trash_dirs']) assert UserIntention(ok_to_empty=True, trash_dirs=['trash_dirs']) == result trash-cli-0.24.5.26/tests/test_empty/components/test_make_parser.py000066400000000000000000000027511462460053300253540ustar00rootroot00000000000000import unittest from typing import Union from trashcli.empty.parser import Parser class TestMakeParser(unittest.TestCase): def setUp(self): self.parser = Parser() def test(self): parsed = self.parse(args=['--trash-dir=foo']) assert ['foo'] == parsed.user_specified_trash_dirs def test_non_interactive_default_is_non_interactive(self): parsed = self.parse(default_is_interactive=False, args=[]) assert parsed.interactive == False def test_interactive_default_is_interactive(self): parsed = self.parse(default_is_interactive=True, args=[]) assert parsed.interactive == True def test_interactive_made_non_interactive(self): parsed = self.parse(default_is_interactive=True, args=['-f']) assert parsed.interactive == False def test_dry_run(self): parsed = self.parse(args=['--dry-run']) assert parsed.dry_run == True def test_dry_run_default(self): parsed = self.parse(args=[]) assert parsed.dry_run == False def parse(self, args, default_is_interactive="ignored", # type: Union[bool, str] ): return self.parser.parse(default_is_interactive=default_is_interactive, args=args, environ={}, uid=0, argv0="ignored",) trash-cli-0.24.5.26/tests/test_empty/components/test_parse_reply.py000066400000000000000000000006661462460053300254130ustar00rootroot00000000000000import unittest from trashcli.empty.parse_reply import parse_reply class TestParseReply(unittest.TestCase): def test_y(self): assert parse_reply('y') == True def test_Y(self): assert parse_reply('Y') == True def test_n(self): assert parse_reply('n') == False def test_N(self): assert parse_reply('N') == False def test_empty_string(self): assert parse_reply('') == False trash-cli-0.24.5.26/tests/test_empty/components/test_prepare_output_message.py000066400000000000000000000017251462460053300276450ustar00rootroot00000000000000import unittest from trashcli.empty.prepare_output_message import prepare_output_message from trashcli.trash_dirs_scanner import trash_dir_found class TestPrepareOutputMessage(unittest.TestCase): def test_one_dir(self): trash_dirs = [ (trash_dir_found, ('/Trash', '/')), ] result = prepare_output_message(trash_dirs) assert """\ Would empty the following trash directories: - /Trash Proceed? (y/N) """ == result def test_multiple_dirs(self): trash_dirs = [ (trash_dir_found, ('/Trash1', '/')), (trash_dir_found, ('/Trash2', '/')), ] result = prepare_output_message(trash_dirs) assert """\ Would empty the following trash directories: - /Trash1 - /Trash2 Proceed? (y/N) """ == result def test_no_dirs(self): trash_dirs = [] result = prepare_output_message(trash_dirs) assert """\ No trash directories to empty. """ == result trash-cli-0.24.5.26/tests/test_empty/components/test_user.py000066400000000000000000000020011462460053300240250ustar00rootroot00000000000000import unittest from mock import Mock, call from trashcli.empty.user import User from trashcli.lib.my_input import HardCodedInput class TestUser(unittest.TestCase): def setUp(self): self.prepare_output_message = Mock(spec=[]) self.input = HardCodedInput() self.parse_reply = Mock(spec=[]) self.user = User(self.prepare_output_message, self.input, self.parse_reply) def test(self): self.prepare_output_message.return_value = 'output_msg' self.parse_reply.return_value = 'result' self.input.set_reply('reply') result = self.user.do_you_wanna_empty_trash_dirs(['trash_dirs']) assert [ result, self.input.used_prompt, self.prepare_output_message.mock_calls, self.parse_reply.mock_calls, ] == [ 'result', 'output_msg', [call(['trash_dirs'])], [call('reply')] ] trash-cli-0.24.5.26/tests/test_fs/000077500000000000000000000000001462460053300165325ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_fs/__init__.py000066400000000000000000000000001462460053300206310ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_fs/test_move.py000066400000000000000000000013731462460053300211150ustar00rootroot00000000000000import unittest from tests.support.files import make_file from tests.support.dirs.my_path import MyPath from trashcli.fs import move, read_file class TestMove(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() def test_two_files(self): make_file(self.tmp_dir / 'a', "AAAA") make_file(self.tmp_dir / 'b', "BBBB") result = read_file(self.tmp_dir / 'b') assert result == 'BBBB' def test_move(self): make_file(self.tmp_dir / 'a', "AAAA") make_file(self.tmp_dir / 'b', "BBBB") move(self.tmp_dir / 'a', self.tmp_dir / 'b') result = read_file(self.tmp_dir / 'b') assert result == 'AAAA' def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_list/000077500000000000000000000000001462460053300170755ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/__init__.py000066400000000000000000000000001462460053300211740ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/cmd/000077500000000000000000000000001462460053300176405ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/cmd/__init__.py000066400000000000000000000000001462460053300217370ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/cmd/support/000077500000000000000000000000001462460053300213545ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/cmd/support/__init__.py000066400000000000000000000000001462460053300234530ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/cmd/support/run_result.py000066400000000000000000000007321462460053300241320ustar00rootroot00000000000000from typing import NamedTuple from tests.support.text.sort_lines import sort_lines class RunResult(NamedTuple("RunResult", [ ('stdout', str), ('stderr', str), ])): def whole_output(self): return self.stderr + self.stdout def sorted_whole_output(self): return sort_lines(self.whole_output()) def all_lines(self): return set(self.whole_output().splitlines()) def err_and_out(self): return self.stderr, self.stdout trash-cli-0.24.5.26/tests/test_list/cmd/support/trash_list_user.py000066400000000000000000000053531462460053300251460ustar00rootroot00000000000000import os import pytest from six import StringIO from tests.support.dirs.my_path import MyPath from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.fakes.stub_volume_of import StubVolumeOf from tests.support.files import make_empty_dir from trashcli.empty.main import FileSystemContentReader from trashcli.empty.top_trash_dir_rules_file_system_reader import \ RealTopTrashDirRulesReader from trashcli.file_system_reader import FileSystemReader from trashcli.fstab.volume_listing import FixedVolumesListing from trashcli.lib.dir_reader import RealDirReader from trashcli.list.main import ListCmd from .run_result import RunResult @pytest.fixture def trash_list_user(): temp_dir = MyPath.make_temp_dir() yield TrashListUser(temp_dir) temp_dir.clean_up() def adjust_for_root(path): if path.startswith("/"): return os.path.relpath(path, "/") return path class TrashListUser: def __init__(self, root): self.root = root self.xdg_data_home = root / 'xdg-data-home' self.environ = {'XDG_DATA_HOME': self.xdg_data_home} self.fake_uid = None self.volumes = [] self.version = None def run_trash_list(self, *args): # type: (...) -> RunResult file_reader = FileSystemReader() file_reader.list_volumes = lambda: self.volumes stdout = StringIO() stderr = StringIO() ListCmd( out=stdout, err=stderr, environ=self.environ, volumes_listing=FixedVolumesListing(self.volumes), uid=self.fake_uid, volumes=StubVolumeOf(), dir_reader=RealDirReader(), file_reader=RealTopTrashDirRulesReader(), content_reader=FileSystemContentReader(), version=self.version ).run(['trash-list'] + list(args)) return RunResult(clean(stdout.getvalue(), self.root), clean(stderr.getvalue(), self.root)) def set_fake_uid(self, uid): self.fake_uid = uid def add_disk(self, disk_name): top_dir = self.root / adjust_for_root(disk_name) make_empty_dir(top_dir) self.volumes.append(top_dir) def trash_dir1(self, disk_name): return FakeTrashDir( self._trash_dir1_parent(disk_name) / str(self.fake_uid)) def trash_dir2(self, disk_name): return FakeTrashDir(self.root / disk_name / '.Trash-%s' % self.fake_uid) def _trash_dir1_parent(self, disk_name): return self.root / disk_name / '.Trash' def home_trash_dir(self): return FakeTrashDir(self.xdg_data_home / "Trash") def set_version(self, version): self.version = version def clean(stream, xdg_data_home): return stream.replace(xdg_data_home, '') trash-cli-0.24.5.26/tests/test_list/cmd/test_adjust_for_root.py000066400000000000000000000004151462460053300244540ustar00rootroot00000000000000from tests.test_list.cmd.support.trash_list_user import adjust_for_root class TestAdjustForRoot: def test(self): assert adjust_for_root("disk") == "disk" assert adjust_for_root("/disk") == "disk" assert adjust_for_root("//disk") == "disk" trash-cli-0.24.5.26/tests/test_list/cmd/test_end_to_end_list.py000066400000000000000000000056441462460053300244130ustar00rootroot00000000000000import datetime import unittest import pytest from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.help.help_reformatting import reformat_help_message from tests.support.dirs.my_path import MyPath from tests.support.run.run_command import run_command @pytest.mark.slow class TestEndToEndList(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() self.trash_dir = self.temp_dir / 'trash-dir' self.fake_trash_dir = FakeTrashDir(self.trash_dir) def test_list(self): self.fake_trash_dir.add_trashinfo4("/file1", '2000-01-01 00:00:01') self.fake_trash_dir.add_trashinfo4("/file2", '2000-01-01 00:00:02') result = run_command(self.temp_dir, "trash-list", ['--trash-dir', self.trash_dir]) assert result.all_lines() == {'2000-01-01 00:00:01 /file1', '2000-01-01 00:00:02 /file2', } def test_list_trash_dirs(self): result = run_command(self.temp_dir, "trash-list", ['--trash-dirs', '--trash-dir=/home/user/.local/share/Trash']) assert result.all == ('/home/user/.local/share/Trash\n', '', 0) def test_list_with_paths(self): self.fake_trash_dir.add_trashinfo3("base1", "/file1", datetime.datetime(2000, 1, 1, 0, 0, 1)) self.fake_trash_dir.add_trashinfo3("base2", "/file2", datetime.datetime(2000, 1, 1, 0, 0, 1)) result = run_command(self.temp_dir, "trash-list", ['--trash-dir', self.trash_dir, '--files']) assert ('', [ '2000-01-01 00:00:01 /file1 -> %s/files/base1' % self.trash_dir, '2000-01-01 00:00:01 /file2 -> %s/files/base2' % self.trash_dir, ]) == (result.stderr, sorted(result.stdout.splitlines())) def test_help(self): result = run_command(self.temp_dir, "trash-list", ['--help']) self.assertEqual(reformat_help_message("""\ usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] [--version] [--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS] [--all-users] List trashed files options: -h, --help show this help message and exit --print-completion {bash,zsh,tcsh} print shell completion script --version show program's version number and exit --volumes list volumes --trash-dirs list trash dirs --trash-dir TRASH_DIRS specify the trash directory to use --all-users list trashcans of all the users Report bugs to https://github.com/andreafrancia/trash-cli/issues """), result.stderr + result.reformatted_help()) def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_list/cmd/test_list_uses_volume_trash_dirs.py000066400000000000000000000016061462460053300270770ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import pytest from tests.test_list.cmd.support.trash_list_user import trash_list_user # noqa class TestListUsesVolumeTrashDirs: @pytest.fixture def user(self, trash_list_user): u = trash_list_user u.set_fake_uid(123) u.add_disk("disk") return u def test_it_should_lists_content_from_method_1_trash_dir(self, user): user.trash_dir1("disk").make_parent_sticky() user.trash_dir1("disk").add_trashinfo4('file', "2000-01-01") result = user.run_trash_list() assert result.stdout == "2000-01-01 00:00:00 /disk/file\n" def test_it_should_lists_content_from_method_2_trash_dir(self, user): user.trash_dir2("disk").add_trashinfo4('file', "2000-01-01") result = user.run_trash_list() assert result.stdout == "2000-01-01 00:00:00 /disk/file\n" trash-cli-0.24.5.26/tests/test_list/cmd/test_list_volumes.py000066400000000000000000000007151462460053300240010ustar00rootroot00000000000000from tests.test_list.cmd.support.trash_list_user import trash_list_user # noqa user = trash_list_user class TestListVolumes: def test(self, user): user.add_disk("/disk1") user.add_disk("/disk2") user.add_disk("/disk3") output = user.run_trash_list('--volumes') assert output.whole_output() == ('/disk1\n' '/disk2\n' '/disk3\n') trash-cli-0.24.5.26/tests/test_list/cmd/test_trash_list.py000066400000000000000000000054161462460053300234330ustar00rootroot00000000000000# Copyright (C) 2011-2024 Andrea Francia Trivolzio(PV) Italy from tests.test_list.cmd.support.trash_list_user import trash_list_user user = trash_list_user class TestTrashList: def test_should_output_nothing_when_trashcan_is_empty(self, user): output = user.run_trash_list() assert output.whole_output() == '' def test_should_output_deletion_date_and_path(self, user): user.home_trash_dir().add_trashinfo4('/absolute/path', "2001-02-03 23:55:59") output = user.run_trash_list() assert (output.whole_output() == "2001-02-03 23:55:59 /absolute/path\n") def test_should_output_info_for_multiple_files(self, user): user.home_trash_dir().add_trashinfo4('/file1', "2000-01-01 00:00:01") user.home_trash_dir().add_trashinfo4('/file2', "2000-01-01 00:00:02") user.home_trash_dir().add_trashinfo4('/file3', "2000-01-01 00:00:03") output = user.run_trash_list() assert output.all_lines() == {"2000-01-01 00:00:01 /file1", "2000-01-01 00:00:02 /file2", "2000-01-01 00:00:03 /file3"} def test_should_output_unknown_dates_with_question_marks(self, user): user.home_trash_dir().add_trashinfo_without_date('without-date') output = user.run_trash_list() assert output.whole_output() == "????-??-?? ??:??:?? /without-date\n" def test_should_output_invalid_dates_using_question_marks(self, user): user.home_trash_dir().add_trashinfo_wrong_date('with-invalid-date', 'Wrong date') output = user.run_trash_list() assert (output.whole_output() == "????-??-?? ??:??:?? /with-invalid-date\n") def test_should_warn_about_empty_trashinfos(self, user): user.home_trash_dir().add_trashinfo_content('empty', '') output = user.run_trash_list() assert output.err_and_out() == ( "Parse Error: /xdg-data-home/Trash/info/empty.trashinfo: Unable " "to parse Path.\n", '') def test_should_warn_about_unreadable_trashinfo(self, user): user.home_trash_dir().add_unreadable_trashinfo('unreadable') output = user.run_trash_list() assert output.err_and_out() == ( "[Errno 13] Permission denied: " "'/xdg-data-home/Trash/info/unreadable.trashinfo'\n", '') def test_should_warn_about_unexistent_path_entry(self, user): user.home_trash_dir().add_trashinfo_without_path("foo") output = user.run_trash_list() assert output.err_and_out() == ( "Parse Error: /xdg-data-home/Trash/info/foo.trashinfo: " "Unable to parse Path.\n", '') trash-cli-0.24.5.26/tests/test_list/cmd/test_version.py000066400000000000000000000005721462460053300227420ustar00rootroot00000000000000# Copyright (C) 2011-2024 Andrea Francia Trivolzio(PV) Italy from tests.test_list.cmd.support.trash_list_user import trash_list_user user = trash_list_user class TestVersion: def test_should_output_the_version(self, user): user.set_version('1.2.3') output = user.run_trash_list('--version') assert output.whole_output() == 'trash-list 1.2.3\n' trash-cli-0.24.5.26/tests/test_list/cmd/test_with_a_top_trash_dir.py000066400000000000000000000036751462460053300254600ustar00rootroot00000000000000# Copyright (C) 2011-2024 Andrea Francia Trivolzio(PV) Italy import pytest from tests.test_list.cmd.support.trash_list_user import trash_list_user # noqa class TestWithATopTrashDir: @pytest.fixture def user(self, trash_list_user): u = trash_list_user u.set_fake_uid(123) u.add_disk("topdir") return u def test_should_list_its_contents_if_parent_is_sticky(self, user): user.trash_dir1("topdir").make_parent_sticky() user.trash_dir1("topdir").add_a_valid_trashinfo() output = user.run_trash_list() assert output.whole_output() == "2000-01-01 00:00:00 /topdir/file1\n" def test_and_should_warn_if_parent_is_not_sticky(self, user): user.trash_dir1("topdir").make_parent_unsticky() user.trash_dir1("topdir").make_dir() output = user.run_trash_list() assert output.whole_output() == ( "TrashDir skipped because parent not sticky: /topdir/.Trash/123\n") def test_but_it_should_not_warn_when_the_parent_is_unsticky_but_there_is_no_trashdir( self, user): user.trash_dir1("topdir").make_parent_unsticky() user.trash_dir1("topdir").does_not_exist() output = user.run_trash_list() assert output.whole_output() == '' def test_should_ignore_trash_from_a_unsticky_topdir(self, user): user.trash_dir1("topdir").make_parent_unsticky() user.trash_dir1("topdir").add_a_valid_trashinfo() output = user.run_trash_list() assert output.whole_output() == ( 'TrashDir skipped because parent not sticky: /topdir/.Trash/123\n') def test_it_should_skip_a_symlink(self, user): user.trash_dir1("topdir").make_parent_symlink() user.trash_dir1("topdir").add_a_valid_trashinfo() output = user.run_trash_list() assert output.err_and_out() == ( 'TrashDir skipped because parent not sticky: /topdir/.Trash/123\n', '') trash-cli-0.24.5.26/tests/test_list/components/000077500000000000000000000000001462460053300212625ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/components/__init__.py000066400000000000000000000000001462460053300233610ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_list/components/test_deletion_date_extractor.py000066400000000000000000000011131462460053300275620ustar00rootroot00000000000000import datetime import unittest from trashcli.list.extractors import DeletionDateExtractor class TestDeletionDateExtractor(unittest.TestCase): def setUp(self): self.extractor = DeletionDateExtractor() def test_extract_attribute_default(self): result = self.extractor.extract_attribute(None, "DeletionDate=") assert result == '????-??-?? ??:??:??' def test_extract_attribute_value(self): result = self.extractor.extract_attribute(None, "DeletionDate=2001-01-01T10:10:10") assert result == datetime.datetime(2001, 1, 1, 10, 10, 10) trash-cli-0.24.5.26/tests/test_list/components/test_file_size.py000066400000000000000000000004541462460053300246470ustar00rootroot00000000000000from tests.support.dirs.temp_dir import temp_dir # noqa from tests.support.files import make_file from trashcli import fs class TestFileSize: def test(self, temp_dir): make_file(temp_dir / 'a-file', '123') result = fs.file_size(temp_dir / 'a-file') assert 3 == result trash-cli-0.24.5.26/tests/test_list/components/test_trash_dirs_selector.py000066400000000000000000000025001462460053300267320ustar00rootroot00000000000000import unittest from tests.support.fakes.stub_volume_of import StubVolumeOf from trashcli.list.trash_dir_selector import TrashDirsSelector from trashcli.trash_dirs_scanner import trash_dir_found class MockScanner: def __init__(self, name): self.name = name def scan_trash_dirs(self, environ, uid): return [self.name, environ, uid] class TestTrashDirsSelector(unittest.TestCase): def setUp(self): volumes = StubVolumeOf() self.selector = TrashDirsSelector(MockScanner("user"), MockScanner("all"), volumes) def test_default(self): result = list(self.selector.select(False, [], 'environ', 'uid')) assert result == ['user', 'environ', 'uid'] def test_user_specified(self): result = list(self.selector.select(False, ['user-specified-dirs'], 'environ', 'uid')) assert result == [(trash_dir_found, ('user-specified-dirs', 'volume_of user-specified-dirs'))] def test_all_user_specified(self): result = list(self.selector.select(True, ['user-specified-dirs'], 'environ', 'uid')) assert result == ['all', 'environ', 'uid'] trash-cli-0.24.5.26/tests/test_list/components/test_trash_list_parser.py000066400000000000000000000022341462460053300264240ustar00rootroot00000000000000import unittest import trashcli.list import trashcli.list.main import trashcli.list.parser from trashcli.lib.print_version import PrintVersionArgs class TestTrashListParser(unittest.TestCase): def setUp(self): self.parser = trashcli.list.parser.Parser("trash-list") def test_version(self): args = self.parse(['--version']) assert PrintVersionArgs == type(args) def test_trash_dir_not_specified(self): args = self.parse([]) assert [] == args.trash_dirs def test_trash_dir_specified(self): args = self.parse(['--trash-dir=foo']) assert ['foo'] == args.trash_dirs def test_size_off(self): args = self.parse([]) assert 'deletion_date' == args.attribute_to_print def test_size_on(self): args = self.parse(['--size']) assert 'size' == args.attribute_to_print def test_files_off(self): args = self.parse([]) assert False == args.show_files def test_files_on(self): args = self.parse(['--files']) assert True == args.show_files def parse(self, args): return self.parser.parse_list_args(args, 'trash-list') trash-cli-0.24.5.26/tests/test_put/000077500000000000000000000000001462460053300167325ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/__init__.py000066400000000000000000000000001462460053300210310ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/cmd/000077500000000000000000000000001462460053300174755ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/cmd/__init__.py000066400000000000000000000000001462460053300215740ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/cmd/e2e/000077500000000000000000000000001462460053300201505ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/cmd/e2e/__init__.py000066400000000000000000000000001462460053300222470ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/cmd/e2e/run_trash_put/000077500000000000000000000000001462460053300230455ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/cmd/e2e/run_trash_put/__init__.py000066400000000000000000000051361462460053300251630ustar00rootroot00000000000000from typing import List from typing import Optional from tests.support.run.run_command import run_command from tests.support.dirs.my_path import MyPath from tests.test_put.cmd.e2e.run_trash_put.put_result import PutResult from tests.test_put.cmd.e2e.run_trash_put.put_result import make_put_result from tests.test_put.cmd.e2e.run_trash_put.stream import Stream from trashcli.lib.environ import Environ def run_trash_put(tmp_dir, # type: MyPath args, # type: List[str] env=None, # type: Optional[Environ] ): # type: (...) -> PutResult env = env or {} root_dir = tmp_dir cur_dir = tmp_dir trash_dir = tmp_dir / 'trash-dir' return run_trash_put4(root_dir, cur_dir, trash_dir, args, env) def run_trash_put3(cur_dir, # type: MyPath trash_dir, # type: MyPath args, # type: List[str] ): # type: (...) -> PutResult return run_trash_put4(cur_dir, cur_dir, trash_dir, args, env={}) def run_trash_put4(root_dir, # type: MyPath cur_dir, # type: MyPath trash_dir, # type: MyPath args, # type: List[str] env, # type: Environ ): # type: (...) -> PutResult extra_args = [ '-v', '--trash-dir', trash_dir, ] args = extra_args + args return run_trash_put23(root_dir, cur_dir, args, env) def run_trashput_with_vol(temp_dir, # type: MyPath fake_vol, # type: MyPath args, # type: List[str] ): # type: (...) -> Stream result = run_trash_put2(temp_dir, ["--force-volume=%s" % fake_vol, '-v'] + args, env=with_uid(123)) output = result.both().replace(fake_vol, "/vol") return output def run_trash_put2(cur_dir, # type: MyPath args, # type: List[str] env, # type: Environ ): # type: (...) -> PutResult return run_trash_put23(cur_dir, cur_dir, args, env) def run_trash_put23(root_dir, # type: MyPath cur_dir, # type: MyPath args, # type: List[str] env, # type: Environ ): # type: (...) -> PutResult result = run_command(cur_dir, 'trash-put', args, env=env) return make_put_result(result, root_dir) def with_uid(uid): return {'TRASH_PUT_FAKE_UID_FOR_TESTING': str(uid)} trash-cli-0.24.5.26/tests/test_put/cmd/e2e/run_trash_put/directory_layout.py000066400000000000000000000030651462460053300270240ustar00rootroot00000000000000from tests.test_put.cmd.e2e.run_trash_put import PutResult from tests.test_put.cmd.e2e.run_trash_put import run_trash_put4 class Result: def __init__(self, layout, # type: DirectoriesLayout output, # type: PutResult ): self.output = output self.layout = layout def status(self): return { 'command output': "\n".join(self.output.stderr.lines()), 'file left in current_dir': self.layout.cur_dir.list_all_files_sorted(), 'file in trash dir': self.layout.trash_dir.list_all_files_sorted(), } class DirectoriesLayout: def __init__(self, root_dir, fs): self.root_dir = root_dir self.fs = fs @property def cur_dir(self): return self.root_dir / 'cur-dir' @property def trash_dir(self): return self.root_dir / 'trash-dir' def make_cur_dir(self): self.fs.mkdir(self.cur_dir) def run_trash_put(self, args, # type: list[str] ): # type: (...) -> Result output = run_trash_put4(self.root_dir, self.cur_dir, self.trash_dir, args, env={}) return Result(self, output) def mkdir_in_cur_dir(self, relative_path): self.fs.mkdir(self.cur_dir / relative_path) def touch_in_cur_dir(self, relative_path): self.fs.touch(self.cur_dir / "a-file") def symlink_in_cur_dir(self, src, dest): self.fs.symlink(self.cur_dir / src, self.cur_dir / dest) trash-cli-0.24.5.26/tests/test_put/cmd/e2e/run_trash_put/put_result.py000066400000000000000000000024131462460053300256250ustar00rootroot00000000000000from typing import NamedTuple from tests.support.run.cmd_result import CmdResult from tests.support.help.help_reformatting import reformat_help_message from tests.support.dirs.my_path import MyPath from tests.test_put.cmd.e2e.run_trash_put.stream import Stream class PutResult(NamedTuple('Output', [ ('stderr', Stream), ('stdout', Stream), ('exit_code', int), ('temp_dir', MyPath), ])): def help_message(self): return reformat_help_message(self.stdout.stream) def combined(self): return [self.stderr.cleaned() + self.stdout.cleaned(), self.exit_code] def status(self): return ["output is %s" % self.both().describe_stream(), "exit code is %s" % self.exit_code] def both(self): return Stream(stream=self.stderr.stream + self.stdout.stream, temp_dir=self.temp_dir) def make_put_result(result, # type: CmdResult temp_dir, # type: MyPath ): # type: (...) -> PutResult return PutResult(stdout=Stream(stream=result.stdout, temp_dir=temp_dir), stderr=Stream(stream=result.stderr, temp_dir=temp_dir), exit_code=result.exit_code, temp_dir=temp_dir) trash-cli-0.24.5.26/tests/test_put/cmd/e2e/run_trash_put/stream.py000066400000000000000000000016351462460053300247170ustar00rootroot00000000000000from typing import NamedTuple from tests.support.text.grep import grep from tests.support.dirs.my_path import MyPath class Stream(NamedTuple('Output', [ ('stream', str), ('temp_dir', MyPath) ])): def lines(self): return self.stream.replace(self.temp_dir, '').splitlines() def last_line(self): return self.lines()[-1] def first_line(self): return self.lines()[0] def cleaned(self): return self.stream.replace(self.temp_dir, '') def describe_stream(self): # type: () -> str if len(self.stream) == 0: return "empty" else: return repr(self.stream) def grep(self, pattern): return Stream(stream=grep(self.stream, pattern), temp_dir=self.temp_dir) def replace(self, old, new): return Stream(stream=self.stream.replace(old, new), temp_dir=self.temp_dir) trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_end_to_end_put.py000066400000000000000000000074711462460053300245600ustar00rootroot00000000000000# Copyright (C) 2021 Andrea Francia Bereguardo(PV) Italy from textwrap import dedent import pytest from tests.support.dirs.my_path import MyPath from tests.support.dirs.temp_dir import temp_dir from tests.test_put.cmd.e2e.run_trash_put import run_trash_put from trashcli.lib.exit_codes import EX_IOERR temp_dir = temp_dir @pytest.mark.slow class TestEndToEndPut: def setup_method(self): self.tmp_dir = MyPath.make_temp_dir() def test_last_line_of_help(self, temp_dir): result = run_trash_put(temp_dir, ['--help']) assert result.stdout.last_line() == \ 'Report bugs to https://github.com/andreafrancia/trash-cli/issues' def test_without_args(self, temp_dir): result = run_trash_put(temp_dir, []) assert ([result.stderr.first_line(), result.exit_code] == ['usage: trash-put [OPTION]... FILE...', 2]) def test_wrong_option(self, temp_dir): result = run_trash_put(temp_dir, ['--wrong-option']) assert [result.stderr.last_line(), result.exit_code] == \ ['trash-put: error: unrecognized arguments: --wrong-option', 2] def test_on_help(self, temp_dir): result = run_trash_put(temp_dir, ['--help']) assert [result.help_message(), result.exit_code] == \ [dedent('''\ usage: trash-put [OPTION]... FILE... Put files in trash positional arguments: files options: -h, --help show this help message and exit --print-completion {bash,zsh,tcsh} print shell completion script -d, --directory ignored (for GNU rm compatibility) -f, --force silently ignore nonexistent files -i, --interactive prompt before every removal -r, -R, --recursive ignored (for GNU rm compatibility) --trash-dir TRASHDIR use TRASHDIR as trash folder -v, --verbose explain what is being done --version show program's version number and exit all trash-cli commands: trash-put trash files and directories. trash-empty empty the trashcan(s). trash-list list trashed files. trash-restore restore a trashed file. trash-rm remove individual files from the trashcan To remove a file whose name starts with a '-', for example '-foo', use one of these commands: trash -- -foo trash ./-foo Report bugs to https://github.com/andreafrancia/trash-cli/issues '''), 0] def test_it_should_skip_dot_entry(self, temp_dir): result = run_trash_put(temp_dir, ['.']) assert result.combined() == \ ["trash-put: cannot trash directory '.'\n", EX_IOERR] def test_it_should_skip_dotdot_entry(self, temp_dir): result = run_trash_put(temp_dir, ['..']) assert result.combined() == \ ["trash-put: cannot trash directory '..'\n", EX_IOERR] def test_it_should_print_usage_on_no_argument(self, temp_dir): result = run_trash_put(temp_dir, []) assert result.combined() == \ ['usage: trash-put [OPTION]... FILE...\n' 'trash-put: error: Please specify the files to trash.\n', 2] def test_it_should_skip_missing_files(self, temp_dir): result = run_trash_put(temp_dir, ['-f', 'this_file_does_not_exist', 'nor_does_this_file']) assert result.combined() == ['', 0] trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_on_dot_arguments.py000066400000000000000000000036051462460053300251340ustar00rootroot00000000000000import pytest from tests.support.dirs.temp_dir import temp_dir from tests.support.files import mkdir_p from tests.test_put.cmd.e2e.run_trash_put import run_trash_put from trashcli.lib.exit_codes import EX_IOERR temp_dir = temp_dir @pytest.mark.slow class TestWhenFedWithDotArguments: def test_dot_argument_is_skipped(self, temp_dir): result = run_trash_put(temp_dir, ["."]) # the dot directory shouldn't be operated, but a diagnostic message # shall be written on stderr assert result.combined() == [ "trash-put: cannot trash directory '.'\n", EX_IOERR] def test_dot_dot_argument_is_skipped(self, temp_dir): result = run_trash_put(temp_dir, [".."]) # the dot directory shouldn't be operated, but a diagnostic message # shall be written on stderr assert result.combined() == [ "trash-put: cannot trash directory '..'\n", EX_IOERR] def test_dot_argument_is_skipped_even_in_subdirs(self, temp_dir): sandbox = temp_dir / 'sandbox' mkdir_p(sandbox) result = run_trash_put(temp_dir, ["%s/." % sandbox]) # the dot directory shouldn't be operated, but a diagnostic message # shall be written on stderr assert result.combined() + temp_dir.existence_of(sandbox) == [ "trash-put: cannot trash '.' directory '/sandbox/.'\n", EX_IOERR, "/sandbox: exists"] def test_dot_dot_argument_is_skipped_even_in_subdirs(self, temp_dir): sandbox = temp_dir / 'sandbox' mkdir_p(sandbox) result = run_trash_put(temp_dir, ["%s/.." % sandbox]) # the dot directory shouldn't be operated, but a diagnostic message # shall be written on stderr assert result.combined() + temp_dir.existence_of(sandbox) == [ "trash-put: cannot trash '..' directory '/sandbox/..'\n", EX_IOERR, "/sandbox: exists"] trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_on_existing_file.py000066400000000000000000000022451462460053300251110ustar00rootroot00000000000000# Copyright (C) 2009-2020 Andrea Francia Trivolzio(PV) Italy import pytest from tests.support.dirs.temp_dir import temp_dir from tests.support.files import make_empty_file from tests.test_put.cmd.e2e.run_trash_put import run_trash_put2 temp_dir = temp_dir @pytest.mark.slow class TestOnExistingFile: def test_it_should_be_trashed(self, temp_dir): make_empty_file(temp_dir / 'foo') result = run_trash_put2(temp_dir, [temp_dir / "foo"], self._with_xdg_data_dir(temp_dir)) assert self._status_of_trash(temp_dir) + result.status() == [ '/foo: does not exist', '/XDG_DATA_HOME/Trash/info/foo.trashinfo: exists', '/XDG_DATA_HOME/Trash/files/foo: exists', 'output is empty', 'exit code is 0', ] @staticmethod def _status_of_trash(temp_dir): return temp_dir.existence_of('foo', 'XDG_DATA_HOME/Trash/info/foo.trashinfo', 'XDG_DATA_HOME/Trash/files/foo') @staticmethod def _with_xdg_data_dir(temp_dir): return {'XDG_DATA_HOME': temp_dir / 'XDG_DATA_HOME'} trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_on_links_to_dirs.py000066400000000000000000000033151462460053300251220ustar00rootroot00000000000000import pytest from tests.support.dirs.temp_dir import temp_dir from tests.test_put.cmd.e2e.run_trash_put.directory_layout import \ DirectoriesLayout from trashcli.put.fs.real_fs import RealFs temp_dir = temp_dir @pytest.mark.slow class TestOnLinksToDirs: def test_link_to_dir_without_slashes(self, temp_dir): layout = DirectoriesLayout(temp_dir, RealFs()) layout.make_cur_dir() layout.mkdir_in_cur_dir('a-dir') layout.touch_in_cur_dir('a-file') layout.symlink_in_cur_dir('a-dir', 'link-to-dir') result = layout.run_trash_put(['link-to-dir']) assert result.status() == { 'command output': "trash-put: 'link-to-dir' trashed in /trash-dir", 'file left in current_dir': ['/a-dir', '/a-file'], 'file in trash dir': ['/files', '/files/link-to-dir', '/info', '/info/link-to-dir.trashinfo'], } def test_link_to_dir_with_slashes(self, temp_dir): layout = DirectoriesLayout(temp_dir, RealFs()) layout.make_cur_dir() layout.mkdir_in_cur_dir("a-dir") layout.touch_in_cur_dir("a-file") layout.symlink_in_cur_dir('a-dir', 'link-to-dir') result = layout.run_trash_put(['link-to-dir/']) assert result.status() == { 'command output': "trash-put: 'link-to-dir/' trashed in /trash-dir", 'file left in current_dir': ['/a-dir', '/a-file'], 'file in trash dir': ['/files', '/files/link-to-dir', '/info', '/info/link-to-dir.trashinfo'], } trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_on_non_existent_file.py000066400000000000000000000007521462460053300257750ustar00rootroot00000000000000import pytest from tests.test_put.cmd.e2e.run_trash_put import run_trash_put from tests.test_put.cmd.e2e.test_end_to_end_put import temp_dir from trashcli.lib.exit_codes import EX_IOERR temp_dir = temp_dir @pytest.mark.slow class TestOnNonExistentFile: def test_fails(self, temp_dir): result = run_trash_put(temp_dir, ['non-existent']) assert (result.combined() == ["trash-put: cannot trash non existent 'non-existent'\n", EX_IOERR]) trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_on_symbolic_links.py000066400000000000000000000027031462460053300253000ustar00rootroot00000000000000import os import pytest from tests.support.dirs.temp_dir import temp_dir from tests.support.files import make_file from tests.support.dirs.my_path import MyPath from tests.test_put.cmd.e2e.run_trash_put import run_trash_put from trashcli.put.fs.real_fs import RealFs fs = RealFs() temp_dir = temp_dir def _make_connected_link(path): # type: (MyPath) -> None make_file(path.parent / 'link-target') os.symlink('link-target', path) def _make_dangling_link(path): # type: (MyPath) -> None os.symlink('non-existent', path) @pytest.mark.slow class TestOnSymbolicLinks: def test_trashes_dangling_symlink(self, temp_dir): _make_dangling_link(temp_dir / 'link') output = run_trash_put(temp_dir, ['link'], env={"TRASH_PUT_DISABLE_SHRINK": "1"}) assert output.stderr.lines() == [ "trash-put: 'link' trashed in /trash-dir"] assert not os.path.lexists(temp_dir / 'link') assert os.path.lexists(temp_dir / 'trash-dir' / 'files' / 'link') def test_trashes_connected_symlink(self, temp_dir): _make_connected_link(temp_dir / 'link') output = run_trash_put(temp_dir, ['link'], env={"TRASH_PUT_DISABLE_SHRINK": "1"}) assert output.stderr.lines() == ["trash-put: 'link' trashed in /trash-dir"] assert not os.path.lexists(temp_dir / 'link') assert os.path.lexists(temp_dir / 'trash-dir' / 'files' / 'link') trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_on_trashing_a_file.py000066400000000000000000000013761462460053300254020ustar00rootroot00000000000000import pytest from tests.support.files import make_empty_file from tests.test_put.cmd.e2e.run_trash_put import run_trash_put2 from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir @pytest.mark.slow class TestOnTrashingAFile: def test_in_verbose_mode_should_tell_where_a_file_is_trashed(self, temp_dir): env = {'XDG_DATA_HOME': temp_dir / 'XDG_DATA_HOME', 'HOME': temp_dir / 'home'} make_empty_file(temp_dir / "foo") result = run_trash_put2(temp_dir, ["-v", temp_dir / "foo"], env) assert [result.both().cleaned(), result.exit_code] == [ "trash-put: '/foo' trashed in /XDG_DATA_HOME/Trash\n", 0 ] trash-cli-0.24.5.26/tests/test_put/cmd/e2e/test_unsecure_trash_dir_messages.py000066400000000000000000000043041462460053300273410ustar00rootroot00000000000000# Copyright (C) 2009-2020 Andrea Francia Trivolzio(PV) Italy import os import pytest from tests.support.files import make_empty_file from tests.support.files import make_sticky_dir from tests.support.files import require_empty_dir from tests.test_put.cmd.e2e.run_trash_put import run_trashput_with_vol from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir @pytest.mark.slow class TestUnsecureTrashDirMessages: @pytest.fixture def fake_vol(self, temp_dir): return temp_dir / 'fake-vol' def test_when_is_unsticky(self, temp_dir, fake_vol): require_empty_dir(fake_vol) make_empty_file(fake_vol / 'foo') require_empty_dir(fake_vol / '.Trash') output = run_trashput_with_vol(temp_dir, fake_vol, [fake_vol / 'foo']) assert output.grep("/.Trash/123").stream == ( 'trash-put: `- failed to trash /vol/foo in /vol/.Trash/123, because trash ' 'dir is insecure, its parent should be sticky, trash-dir: /vol/.Trash/123, ' 'parent: /vol/.Trash\n' ) def test_when_it_is_not_a_dir(self, fake_vol, temp_dir): require_empty_dir(fake_vol) make_empty_file(fake_vol / 'foo') make_empty_file(fake_vol / '.Trash') output = run_trashput_with_vol(temp_dir, fake_vol, [fake_vol / 'foo']) assert output.grep("/.Trash/123").stream == ( 'trash-put: `- failed to trash /vol/foo in /vol/.Trash/123, because trash ' 'dir cannot be created as its parent is a file instead of being a directory, ' 'trash-dir: /vol/.Trash/123, parent: /vol/.Trash\n' ) def test_when_is_a_symlink(self, fake_vol, temp_dir): require_empty_dir(fake_vol) make_empty_file(fake_vol / 'foo') make_sticky_dir(fake_vol / 'link-destination') os.symlink('link-destination', fake_vol / '.Trash') output = run_trashput_with_vol(temp_dir, fake_vol, [fake_vol / 'foo']) assert output.grep("insecure").stream == ( 'trash-put: `- failed to trash /vol/foo in /vol/.Trash/123, because ' 'trash dir is insecure, its parent should not be a symlink, trash-dir: ' '/vol/.Trash/123, parent: /vol/.Trash\n') trash-cli-0.24.5.26/tests/test_put/cmd/test_put.py000066400000000000000000000341411462460053300217210ustar00rootroot00000000000000import os from typing import List from typing import Optional import flexmock from six import StringIO from tests.support.fakes.fake_is_mount import FakeIsMount from tests.support.put.dummy_clock import FixedClock from tests.support.put.dummy_clock import jan_1st_2024 from tests.support.put.fake_fs.failing_fake_fs import FailingFakeFs from tests.support.put.fake_random import FakeRandomInt from tests.test_put.support.recording_backend import RecordingBackend from tests.test_put.support.result import Result from trashcli.fstab.volume_of_impl import VolumeOfImpl from trashcli.lib.environ import Environ from trashcli.lib.exit_codes import EX_IOERR from trashcli.lib.exit_codes import EX_OK from trashcli.lib.my_input import HardCodedInput from trashcli.put.core.logs import LogTag from trashcli.put.main import make_cmd from trashcli.put.parser import ensure_int class TestPut: def setup_method(self): self.fs = FailingFakeFs() self.user_input = HardCodedInput('y') self.randint = FakeRandomInt([]) def add_mount(self, path): self.fs.add_volume(path) def test_when_needs_a_different_suffix(self): self.fs.touch("/foo") self.fs.fail_atomic_create_unless("foo_1.trashinfo") self.run_cmd(['trash-put', '/foo']) assert self.fs.ls_aa('/.Trash-123/files') == ['foo_1'] def test_when_needs_a_random_suffix(self): self.fs.touch("/foo") self.fs.fail_atomic_create_unless("foo_123.trashinfo") self.randint.set_reply(123) self.run_cmd(['trash-put', '/foo']) assert self.fs.ls_aa('/.Trash-123/files') == ['foo_123'] def test_when_a_trashinfo_file_already_exists(self): def touch_and_trash(path): self.fs.touch(path) self.run_cmd(['trash-put', path]) touch_and_trash("/foo") touch_and_trash("/foo") touch_and_trash("/foo") assert self.fs.ls_aa('/.Trash-123/info') == [ 'foo.trashinfo', 'foo_1.trashinfo', 'foo_2.trashinfo' ] def test_when_moving_file_in_trash_dir_fails(self): self.fs.touch("foo") self.fs.fail_move_on("/foo") result = self.run_cmd(['trash-put', '-v', '/foo']) assert result.exit_code_and_stderr() == [EX_IOERR, [ "trash-put: cannot trash regular empty file '/foo' (from volume '/')", 'trash-put: `- failed to trash /foo in /.Trash/123, because trash dir cannot be created because its parent does not exists, trash-dir: /.Trash/123, parent: /.Trash', 'trash-put: `- failed to trash /foo in /.Trash-123, because failed to move /foo in /.Trash-123/files: move failed']] def test_should_not_trash_dot_entry(self): result = self.run_cmd(['trash-put', '.']) assert result.exit_code_and_stderr() == [ EX_IOERR, ["trash-put: cannot trash directory '.'"]] def test_should_not_trash_dot_dot_entry(self): result = self.run_cmd(['trash-put', '..']) assert result.exit_code_and_stderr() == [ EX_IOERR, ["trash-put: cannot trash directory '..'"]] def test_user_reply_no(self): self.fs.touch("foo") self.user_input.set_reply('n') result = self.run_cmd(['trash-put', '-i', 'foo']) assert result.exit_code_and_stderr() + [self.user_input.last_prompt(), self.fs.ls_existing( ["foo"])] == [ EX_OK, [], "trash-put: trash regular empty file 'foo'? ", ['foo'], ] def test_user_reply_yes(self): self.fs.touch("foo") self.user_input.set_reply('y') result = self.run_cmd(['trash-put', '-i', 'foo']) assert result.exit_code_and_stderr() + [self.user_input.last_prompt(), self.fs.ls_existing( ["foo"])] == [ EX_OK, [], "trash-put: trash regular empty file 'foo'? ", [] ] def test_when_file_does_not_exist(self): result = self.run_cmd(['trash-put', 'non-existent'], {"HOME": "/home/user"}, 123) assert result.exit_code_and_stderr() == [ EX_IOERR, ["trash-put: cannot trash non existent 'non-existent'"]] def test_when_file_does_not_exist_with_force(self): result = self.run_cmd(['trash-put', '-f', 'non-existent'], {"HOME": "/home/user"}, 123) assert result.exit_code_and_stderr() == [EX_OK, []] def test_put_does_not_try_to_trash_non_existing_file(self): result = self.run_cmd(['trash-put', '-vvv', 'non-existing'], {"HOME": "/home/user"}, 123) assert result.exit_code_and_stderr() == \ [ EX_IOERR, ["trash-put: cannot trash non existent 'non-existing'"] ] def test_when_file_cannot_be_trashed(self): self.fs.touch("foo") self.fs.fail_move_on("foo") result = self.run_cmd(['trash-put', 'foo']) assert (result.exit_code_and_logs(LogTag.trash_failed) == (74, [ "cannot trash regular empty file 'foo' (from volume '/')", ' `- failed to trash foo in /.Trash/123, because trash dir ' 'cannot be created because its parent does not exists, ' 'trash-dir: /.Trash/123, parent: /.Trash', ' `- failed to trash foo in /.Trash-123, because failed to ' 'move foo in /.Trash-123/files: move failed', ])) def test_exit_code_will_be_0_when_trash_succeeds(self): self.fs.touch("pippo") result = self.run_cmd(['trash-put', 'pippo']) assert result.exit_code == EX_OK def test_exit_code_will_be_non_0_when_trash_fails(self): self.fs.assert_does_not_exist("a") result = self.run_cmd(['trash-put', 'a']) assert result.exit_code == EX_IOERR def test_exit_code_will_be_non_0_when_just_one_trash_fails(self): self.fs.touch("a") self.fs.assert_does_not_exist("b") self.fs.touch("c") result = self.run_cmd(['trash-put', 'a', 'b', 'c']) assert result.exit_code == EX_IOERR def test_when_there_is_no_working_trash_dir(self): self.fs.make_file("pippo") self.fs.makedirs('/.Trash-123', 0o000) result = self.run_cmd(['trash-put', '-v', 'pippo'], {}, 123) assert result.exit_code_and_stderr() == [ EX_IOERR, [ "trash-put: cannot trash regular empty file 'pippo' (from volume '/')", 'trash-put: `- failed to trash pippo in /.Trash/123, because trash dir cannot be created because its parent does not exists, trash-dir: /.Trash/123, parent: /.Trash', "trash-put: `- failed to trash pippo in /.Trash-123, because error during directory creation: [Errno 13] Permission denied: '/.Trash-123/files'" ] ] def test_multiple_volumes(self): self.fs.makedirs('/disk1', 0o700) self.fs.makedirs('/disk1/.Trash-123', 0o000) self.fs.make_file("/disk1/pippo") self.add_mount('/disk1') result = self.run_cmd(['trash-put', '-v', '--home-fallback', '/disk1/pippo'], {'HOME': '/home/user'}, 123) assert result.stderr == [ "trash-put: cannot trash regular empty file '/disk1/pippo' (from volume '/disk1')", 'trash-put: `- failed to trash /disk1/pippo in /home/user/.local/share/Trash, because trash dir and file to be trashed are not in the same volume, trash-dir volume: /, file volume: /disk1', 'trash-put: `- failed to trash /disk1/pippo in /disk1/.Trash/123, because trash dir cannot be created because its parent does not exists, trash-dir: /disk1/.Trash/123, parent: /disk1/.Trash', "trash-put: `- failed to trash /disk1/pippo in /disk1/.Trash-123, because error during directory creation: [Errno 13] Permission denied: '/disk1/.Trash-123/files'", 'trash-put: `- failed to trash /disk1/pippo in /home/user/.local/share/Trash, because home fallback not enabled'] def test_when_it_fails_to_prepare_trash_info_data(self): flexmock.flexmock(self.fs).should_receive('parent_realpath2'). \ and_raise(IOError, 'Corruption') self.fs.make_file("foo") result = self.run_cmd(['trash-put', '-v', 'foo'], {"HOME": "/home/user"}, 123) assert result.exit_code_and_stderr() == [ EX_IOERR, [ "trash-put: cannot trash regular empty file 'foo' (from volume '/')", 'trash-put: `- failed to trash foo in /home/user/.local/share/Trash, because failed to generate trashinfo content: Corruption', 'trash-put: `- failed to trash foo in /.Trash/123, because trash dir cannot be created because its parent does not exists, trash-dir: /.Trash/123, parent: /.Trash', 'trash-put: `- failed to trash foo in /.Trash-123, because failed to generate trashinfo content: Corruption']] def test_make_file(self): self.fs.make_file("pippo", 'content') assert True == self.fs.exists("pippo") def test_when_file_exists(self): self.fs.make_file("pippo", 'content') result = self.run_cmd(['trash-put', 'pippo'], {"HOME": "/home/user"}, 123) actual = { 'file_pippo_exists': self.fs.exists("pippo"), 'exit_code': result.exit_code, 'files_in_info_dir': self.fs.ls_aa( '/home/user/.local/share/Trash/info'), "content_of_trashinfo": self.fs.read( '/home/user/.local/share/Trash/info/pippo.trashinfo' ).decode('utf-8'), 'files_in_files_dir': self.fs.ls_aa( '/home/user/.local/share/Trash/files'), "content_of_trashed_file": self.fs.read( '/home/user/.local/share/Trash/files/pippo'), } assert actual == {'content_of_trashed_file': 'content', 'content_of_trashinfo': '[Trash Info]\nPath=/pippo\nDeletionDate=2014-01-01T00:00:00\n', 'exit_code': 0, 'file_pippo_exists': False, 'files_in_files_dir': ['pippo'], 'files_in_info_dir': ['pippo.trashinfo']} def test_when_file_move_fails(self): flexmock.flexmock(self.fs).should_receive('move'). \ and_raise(IOError, 'No space left on device') self.fs.make_file("pippo", 'content') result = self.run_cmd(['trash-put', 'pippo'], {"HOME": "/home/user"}, 123) actual = { 'file_pippo_exists': self.fs.exists("pippo"), 'exit_code': result.exit_code, 'stderr': result.stderr, 'files_in_info_dir': self.fs.ls_aa( '/home/user/.local/share/Trash/info'), "content_of_trashinfo": self.fs.read_null( '/home/user/.local/share/Trash/info/pippo.trashinfo'), 'files_in_files_dir': self.fs.ls_aa( '/home/user/.local/share/Trash/files'), "content_of_trashed_file": self.fs.read_null( '/home/user/.local/share/Trash/files/pippo'), } assert actual == {'content_of_trashed_file': None, 'content_of_trashinfo': None, 'exit_code': EX_IOERR, 'stderr': [ "trash-put: cannot trash regular file 'pippo' (from volume '/')", 'trash-put: `- failed to trash pippo in /home/user/.local/share/Trash, because failed to move pippo in /home/user/.local/share/Trash/files: No space left on device', 'trash-put: `- failed to trash pippo in /.Trash/123, because trash dir cannot be created because its parent does not exists, trash-dir: /.Trash/123, parent: /.Trash', 'trash-put: `- failed to trash pippo in /.Trash-123, because failed to move pippo in /.Trash-123/files: No space left on device'], 'file_pippo_exists': True, 'files_in_files_dir': [], 'files_in_info_dir': []} def test_when_a_error_during_move(self): self.fs.make_file("pippo", 'content') result = self.run_cmd(['trash-put', 'pippo'], {"HOME": "/home/user"}, 123) assert False == self.fs.exists("pippo") assert EX_OK == result.exit_code assert ['pippo.trashinfo'] == self.fs.ls_aa( '/home/user/.local/share/Trash/info') trash_info = self.fs.read( '/home/user/.local/share/Trash/info/pippo.trashinfo' ).decode('utf-8') assert trash_info == '[Trash Info]\nPath=/pippo\nDeletionDate=2014-01-01T00:00:00\n' assert ['pippo'] == self.fs.ls_aa( '/home/user/.local/share/Trash/files') assert self.fs.read('/home/user/.local/share/Trash/files/pippo') \ == 'content' def run_cmd(self, args, # type: List[str] environ=None, # type: Optional[Environ] uid=None, # type: Optional[int] ): # type: (...) -> Result environ = environ or {} uid = uid or 123 err = None exit_code = None stderr = StringIO() clock = FixedClock(jan_1st_2024()) backend = RecordingBackend(stderr) cmd = make_cmd(clock=clock, fs=self.fs, user_input=self.user_input, randint=self.randint, backend=backend) try: exit_code = cmd.run_put(args, environ, uid) except IOError as e: err = e stderr_lines = stderr.getvalue().splitlines() return Result(stderr_lines, str(err), ensure_int(exit_code), backend.collected()) trash-cli-0.24.5.26/tests/test_put/cmd/test_put_script.py000066400000000000000000000010451462460053300233020ustar00rootroot00000000000000import unittest import pytest from tests.support.run.run_command import run_command @pytest.mark.slow class TestRmScript(unittest.TestCase): def test_trash_put_works(self): result = run_command('.', 'trash-put') assert ("usage: trash-put [OPTION]... FILE..." in result.stderr.splitlines()) def test_trash_put_touch_filesystem(self): result = run_command('.', 'trash-put', ['non-existent']) assert ("trash-put: cannot trash non existent 'non-existent'\n" == result.stderr) trash-cli-0.24.5.26/tests/test_put/components/000077500000000000000000000000001462460053300211175ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/__init__.py000066400000000000000000000000001462460053300232160ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/real_fs/000077500000000000000000000000001462460053300225325ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/real_fs/__init__.py000066400000000000000000000000001462460053300246310ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_fake_fs_list_dir.py000066400000000000000000000011501462460053300274270ustar00rootroot00000000000000import unittest from tests.support.dirs.my_path import MyPath from tests.support.put.fake_fs.fake_fs import FakeFs class TestFakeFsListDir(unittest.TestCase): def setUp(self): self.fs = FakeFs() self.tmp_dir = MyPath('/tmp') self.fs.makedirs(self.tmp_dir, 0o700) def test(self): self.fs.make_file(self.tmp_dir / 'a', 'content') self.fs.make_file(self.tmp_dir / 'b', 'content') self.fs.make_file(self.tmp_dir / 'c', 'content') self.fs.makedirs(self.tmp_dir / 'd', 0o700) assert sorted(self.fs.listdir(self.tmp_dir)) == ['a', 'b', 'c', 'd'] trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_fake_fs_walk_no_follow.py000066400000000000000000000011201462460053300306270ustar00rootroot00000000000000from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.fs.fs import list_all class TestWalkNoFollow: def setup_method(self): self.fs = FakeFs() def test(self): self.fs.make_file("pippo") self.fs.makedirs("/a/b/c/d", 0o700) assert "\n".join(list_all(self.fs, "/")) == '/a\n' \ '/pippo\n' \ '/a/b\n' \ '/a/b/c\n' \ '/a/b/c/d' trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_real_fs_is_accessible.py000066400000000000000000000021701462460053300304260ustar00rootroot00000000000000import os import unittest from tests.support.dirs.my_path import MyPath from trashcli.put.fs.real_fs import RealFs class TestRealFsIsAccessible(unittest.TestCase): def setUp(self): self.fs = RealFs() self.tmp_dir = MyPath.make_temp_dir() def test_dangling_link(self): os.symlink('non-existent', self.tmp_dir / 'link') result = self.fs.is_accessible(self.tmp_dir / 'link') assert result is False def test_connected_link(self): self.fs.make_file(self.tmp_dir / 'link-target', '') os.symlink('link-target', self.tmp_dir / 'link') result = self.fs.is_accessible(self.tmp_dir / 'link') assert result is True def test_dangling_link_with_lexists(self): os.symlink('non-existent', self.tmp_dir / 'link') result = self.fs.lexists(self.tmp_dir / 'link') assert result is True def test_connected_link_with_lexists(self): self.fs.make_file(self.tmp_dir / 'link-target', '') os.symlink('link-target', self.tmp_dir / 'link') result = self.fs.lexists(self.tmp_dir / 'link') assert result is True trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_real_fs_list_dir.py000066400000000000000000000011631462460053300274500ustar00rootroot00000000000000import unittest from tests.support.dirs.my_path import MyPath from trashcli.put.fs.real_fs import RealFs class TestRealFsListDir(unittest.TestCase): def setUp(self): self.fs = RealFs() self.tmp_dir = MyPath.make_temp_dir() def test(self): self.fs.make_file(self.tmp_dir / 'a' , 'content') self.fs.make_file(self.tmp_dir / 'b' , 'content') self.fs.make_file(self.tmp_dir / 'c', 'content') self.fs.makedirs(self.tmp_dir / 'd', 0o700) assert sorted(self.fs.listdir(self.tmp_dir)) == ['a', 'b', 'c', 'd'] def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_real_fs_permissions.py000066400000000000000000000016321462460053300302130ustar00rootroot00000000000000import unittest from tests.support.capture_error import capture_error from tests.support.dirs.my_path import MyPath from trashcli.put.fs.real_fs import RealFs class TestRealFsPermissions(unittest.TestCase): def setUp(self): self.fs = RealFs() self.tmp_dir = MyPath.make_temp_dir() def test(self): self.fs.makedirs(self.tmp_dir / 'dir', 0o000) error = capture_error( lambda: self.fs.make_file(self.tmp_dir / 'dir' / 'file', 'content')) assert str(error) == "[Errno 13] Permission denied: '%s'" % ( self.tmp_dir / 'dir' / 'file') self.fs.chmod(self.tmp_dir / 'dir', 0o755) def test_chmod_and_get_mod(self): path = self.tmp_dir / 'file' self.fs.make_file(path, 'content') self.fs.chmod(path, 0o123) assert self.fs.get_mod(path) == 0o123 def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_size_counter_on_fake_fs.py000066400000000000000000000022371462460053300310320ustar00rootroot00000000000000import unittest from tests.support.dirs.my_path import MyPath from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.fs.size_counter import SizeCounter class TestSizeCounterOnFakeFs(unittest.TestCase): def setUp(self): self.fs = FakeFs() self.counter = SizeCounter(self.fs) self.fs.makedirs('/tmp', 0o777) self.tmp_dir = MyPath('/tmp') def test_a_single_file(self): self.fs.make_file(self.tmp_dir / 'file', 10 * 'a') assert self.counter.get_size_recursive(self.tmp_dir / 'file') == 10 def test_two_files(self): self.fs.make_file(self.tmp_dir / 'a', 100 * 'a') self.fs.make_file(self.tmp_dir / 'b', 23 * 'b') assert self.counter.get_size_recursive(self.tmp_dir) == 123 def test_recursive(self): self.fs.make_file(self.tmp_dir / 'a', 3 * '-') self.fs.makedirs(self.tmp_dir / 'dir', 0o777) self.fs.make_file(self.tmp_dir / 'dir' / 'a', 20 * '-') self.fs.makedirs(self.tmp_dir / 'dir' / 'dir', 0o777) self.fs.make_file(self.tmp_dir / 'dir' / 'dir' / 'b', 100 * '-') assert self.counter.get_size_recursive(self.tmp_dir) == 123 trash-cli-0.24.5.26/tests/test_put/components/real_fs/test_size_counter_on_real_fs.py000066400000000000000000000023171462460053300310460ustar00rootroot00000000000000import unittest import pytest from tests.support.dirs.my_path import MyPath from trashcli.put.fs.size_counter import SizeCounter from trashcli.put.fs.real_fs import RealFs @pytest.mark.slow class TestSizeCounterOnRealFs(unittest.TestCase): def setUp(self): self.fs = RealFs() self.counter = SizeCounter(self.fs) self.tmp_dir = MyPath.make_temp_dir() def test_a_single_file(self): self.fs.make_file(self.tmp_dir / 'file', 10 * 'a') assert self.counter.get_size_recursive(self.tmp_dir / 'file') == 10 def test_two_files(self): self.fs.make_file(self.tmp_dir / 'a', 100 * 'a') self.fs.make_file(self.tmp_dir / 'b', 23 * 'b') assert self.counter.get_size_recursive(self.tmp_dir) == 123 def test_recursive(self): self.fs.make_file(self.tmp_dir / 'a', 3 * '-') self.fs.makedirs(self.tmp_dir / 'dir', 0o777) self.fs.make_file(self.tmp_dir / 'dir' / 'a', 20 * '-') self.fs.makedirs(self.tmp_dir / 'dir' / 'dir', 0o777) self.fs.make_file(self.tmp_dir / 'dir' / 'dir' / 'b', 100 * '-') assert self.counter.get_size_recursive(self.tmp_dir) == 123 def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_put/components/test_atomic_write.py000066400000000000000000000026231462460053300252210ustar00rootroot00000000000000import errno import os import unittest import pytest from trashcli.fs import ( atomic_write, open_for_write_in_exclusive_and_create_mode, read_file, ) from tests.support.dirs.my_path import MyPath @pytest.mark.slow class Test_atomic_write(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() def test_the_second_open_should_fail(self): path = self.temp_dir / "a" file_handle = open_for_write_in_exclusive_and_create_mode(path) try: open_for_write_in_exclusive_and_create_mode(path) self.fail() except OSError as e: assert e.errno == errno.EEXIST os.close(file_handle) def test_short_filename(self): path = self.temp_dir / 'a' atomic_write(path, b'contents') assert 'contents' == read_file(path) def test_too_long_filename(self): path = self.temp_dir / ('a' * 2000) try: atomic_write(path, b'contents') self.fail() except OSError as e: assert e.errno == errno.ENAMETOOLONG def test_filename_already_taken(self): atomic_write(self.temp_dir / "a", b'contents') try: atomic_write(self.temp_dir / "a", b'contents') self.fail() except OSError as e: assert e.errno == errno.EEXIST def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_put/components/test_candidate_shrink_user.py000066400000000000000000000033631462460053300270650ustar00rootroot00000000000000import unittest from trashcli.put.core.candidate import Candidate class TestCandidateShrinkUser(unittest.TestCase): def setUp(self): self.environ = {} def test_should_substitute_tilde_in_place_of_home_dir(self): self.environ['HOME'] = '/home/user' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('~/.local/share/Trash') def test_should_not_substitute(self): self.environ['HOME'] = '/home/user' self.environ['TRASH_PUT_DISABLE_SHRINK'] = '1' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('/home/user/.local/share/Trash') def test_when_not_in_home_dir(self): self.environ['HOME'] = '/home/user' self.trash_dir = "/not-in-home/Trash" self.assert_name_is('/not-in-home/Trash') def test_tilde_works_also_with_trailing_slash(self): self.environ['HOME'] = '/home/user/' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('~/.local/share/Trash') def test_str_uses_tilde_with_many_slashes(self): self.environ['HOME'] = '/home/user////' self.trash_dir = "/home/user/.local/share/Trash" self.assert_name_is('~/.local/share/Trash') def test_dont_get_confused_by_empty_home_dir(self): self.environ['HOME'] = '' self.trash_dir = "/foo/Trash" self.assert_name_is('/foo/Trash') def test_should_work_even_if_HOME_does_not_exists(self): self.trash_dir = "/foo/Trash" self.assert_name_is('/foo/Trash') def assert_name_is(self, expected_name): self.candidate = Candidate(self.trash_dir, '', '', '', None) shrinked = self.candidate.shrink_user(self.environ) assert expected_name == shrinked trash-cli-0.24.5.26/tests/test_put/components/test_create_trashinfo_basename.py000066400000000000000000000013521462460053300277040ustar00rootroot00000000000000from trashcli.put.janitor_tools.info_file_persister import \ create_trashinfo_basename class TestCreateTrashinfoBasename: def test_when_file_name_is_not_too_long(self): assert 'basename_1.trashinfo' == create_trashinfo_basename('basename', '_1', False) def test_when_file_name_too_long(self): assert '12345678_1.trashinfo' == create_trashinfo_basename( '12345678901234567890', '_1', True) def test_when_file_name_too_long_with_big_suffix(self): assert '12345_9999.trashinfo' == create_trashinfo_basename( '12345678901234567890', '_9999', True) trash-cli-0.24.5.26/tests/test_put/components/test_describer.py000066400000000000000000000030771462460053300245010ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import unittest from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.describer import Describer class TestDescriber(unittest.TestCase): def setUp(self): self.fs = FakeFs() self.describer = Describer(self.fs) def test_on_directories(self): self.fs.mkdir('a-dir') assert "directory" == self.describer.describe('.') assert "directory" == self.describer.describe("..") assert "directory" == self.describer.describe('a-dir') def test_on_dot_directories(self): self.fs.mkdir('a-dir') assert "'.' directory" == self.describer.describe("a-dir/.") assert "'.' directory" == self.describer.describe("./.") def test_on_dot_dot_directories(self): self.fs.mkdir('a-dir') assert "'..' directory" == self.describer.describe("./..") assert "'..' directory" == self.describer.describe("a-dir/..") def test_name_for_regular_files_non_empty_files(self): self.fs.make_file("non-empty", "contents") assert "regular file" == self.describer.describe("non-empty") def test_name_for_empty_file(self): self.fs.make_file("empty") assert "regular empty file" == self.describer.describe("empty") def test_name_for_symbolic_links(self): self.fs.symlink("nowhere", "/symlink") assert "symbolic link" == self.describer.describe("symlink") def test_name_for_non_existent_entries(self): assert "non existent" == self.describer.describe('non-existent') trash-cli-0.24.5.26/tests/test_put/components/test_describer_integration.py000066400000000000000000000041061462460053300270760ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import os import unittest import pytest from tests.support.files import make_empty_file, make_file, require_empty_dir from tests.support.dirs.my_path import MyPath from trashcli.put.describer import Describer from trashcli.put.fs.real_fs import RealFs @pytest.mark.slow class TestDescriber(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() self.describer = Describer(RealFs()) def test_on_directories(self): require_empty_dir(self.temp_dir / 'a-dir') assert "directory" == self.describer.describe('.') assert "directory" == self.describer.describe("..") assert "directory" == self.describer.describe(self.temp_dir / 'a-dir') def test_on_dot_directories(self): require_empty_dir(self.temp_dir / 'a-dir') assert "'.' directory" == self.describer.describe( self.temp_dir / "a-dir/.") assert "'.' directory" == self.describer.describe("./.") def test_on_dot_dot_directories(self): require_empty_dir(self.temp_dir / 'a-dir') assert "'..' directory" == self.describer.describe("./..") assert "'..' directory" == self.describer.describe(self.temp_dir / "a-dir/..") def test_name_for_regular_files_non_empty_files(self): make_file(self.temp_dir / "non-empty", "contents") assert "regular file" == self.describer.describe(self.temp_dir / "non-empty") def test_name_for_empty_file(self): make_empty_file(self.temp_dir / 'empty') assert "regular empty file" == self.describer.describe(self.temp_dir / "empty") def test_name_for_symbolic_links(self): os.symlink('nowhere', self.temp_dir / "symlink") assert "symbolic link" == self.describer.describe(self.temp_dir / "symlink") def test_name_for_non_existent_entries(self): assert not os.path.exists(self.temp_dir / 'non-existent') assert "non existent" == self.describer.describe(self.temp_dir / 'non-existent') def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_put/components/test_ensure_dir.py000066400000000000000000000016341462460053300246730ustar00rootroot00000000000000import unittest from tests.support.put.fake_fs.fake_fs import FakeFs from tests.support.put.format_mode import format_mode from trashcli.put.dir_maker import DirMaker class TestEnsureDir(unittest.TestCase): def setUp(self): self.fs = FakeFs('/') self.dir_maker = DirMaker(self.fs) def test_happy_path(self): self.dir_maker.mkdir_p('/foo', 0o755) assert [self.fs.isdir('/foo'), format_mode(self.fs.get_mod('/foo'))] == [True, '0o755'] def test_makedirs_honor_permissions(self): self.fs.makedirs('/foo', 0o000) assert [format_mode(self.fs.get_mod('/foo'))] == ['0o000'] def test_bug_when_no_permissions_it_overrides_the_permissions(self): self.fs.makedirs('/foo', 0o000) self.dir_maker.mkdir_p('/foo', 0o755) assert [self.fs.isdir('/foo'), format_mode(self.fs.get_mod('/foo'))] == [True, '0o000'] trash-cli-0.24.5.26/tests/test_put/components/test_fake_fs/000077500000000000000000000000001462460053300235545ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/test_fake_fs/__init__.py000066400000000000000000000000001462460053300256530ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/test_fake_fs/test_fake_fs.py000066400000000000000000000117541462460053300265730ustar00rootroot00000000000000import unittest from tests.support.put.fake_fs.fake_fs import FakeFs from tests.support.put.format_mode import format_mode from tests.support.capture_error import capture_error class TestFakeFs(unittest.TestCase): def setUp(self): self.fs = FakeFs('/') def test(self): result = self.fs.ls_a("/") assert result == [".", ".."] def test_create_dir(self): self.fs.mkdir("/foo") result = self.fs.ls_a("/") assert result == [".", "..", "foo"] def test_find_dir_root(self): assert '/' == self.fs.get_entity_at('/').name def test_find_dir_root_subdir(self): self.fs.mkdir("/foo") assert 'foo' == self.fs.get_entity_at('/foo').name def test_create_dir_in_dir(self): self.fs.mkdir("/foo") self.fs.mkdir("/foo/bar") result = self.fs.ls_a("/foo") assert result == [".", "..", "bar"] def test_create_file(self): self.fs.mkdir("/foo") self.fs.mkdir("/foo/bar") self.fs.atomic_write("/foo/bar/baz", "content") result = self.fs.read("/foo/bar/baz") assert result == "content" def test_chmod(self): self.fs.make_file("/foo") self.fs.chmod("/foo", 0o755) assert oct(self.fs.get_mod("/foo")) == oct(0o755) def test_is_dir_when_file(self): self.fs.make_file("/foo") assert self.fs.isdir("/foo") is False def test_is_dir_when_dir(self): self.fs.mkdir("/foo") assert self.fs.isdir("/foo") is True def test_is_dir_when_it_does_not_exists(self): assert self.fs.isdir("/does-not-exists") is False def test_exists_false(self): assert self.fs.exists("/foo") is False def test_exists_true(self): self.fs.make_file("/foo") assert self.fs.exists("/foo") is True def test_remove_file(self): self.fs.make_file("/foo") self.fs.remove_file("/foo") assert self.fs.exists("/foo") is False def test_move(self): self.fs.make_file("/foo") self.fs.move("/foo", "/bar") assert self.fs.exists("/foo") is False assert self.fs.exists("/bar") is True def test_move_dir(self): self.fs.mkdir("/fruits") self.fs.make_file("/apple") self.fs.move("/apple", "/fruits") assert self.fs.ls_a('/fruits') == ['.', '..', 'apple'] def test_islink_on_a_file(self): self.fs.make_file("/foo", "content") assert self.fs.islink("/foo") is False def test_islink_on_a_link(self): self.fs.symlink("dest", "/foo") assert self.fs.islink("/foo") is True def test_set_sticky_bit_when_unset(self): self.fs.make_file("/foo") assert self.fs.has_sticky_bit("/foo") is False def test_set_sticky_bit_when_set(self): self.fs.make_file("/foo") self.fs.set_sticky_bit("/foo") assert self.fs.has_sticky_bit("/foo") is True def test_islink_when_not_found(self): assert self.fs.islink("/foo") is False def test_islink_when_directory_not_exisiting(self): assert self.fs.islink("/foo/bar/baz") is False def test_absolute_path(self): self.fs.make_file('/foo') assert '' == self.fs.get_entity_at('/foo').content def test_relativae_path(self): self.fs.make_file('/foo', 'content') assert 'content' == self.fs.get_entity_at('foo').content def test_relativae_path_with_cd(self): self.fs.makedirs('/foo/bar', 0o755) self.fs.make_file('/foo/bar/baz', 'content') self.fs.cd('/foo/bar') assert 'content' == self.fs.get_entity_at('baz').content def test_isfile_with_file(self): self.fs.make_file('/foo') assert self.fs.isfile("/foo") is True def test_isfile_with_dir(self): self.fs.mkdir('/foo') assert self.fs.isfile("/foo") is False def test_getsize_with_empty_file(self): self.fs.make_file("foo") assert 0 == self.fs.getsize("foo") def test_getsize_with_non_empty_file(self): self.fs.make_file("foo", "1234") assert 4 == self.fs.getsize("foo") def test_getsize_with_dir(self): self.fs.mkdir("foo") self.assertRaises(NotImplementedError, lambda: self.fs.getsize("foo")) def test_mode_lets_create_a_file(self): self.fs.makedirs("/foo/bar/baz", 0o755) self.fs.make_file("/foo/bar/baz/1", "1") assert self.fs.isfile("/foo/bar/baz/1") is True def test_mode_does_not_let_create_a_file(self): self.fs.makedirs("/foo/bar/baz", 0o755) self.fs.chmod("/foo/bar/baz", 0o055) error = capture_error( lambda: self.fs.make_file("/foo/bar/baz/1", "1")) assert str(error) == "[Errno 13] Permission denied: '/foo/bar/baz/1'" def test_get_mod_s_1(self): self.fs.make_file("/foo", "content") assert format_mode(self.fs.get_mod("/foo")) == '0o644' def test_get_mod_s_2(self): self.fs.makedirs("/foo", 0o000) assert format_mode(self.fs.get_mod("/foo")) == '0o000' trash-cli-0.24.5.26/tests/test_put/components/test_fake_fs/test_makedirs.py000066400000000000000000000032631462460053300267700ustar00rootroot00000000000000from tests.support.capture_error import capture_error from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.fs.fs import list_all class TestMakeDirs: def setup_method(self): self.fs = FakeFs() def test_makedirs(self): self.fs.makedirs("/foo/bar/baz", 0o700) assert [ self.fs.isdir("/foo/bar/baz"), self.fs.get_mod("/foo/bar/baz"), ] == [True, 0o700] def test_makedirs_2(self): self.fs.makedirs("/foo/bar/baz", 0o700) assert (list(list_all(self.fs, "/")) == ['/foo', '/foo/bar', '/foo/bar/baz']) def test_makedirs_with_relative_paths(self): self.fs.makedirs("foo/bar/baz", 0o700) assert (list(list_all(self.fs, "/")) == ['/foo', '/foo/bar', '/foo/bar/baz']) def test_makedirs_from_cur_dir_with_relative_paths(self): self.fs.mkdir("/cur_dir") self.fs.cd("/cur_dir") self.fs.makedirs("foo/bar/baz", 0o700) assert (list(list_all(self.fs, "/")) == ['/cur_dir', '/cur_dir/foo', '/cur_dir/foo/bar', '/cur_dir/foo/bar/baz']) def test_makedirs_from_cur_dir_with_absolute_path(self): self.fs.mkdir("/cur_dir") self.fs.cd("/cur_dir") self.fs.makedirs("/foo/bar/baz", 0o700) assert (list(list_all(self.fs, "/")) == ['/cur_dir', '/foo', '/foo/bar', '/foo/bar/baz']) def test_makedirs_honor_file_permissions(self): self.fs.makedirs("/foo", 0o000) error = capture_error( lambda: self.fs.makedirs("/foo/bar", 0o755)) assert str(error) == "[Errno 13] Permission denied: '/foo/bar'" trash-cli-0.24.5.26/tests/test_put/components/test_fake_fs/test_readlink.py000066400000000000000000000043621462460053300267630ustar00rootroot00000000000000from tests.support.capture_error import capture_error from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.fs.real_fs import RealFs from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir class TestReadLinkOnRealFs: def setup_method(self): self.fs = RealFs() def test_readlink(self, temp_dir): self.fs.symlink("target", temp_dir / "link") assert self.fs.readlink(temp_dir / "link") == "target" def test_readlink_on_regular_file(self, temp_dir): self.fs.make_file(temp_dir / "regular-file", 'contents') exc = capture_error(lambda: self.fs.readlink(temp_dir / "regular-file")) assert ((type(exc), str(exc).replace(temp_dir, '')) == (OSError, "[Errno 22] Invalid argument: '/regular-file'")) def test_lexists(self, temp_dir): self.fs.symlink("target", temp_dir / "link") assert self.fs.lexists(temp_dir / "link") is True class TestReadLink: def setup_method(self): self.fs = FakeFs() def test_readlink(self): self.fs.symlink("target", "link") assert self.fs.readlink("link") == "target" def test_readlink_for_non_links(self): self.fs.make_file("regular-file") exc = capture_error(lambda: self.fs.readlink("regular-file")) assert ((type(exc), str(exc)) == (OSError, "[Errno 22] Invalid argument: '/regular-file'")) def test_read_file(self): self.fs.make_file("regular_file", "contents") assert self.fs.read("regular_file") == "contents" def test_read_linked_file(self): self.fs.make_file("regular_file", "contents") self.fs.symlink("regular_file", "link") assert self.fs.read("link") == "contents" def test_is_dir_for_links(self): self.fs.symlink("target", "link") assert self.fs.isdir("link") is False def test_read_linked_file_with_relative_path(self): self.fs.makedirs("/a/b/c/d", 0o777) self.fs.make_file("/a/b/c/d/regular_file", "contents") self.fs.symlink("c/d/regular_file", "/a/b/link") assert self.fs.read("/a/b/link") == "contents" def test_lexists(self): self.fs.symlink("target", "link") assert self.fs.lexists("link") is True trash-cli-0.24.5.26/tests/test_put/components/test_fake_fs/test_realpath.py000066400000000000000000000007341462460053300267710ustar00rootroot00000000000000from tests.support.put.fake_fs.fake_fs import FakeFs class TestRealpath: def setup_method(self): self.fs = FakeFs() def test(self): self.fs.touch("pippo") assert self.fs.realpath("pippo") == "/pippo" def test_cur_dir_with_several_paths(self): self.fs.mkdir("music") self.fs.cd("music") self.fs.make_file_and_dirs("be/bop/a/lula") assert self.fs.realpath("be/bop/a/lula") == "/music/be/bop/a/lula" trash-cli-0.24.5.26/tests/test_put/components/test_gate.py000066400000000000000000000004741462460053300234550ustar00rootroot00000000000000from trashcli.put.gate import Gate class TestGate: def test_gate(self): assert repr(Gate.SameVolume) == 'Gate.SameVolume' assert str(Gate.SameVolume) == 'Gate.SameVolume' assert repr(Gate.HomeFallback) == 'Gate.HomeFallback' assert str(Gate.HomeFallback) == 'Gate.HomeFallback' trash-cli-0.24.5.26/tests/test_put/components/test_gentle_stat_read.py000066400000000000000000000016631462460053300260420ustar00rootroot00000000000000import grp import os import pwd from tests.support.files import make_file from trashcli.put.reporting.stats_reader import gentle_stat_read from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir class TestGentleStatRead: def test_file_non_found(self, temp_dir): result = gentle_stat_read(temp_dir / 'not-existent') assert (result.replace(temp_dir, '...') == "[Errno 2] No such file or directory: '.../not-existent'") def test_file(self, temp_dir): make_file(temp_dir / 'pippo.txt') os.chmod(temp_dir / 'pippo.txt', 0o531) result = gentle_stat_read(temp_dir / 'pippo.txt') assert result == '531 %s %s' % ( self.current_user(), self.current_group() ) @staticmethod def current_user(): return pwd.getpwuid(os.getuid()).pw_name @staticmethod def current_group(): return grp.getgrgid(os.getgid()).gr_name trash-cli-0.24.5.26/tests/test_put/components/test_lstat/000077500000000000000000000000001462460053300233055ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/test_lstat/__init__.py000066400000000000000000000000001462460053300254040ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/test_lstat/test_names.py000066400000000000000000000012721462460053300260230ustar00rootroot00000000000000import getpass import os import grp from trashcli.put.fs.real_fs import Names class TestNames: def setup_method(self): self.names = Names() def test_username(self): assert self.names.username(os.getuid()) == getpass.getuser() def test_username_when_not_found(self): assert self.names.username(-1) is None def test_group(self): assert self.names.groupname(os.getgid()) == _current_group() def test_group_when_not_found(self): # this test will fail if run on a system where 99999 is the gid of # a group assert self.names.groupname(99999) is None def _current_group(): return grp.getgrgid(os.getgid()).gr_name trash-cli-0.24.5.26/tests/test_put/components/test_make_parser.py000066400000000000000000000032421462460053300250220ustar00rootroot00000000000000import unittest from trashcli.put.core.mode import Mode from trashcli.put.parser import make_parser class Test_make_parser(unittest.TestCase): def setUp(self): self.parser = make_parser("program-name") def test(self): options = self.parser.parse_args([]) assert options.verbose == 0 def test2(self): options = self.parser.parse_args(['-v']) assert options.verbose == 1 def test3(self): options = self.parser.parse_args(['-vv']) assert options.verbose == 2 def test_trash_dir_not_specified(self): options = self.parser.parse_args([]) assert options.trashdir is None def test_trash_dir_specified(self): options = self.parser.parse_args(['--trash-dir', '/MyTrash']) assert options.trashdir == '/MyTrash' def test_force_volume_off(self): options = self.parser.parse_args([]) assert options.forced_volume is None def test_force_volume_on(self): options = self.parser.parse_args(['--force-volume', '/fake-vol']) assert options.forced_volume == '/fake-vol' def test_force_option_default(self): options = self.parser.parse_args([]) assert options.mode == Mode.mode_unspecified def test_force_option(self): options = self.parser.parse_args(['-f']) assert options.mode == Mode.mode_force def test_interactive_override_force_option(self): options = self.parser.parse_args(['-f', '-i']) assert options.mode == Mode.mode_interactive def test_interactive_option(self): options = self.parser.parse_args(['-i']) assert options.mode == Mode.mode_interactive trash-cli-0.24.5.26/tests/test_put/components/test_move_file.py000066400000000000000000000012341462460053300244750ustar00rootroot00000000000000import pytest from tests.support.dirs.my_path import MyPath from trashcli.put.fs.real_fs import RealFs from trashcli.put.janitor_tools.put_trash_dir import move_file from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir class TestMoveFile: @pytest.mark.slow def test_delete_when_traling_slash(self, temp_dir): # type: (MyPath) -> None fs = RealFs() temp_dir.mkdir_rel("dir") temp_dir.symlink_rel("dir", "link-to-dir") move_file(fs, temp_dir / "link-to-dir/", temp_dir / "trash-location") assert temp_dir.list_all_files_sorted() == ['/dir', '/trash-location'] trash-cli-0.24.5.26/tests/test_put/components/test_octal.py000066400000000000000000000001641462460053300236330ustar00rootroot00000000000000from trashcli.put.octal import octal class TestOctal: def test(self): assert octal(16877) == '0o40755' trash-cli-0.24.5.26/tests/test_put/components/test_original_location.py000066400000000000000000000032541462460053300262300ustar00rootroot00000000000000import os import unittest from parameterized import parameterized # type: ignore from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.core.path_maker_type import PathMakerType from trashcli.put.original_location import OriginalLocation AbsolutePaths = PathMakerType.AbsolutePaths RelativePaths = PathMakerType.RelativePaths class TestOriginalLocation(unittest.TestCase): def setUp(self): self.original_location = OriginalLocation(FakeFsWithRealpath()) @parameterized.expand([ ('/volume', '/file', AbsolutePaths, '/file',), ('/volume', '/file/././', AbsolutePaths, '/file',), ('/volume', '/dir/../file', AbsolutePaths, '/file'), ('/volume', '/dir/../././file', AbsolutePaths, '/file'), ('/volume', '/outside/file', AbsolutePaths, '/outside/file'), ('/volume', '/volume/file', AbsolutePaths, '/volume/file',), ('/volume', '/volume/dir/file', AbsolutePaths, '/volume/dir/file'), ('/volume', '/file', RelativePaths, '/file'), ('/volume', '/dir/../file', RelativePaths, '/file'), ('/volume', '/outside/file', RelativePaths, '/outside/file'), ('/volume', '/volume/file', RelativePaths, 'file'), ('/volume', '/volume/dir/file', RelativePaths, 'dir/file'), ]) def test_original_location(self, volume, file_to_be_trashed, path_type, expected_result): result = self.original_location.for_file(file_to_be_trashed, path_type, volume) assert expected_result == result class FakeFsWithRealpath(FakeFs): def parent_realpath2(self, path): return os.path.dirname(path) trash-cli-0.24.5.26/tests/test_put/components/test_parent_path.py000066400000000000000000000033261462460053300250410ustar00rootroot00000000000000import os import unittest import pytest from trashcli.put.fs.parent_realpath import ParentRealpathFs from trashcli.put.fs.real_fs import RealFs from tests.support.files import make_empty_file, require_empty_dir from tests.support.dirs.my_path import MyPath def parent_path(path): return ParentRealpathFs(RealFs()).parent_realpath(path) @pytest.mark.slow class Test_parent_path(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() def test(self): require_empty_dir(self.tmp_dir / 'other_dir/dir') os.symlink(self.tmp_dir / 'other_dir/dir', self.tmp_dir / 'dir') make_empty_file(self.tmp_dir / 'dir/foo') assert (self.tmp_dir / 'other_dir/dir' == parent_path( self.tmp_dir / 'dir/foo')) def test2(self): require_empty_dir(self.tmp_dir / 'test-disk/dir') os.symlink(self.tmp_dir / 'test-disk/non-existent', self.tmp_dir / 'link-to-non-existent') assert parent_path(self.tmp_dir / 'link-to-non-existent') == \ self.tmp_dir def test3(self): require_empty_dir(self.tmp_dir / 'foo') require_empty_dir(self.tmp_dir / 'bar') os.symlink('../bar/zap', self.tmp_dir / 'foo/zap') assert parent_path(self.tmp_dir / 'foo/zap') == \ os.path.join(self.tmp_dir, 'foo') def test4(self): require_empty_dir(self.tmp_dir / 'foo') require_empty_dir(self.tmp_dir / 'bar') os.symlink('../bar/zap', self.tmp_dir / 'foo/zap') make_empty_file(self.tmp_dir / 'bar/zap') assert parent_path(self.tmp_dir / 'foo/zap') == \ os.path.join(self.tmp_dir, 'foo') def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_put/components/test_persist_trashinfo.py000066400000000000000000000036311462460053300263010ustar00rootroot00000000000000# Copyright (C) 2008-2021 Andrea Francia Trivolzio(PV) Italy import unittest import pytest from six import StringIO from trashcli.fs import read_file from trashcli.put.fs.real_fs import RealFs from trashcli.put.janitor_tools.info_file_persister import InfoFilePersister from trashcli.put.janitor_tools.info_file_persister import TrashinfoData from trashcli.put.my_logger import LogData from trashcli.put.my_logger import MyLogger from trashcli.put.my_logger import StreamBackend from trashcli.put.suffix import Suffix from tests.support.put.fake_random import FakeRandomInt from tests.support.dirs.my_path import MyPath @pytest.mark.slow class TestPersistTrashInfo(unittest.TestCase): def setUp(self): self.path = MyPath.make_temp_dir() self.fs = RealFs() self.stderr = StringIO() self.backend = StreamBackend(self.stderr) self.logger = MyLogger(self.backend) self.suffix = Suffix(FakeRandomInt([0,1])) self.info_dir = InfoFilePersister(self.fs, self.logger, self.suffix) def test_persist_trash_info_first_time(self): trash_info_file = self._persist_trash_info('dummy-path', b'content') assert self.path / 'dummy-path.trashinfo' == trash_info_file assert 'content' == read_file(trash_info_file) def test_persist_trash_info_first_100_times(self): self.test_persist_trash_info_first_time() trash_info_file = self._persist_trash_info('dummy-path', b'content') assert self.path / 'dummy-path_1.trashinfo' == trash_info_file assert 'content' == read_file(trash_info_file) def tearDown(self): self.path.clean_up() def _persist_trash_info(self, basename, content): log_data = LogData('trash-cli', 2) data = TrashinfoData(basename, content, self.path) return self.info_dir.create_trashinfo_file(data, log_data).trashinfo_path trash-cli-0.24.5.26/tests/test_put/components/test_stat_mode.py000066400000000000000000000017011462460053300245060ustar00rootroot00000000000000import os from trashcli.put.fs.real_fs import RealFs from trashcli.put.octal import octal from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir class TestStatMode: def setup_method(self): self.fs = RealFs() self.old_umask = os.umask(0o777 - 0o755) def teardown_method(self): os.umask(self.old_umask) def test_mode_for_a_dir(self, temp_dir): self.fs.mkdir_with_mode(temp_dir / 'foo', 0o755) stat = self.fs.lstat(temp_dir / 'foo') assert octal(stat.mode) == '0o40755' def test_mode_for_a_file(self, temp_dir): self.fs.touch(temp_dir / 'foo') stat = self.fs.lstat(temp_dir / 'foo') assert octal(stat.mode) == '0o100644' def test_mode_for_a_symlink(self, temp_dir): os.umask(0o777 - 0o777) self.fs.symlink(temp_dir / 'foo', temp_dir / 'bar') stat = self.fs.lstat(temp_dir / 'bar') assert octal(stat.mode) == '0o120777' trash-cli-0.24.5.26/tests/test_put/components/test_stat_user.py000066400000000000000000000010161462460053300245370ustar00rootroot00000000000000import os from trashcli.put.fs.real_fs import RealFs from tests.support.dirs.temp_dir import temp_dir temp_dir = temp_dir class TestStatMode: def setup_method(self): self.fs = RealFs() def test_user(self, temp_dir): self.fs.touch(temp_dir / 'foo') stat = self.fs.lstat(temp_dir / 'foo') assert stat.uid == os.getuid() def test_group(self, temp_dir): self.fs.touch(temp_dir / 'foo') stat = self.fs.lstat(temp_dir / 'foo') assert stat.gid == os.getgid() trash-cli-0.24.5.26/tests/test_put/components/test_suffix.py000066400000000000000000000012231462460053300240320ustar00rootroot00000000000000from trashcli.put.core.int_generator import IntGenerator from trashcli.put.suffix import Suffix class TestSuffix: def setup_method(self): self.suffix = Suffix(InlineFakeIntGen(lambda x, y: "%s,%s" % (x, y))) def test_first_attempt(self): assert self.suffix.suffix_for_index(0) == '' def test_second_attempt(self): assert self.suffix.suffix_for_index(1) == '_1' def test_hundredth_attempt(self): assert self.suffix.suffix_for_index(100) == '_0,65535' class InlineFakeIntGen(IntGenerator): def __init__(self, func): self.func = func def new_int(self, a, b): return self.func(a, b) trash-cli-0.24.5.26/tests/test_put/components/test_trash_dir_volume.py000066400000000000000000000010221462460053300260710ustar00rootroot00000000000000import unittest from tests.support.fakes.fake_volume_of import FakeVolumeOf from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.trash_dir_volume_reader import TrashDirVolumeReader class TestTrashDirVolume(unittest.TestCase): def setUp(self): fs = FakeFs() fs.add_volume('/disk1') fs.add_volume('/disk2') self.reader = TrashDirVolumeReader(fs) def test(self): result = self.reader.volume_of_trash_dir('/disk1/trash_dir_path') assert result == '/disk1' trash-cli-0.24.5.26/tests/test_put/components/test_trash_directories_finder.py000066400000000000000000000046111462460053300275760ustar00rootroot00000000000000import unittest from mock import Mock from trashcli.put.core.candidate import Candidate from trashcli.put.core.check_type import NoCheck, TopTrashDirCheck from trashcli.put.core.path_maker_type import PathMakerType from trashcli.put.gate import Gate from trashcli.put.trash_directories_finder import TrashDirectoriesFinder class TestTrashDirectoriesFinder(unittest.TestCase): def setUp(self): volumes = Mock(spec=[]) volumes.volume_of = lambda x: 'volume_of(%s)' % x self.environ = {'HOME': "~"} self.finder = TrashDirectoriesFinder(volumes) def test_no_specific_user_dir(self): result = self.finder.possible_trash_directories_for('/volume', None, self.environ, 123, True) assert result == [ Candidate(trash_dir_path='~/.local/share/Trash', volume='volume_of(~/.local/share/Trash)', path_maker_type=PathMakerType.AbsolutePaths, check_type=NoCheck, gate=Gate.SameVolume), Candidate(trash_dir_path='/volume/.Trash/123', volume='/volume', path_maker_type=PathMakerType.RelativePaths, check_type=TopTrashDirCheck, gate=Gate.SameVolume), Candidate(trash_dir_path='/volume/.Trash-123', volume='/volume', path_maker_type=PathMakerType.RelativePaths, check_type=NoCheck, gate=Gate.SameVolume), Candidate(trash_dir_path='~/.local/share/Trash', volume='volume_of(~/.local/share/Trash)', path_maker_type=PathMakerType.AbsolutePaths, check_type=NoCheck, gate=Gate.HomeFallback), ] def test_specific_user_dir(self): result = self.finder.possible_trash_directories_for('/volume', 'user_dir', self.environ, 123, True) assert result == [('user_dir', 'volume_of(user_dir)', PathMakerType.RelativePaths, NoCheck, Gate.SameVolume)] trash-cli-0.24.5.26/tests/test_put/components/test_trash_put_reporter.py000066400000000000000000000014431462460053300264650ustar00rootroot00000000000000from mock import Mock from six import StringIO from trashcli.put.describer import Describer from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.my_logger import MyLogger from trashcli.put.my_logger import StreamBackend from trashcli.put.reporting.trash_put_reporter import TrashPutReporter class TestTrashPutReporter: def setup_method(self): self.stderr = StringIO() self.backend = StreamBackend(self.stderr) self.fs = FakeFs() self.fs.touch("file") self.reporter = TrashPutReporter(self.fs) def test_it_should_record_failures(self): result = "\n".join( self.reporter.unable_to_trash_file_non_existent('file'). resolve_messages()) assert (result == "cannot trash regular empty file 'file'") trash-cli-0.24.5.26/tests/test_put/components/test_user.py000066400000000000000000000021401462460053300235030ustar00rootroot00000000000000import unittest from typing import cast import flexmock from trashcli.lib.my_input import HardCodedInput from trashcli.put.describer import Describer from trashcli.put.user import ( User, parse_user_reply, user_replied_no, user_replied_yes, ) class TestUser(unittest.TestCase): def setUp(self): self.my_input = HardCodedInput("y") self.describer = flexmock.Mock(spec=Describer) self.describer.should_receive('describe').and_return("description!") self.user = User(self.my_input, cast(Describer, self.describer)) def test_yes(self): result = self.user.ask_user_about_deleting_file('prg', "file") assert result == 'user_replied_yes' class Test_parse_user_reply(unittest.TestCase): def test_y(self): assert parse_user_reply('y') == user_replied_yes def test_Y(self): assert parse_user_reply('Y') == user_replied_yes def test_n(self): assert parse_user_reply('n') == user_replied_no def test_N(self): assert parse_user_reply('N') == user_replied_no def test_other(self): assert parse_user_reply('other') == user_replied_no trash-cli-0.24.5.26/tests/test_put/components/test_volume_of_parent.py000066400000000000000000000011231462460053300260710ustar00rootroot00000000000000import unittest from typing import cast import flexmock from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.fstab.volume_of import VolumeOf from trashcli.put.fs.parent_realpath import ParentRealpathFs from trashcli.put.fs.volume_of_parent import VolumeOfParent class TestVolumeOfParent(unittest.TestCase): def setUp(self): self.fs = FakeFs() self.volume_of_parent = VolumeOfParent(self.fs) def test(self): self.fs.add_volume('/path') result = self.volume_of_parent.volume_of_parent('/path/to/file') assert result == '/path' trash-cli-0.24.5.26/tests/test_put/components/trashing_checker/000077500000000000000000000000001462460053300244225ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/trashing_checker/__init__.py000066400000000000000000000000001462460053300265210ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/components/trashing_checker/test_home_fallback_gate_impl.py000066400000000000000000000030051462460053300326210ustar00rootroot00000000000000from tests.support.put.fake_fs.fake_fs import FakeFs from trashcli.put.core.candidate import Candidate from trashcli.put.core.check_type import NoCheck from trashcli.put.core.either import Left from trashcli.put.core.path_maker_type import PathMakerType from trashcli.put.core.trashee import Trashee from trashcli.put.gate import Gate from trashcli.put.janitor_tools.trash_dir_checker import TrashDirChecker, \ make_ok, HomeFallBackNotEnabled class TestHomeFallbackGate: def setup_method(self): self.fake_fs = FakeFs() self.gate_impl = TrashDirChecker(self.fake_fs) def test_not_enabled(self): result = self.gate_impl.file_could_be_trashed_in( make_trashee(), make_candidate('/xdf/Trash'), {}) assert result == Left(HomeFallBackNotEnabled()) def test_enabled(self): result = self.gate_impl.file_could_be_trashed_in( make_trashee(), make_candidate('/xdf/Trash'), { "TRASH_ENABLE_HOME_FALLBACK": "1" }) assert result == make_ok() # def test(self): # result = os.statvfs('/Users/andrea/trash-cli') # print("") # pprint(result.f_bavail / 1024 / 1024) # pprint(result.f_bfree / 1024 / 1024) # # pprint(psutil.disk_usage('/')) def make_candidate(path): return Candidate(path, '/disk2', PathMakerType.AbsolutePaths, NoCheck, Gate.HomeFallback) def make_trashee(): return Trashee('/disk1/foo', "/disk1") trash-cli-0.24.5.26/tests/test_put/components/trashing_checker/test_trashing_checker.py000066400000000000000000000032371462460053300313430ustar00rootroot00000000000000from trashcli.fstab.volumes import FakeVolumes from trashcli.put.core.candidate import Candidate from trashcli.put.core.either import Left from trashcli.put.core.trashee import Trashee from trashcli.put.gate import Gate from trashcli.put.janitor_tools.trash_dir_checker import TrashDirChecker, \ DifferentVolumes from tests.support.put.fake_fs.fake_fs import FakeFs class TestTrashingChecker: def setup_method(self): self.fs = FakeFs() self.checker = TrashDirChecker(self.fs) def test_trashing_checker_same(self): self.fs.add_volume('/volume1') result = self.checker.file_could_be_trashed_in( Trashee('/path1', '/volume1'), make_candidate('/volume1/trash-dir', Gate.SameVolume), {}) assert result.is_valid() is True def test_home_in_same_volume(self): result = self.checker.file_could_be_trashed_in( Trashee('/path1', '/volume1'), make_candidate('/home-vol/trash-dir', Gate.HomeFallback), {}) assert result.is_valid() is False def test_trashing_checker_different(self): self.fs.add_volume("/vol1") self.fs.add_volume("/vol2") result = self.checker.file_could_be_trashed_in( Trashee('/path1', '/vol1'), make_candidate('/vol2/trash-dir-path', Gate.SameVolume), {}) assert result == Left(DifferentVolumes("/vol2", "/vol1")) def make_candidate(trash_dir_path, gate): return Candidate(trash_dir_path=trash_dir_path, path_maker_type=None, check_type=None, gate=gate, volume="ignored") trash-cli-0.24.5.26/tests/test_put/support/000077500000000000000000000000001462460053300204465ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/support/__init__.py000066400000000000000000000000001462460053300225450ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_put/support/log_line.py000066400000000000000000000004311462460053300226060ustar00rootroot00000000000000from typing import NamedTuple from trashcli.put.core.logs import Level from trashcli.put.core.logs import LogTag class LogLine(NamedTuple('LogLine', [ ('level', Level), ('verbose', int), ('program_name', str), ('message', str), ('tag', LogTag) ])): pass trash-cli-0.24.5.26/tests/test_put/support/logs.py000066400000000000000000000013101462460053300217570ustar00rootroot00000000000000from typing import List from typing import NamedTuple from tests.test_put.support.log_line import LogLine from trashcli.put.core.logs import LogTag from trashcli.put.my_logger import is_right_for_level class Logs(NamedTuple('Logs', [ ('logs', List[LogLine]) ])): def as_stderr_lines(self): return ["%s: %s" % (line.program_name, line.message) for line in self.logs if is_right_for_level(line.verbose, line.level)] def with_tag(self, log_tag, # type: LogTag ): # type: (...) -> List[str] return ["%s" % line.message for line in self.logs if log_tag == line.tag ] trash-cli-0.24.5.26/tests/test_put/support/recording_backend.py000066400000000000000000000021551462460053300244460ustar00rootroot00000000000000from typing import IO from typing import List from tests.test_put.support.logs import Logs from tests.test_put.support.log_line import LogLine from trashcli.put.core.logs import LogData from trashcli.put.core.logs import LogEntry from trashcli.put.my_logger import LoggerBackend from trashcli.put.my_logger import StreamBackend class RecordingBackend(LoggerBackend): def __init__(self, stderr, # type: IO[str] ): self.stderr = stderr self.logs = [] # type: List[LogLine] def write_message(self, log_entry, # type: LogEntry log_data, # type: LogData ): StreamBackend(self.stderr).write_message(log_entry, log_data) for message in log_entry.resolve_messages(): self.logs.append(LogLine(log_entry.level, log_data.verbose, log_data.program_name, message, log_entry.tag)) def collected(self): return Logs(self.logs) trash-cli-0.24.5.26/tests/test_put/support/result.py000066400000000000000000000015041462460053300223360ustar00rootroot00000000000000from typing import List from typing import Tuple from tests.test_put.support.logs import Logs from trashcli.put.core.logs import LogTag class Result: def __init__(self, stderr, # type: List[str] err, # type: str exit_code, # type: int collected_logs, # type: Logs ): self.stderr = stderr self.err = err self.exit_code = exit_code self.collected_logs = collected_logs def exit_code_and_stderr(self): return [self.exit_code, self.stderr] def exit_code_and_logs(self, log_tag, # type: LogTag ): # type: (...) -> Tuple[int, List[str]] return (self.exit_code, self.collected_logs.with_tag(log_tag)) trash-cli-0.24.5.26/tests/test_restore/000077500000000000000000000000001462460053300176055ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/__init__.py000066400000000000000000000000001462460053300217040ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/cmd/000077500000000000000000000000001462460053300203505ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/cmd/__init__.py000066400000000000000000000000001462460053300224470ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/cmd/test_end_to_end_restore.py000066400000000000000000000071661462460053300256340ustar00rootroot00000000000000import os import unittest from datetime import datetime from os.path import exists as file_exists from os.path import join as pj import pytest from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.dirs.my_path import MyPath from tests.support.run.run_command import run_command from trashcli.fs import read_file @pytest.mark.slow class TestEndToEndRestore(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.curdir = self.tmp_dir / "cwd" self.trash_dir = self.tmp_dir / "trash-dir" os.makedirs(self.curdir) self.fake_trash_dir = FakeTrashDir(self.trash_dir) def test_no_file_trashed(self): result = self.run_command("trash-restore") self.assertEqual("""\ No files trashed from current dir ('%s') """ % self.curdir, result.output()) def test_original_file_not_existing(self): self.fake_trash_dir.add_trashinfo3("foo", "/path", datetime(2000,1,1,0,0,1)) result = self.run_command("trash-restore", ["/"], input='0') self.assertEqual(" 0 2000-01-01 00:00:01 /path\n" "What file to restore [0..0]: \n" "[Errno 2] No such file or directory: '%s/files/foo'\n" % self.trash_dir, result.output()) def test_restore_happy_path(self): self.fake_trash_dir.add_trashed_file( "file1", pj(self.curdir, "path", "to", "file1"), "contents") self.fake_trash_dir.add_trashed_file( "file2", pj(self.curdir, "path", "to", "file2"), "contents") self.assertEqual(True, file_exists(pj(self.trash_dir, "info", "file2.trashinfo"))) self.assertEqual(True, file_exists(pj(self.trash_dir, "files", "file2"))) result = self.run_command("trash-restore", ["/", '--sort=path'], input='1') self.assertEqual("""\ 0 2000-01-01 00:00:01 %(curdir)s/path/to/file1 1 2000-01-01 00:00:01 %(curdir)s/path/to/file2 What file to restore [0..1]: """ % { 'curdir': self.curdir}, result.stdout) self.assertEqual("", result.stderr) self.assertEqual("contents", read_file(pj(self.curdir, "path/to/file2"))) self.assertEqual(False, file_exists(pj(self.trash_dir, "info", "file2.trashinfo"))) self.assertEqual(False, file_exists(pj(self.trash_dir, "files", "file2"))) def test_restore_with_relative_path(self): self.fake_trash_dir.add_trashed_file( "file1", pj(self.curdir, "path", "to", "file1"), "contents") self.assertEqual(True, file_exists(pj(self.trash_dir, "info", "file1.trashinfo"))) self.assertEqual(True, file_exists(pj(self.trash_dir, "files", "file1"))) result = self.run_command("trash-restore", ["%(curdir)s" % {'curdir': "."}, '--sort=path'], input='0') self.assertEqual("""\ 0 2000-01-01 00:00:01 %(curdir)s/path/to/file1 What file to restore [0..0]: """ % {'curdir': self.curdir}, result.stdout) self.assertEqual("", result.stderr) self.assertEqual("contents", read_file(pj(self.curdir, "path/to/file1"))) self.assertEqual(False, file_exists(pj(self.trash_dir, "info", "file1.trashinfo"))) self.assertEqual(False, file_exists(pj(self.trash_dir, "files", "file1"))) def run_command(self, command, args=None, input=''): if args is None: args = [] return run_command(self.curdir, command, ["--trash-dir", self.trash_dir] + args, input) def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_restore/cmd/test_listing_in_restore_cmd.py000066400000000000000000000050421462460053300265070ustar00rootroot00000000000000import unittest from mock import Mock from six import StringIO from trashcli.restore.file_system import FakeReadCwd from trashcli.restore.handler import Handler from trashcli.restore.restore_cmd import RestoreCmd from trashcli.restore.trashed_file import TrashedFile from trashcli.restore.trashed_files import TrashedFiles class TestListingInRestoreCmd(unittest.TestCase): def setUp(self): self.logger = Mock(spec=[]) self.trashed_files = Mock(spec=TrashedFiles) self.trashed_files.all_trashed_files = Mock() self.original_locations = [] self.fake_handler = FakeHandler(self.original_locations) self.cmd = RestoreCmd(stdout=StringIO(), version="0.0.0", trashed_files=self.trashed_files, read_cwd=FakeReadCwd("dir"), handler=self.fake_handler) def test_with_no_args_and_files_in_trashcan(self): self.trashed_files.all_trashed_files.return_value = [ a_trashed_file('dir/location'), a_trashed_file('dir/location'), a_trashed_file('anotherdir/location') ] self.cmd.run(['trash-restore']) assert [ 'dir/location', 'dir/location' ] == self.original_locations def test_with_no_args_and_files_in_trashcan_2(self): self.trashed_files.all_trashed_files.return_value = [ a_trashed_file('/dir/location'), a_trashed_file('/dir/location'), a_trashed_file('/specific/path'), ] self.cmd.run(['trash-restore', '/specific/path']) assert self.original_locations == ['/specific/path'] def test_with_with_path_prefix_bug(self): self.trashed_files.all_trashed_files.return_value = [ a_trashed_file('/prefix'), a_trashed_file('/prefix-with-other'), ] self.cmd.run(['trash-restore', '/prefix']) assert self.original_locations == ['/prefix'] def a_trashed_file(original_location): return TrashedFile(original_location=original_location, deletion_date="a date", info_file="", original_file="") class FakeHandler(Handler): def __init__(self, original_locations): self.original_locations = original_locations def handle_trashed_files(self, trashed_files, _overwrite): for trashed_file in trashed_files: self.original_locations.append(trashed_file.original_location) trash-cli-0.24.5.26/tests/test_restore/cmd/test_restore.py000066400000000000000000000100221462460053300234370ustar00rootroot00000000000000import datetime from tests.support.asserts.assert_that import assert_that from tests.support.restore.fake_restore_fs import FakeRestoreFs from tests.support.restore.has_been_restored_matcher import \ has_been_restored, has_not_been_restored from tests.support.restore.restore_user import RestoreUser class TestSearcher: def setup_method(self): self.fs = FakeRestoreFs() self.user = RestoreUser(environ={'HOME': '/home/user'}, uid=123, file_reader=self.fs, read_fs=self.fs, write_fs=self.fs, listing_file_system=self.fs, version='1.0', volumes=self.fs) def test_will_not_detect_trashed_file_in_dirs_other_than_cur_dir(self): self.fs.add_volume('/disk1') self.fs.add_file('/disk1/.Trash-123/info/not_a_trashinfo') self.fs.add_trash_file("/foo", '/home/user/.local/share/Trash', date_at(2018, 1, 1), '') self.fs.add_trash_file("/disk1/bar", '/disk1/.Trash-123', date_at(2018, 1, 1), '') res = self.run_restore([], from_dir='/home/user') assert (res.output() == "No files trashed from current dir ('/home/user')\n") def test_will_show_file_in_cur_dir(self): self.fs.add_trash_file("/home/user/foo", '/home/user/.local/share/Trash', date_at(2018, 1, 1), '') res = self.run_restore([], from_dir='/home/user') assert (res.output() == ' 0 2018-01-01 00:00:00 /home/user/foo\n' 'No files were restored\n') def test_actual_restore(self): trashed_file = self.fs.make_trashed_file("/home/user/foo", '/home/user/.local/share/Trash', date_at(2018, 1, 1), "contents of foo\n") assert_that(trashed_file, has_not_been_restored(self.fs)) res = self.run_restore([], reply="0", from_dir='/home/user') assert (res.output() == ' 0 2018-01-01 00:00:00 /home/user/foo\n') assert_that(trashed_file, has_been_restored(self.fs)) assert (self.fs.contents_of('/home/user/foo') == "contents of foo\n") def test_will_sort_by_date_by_default(self): self.add_file_trashed_at("/home/user/third", date_at(2013, 1, 1)) self.add_file_trashed_at("/home/user/second", date_at(2012, 1, 1)) self.add_file_trashed_at("/home/user/first", date_at(2011, 1, 1)) res = self.run_restore([], from_dir='/home/user') assert (res.output() == ' 0 2011-01-01 00:00:00 /home/user/first\n' ' 1 2012-01-01 00:00:00 /home/user/second\n' ' 2 2013-01-01 00:00:00 /home/user/third\n' 'No files were restored\n') def test_will_sort_by_path(self): self.add_file_trashed_at("/home/user/ccc", date_at(2011, 1, 1)) self.add_file_trashed_at("/home/user/bbb", date_at(2011, 1, 1)) self.add_file_trashed_at("/home/user/aaa", date_at(2011, 1, 1)) res = self.run_restore(['trash-restore', '--sort=path'], from_dir='/home/user') assert (res.output() == ' 0 2011-01-01 00:00:00 /home/user/aaa\n' ' 1 2011-01-01 00:00:00 /home/user/bbb\n' ' 2 2011-01-01 00:00:00 /home/user/ccc\n' 'No files were restored\n') def run_restore(self, args, reply='', from_dir=None): return self.user.run_restore(args, reply, from_dir) def add_file_trashed_at(self, original_location, deletion_date): self.fs.make_trashed_file(original_location, '/home/user/.local/share/Trash', deletion_date, '') def date_at(year, month, day): return datetime.datetime(year, month, day, 0, 0) trash-cli-0.24.5.26/tests/test_restore/cmd/test_restore2.py000066400000000000000000000056731462460053300235410ustar00rootroot00000000000000import datetime import unittest from mock import Mock, call from tests.support.restore.fake_restore_fs import FakeRestoreFs from tests.support.restore.restore_user import RestoreUser from trashcli.restore.file_system import RestoreWriteFileSystem class TestRestore2(unittest.TestCase): def setUp(self): self.write_fs = Mock(spec=RestoreWriteFileSystem) self.fs = FakeRestoreFs() self.user = RestoreUser( environ={'XDG_DATA_HOME': '/data_home'}, uid=1000, file_reader=self.fs, read_fs=self.fs, write_fs=self.write_fs, listing_file_system=self.fs, version='1.2.3', volumes=self.fs, ) def test_should_print_version(self): res = self.cmd_run(['trash-restore', '--version']) assert 'trash-restore 1.2.3\n' == res.stdout def test_with_no_args_and_no_files_in_trashcan(self): res = self.cmd_run(['trash-restore'], from_dir='cwd') assert ("No files trashed from current dir ('cwd')\n" == res.stdout) def test_restore_operation(self): self.fs.add_trash_file('/cwd/parent/foo.txt', '/data_home/Trash', datetime.datetime(2016, 1, 1), 'boo') res = self.cmd_run(['trash-restore'], reply='0', from_dir='/cwd') assert '' == res.stderr assert ([call.mkdirs('/cwd/parent'), call.move('/data_home/Trash/files/foo.txt', '/cwd/parent/foo.txt'), call.remove_file('/data_home/Trash/info/foo.txt.trashinfo')] == self.write_fs.mock_calls) def test_restore_operation_when_dest_exists(self): self.fs.add_trash_file('/cwd/parent/foo.txt', '/data_home/Trash', datetime.datetime(2016, 1, 1), 'boo') self.fs.add_file('/cwd/parent/foo.txt') res = self.cmd_run(['trash-restore'], reply='0', from_dir='/cwd') assert 'Refusing to overwrite existing file "foo.txt".\n' == res.stderr assert ([] == self.write_fs.mock_calls) def test_when_user_reply_with_empty_string(self): self.fs.add_trash_file('/cwd/parent/foo.txt', '/data_home/Trash', datetime.datetime(2016, 1, 1), 'boo') res = self.cmd_run(['trash-restore'], reply='', from_dir='/cwd') assert res.last_line_of_stdout() == 'No files were restored' def test_when_user_reply_with_not_number(self): self.fs.add_trash_file('/cwd/parent/foo.txt', '/data_home/Trash', datetime.datetime(2016, 1, 1), 'boo') res = self.cmd_run(['trash-restore'], reply='non numeric', from_dir='/cwd') assert res.last_line_of_stderr() == \ 'Invalid entry: not an index: non numeric' assert 1 == res.exit_code def cmd_run(self, args, reply=None, from_dir=None): return self.user.run_restore(args, reply=reply, from_dir=from_dir) trash-cli-0.24.5.26/tests/test_restore/cmd/test_restore_with_real_fs.py000066400000000000000000000110651462460053300261750ustar00rootroot00000000000000import os import unittest import pytest from tests.support.asserts.assert_that import assert_that from tests.support.dirs.my_path import MyPath from tests.support.restore.a_trashed_file import ATrashedFile from tests.support.restore.has_been_restored_matcher import \ has_been_restored from tests.support.restore.restore_file_fixture import RestoreFileFixture from tests.support.restore.restore_user import RestoreUser from trashcli.fs import RealExists from trashcli.fstab.volumes import FakeVolumes from trashcli.restore.file_system import RealFileReader, \ RealRestoreReadFileSystem, RealRestoreWriteFileSystem, RealListingFileSystem @pytest.mark.slow class TestRestoreTrash(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.fixture = RestoreFileFixture(self.tmp_dir / 'XDG_DATA_HOME') self.fs = RealExists() self.cwd = self.tmp_dir / "cwd" XDG_DATA_HOME = self.tmp_dir / 'XDG_DATA_HOME' self.trash_dir = XDG_DATA_HOME / 'Trash' self.user = RestoreUser(environ={'XDG_DATA_HOME': XDG_DATA_HOME}, uid=os.getuid(), file_reader=RealFileReader(), read_fs=RealRestoreReadFileSystem(), write_fs=RealRestoreWriteFileSystem(), listing_file_system=RealListingFileSystem(), version='0.0.0', volumes=FakeVolumes([])) def test_it_does_nothing_when_no_file_have_been_found_in_current_dir(self): res = self.user.run_restore(from_dir='/') self.assertEqual("No files trashed from current dir ('/')\n", res.output()) def test_gives_an_error_on_not_a_number_input(self): self.fixture.having_a_trashed_file('/foo/bar') res = self.user.run_restore(reply='+@notanumber', from_dir='/foo') self.assertEqual('Invalid entry: not an index: +@notanumber\n', res.stderr) def test_it_gives_error_when_user_input_is_too_small(self): self.fixture.having_a_trashed_file('/foo/bar') res = self.user.run_restore(reply='1', from_dir='/foo') self.assertEqual('Invalid entry: out of range 0..0: 1\n', res.stderr) def test_it_gives_error_when_user_input_is_too_large(self): self.fixture.having_a_trashed_file('/foo/bar') res = self.user.run_restore(reply='1', from_dir='/foo') self.assertEqual('Invalid entry: out of range 0..0: 1\n', res.stderr) def test_it_shows_the_file_deleted_from_the_current_dir(self): self.fixture.having_a_trashed_file('/foo/bar') res = self.user.run_restore(reply='', from_dir='/foo') self.assertEqual(' 0 2000-01-01 00:00:01 /foo/bar\n' 'No files were restored\n', res.output()) self.assertEqual('', res.stderr) def test_it_restores_the_file_selected_by_the_user(self): self.fixture.having_a_trashed_file(self.cwd / 'foo') self.user.run_restore(reply='0', from_dir=self.cwd) self.fixture.file_should_have_been_restored(self.cwd / 'foo') def test_it_refuses_overwriting_existing_file(self): self.fixture.having_a_trashed_file(self.cwd / 'foo') self.fixture.make_file(self.cwd / "foo") res = self.user.run_restore(reply='0', from_dir=self.cwd) self.assertEqual('Refusing to overwrite existing file "foo".\n', res.stderr) def test_it_restores_the_file_and_delete_the_trash_info(self): a_trashed_file = self.make_trashed_file() res = self.user.run_restore(reply='0', from_dir=self.cwd) assert res.stderr == '' assert_that(a_trashed_file, has_been_restored(self.fs)) def make_trashed_file(self): # type: () -> ATrashedFile original_location = self.cwd / 'parent/path' backup_copy = self.trash_dir / 'files/path' info_file = self.trash_dir / 'info/path.trashinfo' self.fixture.make_file(info_file, '[Trash Info]\n' 'Path=%s\n' % original_location + 'DeletionDate=2000-01-01T00:00:01\n') self.fixture.make_empty_file(backup_copy) return ATrashedFile( trashed_from=self.cwd / 'parent/path', info_file=self.trash_dir / 'info/path.trashinfo', backup_copy=self.trash_dir / 'files/path', ) def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_restore/cmd/test_trashed_file_restore_integration.py000066400000000000000000000047751462460053300305750ustar00rootroot00000000000000import os import unittest from mock import Mock from six import StringIO from tests.support.files import make_empty_file from tests.support.dirs.my_path import MyPath from trashcli.lib.my_input import HardCodedInput from trashcli.restore.file_system import RealRestoreWriteFileSystem, \ FakeReadCwd, RealRestoreReadFileSystem from trashcli.restore.restore_cmd import RestoreCmd from trashcli.restore.trashed_file import TrashedFile from trashcli.restore.trashed_files import TrashedFiles class TestTrashedFileRestoreIntegration(unittest.TestCase): def setUp(self): self.stdout = StringIO() self.stderr = StringIO() self.input = HardCodedInput() self.temp_dir = MyPath.make_temp_dir() cwd = self.temp_dir self.logger = Mock(spec=[]) self.trashed_files = Mock(spec=TrashedFiles) self.cmd = RestoreCmd.make(stdout=self.stdout, stderr=self.stderr, exit=lambda _: None, input=self.input, version="0.0.0", trashed_files=self.trashed_files, read_fs=RealRestoreReadFileSystem(), write_fs=RealRestoreWriteFileSystem(), read_cwd=FakeReadCwd(cwd)) def test_restore(self): trashed_file = TrashedFile(self.temp_dir / 'parent/path', None, self.temp_dir / 'info_file', self.temp_dir / 'orig') make_empty_file(self.temp_dir / 'orig') make_empty_file(self.temp_dir / 'info_file') self.input.set_reply('0') self.trashed_files.all_trashed_files.return_value = [trashed_file] self.cmd.run(['trash-restore']) assert os.path.exists(self.temp_dir / 'parent/path') assert not os.path.exists(self.temp_dir / 'info_file') assert not os.path.exists(self.temp_dir / 'orig') def test_restore_over_existing_file(self): trashed_file = TrashedFile(self.temp_dir / 'path', None, None, None) make_empty_file(self.temp_dir / 'path') self.input.set_reply('0') self.trashed_files.all_trashed_files.return_value = [trashed_file] self.cmd.run(['trash-restore']) assert self.stderr.getvalue() == 'Refusing to overwrite existing file "path".\n' def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_restore/components/000077500000000000000000000000001462460053300217725ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/__init__.py000066400000000000000000000000001462460053300240710ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/arg_parser/000077500000000000000000000000001462460053300241175ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/arg_parser/__init__.py000066400000000000000000000000001462460053300262160ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/arg_parser/test_restore_arg_parser.py000066400000000000000000000031571462460053300314260ustar00rootroot00000000000000import unittest from trashcli.lib.print_version import PrintVersionArgs from trashcli.restore.args import RunRestoreArgs, Sort from trashcli.restore.restore_arg_parser import RestoreArgParser class TestRestoreArgs(unittest.TestCase): def setUp(self): self.parser = RestoreArgParser() def test_default_path(self): args = self.parser.parse_restore_args([''], "curdir") self.assertEqual(RunRestoreArgs(path='curdir', sort=Sort.ByDate, trash_dir=None, overwrite=False), args) def test_path_specified_relative_path(self): args = self.parser.parse_restore_args(['', 'path'], "curdir") self.assertEqual(RunRestoreArgs(path='curdir/path', sort=Sort.ByDate, trash_dir=None, overwrite=False), args) def test_path_specified_fullpath(self): args = self.parser.parse_restore_args(['', '/a/path'], "ignored") self.assertEqual(RunRestoreArgs(path='/a/path', sort=Sort.ByDate, trash_dir=None, overwrite=False), args) def test_show_version(self): args = self.parser.parse_restore_args(['program', '--version'], "ignored") self.assertEqual(PrintVersionArgs(argv0='program'), args) trash-cli-0.24.5.26/tests/test_restore/components/collaborators/000077500000000000000000000000001462460053300246405ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/collaborators/__init__.py000066400000000000000000000000001462460053300267370ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_all_trash_directories.py000066400000000000000000000015061462460053300326200ustar00rootroot00000000000000import unittest from trashcli.fstab.volumes import FakeVolumes2 from trashcli.restore.trash_directories import TrashDirectories1 class TestTrashDirectories(unittest.TestCase): def setUp(self): environ = {'HOME': '~'} self.volumes = FakeVolumes2("volume_of(%s)", []) self.trash_directories = TrashDirectories1(self.volumes, 123, environ) def test_list_all_directories(self): self.volumes.set_volumes(['/', '/mnt']) result = list(self.trash_directories.all_trash_directories()) assert ([ ('~/.local/share/Trash', 'volume_of(~/.local/share/Trash)'), ('/.Trash/123', '/'), ('/.Trash-123', '/'), ('/mnt/.Trash/123', '/mnt'), ('/mnt/.Trash-123', '/mnt')] == result) trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_is_trashed_from_path.py000066400000000000000000000010651462460053300324370ustar00rootroot00000000000000import unittest from trashcli.restore.run_restore_action import original_location_matches_path class TestOriginalLocationMatchesPath(unittest.TestCase): def test1(self): assert original_location_matches_path("/full/path", "/full") == True def test2(self): assert original_location_matches_path("/full/path", "/full/path") == True def test3(self): assert original_location_matches_path("/prefix-extension", "/prefix") == False def test_root(self): assert original_location_matches_path("/any/path", "/") == True trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_parse_indexes.py000066400000000000000000000030541462460053300311040ustar00rootroot00000000000000import unittest import six from trashcli.restore.range import Range from trashcli.restore.restore_asking_the_user import InvalidEntry, parse_indexes from trashcli.restore.sequences import Sequences from trashcli.restore.single import Single class TestParseIndexes(unittest.TestCase): def test_non_numeric(self): with six.assertRaisesRegex(self, InvalidEntry, "^not an index: a$"): parse_indexes("a", 10) def test(self): with six.assertRaisesRegex(self, InvalidEntry, "^out of range 0..9: 10$"): parse_indexes("10", 10) def test2(self): self.assertEqual(Sequences([Single(9)]), parse_indexes("9", 10)) def test3(self): self.assertEqual(Sequences([Single(0)]), parse_indexes("0", 10)) def test4(self): assert Sequences([Range(1, 4)]) == parse_indexes("1-4", 10) def test5(self): self.assertEqual(Sequences([Single(1), Single(2), Single(3), Single(4)]), parse_indexes("1,2,3,4", 10)) def test_interval_without_start(self): with six.assertRaisesRegex(self, InvalidEntry, "^open interval: -1$"): parse_indexes("-1", 10) def test_interval_without_end(self): with six.assertRaisesRegex(self, InvalidEntry, "^open interval: 1-$"): parse_indexes("1-", 10) def test_complex(self): indexes = parse_indexes("1-5,7", 10) self.assertEqual(Sequences([Range(1, 5), Single(7)]), indexes) trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_restore_asking_the_user.py000066400000000000000000000031331462460053300331660ustar00rootroot00000000000000import unittest from mock import Mock, call from trashcli.lib.my_input import HardCodedInput from trashcli.restore.output_event import Quit from trashcli.restore.output_recorder import OutputRecorder from trashcli.restore.restore_asking_the_user import RestoreAskingTheUser from trashcli.restore.restorer import Restorer class TestRestoreAskingTheUser(unittest.TestCase): def setUp(self): self.input = HardCodedInput() self.restorer = Mock(spec=Restorer) self.output = OutputRecorder() self.asking_user = RestoreAskingTheUser(self.input, self.restorer, self.output) def test(self): self.input.set_reply('0') self.asking_user.restore_asking_the_user(['trashed_file1', 'trashed_file2'], False) self.assertEqual('What file to restore [0..1]: ', self.input.last_prompt()) self.assertEqual([call.restore_trashed_file('trashed_file1', False)], self.restorer.mock_calls) self.assertEqual([], self.output.events) def test2(self): self.input.raise_exception(KeyboardInterrupt) self.asking_user.restore_asking_the_user(['trashed_file1', 'trashed_file2'], False) self.assertEqual('What file to restore [0..1]: ', self.input.last_prompt()) self.assertEqual([], self.restorer.mock_calls) self.assertEqual([Quit()], self.output.events) trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_sequences.py000066400000000000000000000004641462460053300302500ustar00rootroot00000000000000import unittest from trashcli.restore.restore_asking_the_user import parse_indexes class TestSequences(unittest.TestCase): def test(self): sequences = parse_indexes("1-5,7", 10) result = [index for index in sequences.all_indexes()] self.assertEqual([1, 2, 3, 4, 5, 7], result) trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_trash_directories2.py000066400000000000000000000024611462460053300320530ustar00rootroot00000000000000import unittest import pytest from mock import Mock, call from tests.support.fakes.stub_volume_of import StubVolumeOf from trashcli.restore.trash_directories import TrashDirectories2 @pytest.mark.slow class TestTrashDirectories2(unittest.TestCase): def setUp(self): self.trash_directories = Mock(spec=['all_trash_directories']) self.volumes = StubVolumeOf() self.trash_directories2 = TrashDirectories2( self.volumes, self.trash_directories, ) def test_when_user_dir_is_none(self): self.trash_directories.all_trash_directories.return_value = \ "os-trash-directories" result = self.trash_directories2.trash_directories_or_user(None) self.assertEqual([call.all_trash_directories()], self.trash_directories.mock_calls) self.assertEqual('os-trash-directories', result) def test_when_user_dir_is_specified(self): self.trash_directories.all_trash_directories.return_value = \ "os-trash-directories" result = self.trash_directories2.trash_directories_or_user( 'user-trash_dir') self.assertEqual([], self.trash_directories.mock_calls) self.assertEqual([('user-trash_dir', 'volume_of user-trash_dir')], result) trash-cli-0.24.5.26/tests/test_restore/components/collaborators/test_trash_directory.py000066400000000000000000000036271462460053300314660ustar00rootroot00000000000000import unittest import pytest import six from tests.support.files import make_file, require_empty_dir from tests.support.dirs.my_path import MyPath from trashcli.restore.file_system import RealListingFileSystem from trashcli.restore.info_files import InfoFiles @pytest.mark.slow class TestTrashDirectory(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() require_empty_dir(self.temp_dir / 'trash-dir') self.info_files = InfoFiles(RealListingFileSystem()) def test_should_list_a_trashinfo(self): make_file(self.temp_dir / 'trash-dir/info/foo.trashinfo') result = self.list_trashinfos() assert [('trashinfo', self.temp_dir / 'trash-dir/info/foo.trashinfo')] == result def test_should_list_multiple_trashinfo(self): make_file(self.temp_dir / 'trash-dir/info/foo.trashinfo') make_file(self.temp_dir / 'trash-dir/info/bar.trashinfo') make_file(self.temp_dir / 'trash-dir/info/baz.trashinfo') result = self.list_trashinfos() six.assertCountEqual(self, [('trashinfo', self.temp_dir / 'trash-dir/info/foo.trashinfo'), ('trashinfo', self.temp_dir / 'trash-dir/info/baz.trashinfo'), ('trashinfo', self.temp_dir / 'trash-dir/info/bar.trashinfo')], result) def test_non_trashinfo_should_reported_as_a_warn(self): make_file(self.temp_dir / 'trash-dir/info/not-a-trashinfo') result = self.list_trashinfos() six.assertCountEqual(self, [('non_trashinfo', self.temp_dir / 'trash-dir/info/not-a-trashinfo')], result) def list_trashinfos(self): return list(self.info_files.all_info_files(self.temp_dir / 'trash-dir')) def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_restore/components/trashed_files/000077500000000000000000000000001462460053300246065ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/trashed_files/__init__.py000066400000000000000000000000001462460053300267050ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/components/trashed_files/test_trashed_files.py000066400000000000000000000071641462460053300310430ustar00rootroot00000000000000import datetime import unittest from six import StringIO from tests.support.put.fake_fs.fake_fs import FakeFs from tests.test_restore.support.fake_logger import FakeLogger from tests.test_restore.support.restore_fake_fs import RestoreFakeFs from trashcli.restore.info_dir_searcher import InfoDirSearcher from trashcli.restore.info_files import InfoFiles from trashcli.restore.trash_directories import TrashDirectories from trashcli.restore.trashed_file import TrashedFile from trashcli.restore.trashed_files import TrashedFiles class TestTrashedFiles(unittest.TestCase): def setUp(self): self.fs = FakeFs() self.out = StringIO() self.logger = FakeLogger(self.out) class FakeTrashDirectories(TrashDirectories): def list_trash_dirs(self, trash_dir_from_cli): return [('/trash-dir', '/')] self.searcher = InfoDirSearcher(FakeTrashDirectories(), InfoFiles(RestoreFakeFs(self.fs))) self.trashed_files = TrashedFiles(self.logger, RestoreFakeFs(self.fs), self.searcher) self.fs.mkdir_p("/trash-dir/info") def test(self): self.fs.write_file('/trash-dir/info/info_path.trashinfo', 'Path=name\n' 'DeletionDate=2001-01-01T10:10:10') trashed_files = list(self.trashed_files.all_trashed_files(None)) assert { 'trashed_files': trashed_files, 'out': self.out.getvalue()} == { 'trashed_files': [ TrashedFile('/name', datetime.datetime(2001, 1, 1, 10, 10, 10), '/trash-dir/info/info_path.trashinfo', '/trash-dir/files/info_path'), ], 'out': '' } def test_on_non_trashinfo(self): self.fs.touch('/trash-dir/info/info_path.non-trashinfo') trashed_files = list(self.trashed_files.all_trashed_files(None)) assert { 'trashed_files': trashed_files, 'out': self.out.getvalue()} == { 'trashed_files': [], 'out': 'WARN: Non .trashinfo file in info dir\n' } def test_on_non_parsable_trashinfo(self): self.fs.write_file('/trash-dir/info/info_path.trashinfo', '') trashed_files = list(self.trashed_files.all_trashed_files(None)) assert { 'trashed_files': trashed_files, 'out': self.out.getvalue()} == { 'trashed_files': [], 'out': 'WARN: Non parsable trashinfo file: ' '/trash-dir/info/info_path.trashinfo, because ' 'Unable to parse Path\n' } def test_on_io_error(self): self.fs.mkdir_p('/trash-dir/info/info_path.trashinfo') trashed_files = list(self.trashed_files.all_trashed_files(None)) assert { 'trashed_files': trashed_files, 'out': self.out.getvalue() } == { 'trashed_files': [], 'out': "WARN: IOErrorReadingTrashInfo(" "path='/trash-dir/info/info_path.trashinfo', " "error='Unable to read: " "/trash-dir/info/info_path.trashinfo')\n" } trash-cli-0.24.5.26/tests/test_restore/components/trashed_files/test_trashed_files_integration.py000066400000000000000000000030571462460053300334430ustar00rootroot00000000000000import datetime import unittest from mock import Mock from tests.support.files import make_file, require_empty_dir from tests.support.dirs.remove_dir_if_exists import remove_dir_if_exists from trashcli.fs import remove_file from trashcli.restore.file_system import RealFileReader from trashcli.restore.info_dir_searcher import InfoDirSearcher, FileFound from trashcli.restore.trashed_files import TrashedFiles class TestTrashedFilesIntegration(unittest.TestCase): def setUp(self): self.logger = Mock(spec=[]) self.searcher = Mock(spec=InfoDirSearcher) self.trashed_files = TrashedFiles(self.logger, RealFileReader(), self.searcher) def test(self): require_empty_dir('info') self.searcher.all_file_in_info_dir.return_value = [ FileFound('trashinfo', 'info/info_path.trashinfo', '/volume') ] make_file('info/info_path.trashinfo', 'Path=name\nDeletionDate=2001-01-01T10:10:10') trashed_files = list(self.trashed_files.all_trashed_files(None)) trashed_file = trashed_files[0] assert '/volume/name' == trashed_file.original_location assert (datetime.datetime(2001, 1, 1, 10, 10, 10) == trashed_file.deletion_date) assert 'info/info_path.trashinfo' == trashed_file.info_file assert 'files/info_path' == trashed_file.original_file def tearDown(self): remove_file('info/info_path.trashinfo') remove_dir_if_exists('info') trash-cli-0.24.5.26/tests/test_restore/support/000077500000000000000000000000001462460053300213215ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/support/__init__.py000066400000000000000000000000001462460053300234200ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_restore/support/fake_logger.py000066400000000000000000000004121462460053300241350ustar00rootroot00000000000000from __future__ import print_function from trashcli.restore.trashed_files import RestoreLogger class FakeLogger(RestoreLogger): def __init__(self, out): self.out = out def warning(self, message): print("WARN: %s" % message, file=self.out) trash-cli-0.24.5.26/tests/test_restore/support/restore_fake_fs.py000066400000000000000000000010611462460053300250320ustar00rootroot00000000000000import os from typing import Iterable from trashcli.restore.file_system import FileReader from trashcli.restore.file_system import ListingFileSystem class RestoreFakeFs(FileReader, ListingFileSystem): def __init__(self, fs, # type FakeFs ): self.fs = fs def contents_of(self, path): return self.fs.read(path) def list_files_in_dir(self, path): # type: (str) -> Iterable[str] for entry in self.fs.listdir(path): result = os.path.join(path, entry) yield result trash-cli-0.24.5.26/tests/test_rm/000077500000000000000000000000001462460053300165405ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_rm/__init__.py000066400000000000000000000000001462460053300206370ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_rm/cmd/000077500000000000000000000000001462460053300173035ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_rm/cmd/__init__.py000066400000000000000000000000001462460053300214020ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_rm/cmd/test_trash_rm.py000066400000000000000000000022041462460053300225310ustar00rootroot00000000000000import unittest from six import StringIO from mock import Mock from tests.support.asserts import assert_starts_with from trashcli.rm.rm_cmd import RmCmd class TestTrashRmCmdRun(unittest.TestCase): def setUp(self): self.volumes_listing = Mock() self.stderr = StringIO() self.file_reader = Mock([]) self.file_reader.exists = Mock([], return_value=None) self.file_reader.entries_if_dir_exists = Mock([], return_value=[]) self.environ = {} self.getuid = lambda: '111' self.cmd = RmCmd(self.environ, self.getuid, self.volumes_listing, self.stderr, self.file_reader) def test_without_arguments(self): self.cmd.run([None], uid=None) assert_starts_with(self.stderr.getvalue(), 'Usage:\n trash-rm PATTERN\n\nPlease specify PATTERN.\n') def test_without_pattern_argument(self): self.volumes_listing.list_volumes.return_value = ['/vol1'] self.cmd.run([None, None], uid=None) assert '' == self.stderr.getvalue() trash-cli-0.24.5.26/tests/test_rm/cmd/test_trash_rm_slow.py000066400000000000000000000030321462460053300235750ustar00rootroot00000000000000import unittest import pytest from six import StringIO from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.dirs.my_path import MyPath from trashcli.fstab.volume_listing import NoVolumesListing from trashcli.rm.main import RealRmFileSystemReader from trashcli.rm.rm_cmd import RmCmd @pytest.mark.slow class TestTrashRm(unittest.TestCase): def setUp(self): self.xdg_data_home = MyPath.make_temp_dir() self.stderr = StringIO() self.trash_rm = RmCmd(environ={'XDG_DATA_HOME': self.xdg_data_home}, getuid=lambda: 123, volumes_listing=NoVolumesListing(), stderr=self.stderr, file_reader=RealRmFileSystemReader()) self.fake_trash_dir = FakeTrashDir(self.xdg_data_home / 'Trash') def test_issue69(self): self.fake_trash_dir.add_trashinfo_without_path('foo') self.trash_rm.run(['trash-rm', 'ignored'], uid=None) assert (self.stderr.getvalue() == "trash-rm: %s/Trash/info/foo.trashinfo: unable to parse 'Path'" '\n' % self.xdg_data_home) def test_integration(self): self.fake_trash_dir.add_trashinfo_basename_path("del", 'to/be/deleted') self.fake_trash_dir.add_trashinfo_basename_path("keep", 'to/be/kept') self.trash_rm.run(['trash-rm', 'delete*'], uid=None) assert self.fake_trash_dir.ls_info() == ['keep.trashinfo'] def tearDown(self): self.xdg_data_home.clean_up() trash-cli-0.24.5.26/tests/test_rm/components/000077500000000000000000000000001462460053300207255ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_rm/components/__init__.py000066400000000000000000000000001462460053300230240ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_rm/components/test_file_remover.py000066400000000000000000000006461462460053300250220ustar00rootroot00000000000000import unittest from trashcli.rm.file_remover import FileRemover try: FileNotFoundError except NameError: FileNotFoundError = OSError # python 2 class TestFileRemover(unittest.TestCase): def test_remove_file_fails_when_file_does_not_exists(self): file_remover = FileRemover() self.assertRaises(FileNotFoundError, file_remover.remove_file2, '/non/existing/path') trash-cli-0.24.5.26/tests/test_rm/components/test_filter.py000066400000000000000000000020611462460053300236220ustar00rootroot00000000000000import unittest from trashcli.rm.filter import Filter class TestFilter(unittest.TestCase): def test_a_star_matches_all(self): self.cmd = Filter('*') assert self.cmd.matches('foo') == True assert self.cmd.matches('bar') == True def test_basename_matches(self): self.cmd = Filter('foo') assert self.cmd.matches('foo') == True assert self.cmd.matches('bar') == False def test_example_with_star_dot_o(self): self.cmd = Filter('*.o') assert self.cmd.matches('/foo.h') == False assert self.cmd.matches('/foo.c') == False assert self.cmd.matches('/foo.o') == True assert self.cmd.matches('/bar.o') == True def test_absolute_pattern(self): self.cmd = Filter('/foo/bar.baz') assert self.cmd.matches('/foo/bar.baz') == True assert self.cmd.matches('/foo/bar') == False def test(self): self.cmd = Filter('/foo/*.baz') assert self.cmd.matches('/foo/bar.baz') == True assert self.cmd.matches('/foo/bar.bar') == False trash-cli-0.24.5.26/tests/test_rm/components/test_list_trash_info.py000066400000000000000000000025561462460053300255350ustar00rootroot00000000000000import unittest import pytest from tests.support.fakes.fake_trash_dir import FakeTrashDir from tests.support.dirs.my_path import MyPath from trashcli.file_system_reader import FileSystemReader from trashcli.rm.list_trashinfo import ListTrashinfos @pytest.mark.slow class TestListTrashinfos(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.trash_dir = self.tmp_dir / 'Trash' self.fake_trash_dir = FakeTrashDir(self.trash_dir) self.listing = ListTrashinfos.make(FileSystemReader(), FileSystemReader()) def test_absolute_path(self): self.fake_trash_dir.add_trashinfo_basename_path('a', '/foo') result = list(self.listing.list_from_volume_trashdir(self.trash_dir, '/volume/')) assert result == [('trashed_file', ('/foo', '%s/info/a.trashinfo' % self.trash_dir))] def test_relative_path(self): self.fake_trash_dir.add_trashinfo_basename_path('a', 'foo') result = list(self.listing.list_from_volume_trashdir(self.trash_dir, '/volume/')) assert result == [('trashed_file', ('/volume/foo', '%s/info/a.trashinfo' % self.trash_dir))] def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_support/000077500000000000000000000000001462460053300176365ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_support/__init__.py000066400000000000000000000000001462460053300217350ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_support/files/000077500000000000000000000000001462460053300207405ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_support/files/__init__.py000066400000000000000000000000001462460053300230370ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_support/files/test_make_unreadable_dir.py000066400000000000000000000016131462460053300263070ustar00rootroot00000000000000import errno import os import shutil import unittest from trashcli.fs import remove_file2 from ...support.files import make_unreadable_dir, \ make_readable from tests.support.dirs.my_path import MyPath class Test_make_unreadable_dir(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() self.unreadable_dir = self.tmp_dir / 'unreadable-dir' make_unreadable_dir(self.unreadable_dir) def test_the_directory_has_been_created(self): assert os.path.exists(self.unreadable_dir) def test_and_can_not_be_removed(self): try: remove_file2(self.unreadable_dir) self.fail() except OSError as e: self.assertEqual(errno.errorcode[e.errno], 'EACCES') def tearDown(self): make_readable(self.unreadable_dir) shutil.rmtree(self.unreadable_dir) self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_support/files/test_make_unreadable_file.py000066400000000000000000000010461462460053300264500ustar00rootroot00000000000000import unittest import pytest from trashcli.fs import read_file from ...support.files import make_unreadable_file from tests.support.dirs.my_path import MyPath @pytest.mark.slow class Test_make_unreadable_file(unittest.TestCase): def setUp(self): self.tmp_dir = MyPath.make_temp_dir() def test(self): path = self.tmp_dir / "unreadable" make_unreadable_file(self.tmp_dir / "unreadable") with self.assertRaises(IOError): read_file(path) def tearDown(self): self.tmp_dir.clean_up() trash-cli-0.24.5.26/tests/test_support/test_fake_file_system.py000066400000000000000000000025411462460053300245620ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy import unittest from tests.support.fakes.fake_file_system import FakeFileSystem class TestFakeFileSystem(unittest.TestCase): 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 '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 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 'one' == self.fs.contents_of('/path/to/file1') assert '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 (set(['passwd', 'shadow', 'hosts']) == set(self.fs.entries_if_dir_exists('/etc'))) trash-cli-0.24.5.26/tests/test_support/test_fake_fstab.py000066400000000000000000000012701462460053300233340ustar00rootroot00000000000000import unittest from tests.support.fakes.fake_volume_of import FakeVolumeOf class TestFakeFstab(unittest.TestCase): def test_default(self): volumes = FakeVolumeOf() assert ["/"] == only_mount_points(volumes, ["/"]) def test_it_should_accept_fake_mount_points(self): volumes = FakeVolumeOf() volumes.add_volume('/fake') assert ['/', '/fake'] == only_mount_points(volumes, ['/', '/fake']) def test_something(self): volumes = FakeVolumeOf() volumes.add_volume("/fake") assert '/fake' == volumes.volume_of('/fake/foo') def only_mount_points(volumes, paths): return [p for p in paths if p == volumes.volume_of(p)] trash-cli-0.24.5.26/tests/test_support/test_fake_ismount.py000066400000000000000000000023551462460053300237400ustar00rootroot00000000000000import unittest from tests.support.fakes.fake_is_mount import FakeIsMount class TestOnDefault(unittest.TestCase): def setUp(self): self.ismount = FakeIsMount([]) def test_by_default_root_is_mount(self): assert self.ismount.is_mount('/') def test_while_by_default_any_other_is_not_a_mount_point(self): assert not self.ismount.is_mount('/any/other') class WhenOneFakeVolumeIsDefined(unittest.TestCase): def setUp(self): self.ismount = FakeIsMount(['/fake-vol']) def test_accept_fake_mount_point(self): assert self.ismount.is_mount('/fake-vol') def test_other_still_are_not_mounts(self): assert not self.ismount.is_mount('/other') def test_dont_get_confused_by_traling_slash(self): assert self.ismount.is_mount('/fake-vol/') class TestWhenMultipleFakesMountPoints(unittest.TestCase): def setUp(self): self.ismount = FakeIsMount(['/vol1', '/vol2']) def test_recognize_both(self): assert self.ismount.is_mount('/vol1') assert self.ismount.is_mount('/vol2') assert not self.ismount.is_mount('/other') def test_should_handle_relative_volumes(): ismount = FakeIsMount(['fake-vol']) assert ismount.is_mount('fake-vol') trash-cli-0.24.5.26/tests/test_support/test_fake_volume_of.py000066400000000000000000000012711462460053300242310ustar00rootroot00000000000000import pytest from tests.support.fakes.fake_volume_of import FakeVolumeOf class TestFakeVolumeOf: @pytest.fixture def volumes(self): return FakeVolumeOf() def test_return_the_containing_volume(self, volumes): volumes.add_volume('/fake-vol') assert '/fake-vol' == volumes.volume_of('/fake-vol/foo') def test_with_file_that_are_outside(self, volumes): volumes.add_volume('/fake-vol') assert '/' == volumes.volume_of('/foo') def test_it_work_also_with_relative_mount_point(self, volumes): volumes.add_volume('relative-fake-vol') assert 'relative-fake-vol' == volumes.volume_of( 'relative-fake-vol/foo') trash-cli-0.24.5.26/tests/test_support/test_filesystem.py000066400000000000000000000034031462460053300234330ustar00rootroot00000000000000# Copyright (C) 2008-2021 Andrea Francia Bereguardo(PV) Italy import os import unittest import pytest from trashcli.fs import has_sticky_bit, mkdirs, is_sticky_dir from tests.support.files import make_empty_file, set_sticky_bit, unset_sticky_bit from tests.support.dirs.my_path import MyPath @pytest.mark.slow class TestWithInSandbox(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() def test_mkdirs_with_default_mode(self): mkdirs(self.temp_dir / "test-dir/sub-dir") assert os.path.isdir(self.temp_dir / "test-dir/sub-dir") def test_has_sticky_bit_returns_true(self): make_empty_file(self.temp_dir / "sticky") set_sticky_bit(self.temp_dir / "sticky") assert has_sticky_bit(self.temp_dir / 'sticky') def test_has_sticky_bit_returns_false(self): make_empty_file(self.temp_dir / "non-sticky") set_sticky_bit(self.temp_dir / "non-sticky") unset_sticky_bit(self.temp_dir / "non-sticky") assert not has_sticky_bit(self.temp_dir / "non-sticky") def tearDown(self): self.temp_dir.clean_up() class Test_is_sticky_dir(unittest.TestCase): def setUp(self): self.temp_dir = MyPath.make_temp_dir() def test_dir_non_sticky(self): mkdirs(self.temp_dir / 'dir') assert not is_sticky_dir(self.temp_dir / 'dir') def test_dir_sticky(self): mkdirs(self.temp_dir / 'dir') set_sticky_bit(self.temp_dir / 'dir') assert is_sticky_dir(self.temp_dir / 'dir') def test_non_dir_but_sticky(self): make_empty_file(self.temp_dir / 'dir') set_sticky_bit(self.temp_dir / 'dir') assert not is_sticky_dir(self.temp_dir / 'dir') def tearDown(self): self.temp_dir.clean_up() trash-cli-0.24.5.26/tests/test_support/test_has_been_restored.py000066400000000000000000000102201462460053300247150ustar00rootroot00000000000000import unittest from tests.support.put.fake_fs.fake_fs import FakeFs from tests.support.restore.a_trashed_file import a_trashed_file from tests.support.restore.has_been_restored_matcher import \ has_been_restored class TestHasBeenRestored(unittest.TestCase): def setUp(self): self.fs = FakeFs() self.trashed_file = a_trashed_file(trashed_from='/original_location', info_file='/info_path.trashinfo', backup_copy='/backup_copy') def test_fail_if_original_location_does_not_exists(self): result = has_been_restored(self.fs).describe_mismatch(self.trashed_file, focus_on='original_location') assert result == ( "Expected file to be restore but it has not:\n" " - FAIL original_location should exists but it does not: '/original_location'\n" ) def test_ok_if_original_location_does_not_exists(self): self.fs.make_file('/original_location') result = has_been_restored(self.fs).describe_mismatch(self.trashed_file, focus_on='original_location') assert result == ( "Expected file to be restore but it has not:\n" " - OK original_location should exists and it does: '/original_location'\n" ) def test_fail_if_info_file_exists(self): self.fs.make_file('/info_path.trashinfo') result = has_been_restored(self.fs).describe_mismatch(self.trashed_file, focus_on='info_file') assert result == ( "Expected file to be restore but it has not:\n" " - FAIL info_file should not exists but it does: '/info_path.trashinfo'\n" ) def test_ok_if_info_file_does_not_exists(self): result = has_been_restored(self.fs).describe_mismatch(self.trashed_file, focus_on='info_file') assert result == ( "Expected file to be restore but it has not:\n" " - OK info_file should not exists and it does not: '/info_path.trashinfo'\n" ) def test_fail_if_backup_copy_exists(self): self.fs.make_file('/backup_copy') result = has_been_restored(self.fs).describe_mismatch(self.trashed_file, focus_on='backup_copy') assert result == ( "Expected file to be restore but it has not:\n" " - FAIL backup_copy should not exists but it does: '/backup_copy'\n" ) def test_ok_if_backup_copy_does_not_exists(self): result = has_been_restored(self.fs).describe_mismatch(self.trashed_file, focus_on='backup_copy') assert result == ( "Expected file to be restore but it has not:\n" " - OK backup_copy should not exists and it does not: '/backup_copy'\n" ) def test_fail_if_not_yet_restored(self): self.fs.make_file('/info_path.trashinfo') self.fs.make_file('/backup_copy') result = has_been_restored(self.fs).describe_mismatch(self.trashed_file) assert result == ( "Expected file to be restore but it has not:\n" " - FAIL original_location should exists but it does not: '/original_location'\n" " - FAIL info_file should not exists but it does: '/info_path.trashinfo'\n" " - FAIL backup_copy should not exists but it does: '/backup_copy'\n" ) def test_ok_if_restored(self): self.fs.make_file('/original_location') result = has_been_restored(self.fs).describe_mismatch(self.trashed_file) assert result == ( "Expected file to be restore but it has not:\n" " - OK original_location should exists and it does: '/original_location'\n" " - OK info_file should not exists and it does not: '/info_path.trashinfo'\n" " - OK backup_copy should not exists and it does not: '/backup_copy'\n" ) trash-cli-0.24.5.26/tests/test_support/test_help_reformatting/000077500000000000000000000000001462460053300244065ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_support/test_help_reformatting/__init__.py000066400000000000000000000000001462460053300265050ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_support/test_help_reformatting/test_normalize_spaces.py000066400000000000000000000010311462460053300313500ustar00rootroot00000000000000from tests.support.help.help_reformatting import normalize_spaces class TestNormalizeSpaces: def test(self): text = """usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] [--version] [--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS] [--all-users]""" assert normalize_spaces(text) == ( "usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] " "[--version] [--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS] " "[--all-users]") trash-cli-0.24.5.26/tests/test_support/test_help_reformatting/test_parse_help.py000066400000000000000000000063101462460053300301410ustar00rootroot00000000000000from tests.support.help.help_reformatting import reformat_help_message, split_paragraphs class TestParseHelp: def test_format_help_message(self): assert reformat_help_message(self.help_message) == ( 'usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] [--version] ' '[--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS] ' '[--all-users]\n' '\n' 'List trashed files\n' '\n' 'options:\n' ' -h, --help show this help message and exit\n' ' --print-completion {bash,zsh,tcsh}\n' ' print shell completion script\n' " --version show program's version number and exit\n" ' --volumes list volumes\n' ' --trash-dirs list trash dirs\n' ' --trash-dir TRASH_DIRS\n' ' specify the trash directory to use\n' ' --all-users list trashcans of all the users\n' '\n' 'Report bugs to https://github.com/andreafrancia/trash-cli/issues\n') def test_first(self): assert split_paragraphs(self.help_message)[0] == ( 'usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] [--version]\n' ' [--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS]\n' ' [--all-users]\n') def test_second(self): assert split_paragraphs(self.help_message)[1] == ( 'List trashed files\n') def test_third(self): assert split_paragraphs(self.help_message)[2] == ( 'options:\n' ' -h, --help show this help message and exit\n' ' --print-completion {bash,zsh,tcsh}\n' ' print shell completion script\n' " --version show program's version number and exit\n" ' --volumes list volumes\n' ' --trash-dirs list trash dirs\n' ' --trash-dir TRASH_DIRS\n' ' specify the trash directory to use\n' ' --all-users list trashcans of all the users\n') def test_fourth(self): assert split_paragraphs(self.help_message)[3] == ( 'Report bugs to https://github.com/andreafrancia/trash-cli/issues\n' ) def test_only_four(self): assert len(split_paragraphs(self.help_message)) == 4 help_message = """\ usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] [--version] [--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS] [--all-users] List trashed files options: -h, --help show this help message and exit --print-completion {bash,zsh,tcsh} print shell completion script --version show program's version number and exit --volumes list volumes --trash-dirs list trash dirs --trash-dir TRASH_DIRS specify the trash directory to use --all-users list trashcans of all the users Report bugs to https://github.com/andreafrancia/trash-cli/issues """ trash-cli-0.24.5.26/tests/test_support/test_help_reformatting/test_split_paragraphs.py000066400000000000000000000006771462460053300313740ustar00rootroot00000000000000from parameterized import parameterized # type: ignore from tests.support.help.help_reformatting import split_paragraphs @parameterized.expand([ ('one line', ['one line']), ('one line\n', ['one line\n']), ('one\ntwo\n', ['one\ntwo\n']), ('one\n\ntwo\n', ['one\n', 'two\n']), ('one\n \ntwo\n', ['one\n', 'two\n']), ]) def test_split_paragraphs(text, expected_result): assert split_paragraphs(text) == expected_result trash-cli-0.24.5.26/tests/test_support/test_joining_paths.py000066400000000000000000000006521462460053300241060ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy def test_how_path_joining_works(): from os.path import join assert '/another-absolute' == join('/absolute', '/another-absolute') assert '/absolute/relative' == join('/absolute', 'relative') assert '/absolute' == join('relative', '/absolute') assert 'relative/relative' == join('relative', 'relative') assert '/absolute' == join('', '/absolute') trash-cli-0.24.5.26/tests/test_support/test_mock_dir_reader.py000066400000000000000000000016131462460053300243610ustar00rootroot00000000000000import unittest from tests.support.fakes.mock_dir_reader import MockDirReader class TestMockDirReader(unittest.TestCase): def setUp(self): self.fs = MockDirReader() def test_empty(self): result = self.fs.entries_if_dir_exists('/') self.assertEqual([], result) def test_add_file_in_root(self): self.fs.add_file('/foo') result = self.fs.entries_if_dir_exists('/') self.assertEqual(['foo'], result) def test_mkdir(self): self.fs.mkdir('/foo') result = self.fs.entries_if_dir_exists('/') self.assertEqual(['foo'], result) def test_add_file_in_dir(self): self.fs.mkdir('/foo') self.fs.add_file('/foo/bar') result = self.fs.entries_if_dir_exists('/') self.assertEqual(['foo'], result) result = self.fs.entries_if_dir_exists('/foo') self.assertEqual(['bar'], result) trash-cli-0.24.5.26/tests/test_support/test_partitions.py000066400000000000000000000016761462460053300234550ustar00rootroot00000000000000import unittest from trashcli.fstab.mount_points_listing import Partitions class MockPartition: def __init__(self, device=None, mountpoint=None, fstype=None): self.device = device self.mountpoint = mountpoint self.fstype = fstype class TestOsMountPoints(unittest.TestCase): def setUp(self): self.partitions = Partitions(['realfs']) def test_a_physical_fs(self): result = self.partitions.should_used_by_trashcli( MockPartition(fstype='realfs')) assert True == result def test_virtual_fs(self): result = self.partitions.should_used_by_trashcli( MockPartition(fstype='virtual_fs')) assert False == result def test_tmpfs(self): result = self.partitions.should_used_by_trashcli( MockPartition( device='tmpfs', mountpoint='/tmp', fstype='tmpfs')) assert True == result trash-cli-0.24.5.26/tests/test_support/test_tox_version_matches.py000066400000000000000000000004431462460053300253330ustar00rootroot00000000000000import os import sys def test_tox_version_matched(): env_name = os.getenv('TOX_ENV_NAME', None) version = sys.version_info assert (env_name, version.major, version.minor) in [ ('py27', 2, 7), ('py310', 3, 10), (None, version.major, version.minor) ]trash-cli-0.24.5.26/tests/test_trashcli_lib/000077500000000000000000000000001462460053300205615ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_trashcli_lib/__init__.py000066400000000000000000000000001462460053300226600ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_trashcli_lib/test_fstab/000077500000000000000000000000001462460053300227175ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_trashcli_lib/test_fstab/__init__.py000066400000000000000000000000001462460053300250160ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_trashcli_lib/test_fstab/test_volumes_listing.py000066400000000000000000000022731462460053300275570ustar00rootroot00000000000000import unittest from trashcli.fstab.mount_points_listing import FakeMountPointsListing from trashcli.fstab.volume_listing import VolumesListingImpl class TestVolumesListingImpl(unittest.TestCase): def setUp(self): self.volumes_listing = VolumesListingImpl(FakeMountPointsListing(['/os-vol1', '/os-vol2'])) def test_os_mount_points(self): result = self.volumes_listing.list_volumes({}) assert list(result) == ['/os-vol1', '/os-vol2'] def test_one_vol_from_environ(self): result = self.volumes_listing.list_volumes({'TRASH_VOLUMES': '/fake-vol1'}) assert list(result) == ['/fake-vol1'] def test_multiple_vols_from_environ(self): result = self.volumes_listing.list_volumes({'TRASH_VOLUMES': '/fake-vol1:/fake-vol2:/fake-vol3'}) assert list(result) == ['/fake-vol1', '/fake-vol2', '/fake-vol3'] def test_empty_environ(self): result = self.volumes_listing.list_volumes({'TRASH_VOLUMES': ''}) assert list(result) == ['/os-vol1', '/os-vol2'] def test_skip_empty_vol(self): result = self.volumes_listing.list_volumes({'TRASH_VOLUMES': '/vol1::/vol2'}) assert list(result) == ['/vol1', '/vol2'] trash-cli-0.24.5.26/tests/test_trashcli_lib/test_parsing_trashinfo_contents.py000066400000000000000000000072601462460053300276340ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import unittest from datetime import datetime from mock import MagicMock from trashcli.parse_trashinfo.parse_path import parse_path from trashcli.parse_trashinfo.parse_trashinfo import ParseTrashInfo from trashcli.parse_trashinfo.maybe_parse_deletion_date import \ maybe_parse_deletion_date, unknown_date from trashcli.parse_trashinfo.parse_original_location import \ parse_original_location from trashcli.parse_trashinfo.parser_error import ParseError from trashcli.parse_trashinfo.parse_deletion_date import parse_deletion_date class TestParseTrashInfo(unittest.TestCase): def test_it_should_parse_date(self): out = MagicMock() parser = ParseTrashInfo(on_deletion_date=out) parser.parse_trashinfo('[Trash Info]\n' 'Path=foo\n' 'DeletionDate=1970-01-01T00:00:00\n') out.assert_called_with(datetime(1970, 1, 1, 0, 0, 0)) def test_it_should_parse_path(self): out = MagicMock() parser = ParseTrashInfo(on_path=out) parser.parse_trashinfo('[Trash Info]\n' 'Path=foo\n' 'DeletionDate=1970-01-01T00:00:00\n') out.assert_called_with('foo') class TestParseDeletionDate(unittest.TestCase): def test1(self): assert parse_deletion_date('DeletionDate=2000-12-31T23:59:58') == \ datetime(2000, 12, 31, 23, 59, 58) def test2(self): assert parse_deletion_date('DeletionDate=2000-12-31T23:59:58\n') == \ datetime(2000, 12, 31, 23, 59, 58) def test3(self): assert parse_deletion_date( '[Trash Info]\nDeletionDate=2000-12-31T23:59:58') == \ datetime(2000, 12, 31, 23, 59, 58) def test_two_deletion_dates(self): assert parse_deletion_date('DeletionDate=2000-01-01T00:00:00\n' 'DeletionDate=2000-12-31T00:00:00\n') == \ datetime(2000, 1, 1, 0, 0) class Test_maybe_parse_deletion_date(unittest.TestCase): def test_on_trashinfo_without_date_parse_to_unknown_date(self): assert (unknown_date == maybe_parse_deletion_date(a_trashinfo_without_deletion_date())) def test_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 (same_date_as_datetime == maybe_parse_deletion_date( make_trashinfo(example_date_as_string))) def test_on_trashinfo_with_invalid_date_parse_to_unknown_date(self): invalid_date = 'A long time ago' assert (unknown_date == maybe_parse_deletion_date(make_trashinfo(invalid_date))) def test_how_to_parse_original_path(): assert 'foo.txt' == parse_path('Path=foo.txt') assert '/path/to/be/escaped' == parse_path( 'Path=%2Fpath%2Fto%2Fbe%2Fescaped') class TestTrashInfoParser(unittest.TestCase): def test_1(self): assert '/foo.txt' == parse_original_location("[Trash Info]\n" "Path=/foo.txt\n", '/') def test_it_raises_error_on_parsing_original_location(self): with self.assertRaises(ParseError): parse_original_location(an_empty_trashinfo(), '/') 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.24.5.26/tests/test_trashcli_lib/test_trash_dir_reader.py000066400000000000000000000012451462460053300254750ustar00rootroot00000000000000# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy import unittest from tests.support.fakes.fake_file_system import FakeFileSystem from trashcli.lib.trash_dir_reader import TrashDirReader class TestTrashDirReader(unittest.TestCase): def setUp(self): self.fs = FakeFileSystem() self.trash_dir = TrashDirReader(self.fs) def test(self): self.fs.create_fake_file('/info/foo.trashinfo') result = list(self.trash_dir.list_orphans('/')) assert [] == result def test2(self): self.fs.create_fake_file('/files/foo') result = list(self.trash_dir.list_orphans('/')) assert ['/files/foo'] == result trash-cli-0.24.5.26/tests/test_trashcli_lib/trash_dir_scanner/000077500000000000000000000000001462460053300242515ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_trashcli_lib/trash_dir_scanner/__init__.py000066400000000000000000000000001462460053300263500ustar00rootroot00000000000000trash-cli-0.24.5.26/tests/test_trashcli_lib/trash_dir_scanner/test_top_trash_dir_rules.py000066400000000000000000000037401462460053300317410ustar00rootroot00000000000000import unittest from mock import Mock, call from trashcli.trash_dirs_scanner import ( TopTrashDirRules, top_trash_dir_does_not_exist, top_trash_dir_invalid_because_not_sticky, top_trash_dir_invalid_because_parent_is_symlink, top_trash_dir_valid, ) class TestTopTrashDirRules(unittest.TestCase): def setUp(self): self.fs = Mock(spec=['exists', 'is_sticky_dir', 'is_symlink']) self.rules = TopTrashDirRules(self.fs) def test_path_not_exists(self): self.fs.exists.return_value = False result = self.rules.valid_to_be_read("/path") assert (result, self.fs.mock_calls) == \ (top_trash_dir_does_not_exist, [call.exists('/path')]) def test_parent_not_sticky(self): self.fs.exists.return_value = True self.fs.is_sticky_dir.return_value = False result = self.rules.valid_to_be_read("/path") assert (result, self.fs.mock_calls) == \ (top_trash_dir_invalid_because_not_sticky, [call.exists('/path'), call.is_sticky_dir('/')]) def test_parent_is_symlink(self): self.fs.exists.return_value = True self.fs.is_sticky_dir.return_value = True self.fs.is_symlink.return_value = True result = self.rules.valid_to_be_read("/path") assert (result, self.fs.mock_calls) == \ (top_trash_dir_invalid_because_parent_is_symlink, [call.exists('/path'), call.is_sticky_dir('/'), call.is_symlink('/')]) def test_parent_is_sym(self): self.fs.exists.return_value = True self.fs.is_sticky_dir.return_value = True self.fs.is_symlink.return_value = False result = self.rules.valid_to_be_read("/path") assert (result, self.fs.mock_calls) == \ (top_trash_dir_valid, [call.exists('/path'), call.is_sticky_dir('/'), call.is_symlink('/')]) trash-cli-0.24.5.26/tests/test_trashcli_lib/trash_dir_scanner/test_trash_dir_scanner.py000066400000000000000000000021601462460053300313510ustar00rootroot00000000000000import unittest from mock import Mock from trashcli.fstab.volume_listing import VolumesListing from trashcli.lib.dir_checker import DirChecker from trashcli.lib.user_info import SingleUserInfoProvider from trashcli.trash_dirs_scanner import TrashDirsScanner, trash_dir_found class TestTrashDirScanner(unittest.TestCase): def test_scan_trash_dirs(self): volumes_listing = Mock(spec=VolumesListing) user_info_provider = SingleUserInfoProvider() dir_checker = Mock(spec=DirChecker) scanner = TrashDirsScanner( user_info_provider, volumes_listing=volumes_listing, top_trash_dir_rules=Mock(), dir_checker=dir_checker ) dir_checker.is_dir.return_value = True volumes_listing.list_volumes.return_value = ['/vol', '/vol2'] result = list(scanner.scan_trash_dirs({'HOME': '/home/user'}, 123)) self.assertEqual( [(trash_dir_found, ('/home/user/.local/share/Trash', '/')), (trash_dir_found, ('/vol/.Trash-123', '/vol')), (trash_dir_found, ('/vol2/.Trash-123', '/vol2'))], result) trash-cli-0.24.5.26/tests/test_trashcli_lib/trash_dir_scanner/test_user_info_provider.py000066400000000000000000000010011462460053300315550ustar00rootroot00000000000000import unittest from trashcli.lib.user_info import SingleUserInfoProvider class TestUserInfoProvider(unittest.TestCase): def setUp(self): self.provider = SingleUserInfoProvider() def test_getuid(self): info = self.provider.get_user_info({}, 123) assert [123] == [i.uid for i in info] def test_home(self): info = self.provider.get_user_info({'HOME':"~"}, 123) assert [['~/.local/share/Trash']] == \ [i.home_trash_dir_paths for i in info] trash-cli-0.24.5.26/tox.ini000066400000000000000000000004631462460053300152370ustar00rootroot00000000000000[tox] requires = virtualenv<20.22.0 envlist = py27, py310 [testenv] deps = # run psutil six typing; python_version < '3.8' enum34; python_version < '3.4' # testing pytest mock flexmock parameterized # build testing setuptools commands = pytest {posargs} trash-cli-0.24.5.26/trash000077500000000000000000000002041462460053300147640ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.put.main import main as main sys.exit(main()) trash-cli-0.24.5.26/trash-empty000077500000000000000000000002061462460053300161220ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.empty.main import main as main sys.exit(main()) trash-cli-0.24.5.26/trash-list000077500000000000000000000002051462460053300157360ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.list.main import main as main sys.exit(main()) trash-cli-0.24.5.26/trash-put000077500000000000000000000002041462460053300155720ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.put.main import main as main sys.exit(main()) trash-cli-0.24.5.26/trash-restore000077500000000000000000000002101462460053300164420ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.restore.main import main as main sys.exit(main()) trash-cli-0.24.5.26/trash-rm000077500000000000000000000002031462460053300153770ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import import sys from trashcli.rm.main import main as main sys.exit(main()) trash-cli-0.24.5.26/trashcli/000077500000000000000000000000001462460053300155325ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/__init__.py000066400000000000000000000000001462460053300176310ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/compat.py000066400000000000000000000003371462460053300173720ustar00rootroot00000000000000def protocol(): try: from typing import Protocol return Protocol except ImportError as e: from typing_extensions import Protocol return Protocol Protocol = protocol() del protocol trash-cli-0.24.5.26/trashcli/empty/000077500000000000000000000000001462460053300166705ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/empty/__init__.py000066400000000000000000000000001462460053300207670ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/empty/clock.py000066400000000000000000000012021462460053300203300ustar00rootroot00000000000000from __future__ import absolute_import import datetime class Clock: def __init__(self, real_now, errors): self.real_now = real_now self.errors = errors def get_now_value(self, environ): if 'TRASH_DATE' in environ: try: return datetime.datetime.strptime(environ['TRASH_DATE'], "%Y-%m-%dT%H:%M:%S") except ValueError: self.errors.print_error('invalid TRASH_DATE: %s' % environ['TRASH_DATE']) return self.real_now() return self.real_now() trash-cli-0.24.5.26/trashcli/empty/console.py000066400000000000000000000012171462460053300207050ustar00rootroot00000000000000from typing import TextIO from trashcli.empty.errors import format_error_msg class Console: def __init__(self, program_name, out, err): # type: (str, TextIO, TextIO) -> None self.program_name = program_name self.out = out self.err = err def print_cannot_remove_error(self, path): self.print_error("cannot remove %s" % path) def print_error(self, msg): self.err.write(format_error_msg(self.program_name, msg)) def print_dry_run(self, path): self.out.write("would remove %s\n" % path) def print_removing(self, path): self.out.write("removing %s\n" % path) trash-cli-0.24.5.26/trashcli/empty/delete_according_date.py000066400000000000000000000016701462460053300235160ustar00rootroot00000000000000from trashcli.empty.clock import Clock from trashcli.empty.older_than import older_than from trashcli.fs import ContentsOf from trashcli.parse_trashinfo.parse_deletion_date import parse_deletion_date class DeleteAccordingDate: def __init__(self, reader, # type: ContentsOf clock, # type: Clock ): self.reader = reader self.clock = clock def ok_to_delete(self, trashinfo_path, environ, parsed_days): # type: (str, dict, int) -> bool if parsed_days is None: return True else: contents = self.reader.contents_of(trashinfo_path) now_value = self.clock.get_now_value(environ) deletion_date = parse_deletion_date(contents) if deletion_date is not None: if older_than(parsed_days, now_value, deletion_date): return True return False trash-cli-0.24.5.26/trashcli/empty/description.py000066400000000000000000000004631462460053300215700ustar00rootroot00000000000000def description(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() trash-cli-0.24.5.26/trashcli/empty/emptier.py000066400000000000000000000044301462460053300207100ustar00rootroot00000000000000# Copyright (C) 2022 Andrea Francia Bereguardo(PV) Italy from typing import Iterable from trashcli.empty.console import Console from trashcli.empty.delete_according_date import DeleteAccordingDate from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.lib.path_of_backup_copy import path_of_backup_copy from trashcli.lib.trash_dir_reader import TrashDirReader from trashcli.trash_dirs_scanner import TrashDir, only_found class Emptier: def __init__(self, delete_mode, trash_dir_reader, file_remover, console ): # type: (DeleteAccordingDate, TrashDirReader, ExistingFileRemover, Console) -> None self.console = console self.file_remover = file_remover self.delete_mode = delete_mode self.trash_dir_reader = trash_dir_reader def do_empty(self, trash_dirs, # type: Iterable[TrashDir] environ, # type: dict parsed_days, # type: int dry_run, # type: bool verbose, # type: int ): # type: (...) -> None for path in self.files_to_delete(trash_dirs, environ, parsed_days): if dry_run: self.console.print_dry_run(path) else: if verbose: self.console.print_removing(path) try: self.file_remover.remove_file_if_exists(path) except OSError: self.console.print_cannot_remove_error(path) def files_to_delete(self, trash_dirs, # type: Iterable[TrashDir] environ, # type: dict parsed_days, # type: int ): # type: (...) -> Iterable[str] for trash_dir in only_found(trash_dirs): # type: TrashDir for trash_info_path in self.trash_dir_reader.list_trashinfo( trash_dir.path): if self.delete_mode.ok_to_delete(trash_info_path, environ, parsed_days): yield (path_of_backup_copy(trash_info_path)) yield trash_info_path for orphan in self.trash_dir_reader.list_orphans( trash_dir.path): yield orphan trash-cli-0.24.5.26/trashcli/empty/empty_action.py000066400000000000000000000056571462460053300217520ustar00rootroot00000000000000from typing import NamedTuple, List from trashcli.empty.clock import Clock from trashcli.empty.console import Console from trashcli.empty.delete_according_date import ( DeleteAccordingDate, ) from trashcli.empty.emptier import Emptier from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.empty.guard import Guard from trashcli.empty.parse_reply import parse_reply from trashcli.empty.prepare_output_message import prepare_output_message from trashcli.empty.user import User from trashcli.fs import ContentsOf from trashcli.fstab.volume_listing import VolumesListing from trashcli.fstab.volume_of import VolumeOf from trashcli.lib.dir_reader import DirReader from trashcli.lib.environ import Environ from trashcli.lib.my_input import RealInput from trashcli.lib.trash_dir_reader import TrashDirReader from trashcli.list.trash_dir_selector import TrashDirsSelector from trashcli.trash_dirs_scanner import TopTrashDirRules class EmptyActionArgs( NamedTuple('EmptyActionArgs', [ ('user_specified_trash_dirs', List[str]), ('all_users', bool), ('interactive', bool), ('days', int), ('dry_run', bool), ('verbose', int), ('environ', Environ), ('uid', int), ])): pass class EmptyAction: def __init__(self, clock, # type: Clock file_remover, # type: ExistingFileRemover volumes_listing, # type: VolumesListing file_reader, # type: TopTrashDirRules.Reader volumes, # type: VolumeOf dir_reader, # type: DirReader content_reader, # type: ContentsOf console, # type: Console ): # type: (...) -> None self.selector = TrashDirsSelector.make(volumes_listing, file_reader, volumes) trash_dir_reader = TrashDirReader(dir_reader) delete_mode = DeleteAccordingDate(content_reader, clock) user = User(prepare_output_message, RealInput(), parse_reply) self.emptier = Emptier(delete_mode, trash_dir_reader, file_remover, console) self.guard = Guard(user) def run_action(self, args, # type: EmptyActionArgs ): # type: (...) -> None trash_dirs = self.selector.select(args.all_users, args.user_specified_trash_dirs, args.environ, args.uid) delete_pass = self.guard.ask_the_user(args.interactive, trash_dirs) if delete_pass.ok_to_empty: self.emptier.do_empty(delete_pass.trash_dirs, args.environ, args.days, args.dry_run, args.verbose) trash-cli-0.24.5.26/trashcli/empty/empty_cmd.py000066400000000000000000000063421462460053300212300ustar00rootroot00000000000000import os from datetime import datetime from typing import TextIO, Callable from trashcli.empty.clock import Clock from trashcli.empty.console import Console from trashcli.empty.empty_action import EmptyAction, EmptyActionArgs from trashcli.empty.errors import Errors from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.empty.is_input_interactive import is_input_interactive from trashcli.empty.parser import Parser from trashcli.empty.print_time_action import PrintTimeAction, PrintTimeArgs from trashcli.fs import ContentsOf from trashcli.fstab.volume_listing import VolumesListing from trashcli.fstab.volume_of import VolumeOf from trashcli.lib.dir_reader import DirReader from trashcli.lib.exit_codes import EX_OK from trashcli.lib.print_version import PrintVersionAction, PrintVersionArgs from trashcli.trash_dirs_scanner import TopTrashDirRules class EmptyCmd: def __init__(self, argv0, # type: str out, # type: TextIO err, # type: TextIO volumes_listing, # type: VolumesListing now, # type: Callable[[], datetime] file_reader, # type: TopTrashDirRules.Reader dir_reader, # type: DirReader content_reader, # type: ContentsOf file_remover, # type: ExistingFileRemover version, # type: str volumes, # type: VolumeOf ): self.volumes = volumes self.file_remover = file_remover self.dir_reader = dir_reader self.file_reader = file_reader self.volumes_listing = volumes_listing self.argv0 = argv0 self.out = out self.err = err self.version = version self.now = now self.content_reader = content_reader self.parser = Parser() self.program_name = os.path.basename(argv0) errors = Errors(self.program_name, self.err) clock = Clock(self.now, errors) console = Console(self.program_name, self.out, self.err) self.empty_action = EmptyAction(clock, self.file_remover, self.volumes_listing, self.file_reader, self.volumes, self.dir_reader, self.content_reader, console) self.print_version_action = PrintVersionAction(self.out, self.version) self.print_time_action = PrintTimeAction(self.out, clock) def run_cmd(self, args, environ, uid): args = self.parser.parse( default_is_interactive=is_input_interactive(), args=args, argv0=self.argv0, environ=environ, uid=uid) if type(args) is PrintVersionArgs: return self.print_version_action.run_action(args) elif type(args) is EmptyActionArgs: return self.empty_action.run_action(args) elif type(args) is PrintTimeArgs: return self.print_time_action.run_action(args) return EX_OK trash-cli-0.24.5.26/trashcli/empty/errors.py000066400000000000000000000004621462460053300205600ustar00rootroot00000000000000class Errors: def __init__(self, program_name, err): self.program_name = program_name self.err = err def print_error(self, msg): self.err.write(format_error_msg(self.program_name, msg)) def format_error_msg(program_name, msg): return "%s: %s\n" % (program_name, msg) trash-cli-0.24.5.26/trashcli/empty/existing_file_remover.py000066400000000000000000000002171462460053300236320ustar00rootroot00000000000000from trashcli.fs import RealRemoveFileIfExists, RealRemoveFile2 class ExistingFileRemover(RealRemoveFileIfExists, RealRemoveFile2): pass trash-cli-0.24.5.26/trashcli/empty/file_system_dir_reader.py000066400000000000000000000004161462460053300237460ustar00rootroot00000000000000from trashcli.fs import RealExists, RealEntriesIfDirExists from trashcli.lib.dir_reader import DirReader class FileSystemDirReader(DirReader, RealEntriesIfDirExists, RealExists, ): pass trash-cli-0.24.5.26/trashcli/empty/guard.py000066400000000000000000000027051462460053300203500ustar00rootroot00000000000000from typing import Iterable, NamedTuple from trashcli.empty.user import User from trashcli.trash_dirs_scanner import TrashDir UserIntention = NamedTuple('UserIntention', [('ok_to_empty', bool), ('trash_dirs', Iterable[TrashDir])]) class Guard: def __init__(self, user): # type: (User) -> None self.user = user def ask_the_user(self, parsed_interactive, # type: bool trash_dirs, # type: Iterable[TrashDir] ): # type: (...) -> UserIntention if parsed_interactive: return self._interactive(trash_dirs) else: return self.non_interactive(trash_dirs) def _interactive(self, trash_dirs, # type: Iterable[TrashDir] ): # type: (...) -> UserIntention trash_dirs_list = list(trash_dirs) # type: Iterable[TrashDir] ok_to_empty = \ self.user.do_you_wanna_empty_trash_dirs(trash_dirs_list) list_result = trash_dirs_list if ok_to_empty else [] return UserIntention(ok_to_empty=ok_to_empty, trash_dirs=list_result) def non_interactive(self, trash_dirs, # type: Iterable[TrashDir] ): trash_dirs_list = trash_dirs # type: Iterable[TrashDir] return UserIntention(ok_to_empty=True, trash_dirs=trash_dirs_list) trash-cli-0.24.5.26/trashcli/empty/is_input_interactive.py000066400000000000000000000001001462460053300234600ustar00rootroot00000000000000import os def is_input_interactive(): return os.isatty(0) trash-cli-0.24.5.26/trashcli/empty/main.py000066400000000000000000000025261462460053300201730ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import os import sys from datetime import datetime from trashcli.compat import Protocol from trashcli import trash from trashcli.empty.empty_cmd import EmptyCmd from trashcli.fs import RealContentsOf, ContentsOf from .existing_file_remover import ExistingFileRemover from .file_system_dir_reader import FileSystemDirReader from .top_trash_dir_rules_file_system_reader import \ RealTopTrashDirRulesReader from ..fstab.volume_listing import RealVolumesListing from ..fstab.real_volume_of import RealVolumeOf class ContentReader(ContentsOf, Protocol): pass def main(): empty_cmd = EmptyCmd(argv0=sys.argv[0], out=sys.stdout, err=sys.stderr, volumes_listing=RealVolumesListing(), now=datetime.now, file_reader=RealTopTrashDirRulesReader(), file_remover=ExistingFileRemover(), content_reader=FileSystemContentReader(), dir_reader=FileSystemDirReader(), version=trash.version, volumes=RealVolumeOf()) return empty_cmd.run_cmd(sys.argv[1:], os.environ, os.getuid()) class FileSystemContentReader(ContentReader, RealContentsOf): pass trash-cli-0.24.5.26/trashcli/empty/older_than.py000066400000000000000000000002631462460053300213620ustar00rootroot00000000000000def older_than(days_ago, now_value, deletion_date): from datetime import timedelta limit_date = now_value - timedelta(days=days_ago) return deletion_date < limit_date trash-cli-0.24.5.26/trashcli/empty/parse_reply.py000066400000000000000000000000751462460053300215710ustar00rootroot00000000000000def parse_reply(reply): return reply[0:1].lower() == 'y' trash-cli-0.24.5.26/trashcli/empty/parser.py000066400000000000000000000067221462460053300205450ustar00rootroot00000000000000import argparse from typing import List from trashcli.empty.empty_action import EmptyActionArgs from trashcli.empty.print_time_action import PrintTimeArgs from trashcli.lib.environ import Environ from trashcli.lib.print_version import PrintVersionArgs from trashcli.shell_completion import TRASH_DIRS, add_argument_to class Parser: def parse(self, default_is_interactive, # type: bool environ, # type: Environ args, # type: List[str] uid, # type: int argv0, # type: str ): parser = self.make_parser(default_is_interactive) namespace = parser.parse_args(args) if namespace.version: return PrintVersionArgs( argv0=argv0, ) elif namespace.print_time: return PrintTimeArgs(environ=environ) else: return EmptyActionArgs( user_specified_trash_dirs=namespace.user_specified_trash_dirs, all_users=namespace.all_users, interactive=namespace.interactive, days=namespace.days, dry_run=namespace.dry_run, verbose=namespace.verbose, environ=environ, uid=uid, ) @staticmethod def make_parser(default_is_interactive): parser = argparse.ArgumentParser( description='Purge trashed files.', epilog='Report bugs to https://github.com/andreafrancia/trash-cli/issues') add_argument_to(parser) parser.add_argument('--version', action='store_true', default=False, help="show program's version number and exit") parser.add_argument("-v", "--verbose", default=0, action="count", dest="verbose", help="list files that will be deleted", ) parser.add_argument('--trash-dir', action='append', default=[], metavar='TRASH_DIR', dest='user_specified_trash_dirs', help='specify the trash directory to use' ).complete = TRASH_DIRS parser.add_argument('--print-time', action='store_true', dest='print_time', help=argparse.SUPPRESS) parser.add_argument('--all-users', action='store_true', dest='all_users', help='empty all trashcan of all the users') parser.add_argument('-i', '--interactive', action='store_true', dest='interactive', help='ask before emptying trash directories', default=default_is_interactive) parser.add_argument('-f', action='store_false', help='don\'t ask before emptying trash directories', dest='interactive') parser.add_argument('--dry-run', action='store_true', help='show which files would have been removed', dest='dry_run') parser.add_argument('days', action='store', default=None, type=int, nargs='?') return parser trash-cli-0.24.5.26/trashcli/empty/prepare_output_message.py000066400000000000000000000010001462460053300240130ustar00rootroot00000000000000from trashcli.trash_dirs_scanner import trash_dir_found def prepare_output_message(trash_dirs): result = [] if trash_dirs: result.append("Would empty the following trash directories:") for event, args in trash_dirs: if event == trash_dir_found: trash_dir, volume = args result.append(" - %s" % trash_dir) result.append("Proceed? (y/N) ") return "\n".join(result) else: return 'No trash directories to empty.\n' trash-cli-0.24.5.26/trashcli/empty/print_time_action.py000066400000000000000000000011521462460053300227500ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from __future__ import print_function from typing import NamedTuple from trashcli.lib.environ import Environ class PrintTimeArgs( NamedTuple('PrintTimeArgs', [ ('environ', Environ), ])): pass class PrintTimeAction: def __init__(self, out, clock): self.out = out self.clock = clock def run_action(self, args, # type: PrintTimeArgs ): now_value = self.clock.get_now_value(args.environ) print(now_value.replace(microsecond=0).isoformat(), file=self.out) trash-cli-0.24.5.26/trashcli/empty/top_trash_dir_rules_file_system_reader.py000066400000000000000000000004011462460053300272350ustar00rootroot00000000000000from trashcli.fs import RealExists, RealIsStickyDir, RealIsSymLink from trashcli.trash_dirs_scanner import TopTrashDirRules class RealTopTrashDirRulesReader( TopTrashDirRules.Reader, RealExists, RealIsStickyDir, RealIsSymLink, ): pass trash-cli-0.24.5.26/trashcli/empty/user.py000066400000000000000000000007521462460053300202240ustar00rootroot00000000000000from trashcli.lib.my_input import Input class User: def __init__(self, prepare_output_message, input, # type: Input parse_reply): self.prepare_output_message = prepare_output_message self.input = input self.parse_reply = parse_reply def do_you_wanna_empty_trash_dirs(self, trash_dirs): reply = self.input.read_input(self.prepare_output_message(trash_dirs)) return self.parse_reply(reply) trash-cli-0.24.5.26/trashcli/file_system_reader.py000066400000000000000000000010021462460053300217420ustar00rootroot00000000000000from trashcli.fs import RealIsStickyDir, RealHasStickyBit, \ RealIsSymLink, RealContentsOf, RealEntriesIfDirExists, RealExists from trashcli.list.fs import FileSystemReaderForListCmd class FileSystemReader(FileSystemReaderForListCmd, RealIsStickyDir, RealHasStickyBit, RealIsSymLink, RealContentsOf, RealEntriesIfDirExists, RealExists ): pass trash-cli-0.24.5.26/trashcli/fs.py000066400000000000000000000142611462460053300165200ustar00rootroot00000000000000import os import shutil import stat from abc import abstractmethod from typing import Iterable, List from trashcli.compat import Protocol class FileSize(Protocol): @abstractmethod def file_size(self, path): raise NotImplementedError() class MakeFileExecutable(Protocol): @abstractmethod def make_file_executable(self, path): raise NotImplementedError() class WriteFile(Protocol): @abstractmethod def write_file(self, name, contents): raise NotImplementedError() class ReadFile(Protocol): @abstractmethod def read_file(self, path): raise NotImplementedError() class Move(Protocol): @abstractmethod def move(self, path, dest): raise NotImplementedError() class RemoveFile2(Protocol): @abstractmethod def remove_file2(self, path): raise NotImplementedError() class RemoveFile(Protocol): @abstractmethod def remove_file(self, path): raise NotImplementedError() class RemoveFileIfExists(Protocol): @abstractmethod def remove_file_if_exists(self, path): raise NotImplementedError() class EntriesIfDirExists(Protocol): @abstractmethod def entries_if_dir_exists(self, path): # type: (str) -> List[str] raise NotImplementedError() class PathExists(Protocol): @abstractmethod def exists(self, path): raise NotImplementedError() class HasStickyBit(Protocol): @abstractmethod def has_sticky_bit(self, path): # type: (str) -> bool raise NotImplementedError class IsStickyDir(Protocol): @abstractmethod def is_sticky_dir(self, path): # type: (str) -> bool raise NotImplementedError class IsSymLink(Protocol): @abstractmethod def is_symlink(self, path): # type: (str) -> bool raise NotImplementedError class ContentsOf(Protocol): @abstractmethod def contents_of(self, path): raise NotImplementedError() class RealEntriesIfDirExists(EntriesIfDirExists): def entries_if_dir_exists(self, path): if os.path.exists(path): for entry in os.listdir(path): yield entry class RealExists(PathExists): def exists(self, path): # type: (str) -> bool return os.path.exists(path) class RealHasStickyBit(HasStickyBit): def has_sticky_bit(self, path): return (os.stat(path).st_mode & stat.S_ISVTX) == stat.S_ISVTX class RealIsStickyDir(IsStickyDir, RealHasStickyBit): def is_sticky_dir(self, path): # type: (str) -> bool return os.path.isdir(path) and self.has_sticky_bit(path) class RealIsSymLink(IsSymLink): def is_symlink(self, path): # type: (str) -> bool return os.path.islink(path) class RealContentsOf(ContentsOf): def contents_of(self, path): return _read_file(path) class RealRemoveFile(RemoveFile): def remove_file(self, path): if os.path.lexists(path): try: os.remove(path) except: return shutil.rmtree(path) class RealRemoveFile2(RemoveFile2): def remove_file2(self, path): try: os.remove(path) except OSError: shutil.rmtree(path) class RealRemoveFileIfExists(RemoveFileIfExists, RemoveFile2): def remove_file_if_exists(self, path): if os.path.lexists(path): self.remove_file2(path) class RealMove(Move): def move(self, path, dest): return shutil.move(path, str(dest)) class ListFilesInDir(Protocol): @abstractmethod def list_files_in_dir(self, path): # type: (str) -> Iterable[str] raise NotImplementedError() class RealListFilesInDir(ListFilesInDir): def list_files_in_dir(self, path): # type: (str) -> Iterable[str] for entry in os.listdir(path): result = os.path.join(path, entry) yield result class MkDirs(Protocol): @abstractmethod def mkdirs(self, path): raise NotImplementedError() class RealMkDirs(MkDirs): def mkdirs(self, path): if os.path.isdir(path): return os.makedirs(path) class AtomicWrite(Protocol): @abstractmethod def atomic_write(self, path, content): raise NotImplementedError() @abstractmethod def open_for_write_in_exclusive_and_create_mode(self, path): raise NotImplementedError() class RealAtomicWrite(AtomicWrite): def atomic_write(self, path, content): file_handle = self.open_for_write_in_exclusive_and_create_mode(path) os.write(file_handle, content) os.close(file_handle) def open_for_write_in_exclusive_and_create_mode(self, path): return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) class RealReadFile(ReadFile): def read_file(self, path): return _read_file(path) class RealWriteFile(WriteFile): def write_file(self, name, contents): with open(name, 'w') as f: f.write(contents) class RealMakeFileExecutable(MakeFileExecutable): def make_file_executable(self, path): os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR) class RealFileSize(FileSize): def file_size(self, path): return os.stat(path).st_size class FsMethods( RealEntriesIfDirExists, RealExists, RealIsStickyDir, RealIsSymLink, RealContentsOf, RealRemoveFile, RealRemoveFile2, RealRemoveFileIfExists, RealMove, RealListFilesInDir, RealMkDirs, RealAtomicWrite, RealReadFile, RealWriteFile, RealMakeFileExecutable, RealFileSize, ): pass def _read_file(path): with open(path) as f: return f.read() has_sticky_bit = RealHasStickyBit().has_sticky_bit contents_of = RealContentsOf().contents_of remove_file = RealRemoveFile().remove_file move = RealMove().move list_files_in_dir = RealListFilesInDir().list_files_in_dir mkdirs = RealMkDirs().mkdirs atomic_write = RealAtomicWrite().atomic_write open_for_write_in_exclusive_and_create_mode = RealAtomicWrite().open_for_write_in_exclusive_and_create_mode read_file = RealReadFile().read_file write_file = RealWriteFile().write_file make_file_executable = RealMakeFileExecutable().make_file_executable file_size = RealFileSize().file_size remove_file2 = RealRemoveFile2().remove_file2 is_sticky_dir = RealIsStickyDir().is_sticky_dir trash-cli-0.24.5.26/trashcli/fstab/000077500000000000000000000000001462460053300166315ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/fstab/__init__.py000066400000000000000000000000001462460053300207300ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/fstab/mount_points_listing.py000066400000000000000000000034371462460053300235010ustar00rootroot00000000000000# Copyright (C) 2009-2020 Andrea Francia Trivolzio(PV) Italy import os from abc import ABCMeta, abstractmethod import six @six.add_metaclass(ABCMeta) class MountPointsListing: @abstractmethod def list_mount_points(self): raise NotImplementedError() class RealMountPointsListing(MountPointsListing): def list_mount_points(self): return os_mount_points() class FakeMountPointsListing(MountPointsListing): def __init__(self, mount_points): self.mount_points = mount_points def set_mount_points(self, mount_points): self.mount_points = mount_points def list_mount_points(self): return self.mount_points def os_mount_points(): import psutil # List of accepted non-physical fstypes fstypes = [ 'nfs', 'nfs4', 'p9', # file system used in WSL 2 (Windows Subsystem for Linux) 'btrfs', 'fuse', # https://github.com/andreafrancia/trash-cli/issues/250 'fuse.glusterfs', # https://github.com/andreafrancia/trash-cli/issues/255 'fuse.mergerfs', 'fuse.gocryptfs', ] # Append fstypes of physical devices to list fstypes += set([p.fstype for p in psutil.disk_partitions()]) partitions = Partitions(fstypes) for p in psutil.disk_partitions(all=True): if os.path.isdir(p.mountpoint) and \ partitions.should_used_by_trashcli(p): yield p.mountpoint class Partitions: def __init__(self, physical_fstypes): self.physical_fstypes = physical_fstypes def should_used_by_trashcli(self, partition): if ((partition.device, partition.mountpoint, partition.fstype) == ('tmpfs', '/tmp', 'tmpfs')): return True return partition.fstype in self.physical_fstypes trash-cli-0.24.5.26/trashcli/fstab/real_volume_of000066400000000000000000000000001462460053300215400ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/fstab/real_volume_of.py000066400000000000000000000005501462460053300222010ustar00rootroot00000000000000import os from trashcli.fstab.volume_listing import RealIsMount from trashcli.fstab.volume_of import VolumeOf from trashcli.fstab.volume_of_impl import VolumeOfImpl class RealVolumeOf(VolumeOf): def __init__(self): self.impl = VolumeOfImpl(RealIsMount(), os.path.abspath) def volume_of(self, path): return self.impl.volume_of(path) trash-cli-0.24.5.26/trashcli/fstab/volume_listing.py000066400000000000000000000025211462460053300222430ustar00rootroot00000000000000import os from abc import ABCMeta, abstractmethod import six from trashcli.fstab.mount_points_listing import MountPointsListing, \ RealMountPointsListing @six.add_metaclass(ABCMeta) class VolumesListing: @abstractmethod def list_volumes(self, environ): # type (dict) -> Iterable[str] raise NotImplementedError() class FixedVolumesListing(VolumesListing): def __init__(self, volumes): self.volumes = volumes def list_volumes(self, _environ): return self.volumes class RealVolumesListing(VolumesListing): def list_volumes(self, environ): return VolumesListingImpl(RealMountPointsListing()).list_volumes( environ) class VolumesListingImpl: def __init__(self, mount_points_listing, # type: MountPointsListing ): self.mount_points_listing = mount_points_listing def list_volumes(self, environ): if 'TRASH_VOLUMES' in environ and environ['TRASH_VOLUMES']: return [vol for vol in environ['TRASH_VOLUMES'].split(':') if vol != ''] return self.mount_points_listing.list_mount_points() class NoVolumesListing(VolumesListing): def list_volumes(self, environ): return [] class RealIsMount: def is_mount(self, path): return os.path.ismount(path) trash-cli-0.24.5.26/trashcli/fstab/volume_of.py000066400000000000000000000002701462460053300211750ustar00rootroot00000000000000from abc import abstractmethod from trashcli.compat import Protocol class VolumeOf(Protocol): @abstractmethod def volume_of(self, path): raise NotImplementedError() trash-cli-0.24.5.26/trashcli/fstab/volume_of_impl.py000066400000000000000000000006571462460053300222270ustar00rootroot00000000000000import os from trashcli.fstab.volume_of import VolumeOf class VolumeOfImpl(VolumeOf): def __init__(self, ismount, abspath): self.ismount = ismount self.abspath = abspath def volume_of(self, path): path = self.abspath(path) while path != os.path.dirname(path): if self.ismount.is_mount(path): break path = os.path.dirname(path) return path trash-cli-0.24.5.26/trashcli/fstab/volumes.py000066400000000000000000000036511462460053300207020ustar00rootroot00000000000000from abc import ABCMeta import six import os from trashcli.fstab.mount_points_listing import MountPointsListing, \ RealMountPointsListing from trashcli.fstab.volume_of import VolumeOf from trashcli.fstab.real_volume_of import RealVolumeOf @six.add_metaclass(ABCMeta) class Volumes(VolumeOf, MountPointsListing): pass class RealVolumes(Volumes): def volume_of(self, path): return RealVolumeOf().volume_of(path) def list_mount_points(self): return RealMountPointsListing().list_mount_points() class VolumesImpl(Volumes): def __init__(self, volumes, # type: VolumeOf mount_point_listing, # type: MountPointsListing ): self.volumes = volumes self.mount_point_listing = mount_point_listing def volume_of(self, path): return self.volumes.volume_of(path) def list_mount_points(self): return self.mount_point_listing.list_mount_points() class FakeVolumes(Volumes): def __init__(self, mount_points, # type Iterable[str] ): self.mount_points = mount_points def list_mount_points(self): return self.mount_points def volume_of(self, path): while path != os.path.dirname(path): if self.is_a_mount_point(path): break path = os.path.dirname(path) return path def is_a_mount_point(self, path): return path in self.mount_points def add_volume(self, path): self.mount_points.append(path) class FakeVolumes2(Volumes): def __init__(self, volume_of_string, volumes_list): self.volume_of_string = volume_of_string self.volumes_list = volumes_list def volume_of(self, path): return self.volume_of_string % path def set_volumes(self, volumes_list): self.volumes_list = volumes_list def list_mount_points(self): return self.volumes_list trash-cli-0.24.5.26/trashcli/lib/000077500000000000000000000000001462460053300163005ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/lib/__init__.py000066400000000000000000000000001462460053300203770ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/lib/dir_checker.py000066400000000000000000000002211462460053300211070ustar00rootroot00000000000000from __future__ import absolute_import import os class DirChecker: @staticmethod def is_dir(path): return os.path.isdir(path) trash-cli-0.24.5.26/trashcli/lib/dir_reader.py000066400000000000000000000005101462460053300207460ustar00rootroot00000000000000from __future__ import absolute_import from trashcli.compat import Protocol from trashcli.fs import EntriesIfDirExists, PathExists, RealEntriesIfDirExists, \ RealExists class DirReader( EntriesIfDirExists, PathExists, Protocol, ): pass class RealDirReader(RealEntriesIfDirExists, RealExists): pass trash-cli-0.24.5.26/trashcli/lib/enum_repr.py000066400000000000000000000001761462460053300206520ustar00rootroot00000000000000from enum import Enum def repr_for_enum(enum): # type: (Enum) -> str return "%s.%s" % (type(enum).__name__, enum.name) trash-cli-0.24.5.26/trashcli/lib/environ.py000066400000000000000000000003551462460053300203350ustar00rootroot00000000000000import os from typing import Dict Environ = Dict[str, str] def cast_environ(env, ): # type: (...) -> Environ if env == os.environ: return env else: raise ValueError("env must be os.environ") trash-cli-0.24.5.26/trashcli/lib/exit_codes.py000066400000000000000000000004271462460053300210030ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import import os # Error codes (from os on *nix, hard coded for Windows): EX_OK = getattr(os, 'EX_OK', 0) EX_USAGE = getattr(os, 'EX_USAGE', 64) EX_IOERR = getattr(os, 'EX_IOERR', 74) trash-cli-0.24.5.26/trashcli/lib/logger.py000066400000000000000000000003171462460053300201320ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy import logging my_logger = logging.getLogger('trashcli.trash') my_logger.setLevel(logging.WARNING) my_logger.addHandler(logging.StreamHandler()) trash-cli-0.24.5.26/trashcli/lib/my_input.py000066400000000000000000000020201462460053300205100ustar00rootroot00000000000000from abc import abstractmethod, ABCMeta import six from six.moves import input as _my_input @six.add_metaclass(ABCMeta) class Input: @abstractmethod def read_input(self, prompt): # type: (str) -> str raise NotImplementedError class RealInput(Input): def read_input(self, prompt): # type: (str) -> str return _my_input(prompt) class HardCodedInput(Input): def __init__(self, reply=None): self.reply, self.exception = self._reply(reply) def set_reply(self, reply): self.reply, self.exception = self._reply(reply) def _reply(self, reply): if reply is None: return None, ValueError("No reply set") else: return reply, None def raise_exception(self, exception): self.exception = exception def read_input(self, prompt): # type: (str) -> str self.used_prompt = prompt if self.exception: raise self.exception return self.reply def last_prompt(self): return self.used_prompt trash-cli-0.24.5.26/trashcli/lib/my_permission_error.py000066400000000000000000000003431462460053300227600ustar00rootroot00000000000000from typing import Type def get_permission_error_class(): # type: () -> Type[Exception] try: return PermissionError except NameError: return OSError MyPermissionError = get_permission_error_class() trash-cli-0.24.5.26/trashcli/lib/path_of_backup_copy.py000066400000000000000000000004311462460053300226470ustar00rootroot00000000000000from __future__ import absolute_import import os def path_of_backup_copy(trashinfo_path): trash_dir = os.path.dirname(os.path.dirname(trashinfo_path)) basename = os.path.basename(trashinfo_path)[:-len('.trashinfo')] return os.path.join(trash_dir, 'files', basename) trash-cli-0.24.5.26/trashcli/lib/print_version.py000066400000000000000000000014061462460053300215540ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import from __future__ import print_function import os import six from typing import NamedTuple class PrintVersionArgs( NamedTuple('PrintVersionArgs', [ ('argv0', str), ])): def program_name(self): return os.path.basename(self.argv0) class PrintVersionAction(object): def __init__(self, out, version): self.out = out self.version = version def run_action(self, args, # type: PrintVersionArgs ): print_version(self.out, args.program_name(), self.version) def print_version(out, program_name, version): print("%s %s" % (program_name, six.text_type(version)), file=out) trash-cli-0.24.5.26/trashcli/lib/trash_dir_reader.py000066400000000000000000000016271462460053300221610ustar00rootroot00000000000000from __future__ import absolute_import import os from trashcli.lib.dir_reader import DirReader class TrashDirReader: def __init__(self, dir_reader, # type: DirReader ): self.dir_reader = dir_reader def list_orphans(self, path): info_dir = os.path.join(path, 'info') files_dir = os.path.join(path, 'files') for entry in self.dir_reader.entries_if_dir_exists(files_dir): trashinfo_path = os.path.join(info_dir, entry + '.trashinfo') file_path = os.path.join(files_dir, entry) if not self.dir_reader.exists(trashinfo_path): yield file_path def list_trashinfo(self, path): info_dir = os.path.join(path, 'info') for entry in self.dir_reader.entries_if_dir_exists(info_dir): if entry.endswith('.trashinfo'): yield os.path.join(info_dir, entry) trash-cli-0.24.5.26/trashcli/lib/trash_dirs.py000066400000000000000000000016361462460053300210220ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import import os from trashcli.fstab.volume_of import VolumeOf def home_trash_dir_path_from_env(environ): if 'XDG_DATA_HOME' in environ: return ['%(XDG_DATA_HOME)s/Trash' % environ] elif 'HOME' in environ: return ['%(HOME)s/.local/share/Trash' % environ] return [] def home_trash_dir_path_from_home(home_dir): return '%s/.local/share/Trash' % home_dir def home_trash_dir(environ, volume_of, # type: VolumeOf ): paths = home_trash_dir_path_from_env(environ) for path in paths: yield path, volume_of.volume_of(path) def volume_trash_dir1(volume, uid): path = os.path.join(volume, '.Trash/%s' % uid) yield path, volume def volume_trash_dir2(volume, uid): path = os.path.join(volume, ".Trash-%s" % uid) yield path, volume trash-cli-0.24.5.26/trashcli/lib/user_info.py000066400000000000000000000015341462460053300206460ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import import pwd from typing import Union from trashcli.lib.trash_dirs import ( home_trash_dir_path_from_env, home_trash_dir_path_from_home) class UserInfo: def __init__(self, home_trash_dir_paths, uid): self.home_trash_dir_paths = home_trash_dir_paths self.uid = uid class SingleUserInfoProvider: @staticmethod def get_user_info(environ, uid): return [UserInfo(home_trash_dir_path_from_env(environ), uid)] class AllUsersInfoProvider: @staticmethod def get_user_info(_environ, _uid): for user in pwd.getpwall(): yield UserInfo([home_trash_dir_path_from_home(user.pw_dir)], user.pw_uid) UserInfoProvider = Union[SingleUserInfoProvider, AllUsersInfoProvider] trash-cli-0.24.5.26/trashcli/list/000077500000000000000000000000001462460053300165055ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/list/__init__.py000066400000000000000000000000001462460053300206040ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/list/extractors.py000066400000000000000000000012631462460053300212570ustar00rootroot00000000000000import os from trashcli.fs import file_size from trashcli.lib.path_of_backup_copy import path_of_backup_copy from trashcli.parse_trashinfo.maybe_parse_deletion_date import \ maybe_parse_deletion_date class DeletionDateExtractor: def extract_attribute(self, _trashinfo_path, contents): return maybe_parse_deletion_date(contents) class SizeExtractor: def extract_attribute(self, trashinfo_path, _contents): backup_copy = path_of_backup_copy(trashinfo_path) try: return str(file_size(backup_copy)) except FileNotFoundError: if os.path.islink(backup_copy): return 0 else: raise trash-cli-0.24.5.26/trashcli/list/fs.py000066400000000000000000000005201462460053300174640ustar00rootroot00000000000000import abc from six import add_metaclass from trashcli.fs import IsSymLink, ContentsOf, EntriesIfDirExists, PathExists, \ IsStickyDir, HasStickyBit @add_metaclass(abc.ABCMeta) class FileSystemReaderForListCmd( IsStickyDir, HasStickyBit, IsSymLink, ContentsOf, EntriesIfDirExists, PathExists, ): pass trash-cli-0.24.5.26/trashcli/list/list_trash_action.py000066400000000000000000000134331462460053300225740ustar00rootroot00000000000000from __future__ import print_function import os from typing import List from typing import NamedTuple from trashcli.lib.dir_reader import DirReader from trashcli.lib.path_of_backup_copy import path_of_backup_copy from trashcli.lib.trash_dir_reader import TrashDirReader from trashcli.list.extractors import DeletionDateExtractor from trashcli.list.extractors import SizeExtractor from trashcli.parse_trashinfo.parse_path import parse_path from trashcli.parse_trashinfo.parser_error import ParseError from trashcli.trash_dirs_scanner import trash_dir_found from trashcli.trash_dirs_scanner import \ trash_dir_skipped_because_parent_is_symlink from trashcli.trash_dirs_scanner import \ trash_dir_skipped_because_parent_not_sticky class ListTrashArgs( NamedTuple('ListTrashArgs', [ ('trash_dirs', List[str]), ('attribute_to_print', str), ('show_files', bool), ('all_users', bool), ])): pass class ListTrashAction: def __init__(self, environ, uid, selector, out, err, dir_reader, content_reader ): self.environ = environ self.uid = uid self.selector = selector self.out = out self.err = err self.dir_reader = dir_reader self.content_reader = content_reader def run_action(self, args, # type: ListTrashArgs ): for message in ListTrash(self.environ, self.uid, self.selector, self.dir_reader, self.content_reader).list_all_trash(args): self.print_event(message) def print_event(self, event): if isinstance(event, Error): print(event.error, file=self.err) elif isinstance(event, Output): print(event.message, file=self.out) class ListTrash: def __init__(self, environ, uid, selector, dir_reader, # type: DirReader content_reader, ): self.environ = environ self.uid = uid self.selector = selector self.dir_reader = dir_reader self.content_reader = content_reader def list_all_trash(self, args, # type: ListTrashArgs ): extractors = { 'deletion_date': DeletionDateExtractor(), 'size': SizeExtractor(), } user_specified_trash_dirs = args.trash_dirs extractor = extractors[args.attribute_to_print] show_files = args.show_files all_users = args.all_users trash_dirs = self.selector.select(all_users, user_specified_trash_dirs, self.environ, self.uid) for event, event_args in trash_dirs: if event == trash_dir_found: path, volume = event_args trash_dir = TrashDirReader(self.dir_reader) for trash_info in trash_dir.list_trashinfo(path): for msg in self._print_trashinfo(volume, trash_info, extractor, show_files): yield msg elif event == trash_dir_skipped_because_parent_not_sticky: path, = event_args msg = Error( self.top_trashdir_skipped_because_parent_not_sticky(path)) yield msg elif event == trash_dir_skipped_because_parent_is_symlink: path, = event_args msg = Error( self.top_trashdir_skipped_because_parent_is_symlink(path)) yield msg def _print_trashinfo(self, volume, trashinfo_path, extractor, show_files): try: contents = self.content_reader.contents_of(trashinfo_path) except IOError as e: yield Error(str(e)) else: try: relative_location = parse_path(contents) except ParseError: yield Error(self.print_parse_path_error(trashinfo_path)) else: attribute = extractor.extract_attribute(trashinfo_path, contents) original_location = os.path.join(volume, relative_location) if show_files: original_file = path_of_backup_copy(trashinfo_path) line = format_line2(attribute, original_location, original_file) else: line = format_line(attribute, original_location) yield Output(line) def top_trashdir_skipped_because_parent_is_symlink(self, trashdir): return "TrashDir skipped because parent is symlink: %s" % trashdir def top_trashdir_skipped_because_parent_not_sticky(self, trashdir): return "TrashDir skipped because parent not sticky: %s" % trashdir def print_parse_path_error(self, offending_file): return "Parse Error: %s: Unable to parse Path." % offending_file class Event: pass class Error(Event): def __init__(self, error): self.error = error class Output(Event): def __init__(self, message): self.message = message def format_line(attribute, original_location): return "%s %s" % (attribute, original_location) def format_line2(attribute, original_location, original_file): return "%s %s -> %s" % (attribute, original_location, original_file) trash-cli-0.24.5.26/trashcli/list/main.py000066400000000000000000000073771462460053300200210ustar00rootroot00000000000000# Copyright (C) 2011-2022 Andrea Francia Bereguardo(PV) Italy import os import sys import trashcli.trash from trashcli.empty.main import ContentReader from trashcli.file_system_reader import FileSystemReader from trashcli.fs import RealContentsOf from trashcli.fstab.volume_listing import RealVolumesListing from trashcli.fstab.real_volume_of import RealVolumeOf from trashcli.fstab.volume_of import VolumeOf from trashcli.lib.dir_reader import DirReader, RealDirReader from trashcli.lib.print_version import PrintVersionArgs, \ PrintVersionAction from trashcli.list.list_trash_action import ListTrashAction, ListTrashArgs from trashcli.list.minor_actions.debug_volumes import DebugVolumes, \ DebugVolumesArgs from trashcli.list.minor_actions.list_trash_dirs import ListTrashDirs, \ ListTrashDirsArgs from trashcli.list.minor_actions.list_volumes import PrintVolumesList, \ PrintVolumesArgs from trashcli.list.minor_actions.print_python_executable import \ PrintPythonExecutable, PrintPythonExecutableArgs from trashcli.list.parser import Parser from trashcli.list.trash_dir_selector import TrashDirsSelector from trashcli.trash_dirs_scanner import TopTrashDirRules def main(): ListCmd( out=sys.stdout, err=sys.stderr, environ=os.environ, volumes_listing=RealVolumesListing(), uid=os.getuid(), volumes=RealVolumeOf(), dir_reader=RealDirReader(), file_reader=FileSystemReader(), content_reader=RealContentsOf(), version=trashcli.trash.version ).run(sys.argv) class ListCmd: def __init__(self, out, err, environ, volumes_listing, uid, volumes, # type: VolumeOf file_reader, # type: TopTrashDirRules.Reader dir_reader, # type: DirReader content_reader, # type: ContentReader version, ): self.out = out self.err = err self.version = version self.dir_reader = dir_reader self.content_reader = content_reader self.environ = environ self.uid = uid self.volumes_listing = volumes_listing self.selector = TrashDirsSelector.make(volumes_listing, file_reader, volumes) self.actions = {PrintVersionArgs: PrintVersionAction(self.out, self.version), PrintVolumesArgs: PrintVolumesList(self.environ, self.volumes_listing, self.out), DebugVolumesArgs: DebugVolumes(), ListTrashDirsArgs: ListTrashDirs(self.environ, self.uid, self.selector), ListTrashArgs: ListTrashAction(self.environ, self.uid, self.selector, self.out, self.err, self.dir_reader, self.content_reader), PrintPythonExecutableArgs: PrintPythonExecutable()} def run(self, argv): parser = Parser(os.path.basename(argv[0])) args = parser.parse_list_args(argv[1:], argv[0]) action = self.actions[type(args)] action.run_action(args) trash-cli-0.24.5.26/trashcli/list/minor_actions/000077500000000000000000000000001462460053300213515ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/list/minor_actions/__init__.py000066400000000000000000000000001462460053300234500ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/list/minor_actions/debug_volumes.py000066400000000000000000000012071462460053300245630ustar00rootroot00000000000000from pprint import pprint class DebugVolumesArgs: pass class DebugVolumes(object): def run_action(self, _args, # type: DebugVolumesArgs ): import psutil import os all = sorted([p for p in psutil.disk_partitions(all=True)], key=lambda p: p.device) physical = sorted([p for p in psutil.disk_partitions()], key=lambda p: p.device) virtual = [p for p in all if p not in physical] print("physical ->") pprint(physical) print("virtual ->") pprint(virtual) os.system('df -P') trash-cli-0.24.5.26/trashcli/list/minor_actions/list_trash_dirs.py000066400000000000000000000025001462460053300251150ustar00rootroot00000000000000from typing import List, NamedTuple from trashcli.trash_dirs_scanner import trash_dir_found, \ trash_dir_skipped_because_parent_not_sticky, \ trash_dir_skipped_because_parent_is_symlink class ListTrashDirsArgs( NamedTuple('ListTrashDirsArgs', [('trash_dirs', List[str]), ('all_users', bool)]) ): pass class ListTrashDirs: def __init__(self, environ, uid, selector): self.environ = environ self.uid = uid self.selector = selector def run_action(self, args): user_specified_trash_dirs = args.trash_dirs all_users = args.all_users trash_dirs = self.selector.select(all_users, user_specified_trash_dirs, self.environ, self.uid) for event, event_args in trash_dirs: if event == trash_dir_found: path, volume = event_args print("%s" % path) elif event == trash_dir_skipped_because_parent_not_sticky: path = event_args print("parent_not_sticky: %s" % (path)) elif event == trash_dir_skipped_because_parent_is_symlink: path = event_args print("parent_is_symlink: %s" % (path)) trash-cli-0.24.5.26/trashcli/list/minor_actions/list_volumes.py000066400000000000000000000007421462460053300244530ustar00rootroot00000000000000from __future__ import print_function class PrintVolumesArgs(object): pass class PrintVolumesList(object): def __init__(self, environ, volumes_listing, out): self.environ = environ self.volumes_listing = volumes_listing self.out = out def run_action(self, args, # type: PrintVolumesArgs ): for volume in self.volumes_listing.list_volumes(self.environ): print(volume, file=self.out) trash-cli-0.24.5.26/trashcli/list/minor_actions/print_python_executable.py000066400000000000000000000003461462460053300266640ustar00rootroot00000000000000class PrintPythonExecutableArgs: pass class PrintPythonExecutable: def run_action(self, _args, # type: PrintPythonExecutableArgs ): import sys print(sys.executable) trash-cli-0.24.5.26/trashcli/list/parser.py000066400000000000000000000117331462460053300203600ustar00rootroot00000000000000import argparse from enum import Enum from typing import List, Union from trashcli.lib.print_version import PrintVersionArgs from trashcli.list.list_trash_action import ListTrashArgs from trashcli.list.minor_actions.debug_volumes import DebugVolumesArgs from trashcli.list.minor_actions.list_trash_dirs import ListTrashDirsArgs from trashcli.list.minor_actions.list_volumes import PrintVolumesArgs from trashcli.list.minor_actions.print_python_executable import \ PrintPythonExecutableArgs from trashcli.shell_completion import add_argument_to, TRASH_DIRS Args = Union[ PrintVersionArgs, PrintVolumesArgs, DebugVolumesArgs, ListTrashDirsArgs, ListTrashArgs, PrintPythonExecutableArgs ] class Parser: def __init__(self, prog): self.parser = argparse.ArgumentParser(prog=prog, description='List trashed files', epilog='Report bugs to https://github.com/andreafrancia/trash-cli/issues') add_argument_to(self.parser) self.parser.add_argument('--version', dest='action', action='store_const', const=ListAction.print_version, default=ListAction.list_trash, help="show program's version number and exit") self.parser.add_argument('--debug-volumes', dest='action', action='store_const', const=ListAction.debug_volumes, help=argparse.SUPPRESS) self.parser.add_argument('--volumes', dest='action', action='store_const', const=ListAction.list_volumes, help="list volumes") self.parser.add_argument('--trash-dirs', dest='action', action='store_const', const=ListAction.list_trash_dirs, help="list trash dirs") self.parser.add_argument('--trash-dir', action='append', default=[], dest='trash_dirs', help='specify the trash directory to use' ).complete = TRASH_DIRS self.parser.add_argument('--size', action='store_const', default='deletion_date', const='size', dest='attribute_to_print', help=argparse.SUPPRESS) self.parser.add_argument('--files', action='store_true', default=False, dest='show_files', help=argparse.SUPPRESS) self.parser.add_argument('--all-users', action='store_true', dest='all_users', help='list trashcans of all the users') self.parser.add_argument('--python', dest='action', action='store_const', const=ListAction.print_python_executable, help=argparse.SUPPRESS) def parse_list_args(self, args, # type: List[str] argv0, # type: str ): # type: (...) -> Args parsed = self.parser.parse_args(args) if parsed.action == ListAction.print_version: return PrintVersionArgs(argv0=argv0) if parsed.action == ListAction.list_volumes: return PrintVolumesArgs() if parsed.action == ListAction.debug_volumes: return DebugVolumesArgs() if parsed.action == ListAction.list_trash_dirs: return ListTrashDirsArgs( trash_dirs=parsed.trash_dirs, all_users=parsed.all_users ) if parsed.action == ListAction.list_trash: return ListTrashArgs( trash_dirs=parsed.trash_dirs, attribute_to_print=parsed.attribute_to_print, show_files=parsed.show_files, all_users=parsed.all_users ) if parsed.action == ListAction.print_python_executable: return PrintPythonExecutableArgs() raise ValueError('Unknown action: {}'.format(parsed.action)) class ListAction(Enum): debug_volumes = 'debug_volumes' print_version = 'print_version' list_trash = 'list_trash' list_volumes = 'list_volumes' list_trash_dirs = 'list_trash_dirs' print_python_executable = 'print_python_executable' trash-cli-0.24.5.26/trashcli/list/trash_dir_selector.py000066400000000000000000000044461462460053300227460ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from typing import List, Dict, Iterator, Tuple from trashcli.fstab.volume_of import VolumeOf from trashcli.lib.dir_checker import DirChecker from trashcli.lib.user_info import AllUsersInfoProvider, \ SingleUserInfoProvider from trashcli.trash_dirs_scanner import trash_dir_found, TrashDir, \ TopTrashDirRules, TrashDirsScanner class TrashDirsSelector: def __init__(self, current_user_dirs, all_users_dirs, volumes # type: VolumeOf ): self.current_user_dirs = current_user_dirs self.all_users_dirs = all_users_dirs self.volumes = volumes def select(self, all_users_flag, # type: bool user_specified_dirs, # type: List[str] environ, # type: Dict[str, str] uid, # type: int ): # type: (...) -> Iterator[Tuple[str, TrashDir]] if all_users_flag: for dir in self.all_users_dirs.scan_trash_dirs(environ, uid): yield dir else: if not user_specified_dirs: for dir in self.current_user_dirs.scan_trash_dirs(environ, uid): yield dir for dir in user_specified_dirs: yield trash_dir_found, ( TrashDir(dir, self.volumes.volume_of(dir))) @staticmethod def make(volumes_listing, reader, # type: TopTrashDirRules.Reader volumes # type: VolumeOf ): user_info_provider = SingleUserInfoProvider() user_dir_scanner = TrashDirsScanner(user_info_provider, volumes_listing, TopTrashDirRules(reader), DirChecker()) all_users_info_provider = AllUsersInfoProvider() all_users_scanner = TrashDirsScanner(all_users_info_provider, volumes_listing, TopTrashDirRules(reader), DirChecker()) return TrashDirsSelector(user_dir_scanner, all_users_scanner, volumes) trash-cli-0.24.5.26/trashcli/parse_trashinfo/000077500000000000000000000000001462460053300207215ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/parse_trashinfo/__init__.py000066400000000000000000000000001462460053300230200ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/parse_trashinfo/basket.py000066400000000000000000000003101462460053300225360ustar00rootroot00000000000000from __future__ import absolute_import class Basket: def __init__(self, initial_value=None): self.collected = initial_value def collect(self, value): self.collected = value trash-cli-0.24.5.26/trashcli/parse_trashinfo/maybe_parse_deletion_date.py000066400000000000000000000010551462460053300264430ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from __future__ import absolute_import from trashcli.parse_trashinfo.basket import Basket from trashcli.parse_trashinfo.parse_trashinfo import ParseTrashInfo def maybe_parse_deletion_date(contents): result = Basket(unknown_date) parser = ParseTrashInfo( on_deletion_date=lambda date: result.collect(date), on_invalid_date=lambda: result.collect(unknown_date) ) parser.parse_trashinfo(contents) return result.collected unknown_date = '????-??-?? ??:??:??' trash-cli-0.24.5.26/trashcli/parse_trashinfo/parse_deletion_date.py000066400000000000000000000005301462460053300252630ustar00rootroot00000000000000from __future__ import absolute_import from trashcli.parse_trashinfo.parse_trashinfo import ParseTrashInfo from trashcli.parse_trashinfo.basket import Basket def parse_deletion_date(contents): result = Basket() parser = ParseTrashInfo(on_deletion_date=result.collect) parser.parse_trashinfo(contents) return result.collected trash-cli-0.24.5.26/trashcli/parse_trashinfo/parse_original_location.py000066400000000000000000000003571462460053300261660ustar00rootroot00000000000000from __future__ import absolute_import import os from trashcli.parse_trashinfo.parse_path import parse_path def parse_original_location(contents, volume_path): path = parse_path(contents) return os.path.join(volume_path, path) trash-cli-0.24.5.26/trashcli/parse_trashinfo/parse_path.py000066400000000000000000000005251462460053300234230ustar00rootroot00000000000000from __future__ import absolute_import from six.moves.urllib.parse import unquote from trashcli.parse_trashinfo.parser_error import ParseError def parse_path(contents): for line in contents.split('\n'): if line.startswith('Path='): return unquote(line[len('Path='):]) raise ParseError('Unable to parse Path') trash-cli-0.24.5.26/trashcli/parse_trashinfo/parse_trashinfo.py000066400000000000000000000021341462460053300244620ustar00rootroot00000000000000from __future__ import absolute_import import datetime from six.moves.urllib.parse import unquote def do_nothing(*argv, **argvk): pass 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 parse_trashinfo(self, contents): found_deletion_date = False for line in contents.split('\n'): if not found_deletion_date and line.startswith('DeletionDate='): found_deletion_date = True try: date = datetime.datetime.strptime( line, "DeletionDate=%Y-%m-%dT%H:%M:%S") except ValueError: self.found_invalid_date() else: self.found_deletion_date(date) if line.startswith('Path='): path = unquote(line[len('Path='):]) self.found_path(path) trash-cli-0.24.5.26/trashcli/parse_trashinfo/parser_error.py000066400000000000000000000001141462460053300237740ustar00rootroot00000000000000from __future__ import absolute_import class ParseError(ValueError): pass trash-cli-0.24.5.26/trashcli/put/000077500000000000000000000000001462460053300163425ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/__init__.py000066400000000000000000000000001462460053300204410ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/check_cast.py000066400000000000000000000005041462460053300210020ustar00rootroot00000000000000from typing import Any from typing import Type from typing import TypeVar from typing import cast T = TypeVar('T') def check_cast(t, value): # type: (Type[T], Any) -> T if isinstance(value, t): return cast(t, value) # type: ignore else: raise TypeError("expected %s, got %s" % (t, type(value))) trash-cli-0.24.5.26/trashcli/put/clock.py000066400000000000000000000003621462460053300200100ustar00rootroot00000000000000import datetime class PutClock: def now(self): # type: () -> datetime.datetime raise NotImplementedError() class RealClock(PutClock): def now(self): # type: () -> datetime.datetime return datetime.datetime.now() trash-cli-0.24.5.26/trashcli/put/context.py000066400000000000000000000023031462460053300203760ustar00rootroot00000000000000from typing import List from typing import NamedTuple from typing import Optional from trashcli.compat import Protocol from trashcli.lib.environ import Environ from trashcli.put.core.logs import LogData from trashcli.put.core.mode import Mode from trashcli.put.core.trash_all_result import TrashAllResult from trashcli.put.core.trash_result import TrashResult class Context(NamedTuple('Context', [ ('paths', List[str]), ('user_trash_dir', Optional[str]), ('mode', Mode), ('forced_volume', Optional[str]), ('home_fallback', bool), ('program_name', str), ('log_data', LogData), ('environ', Environ), ('uid', int), ])): def trash_each(self, trasher, # type: SingleTrasher ): # type (...) -> TrashAllResult failed_paths = [] for path in self.paths: result = trasher.trash_single(path, self) if result == TrashResult.Failure: failed_paths.append(path) return TrashAllResult(failed_paths) class SingleTrasher(Protocol): def trash_single(self, path, # type: str context, # type: 'Context' ): raise NotImplementedError trash-cli-0.24.5.26/trashcli/put/core/000077500000000000000000000000001462460053300172725ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/core/__init__.py000066400000000000000000000000001462460053300213710ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/core/candidate.py000066400000000000000000000021631462460053300215620ustar00rootroot00000000000000import os import posixpath import re from typing import NamedTuple from trashcli.put.core.check_type import CheckType from trashcli.put.core.path_maker_type import PathMakerType from trashcli.put.gate import Gate class Candidate(NamedTuple('Candidate', [ ('trash_dir_path', str), ('volume', str), ('path_maker_type', PathMakerType), ('check_type', CheckType), ('gate', Gate), ])): def parent_dir(self): return os.path.dirname(self.trash_dir_path) def info_dir(self): return os.path.join(self.trash_dir_path, 'info') def files_dir(self): return os.path.join(self.trash_dir_path, 'files') def norm_path(self): return os.path.normpath(self.trash_dir_path) def shrink_user(self, environ): path = self.norm_path() if environ.get('TRASH_PUT_DISABLE_SHRINK', '') == '1': return path home_dir = environ.get('HOME', '') home_dir = posixpath.normpath(home_dir) if home_dir != '': path = re.sub('^' + re.escape(home_dir + os.path.sep), '~' + os.path.sep, path) return path trash-cli-0.24.5.26/trashcli/put/core/check_type.py000066400000000000000000000002751462460053300217660ustar00rootroot00000000000000from enum import Enum class CheckType(Enum): NoCheck = 'NoCheck' TopTrashDirCheck = 'TopTrashDirCheck' NoCheck = CheckType.NoCheck TopTrashDirCheck = CheckType.TopTrashDirCheck trash-cli-0.24.5.26/trashcli/put/core/either.py000066400000000000000000000034401462460053300211250ustar00rootroot00000000000000from abc import abstractmethod from typing import Callable, TypeVar, Generic S = TypeVar("S") R = TypeVar("R") E = TypeVar("E") class Either(Generic[S, E]): @abstractmethod def bind(self, func): # type: (Callable[[S], Either[R, E]]) -> Either[R, E] raise NotImplementedError @abstractmethod def __eq__(self, other): # type: (object) -> bool raise NotImplementedError def is_error(self): # type: () -> bool return not self.is_valid() def is_valid(self): # type: () -> bool return {Left: False, Right: True}[type(self)] def error(self): # type: () -> E if isinstance(self, Left): return self._error else: raise ValueError("Not an error: %s" % self) def value(self): if isinstance(self, Right): return self._value else: raise ValueError("Not a value: %s" % self) class Right(Either[S, E]): def __init__(self, value): # type: (S) -> None self._value = value def bind(self, func): # type: (Callable[[S], Either[R, E]]) -> Either[R, E] return func(self._value) def __eq__(self, other): # type: (object) -> bool return isinstance(other, Right) and self._value == other._value def __str__(self): # type: () -> str return "Right %s" % self._value class Left(Either[S, E]): def __init__(self, error): # type: (E) -> None self._error = error def bind(self, func): # type: (Callable[[S], Either[R, E]]) -> Either[R, E] return Left(self._error) def __eq__(self, other): # type: (object) -> bool return isinstance(other, Left) and self._error == other._error def __str__(self): # type: () -> str return "Left: %s" % self._error trash-cli-0.24.5.26/trashcli/put/core/failure_reason.py000066400000000000000000000013131462460053300226400ustar00rootroot00000000000000from abc import abstractmethod from typing import NamedTuple from trashcli.compat import Protocol from trashcli.lib.environ import Environ from trashcli.put.core.candidate import Candidate class LogContext(NamedTuple('LogContext', [ ('trashee_path', str), ('candidate', Candidate), ('environ', Environ), ])): def shrunk_candidate_path(self): return self.candidate.shrink_user(self.environ) def trash_dir_norm_path(self): return self.candidate.norm_path() def files_dir(self): return self.candidate.files_dir() class FailureReason(Protocol): @abstractmethod def log_entries(self, context): # type: (LogContext) -> str raise NotImplementedError trash-cli-0.24.5.26/trashcli/put/core/int_generator.py000066400000000000000000000003551462460053300225070ustar00rootroot00000000000000from trashcli.compat import Protocol class IntGenerator(Protocol): def new_int(self, min, # type: int max, # type: int ): # type: (...) -> int raise NotImplementedError trash-cli-0.24.5.26/trashcli/put/core/logs.py000066400000000000000000000031341462460053300206110ustar00rootroot00000000000000from abc import abstractmethod from enum import Enum from typing import List from typing import NamedTuple from trashcli.compat import Protocol class Level(Enum): INFO = "INFO" DEBUG = "DEBUG" WARNING = "WARNING" class LogTag(Enum): "tags used only during testing" unspecified = "unspecified" trash_failed = "trash_failed" class LogData: def __init__(self, program_name, verbose): self.program_name = program_name self.verbose = verbose class Message(Protocol): @abstractmethod def resolve(self): raise NotImplementedError class LogEntry(NamedTuple('LogEntry', [ ('level', Level), ('tag', LogTag), ('messages', List[Message]), ])): def resolve_messages(self): for m in self.messages: yield m.resolve() class MessageStr(NamedTuple('Message', [ ('message', str), ]), Message): def resolve(self): return self.message @staticmethod def from_messages(messages # type: List[str] ): return [MessageStr(msg) for msg in messages] def log_str(level, # type: Level tag, # type: LogTag messages, # type: List[str] ): return LogEntry(level, tag, MessageStr.from_messages(messages)) def warning_str(message): # type: (str) -> LogEntry return log_str(Level.WARNING, LogTag.unspecified, [message]) def info_str(message): # type: (str) -> LogEntry return log_str(Level.INFO, LogTag.unspecified, [message]) def debug_str(message): # type: (str) -> LogEntry return log_str(Level.DEBUG, LogTag.unspecified, [message]) trash-cli-0.24.5.26/trashcli/put/core/mode.py000066400000000000000000000006361462460053300205750ustar00rootroot00000000000000from enum import Enum class Mode(Enum): mode_unspecified = 'mode_unspecified' mode_interactive = 'mode_interactive' mode_force = 'mode_force' def can_ignore_not_existent_path(self): # type: () -> bool return self == Mode.mode_force def should_we_ask_to_the_user(self, is_path_accessible): # type: (bool) -> bool return self == Mode.mode_interactive and is_path_accessible trash-cli-0.24.5.26/trashcli/put/core/path_maker_type.py000066400000000000000000000001731462460053300230210ustar00rootroot00000000000000from enum import Enum class PathMakerType(Enum): AbsolutePaths = 'AbsolutePaths' RelativePaths = 'RelativePaths' trash-cli-0.24.5.26/trashcli/put/core/trash_all_result.py000066400000000000000000000003311462460053300232100ustar00rootroot00000000000000from typing import NamedTuple, List class TrashAllResult(NamedTuple('TrashAllResult', [ ('failed_paths', List[str]), ])): def any_failure(self): # type: () -> bool return len(self.failed_paths) > 0 trash-cli-0.24.5.26/trashcli/put/core/trash_result.py000066400000000000000000000001411462460053300223570ustar00rootroot00000000000000from enum import Enum class TrashResult(Enum): Failure = "Failure" Success = "Success" trash-cli-0.24.5.26/trashcli/put/core/trashee.py000066400000000000000000000004131462460053300212750ustar00rootroot00000000000000import os from typing import NamedTuple class Trashee(NamedTuple('FileToBeTrashed', [ ('path', str), ('volume', str) ])): pass def should_skipped_by_specs(path): basename = os.path.basename(path) return (basename == ".") or (basename == "..") trash-cli-0.24.5.26/trashcli/put/describer.py000066400000000000000000000023051462460053300206560ustar00rootroot00000000000000import os class Describer: def __init__(self, fs): self.fs = fs def describe(self, 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 self.fs.islink(path): return 'symbolic link' elif self.fs.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 self.fs.isfile(path): if self.fs.getsize(path) == 0: return 'regular empty file' else: return 'regular file' elif not self.fs.exists(path): return 'non existent' else: return 'entry' trash-cli-0.24.5.26/trashcli/put/dir_maker.py000066400000000000000000000004521462460053300206520ustar00rootroot00000000000000from trashcli.put.fs.fs import Fs class DirMaker: def __init__(self, fs): # type: (Fs) -> None self.fs = fs def mkdir_p(self, path, mode): try: self.fs.makedirs(path, mode) except OSError: if not self.fs.isdir(path): raise trash-cli-0.24.5.26/trashcli/put/file_trasher.py000066400000000000000000000060011462460053300213600ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from trashcli.put.fs.fs import Fs from trashcli.put.context import Context from trashcli.put.core.trash_result import TrashResult from trashcli.put.core.trashee import Trashee from trashcli.put.fs.parent_realpath import ParentRealpathFs from trashcli.put.fs.volume_of_parent import VolumeOfParent from trashcli.put.janitor import Janitor from trashcli.put.my_logger import LoggerBackend from trashcli.put.my_logger import MyLogger from trashcli.put.reporting.trash_put_reporter import TrashPutReporter from trashcli.put.trash_directories_finder import TrashDirectoriesFinder class FileTrasher: def __init__(self, fs, # type: Fs janitor, # type: Janitor backend, # type: LoggerBackend ): # type: (...) -> None self.trash_directories_finder = TrashDirectoriesFinder(fs) self.parent_realpath_fs = ParentRealpathFs(fs) self.logger = MyLogger(backend) self.reporter = TrashPutReporter(fs) self.janitor = janitor self.volume_of_parent = VolumeOfParent(fs) def trash_file(self, path, # type: str context, # type: Context ): volume = self._figure_out_volume(path, context.forced_volume) trashee = Trashee(path, volume) candidates = self._select_candidates(volume, context.user_trash_dir, context.environ, context.uid, context.home_fallback) failures = [] for candidate in candidates: self.logger.log_put(self.reporter.trash_dir_with_volume(candidate), context.log_data) trashing = self.janitor.trash_file_in( candidate, context.log_data, context.environ, trashee) if trashing.succeeded(): self.logger.log_put( self.reporter.file_has_been_trashed_in_as(path, candidate, context.environ), context.log_data) return TrashResult.Success else: failures.append((candidate, trashing.reason)) self.logger.log_put(self.reporter.unable_to_trash_file( trashee, failures, context.environ), context.log_data) return TrashResult.Failure def _figure_out_volume(self, path, default_volume): if default_volume: return default_volume else: return self.volume_of_parent.volume_of_parent(path) def _select_candidates(self, volume, user_trash_dir, environ, uid, home_fallback): return self.trash_directories_finder. \ possible_trash_directories_for(volume, user_trash_dir, environ, uid, home_fallback) trash-cli-0.24.5.26/trashcli/put/format_trash_info.py000066400000000000000000000012311462460053300224150ustar00rootroot00000000000000import datetime from six.moves.urllib.parse import quote as url_quote def format_trashinfo(original_location, # type: str deletion_date, # type: datetime.datetime ): content = ("[Trash Info]\n" + "Path=%s\n" % format_original_location(original_location) + "DeletionDate=%s\n" % format_date(deletion_date)).encode('utf-8') return content def format_date(deletion_date): # type: (datetime.datetime) -> str return deletion_date.strftime("%Y-%m-%dT%H:%M:%S") def format_original_location(original_location): # type: (str) -> str return url_quote(original_location, '/') trash-cli-0.24.5.26/trashcli/put/fs/000077500000000000000000000000001462460053300167525ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/fs/__init__.py000066400000000000000000000000001462460053300210510ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/fs/fs.py000066400000000000000000000043621462460053300177410ustar00rootroot00000000000000import os from abc import abstractmethod from typing import Iterable from trashcli.compat import Protocol class RealPathFs(Protocol): @abstractmethod def realpath(self, path): raise NotImplementedError class Fs(RealPathFs, Protocol): @abstractmethod def atomic_write(self, path, content): raise NotImplementedError @abstractmethod def chmod(self, path, mode): raise NotImplementedError @abstractmethod def isdir(self, path): raise NotImplementedError @abstractmethod def isfile(self, path): raise NotImplementedError @abstractmethod def getsize(self, path): raise NotImplementedError @abstractmethod def exists(self, path): raise NotImplementedError @abstractmethod def makedirs(self, path, mode): raise NotImplementedError @abstractmethod def move(self, path, dest): raise NotImplementedError @abstractmethod def remove_file(self, path): raise NotImplementedError @abstractmethod def islink(self, path): raise NotImplementedError @abstractmethod def has_sticky_bit(self, path): raise NotImplementedError @abstractmethod def is_accessible(self, path): raise NotImplementedError @abstractmethod def read(self, path): raise NotImplementedError @abstractmethod def write_file(self, path, content): raise NotImplementedError @abstractmethod def make_file(self, path, content): raise NotImplementedError @abstractmethod def get_mod(self, path): raise NotImplementedError @abstractmethod def lexists(self, path): raise NotImplementedError @abstractmethod def walk_no_follow(self, top): raise NotImplementedError def parent_realpath2(self, path): parent = os.path.dirname(path) return self.realpath(parent) def list_sorted(self, path): return sorted(self.listdir(path)) def list_all(fs, path): # type: (Fs, str) -> Iterable[str] result = fs.walk_no_follow(path) for top, dirs, non_dirs in result: for d in dirs: yield os.path.join(top, d) for f in non_dirs: yield os.path.join(top, f) trash-cli-0.24.5.26/trashcli/put/fs/parent_realpath.py000066400000000000000000000004231462460053300224740ustar00rootroot00000000000000import os from trashcli.put.fs.fs import RealPathFs class ParentRealpathFs: def __init__(self, fs): # type: (RealPathFs) -> None self.fs = fs def parent_realpath(self, path): parent = os.path.dirname(path) return self.fs.realpath(parent) trash-cli-0.24.5.26/trashcli/put/fs/real_fs.py000066400000000000000000000055421462460053300207450ustar00rootroot00000000000000import grp import os import pwd import stat from typing import NamedTuple from typing import Optional from trashcli import fs from trashcli.fs import write_file from trashcli.fstab.real_volume_of import RealVolumeOf from trashcli.put.fs.fs import Fs class Names: def username(self, uid): # type: (int) -> Optional[str] try: return pwd.getpwuid(uid).pw_name except KeyError as e: return None def groupname(self, gid): try: return grp.getgrgid(gid).gr_name except KeyError as e: return None class Stat(NamedTuple('Stat', [ ('mode', int), ('uid', int), ('gid', int), ])): pass class RealFs(RealVolumeOf, Fs): def __init__(self): super(RealFs, self).__init__() def readlink(self, path): return os.readlink(path) def symlink(self, src, dest): # type: (str, str) -> None os.symlink(src, dest) def touch(self, path): # type: (str) -> None with open(path, 'a'): import os os.utime(path, None) def atomic_write(self, path, content): fs.atomic_write(path, content) def chmod(self, path, mode): os.chmod(path, mode) def isdir(self, path): return os.path.isdir(path) def isfile(self, path): return os.path.isfile(path) def getsize(self, path): return os.path.getsize(path) def walk_no_follow(self, path): try: import scandir # type: ignore walk = scandir.walk except ImportError: walk = os.walk return walk(path, followlinks=False) def exists(self, path): return os.path.exists(path) def makedirs(self, path, mode): os.makedirs(path, mode) def lstat(self, path): stat = os.lstat(path) return Stat(mode=stat.st_mode, uid=stat.st_uid, gid=stat.st_gid) def mkdir(self, path): os.mkdir(path) def mkdir_with_mode(self, path, mode): os.mkdir(path, mode) def move(self, path, dest): return fs.move(path, dest) def remove_file(self, path): fs.remove_file(path) def islink(self, path): return os.path.islink(path) def has_sticky_bit(self, path): return (os.stat(path).st_mode & stat.S_ISVTX) == stat.S_ISVTX def realpath(self, path): return os.path.realpath(path) def is_accessible(self, path): return os.access(path, os.F_OK) def make_file(self, path, content): write_file(path, content) def get_mod(self, path): return stat.S_IMODE(os.lstat(path).st_mode) def listdir(self, path): return os.listdir(path) def read(self, path): return fs.read_file(path) def write_file(self, path, content): return fs.write_file(path, content) def lexists(selfs, path): return os.path.lexists(path) trash-cli-0.24.5.26/trashcli/put/fs/size_counter.py000066400000000000000000000012431462460053300220350ustar00rootroot00000000000000import os from six.moves import map as imap from trashcli.put.fs.fs import Fs class SizeCounter: def __init__(self, fs, # type: Fs ): self.fs = fs def get_size_recursive(self, path): if self.fs.isfile(path): return self.fs.getsize(path) files = self.list_all_files(path) files_sizes = imap(self.fs.getsize, files) return sum(files_sizes, 0) def list_all_files(self, path, # type: str ): for path, dirs, files in self.fs.walk_no_follow(path): for f in files: yield os.path.join(path, f) trash-cli-0.24.5.26/trashcli/put/fs/volume_of_parent.py000066400000000000000000000006151462460053300226720ustar00rootroot00000000000000from trashcli.fstab.volume_of import VolumeOf from trashcli.put.fs.parent_realpath import ParentRealpathFs class VolumeOfParent: def __init__(self, fs, # type: VolumeOf ): self.fs = fs def volume_of_parent(self, path): parent_realpath = ParentRealpathFs(self.fs).parent_realpath(path) return self.fs.volume_of(parent_realpath) trash-cli-0.24.5.26/trashcli/put/gate.py000066400000000000000000000003051462460053300176320ustar00rootroot00000000000000from enum import Enum class Gate(Enum): HomeFallback = "HomeFallbackGate" SameVolume = "SameVolumeGate" def __repr__(self): return "%s.%s" % (type(self).__name__, self.name) trash-cli-0.24.5.26/trashcli/put/janitor.py000066400000000000000000000066721462460053300203750ustar00rootroot00000000000000from typing import NamedTuple, TypeVar from trashcli.put.suffix import Suffix from trashcli.put.core.int_generator import IntGenerator from trashcli.put.my_logger import LoggerBackend from trashcli.put.clock import PutClock from trashcli.lib.environ import Environ from trashcli.put.core.candidate import Candidate from trashcli.put.core.either import Left from trashcli.put.core.failure_reason import FailureReason, LogContext from trashcli.put.core.trashee import Trashee from trashcli.put.fs.fs import Fs from trashcli.put.janitor_tools.info_creator import TrashInfoCreator from trashcli.put.janitor_tools.info_file_persister import InfoFilePersister, \ TrashedFile from trashcli.put.janitor_tools.put_trash_dir import PutTrashDir from trashcli.put.janitor_tools.security_check import SecurityCheck from trashcli.put.janitor_tools.trash_dir_checker import TrashDirChecker from trashcli.put.janitor_tools.trash_dir_creator import TrashDirCreator from trashcli.put.jobs import JobExecutor from trashcli.put.my_logger import LogData from trashcli.put.my_logger import MyLogger class NoLog(FailureReason): def log_entries(self, context): # type: (LogContext) -> str return "" class Janitor: def __init__(self, fs, # type: Fs clock, # type: PutClock backend, # type: LoggerBackend randint, # type: IntGenerator ): self.trash_dir = PutTrashDir(fs) self.trashing_checker = TrashDirChecker(fs) self.info_dir = TrashInfoCreator(fs, clock) self.security_check = SecurityCheck(fs) self.persister = InfoFilePersister(fs, MyLogger(backend), Suffix(randint)) self.dir_creator = TrashDirCreator(fs) self.executor = JobExecutor(MyLogger(backend), TrashedFile) class Result(NamedTuple('Result', [ ('ok', bool), ('reason', FailureReason) ])): def succeeded(self): return self.ok def trash_file_in(self, candidate, # type: Candidate log_data, # type: LogData environ, # type: Environ trashee, # type: Trashee ): # type: (...) -> Result secure = self.security_check.check_trash_dir_is_secure(candidate) if isinstance(secure, Left): return make_error(secure) can_be_used = self.trashing_checker.file_could_be_trashed_in( trashee, candidate, environ) if isinstance(can_be_used, Left): return make_error(can_be_used) dirs_creation = self.dir_creator.make_candidate_dirs(candidate) if isinstance(dirs_creation, Left): return make_error(dirs_creation) trashinfo_data = self.info_dir.make_trashinfo_data( trashee.path, candidate) if isinstance(trashinfo_data, Left): return make_error(trashinfo_data) persisting_job = self.persister.try_persist(trashinfo_data.value()) trashed_file = self.executor.execute(persisting_job, log_data) trashed = self.trash_dir.try_trash(trashee.path, trashed_file) if isinstance(trashed, Left): return make_error(trashed) return make_ok() S = TypeVar('S') def make_error(reason): # type: (Left[S, FailureReason]) -> Janitor.Result return Janitor.Result(False, reason.error()) def make_ok(): # type: () -> Janitor.Result return Janitor.Result(True, NoLog()) trash-cli-0.24.5.26/trashcli/put/janitor_tools/000077500000000000000000000000001462460053300212305ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/janitor_tools/__init__.py000066400000000000000000000000001462460053300233270ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/janitor_tools/info_creator.py000066400000000000000000000040121462460053300242510ustar00rootroot00000000000000import os from typing import NamedTuple from trashcli.put.clock import PutClock from trashcli.put.core.candidate import Candidate from trashcli.put.core.either import Either from trashcli.put.core.either import Left from trashcli.put.core.either import Right from trashcli.put.core.failure_reason import FailureReason from trashcli.put.core.failure_reason import LogContext from trashcli.put.format_trash_info import format_trashinfo from trashcli.put.fs.fs import Fs from trashcli.put.janitor_tools.info_file_persister import TrashinfoData from trashcli.put.original_location import OriginalLocation class UnableToCreateTrashInfoContent( NamedTuple('UnableToCreateTrashInfoContent', [ ('error', Exception), ]), FailureReason): def log_entries(self, context): # type: (LogContext) -> str return "failed to generate trashinfo content: %s" % ( self.error) class TrashInfoCreator: def __init__(self, fs, # type: Fs clock, # type: PutClock ): self.original_location = OriginalLocation(fs) self.clock = clock Result = Either[TrashinfoData, UnableToCreateTrashInfoContent] def make_trashinfo_data(self, path, # type: str candidate, # type: Candidate ): # type: (...) -> Result try: original_location = self.original_location.for_file(path, candidate.path_maker_type, candidate.volume) content = format_trashinfo(original_location, self.clock.now()) basename = os.path.basename(original_location) trash_info_data = TrashinfoData(basename, content, candidate.info_dir()) return Right(trash_info_data) except (IOError, OSError) as error: return Left(UnableToCreateTrashInfoContent(error)) trash-cli-0.24.5.26/trashcli/put/janitor_tools/info_file_persister.py000066400000000000000000000055201462460053300256360ustar00rootroot00000000000000import errno import os from typing import NamedTuple, Iterator from trashcli.lib.path_of_backup_copy import path_of_backup_copy from trashcli.put.fs.fs import Fs from trashcli.put.jobs import JobStatus, NeedsMoreAttempts, Succeeded, \ JobExecutor from trashcli.put.my_logger import LogData, MyLogger from trashcli.put.suffix import Suffix class TrashinfoData(NamedTuple('TrashinfoData', [ ('basename', str), ('content', str), ('info_dir_path', str), ])): pass class TrashedFile(NamedTuple('TrashedFile', [ ('trashinfo_path', str), ])): @property def backup_copy_path(self): # type: () -> str return path_of_backup_copy(self.trashinfo_path) class InfoFilePersister: def __init__(self, fs, # type: Fs logger, # type: MyLogger suffix, # type: Suffix ): # type: (...) -> None self.fs = fs self.logger = logger self.suffix = suffix def create_trashinfo_file(self, trashinfo_data, # type: TrashinfoData log_data, # type: LogData ): # type: (...) -> TrashedFile return JobExecutor(self.logger, TrashedFile).execute( self.try_persist(trashinfo_data), log_data) Result = Iterator[JobStatus[TrashedFile]] def try_persist(self, data, # type: TrashinfoData ): # type: (...) -> Result index = 0 name_too_long = False while True: suffix = self.suffix.suffix_for_index(index) trashinfo_basename = create_trashinfo_basename(data.basename, suffix, name_too_long) trashinfo_path = os.path.join(data.info_dir_path, trashinfo_basename) if os.path.exists(path_of_backup_copy(trashinfo_path)): index += 1 continue try: self.fs.atomic_write(trashinfo_path, data.content) yield Succeeded(TrashedFile(trashinfo_path), ".trashinfo created as %s." % trashinfo_path) except OSError as e: if e.errno == errno.ENAMETOOLONG: name_too_long = True yield NeedsMoreAttempts(trashinfo_path, "attempt for creating %s failed." % trashinfo_path) index += 1 def create_trashinfo_basename(basename, suffix, name_too_long): after_basename = suffix + ".trashinfo" if name_too_long: truncated_basename = basename[0:len(basename) - len(after_basename)] else: truncated_basename = basename return truncated_basename + after_basename trash-cli-0.24.5.26/trashcli/put/janitor_tools/put_trash_dir.py000066400000000000000000000027761462460053300244650ustar00rootroot00000000000000import os.path from typing import NamedTuple from trashcli.put.core.either import Either from trashcli.put.core.either import Left from trashcli.put.core.either import Right from trashcli.put.core.failure_reason import FailureReason from trashcli.put.core.failure_reason import LogContext from trashcli.put.fs.fs import Fs from trashcli.put.janitor_tools.info_file_persister import TrashedFile class UnableToMoveFileToTrash(NamedTuple('UnableToMoveFileToTrash', [ ('error', Exception), ]), FailureReason): def log_entries(self, context): # type: (LogContext) -> str return "failed to move %s in %s: %s" % ( context.trashee_path, context.files_dir(), self.error, ) class PutTrashDir: def __init__(self, fs, # type: Fs ): self.fs = fs def try_trash(self, path, # type: str paths, # type: TrashedFile ): # type: (...) -> Either[None, Exception] try: move_file(self.fs, path, paths.backup_copy_path) return Right(None) except (IOError, OSError) as error: self.fs.remove_file(paths.trashinfo_path) return Left(UnableToMoveFileToTrash(error)) def move_file(fs, # type: Fs src, # type: str dest, # type: str ): # Using nornpath allow to delete symlink to a dir even if the are # specified with traling slash fs.move(os.path.normpath(src), dest) trash-cli-0.24.5.26/trashcli/put/janitor_tools/security_check.py000066400000000000000000000047611462460053300246160ustar00rootroot00000000000000from trashcli.put.core.candidate import Candidate from trashcli.put.core.check_type import NoCheck, TopTrashDirCheck from trashcli.put.core.either import Either, Right, Left from trashcli.put.core.failure_reason import FailureReason, LogContext class SecurityCheck: def __init__(self, fs): self.fs = fs def check_trash_dir_is_secure(self, candidate, # type: Candidate ): # type: (...) -> Either[None, FailureReason] if candidate.check_type == NoCheck: return Right(None) if candidate.check_type == TopTrashDirCheck: parent = candidate.parent_dir() if not self.fs.lexists(parent): return Left(TrashDirDoesNotHaveParent()) if not self.fs.isdir(parent): return Left(TrashDirCannotBeCreatedBecauseParentIsFile()) if self.fs.islink(parent): return Left(TrashDirIsNotSecureBecauseSymLink()) if not self.fs.has_sticky_bit(parent): return Left(TrashDirIsNotSecureBecauseNotSticky()) return Right(None) raise Exception("Unknown check type: %s" % candidate.check_type) class TrashDirDoesNotHaveParent(FailureReason): def log_entries(self, context): # type: (LogContext) -> str return trash_dir_parent_problem(context, ( "trash dir cannot be created because its parent does not exists")) class TrashDirCannotBeCreatedBecauseParentIsFile(FailureReason): def log_entries(self, context): # type: (LogContext) -> str return trash_dir_parent_problem(context, ( "trash dir cannot be created as its parent is a file instead of being a directory")) class TrashDirIsNotSecureBecauseSymLink(FailureReason): def log_entries(self, context): # type: (LogContext) -> str return trash_dir_parent_problem(context, ( "trash dir is insecure, its parent should not be a symlink")) class TrashDirIsNotSecureBecauseNotSticky(FailureReason): def log_entries(self, context): # type: (LogContext) -> str return trash_dir_parent_problem(context, ( "trash dir is insecure, its parent should be sticky")) def trash_dir_parent_problem(context, # type: LogContext message, # type: str ): # type: (...) -> str return "%s, trash-dir: %s, parent: %s" % ( message, context.trash_dir_norm_path(), context.candidate.parent_dir()) trash-cli-0.24.5.26/trashcli/put/janitor_tools/trash_dir_checker.py000066400000000000000000000057371462460053300252610ustar00rootroot00000000000000from typing import NamedTuple from trashcli.lib.environ import Environ from trashcli.put.core.candidate import Candidate from trashcli.put.core.either import Either from trashcli.put.core.either import Left from trashcli.put.core.either import Right from trashcli.put.core.failure_reason import FailureReason from trashcli.put.core.failure_reason import LogContext from trashcli.put.core.trashee import Trashee from trashcli.put.fs.fs import Fs from trashcli.put.gate import Gate from trashcli.put.trash_dir_volume_reader import TrashDirVolumeReader class DifferentVolumes(NamedTuple('DifferentVolumes', [ ('trash_dir_volume', str), ('file_volume', str), ]), FailureReason): def log_entries(self, context): # type: (LogContext) -> str return ( "trash dir and file to be trashed are not in the same volume, trash-dir volume: %s, file volume: %s" % (self.trash_dir_volume, self.file_volume)) class HomeFallBackNotEnabled(FailureReason): def log_entries(self, context): # type: (LogContext) -> str return "home fallback not enabled" def __eq__(self, other): return isinstance(other, HomeFallBackNotEnabled) GateCheckResult = Either[None, FailureReason] class TrashDirChecker: def __init__(self, fs, # type: Fs ): # type: (...) -> None self.fs = fs def file_could_be_trashed_in(self, trashee, # type: Trashee candidate, # type: Candidate environ, # type: Environ ): # type: (...) -> GateCheckResult if candidate.gate is Gate.HomeFallback: return self._can_be_trashed_in_home_trash_dir(environ) elif candidate.gate is Gate.SameVolume: return SameVolumeGateImpl(self.fs).can_trash_in( trashee, candidate) else: raise ValueError("Unknown gate: %s" % candidate.gate) @staticmethod def _can_be_trashed_in_home_trash_dir(environ, # type: Environ ): if environ.get('TRASH_ENABLE_HOME_FALLBACK', None) == "1": return make_ok() return Left(HomeFallBackNotEnabled()) def make_ok(): return Right(None) class SameVolumeGateImpl: def __init__(self, fs, # type: Fs ): self.fs = fs def can_trash_in(self, trashee, # type: Trashee candidate, # type: Candidate ): trash_dir_volume = self._volume_of_trash_dir(candidate) same_volume = trash_dir_volume == trashee.volume if not same_volume: return Left(DifferentVolumes(trash_dir_volume, trashee.volume)) return make_ok() def _volume_of_trash_dir(self, candidate): # type: (Candidate) -> str return (TrashDirVolumeReader(self.fs) .volume_of_trash_dir(candidate.trash_dir_path)) trash-cli-0.24.5.26/trashcli/put/janitor_tools/trash_dir_creator.py000066400000000000000000000022561462460053300253050ustar00rootroot00000000000000from typing import NamedTuple from trashcli.put.core.candidate import Candidate from trashcli.put.core.either import Either, Right, Left from trashcli.put.core.failure_reason import FailureReason, LogContext from trashcli.put.dir_maker import DirMaker from trashcli.put.fs.fs import Fs class TrashDirCannotBeCreated( NamedTuple('TrashDirCannotBeCreated', [ ('error', Exception), ]), FailureReason): def log_entries(self, context): # type: (LogContext) -> str return "error during directory creation: %s" % ( self.error) class TrashDirCreator: def __init__(self, fs): # type: (Fs) -> None self.dir_maker = DirMaker(fs) def make_candidate_dirs(self, candidate, # type: Candidate ): # type: (...) -> Either[None, TrashDirCannotBeCreated] try: self.dir_maker.mkdir_p(candidate.trash_dir_path, 0o700) self.dir_maker.mkdir_p(candidate.files_dir(), 0o700) self.dir_maker.mkdir_p(candidate.info_dir(), 0o700) return Right(None) except (IOError, OSError) as error: return Left(TrashDirCannotBeCreated(error)) trash-cli-0.24.5.26/trashcli/put/jobs.py000066400000000000000000000042161462460053300176540ustar00rootroot00000000000000from typing import Generic from typing import Iterator from typing import Type from typing import TypeVar from trashcli.put.core.logs import Level from trashcli.put.core.logs import LogData from trashcli.put.core.logs import LogEntry from trashcli.put.core.logs import LogTag from trashcli.put.core.logs import MessageStr from trashcli.put.my_logger import MyLogger R = TypeVar('R') class JobStatus(Generic[R]): def __init__(self, message): # type: (str) -> None self.log_entries = [LogEntry(Level.DEBUG, LogTag.unspecified, MessageStr.from_messages([message]))] def has_succeeded(self): return isinstance(self, Succeeded) def result(self): # type: () -> R raise NotImplementedError def logs(self): return self.log_entries class NeedsMoreAttempts(JobStatus, Generic[R]): def __init__(self, trashinfo_path, # type: str message, # type: str ): super(NeedsMoreAttempts, self).__init__(message) self.trashinfo_path = trashinfo_path def result(self): # type: () -> R raise ValueError("Result not available yet!") class Succeeded(JobStatus, Generic[R]): def __init__(self, result, # type: R message, # type: str ): super(Succeeded, self).__init__(message) self._result = result # type: R def result(self): # type: () -> R return self._result def __repr__(self): return "Succeeded(%s)" % repr(self.result) class JobExecutor(Generic[R]): def __init__(self, logger, # type: MyLogger _result_type, # type: Type[R] ): self.logger = logger def execute(self, job, # type: Iterator[JobStatus[R]] log_data, # type: LogData ): # type: (...) -> R for status in job: self.logger.log_multiple(status.logs(), log_data) if status.has_succeeded(): return status.result() raise ValueError("Should not happen!") trash-cli-0.24.5.26/trashcli/put/main.py000066400000000000000000000035341462460053300176450ustar00rootroot00000000000000import os import random import sys from trashcli.lib.environ import cast_environ from trashcli.lib.my_input import Input from trashcli.lib.my_input import RealInput from trashcli.put.clock import RealClock from trashcli.put.core.int_generator import IntGenerator from trashcli.put.describer import Describer from trashcli.put.file_trasher import FileTrasher from trashcli.put.fs.fs import Fs from trashcli.put.fs.real_fs import RealFs from trashcli.put.janitor import Janitor from trashcli.put.my_logger import LoggerBackend from trashcli.put.my_logger import StreamBackend from trashcli.put.reporting.trash_put_reporter import TrashPutReporter from trashcli.put.trash_put_cmd import TrashPutCmd from trashcli.put.trasher import Trasher from trashcli.put.user import User def main(): cmd = make_cmd(clock=RealClock(), fs=RealFs(), user_input=RealInput(), randint=RandomIntGenerator(), backend=StreamBackend(sys.stderr)) try: uid = int(os.environ["TRASH_PUT_FAKE_UID_FOR_TESTING"]) except KeyError: uid = os.getuid() return cmd.run_put(sys.argv, cast_environ(os.environ), uid) def make_cmd(clock, fs, # type: Fs user_input, # type: Input randint, # type: IntGenerator backend, # type: LoggerBackend ): # type: (...) -> TrashPutCmd reporter = TrashPutReporter(fs) janitor = Janitor(fs, clock, backend, randint) file_trasher = FileTrasher(fs, janitor, backend) user = User(user_input, Describer(fs)) trasher = Trasher(file_trasher, user, fs, backend) return TrashPutCmd(reporter, trasher) class RandomIntGenerator(IntGenerator): def new_int(self, min, # type: int max, # type: int ): return random.randint(min, max) trash-cli-0.24.5.26/trashcli/put/my_logger.py000066400000000000000000000034241462460053300207030ustar00rootroot00000000000000from typing import IO from typing import List from trashcli.compat import Protocol from trashcli.put.core.logs import Level from trashcli.put.core.logs import LogData from trashcli.put.core.logs import LogEntry class LoggerBackend(Protocol): def write_message(self, log_entry, # type: LogEntry log_data, # type: LogData ): raise NotImplementedError() class StreamBackend(LoggerBackend): def __init__(self, stderr, # type: IO[str] ): # type: (...) -> None self.stderr = stderr def write_message(self, log_entry, # type: LogEntry log_data, # type: LogData ): if is_right_for_level(log_data.verbose, log_entry.level): for message in log_entry.resolve_messages(): self.stderr.write("%s: %s\n" % (log_data.program_name, message)) def is_right_for_level(verbose, # type: int level, # type: Level ): min_level = { Level.WARNING: 0, Level.INFO: 1, Level.DEBUG: 2, } return verbose >= min_level[level] class MyLogger: def __init__(self, backend, # type: LoggerBackend ): # type: (...) -> None self.backend = backend def log_put(self, entry, # type: LogEntry log_data, # type: LogData ): self.backend.write_message(entry, log_data) def log_multiple(self, entries, # type: List[LogEntry] log_data, # type: LogData ): # type: (...) -> None for entry in entries: self.log_put(entry, log_data) trash-cli-0.24.5.26/trashcli/put/octal.py000066400000000000000000000004321462460053300200150ustar00rootroot00000000000000def octal(n): # type: (int) -> str return "%s%s" % ("0o", _remove_octal_prefix(oct(n))) def _remove_octal_prefix(o): if o.startswith('0o'): return o[2:] elif o.startswith('0'): return o[1:] else: ValueError('Invalid octal format: %s' % o) trash-cli-0.24.5.26/trashcli/put/original_location.py000066400000000000000000000024311462460053300224100ustar00rootroot00000000000000import os from trashcli.put.core.path_maker_type import PathMakerType from trashcli.put.fs.fs import Fs class OriginalLocation: def __init__(self, fs, # type: Fs ): self.fs = fs def for_file(self, path, path_maker_type, # type: PathMakerType volume_top_dir, ): # type: (...) -> str normalized_path = os.path.normpath(path) basename = os.path.basename(normalized_path) parent = self.fs.parent_realpath2(normalized_path) parent = self._calc_parent_path(parent, volume_top_dir, path_maker_type) return os.path.join(parent, basename) @staticmethod def _calc_parent_path(parent, # type: str volume_top_dir, # type: str path_maker_type, # type: PathMakerType ): if path_maker_type == PathMakerType.AbsolutePaths: return parent if path_maker_type == PathMakerType.RelativePaths: if (parent == volume_top_dir) or parent.startswith( volume_top_dir + os.path.sep): parent = parent[len(volume_top_dir + os.path.sep):] return parent trash-cli-0.24.5.26/trashcli/put/parser.py000066400000000000000000000113031462460053300202060ustar00rootroot00000000000000import os import pprint from argparse import ArgumentParser from argparse import RawDescriptionHelpFormatter from argparse import SUPPRESS from typing import Any from typing import List from typing import NamedTuple from typing import Optional from typing import Union from trashcli.put.core.mode import Mode from trashcli.shell_completion import TRASH_DIRS from trashcli.shell_completion import TRASH_FILES from trashcli.shell_completion import add_argument_to from trashcli.trash import version ExitWithCode = NamedTuple('ExitWithCode', [ ('type', type), ('exit_code', int), ]) Trash = NamedTuple('Trash', [ ('type', type), ('program_name', str), ('options', Any), ('files', List[str]), ('trash_dir', Optional[str]), ('mode', Mode), ('forced_volume', Optional[str]), ('verbose', int), ('home_fallback', bool), ]) class Parser: def parse_args(self, argv): # type: (list) -> Union[ExitWithCode, Trash] program_name = os.path.basename(argv[0]) arg_parser = make_parser(program_name) try: options = arg_parser.parse_args(argv[1:]) if len(options.files) <= 0: arg_parser.error("Please specify the files to trash.") except SystemExit as e: return ExitWithCode(type=ExitWithCode, exit_code=ensure_int(e.code)) return Trash(type=Trash, program_name=program_name, options=options, files=options.files, trash_dir=options.trashdir, mode=options.mode, forced_volume=options.forced_volume, verbose=options.verbose, home_fallback=options.home_fallback) def ensure_int(code): if not isinstance(code, int): raise ValueError("code must be an int, got %s" % pprint.pformat(code)) return code def make_parser(program_name): parser = ArgumentParser(prog=program_name, usage="%(prog)s [OPTION]... FILE...", description="Put files in trash", formatter_class=RawDescriptionHelpFormatter, epilog="""\ all trash-cli commands: trash-put trash files and directories. trash-empty empty the trashcan(s). trash-list list trashed files. trash-restore restore a trashed file. trash-rm remove individual files from the trashcan To remove a file whose name starts with a '-', for example '-foo', use one of these commands: trash -- -foo trash ./-foo Report bugs to https://github.com/andreafrancia/trash-cli/issues""") add_argument_to(parser) parser.add_argument("-d", "--directory", action="store_true", help="ignored (for GNU rm compatibility)") parser.add_argument("-f", "--force", action="store_const", dest="mode", const=Mode.mode_force, default=Mode.mode_unspecified, help="silently ignore nonexistent files") parser.add_argument("-i", "--interactive", action="store_const", dest="mode", const=Mode.mode_interactive, default=Mode.mode_unspecified, help="prompt before every removal") parser.add_argument("-r", "-R", "--recursive", action="store_true", help="ignored (for GNU rm compatibility)") parser.add_argument("--trash-dir", type=str, action="store", dest='trashdir', help='use TRASHDIR as trash folder' ).complete = TRASH_DIRS parser.add_argument("-v", "--verbose", default=0, action="count", dest="verbose", help="explain what is being done") parser.add_argument('--force-volume', default=None, action="store", dest="forced_volume", help=SUPPRESS) parser.add_argument('--home-fallback', default=False, action="store_true", dest="home_fallback", help=SUPPRESS) parser.add_argument("--version", action="version", version=version) parser.add_argument('files', nargs='*' ).complete = TRASH_FILES return parser trash-cli-0.24.5.26/trashcli/put/reporting/000077500000000000000000000000001462460053300203535ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/reporting/__init__.py000066400000000000000000000000001462460053300224520ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/reporting/stats_reader.py000066400000000000000000000031421462460053300234050ustar00rootroot00000000000000# Copyright (C) 2007-2024 Andrea Francia Trivolzio(PV) Italy import os from grp import getgrgid from pwd import getpwuid from typing import NamedTuple import re from trashcli.put.core.either import Either from trashcli.put.core.either import Left from trashcli.put.core.either import Right def gentle_stat_read(path): def stats_str(stats): # type: (Either[Stats, Exception]) -> str if isinstance(result, Right): value = result.value() return "%s %s %s" % (value.octal_mode(), value.user, value.group) elif isinstance(result, Left): return str(result.error()) else: raise ValueError() result = StatReader().read_stats(path) return stats_str(result) class Stats(NamedTuple('Result', [ ('user', str), ('group', str), ('mode', int), ])): def octal_mode(self): # () -> str return self._remove_octal_prefix(oct(self.mode & 0o777)) @staticmethod def _remove_octal_prefix(mode): # type: (str) -> str remove_new_octal_format = mode.replace('0o', '') remove_old_octal_format = re.sub(r"^0", '', remove_new_octal_format) return remove_old_octal_format class StatReader: @staticmethod def read_stats(path, # type: str ): # type: (...) -> Either[Stats, Exception] try: stats = os.lstat(path) user = getpwuid(stats.st_uid).pw_name group = getgrgid(stats.st_gid).gr_name mode = stats.st_mode return Right(Stats(user, group, mode)) except (IOError, OSError) as e: return Left(e) trash-cli-0.24.5.26/trashcli/put/reporting/trash_put_reporter.py000066400000000000000000000072361462460053300246700ustar00rootroot00000000000000# Copyright (C) 2007-2024 Andrea Francia Trivolzio(PV) Italy import os from typing import List from typing import Tuple from trashcli.put.fs.fs import Fs from trashcli.lib.environ import Environ from trashcli.lib.exit_codes import EX_IOERR from trashcli.lib.exit_codes import EX_OK from trashcli.put.core.candidate import Candidate from trashcli.put.core.failure_reason import FailureReason from trashcli.put.core.failure_reason import LogContext from trashcli.put.core.logs import Level from trashcli.put.core.logs import LogEntry from trashcli.put.core.logs import LogTag from trashcli.put.core.logs import debug_str from trashcli.put.core.logs import info_str from trashcli.put.core.logs import log_str from trashcli.put.core.trash_all_result import TrashAllResult from trashcli.put.core.trashee import Trashee from trashcli.put.describer import Describer from trashcli.put.reporting.stats_reader import gentle_stat_read class TrashPutReporter: def __init__(self, fs, # type: Fs ): self.describer = Describer(fs) def _describe(self, path): return self.describer.describe(path) def unable_to_trash_dot_entries(self, file ): # type: (...) -> LogEntry return log_str(Level.WARNING, LogTag.trash_failed, ["cannot trash %s '%s'" % (self._describe(file), file)]) def unable_to_trash_file_non_existent(self, path, # type: str ): # type: (...) -> LogEntry return log_str(Level.WARNING, LogTag.trash_failed, ["cannot trash %s '%s'" % (self._describe(path), path)]) def unable_to_trash_file( self, trashee, # type: Trashee failures, # type: List[Tuple[Candidate, FailureReason]] environ, # type: Environ ): # (...) -> LogEntry path = trashee.path volume = trashee.volume messages = [] messages.append("cannot trash %s '%s' (from volume '%s')" % ( self._describe(path), path, volume)) for candidate, reason in failures: context = LogContext(path, candidate, environ) message = " `- failed to trash %s in %s, because %s" % ( path, candidate.norm_path(), reason.log_entries(context)) messages.append(message) return log_str(Level.WARNING, LogTag.trash_failed, messages) @staticmethod def file_has_been_trashed_in_as(trashed_file, trash_dir, # type: Candidate environ): # type: (...) -> LogEntry return info_str("'%s' trashed in %s" % (trashed_file, trash_dir.shrink_user(environ))) @classmethod def log_data_for_debugging(cls, error): try: filename = error.filename except AttributeError: pass else: if filename is not None: for path in [filename, os.path.dirname(filename)]: info = gentle_stat_read(path) yield "stats for %s: %s" % (path, info) @staticmethod def trash_dir_with_volume(candidate, # type: Candidate ): # type: (...) -> LogEntry return debug_str("trying trash dir: %s from volume: %s" % ( candidate.norm_path(), candidate.volume)) @staticmethod def exit_code(result, # type: TrashAllResult ): # type: (...) -> int if not result.any_failure(): return EX_OK else: return EX_IOERR trash-cli-0.24.5.26/trashcli/put/same_volume_gate.py000066400000000000000000000000001462460053300222160ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/put/suffix.py000066400000000000000000000006331462460053300202220ustar00rootroot00000000000000from trashcli.put.core.int_generator import IntGenerator class Suffix: def __init__(self, int_gen, # type: IntGenerator ): self.int_gen = int_gen def suffix_for_index(self, index): if index == 0: return "" elif index < 100: return "_%s" % index else: return "_%s" % self.int_gen.new_int(0, 65535) trash-cli-0.24.5.26/trashcli/put/trash_dir_volume_reader.py000066400000000000000000000006041462460053300236040ustar00rootroot00000000000000import os from trashcli.put.fs.fs import RealPathFs class TrashDirVolumeReader: def __init__(self, fs, # type: RealPathFs ): self.fs = fs def volume_of_trash_dir(self, trash_dir_path): norm_trash_dir_path = os.path.normpath(trash_dir_path) return self.fs.volume_of( self.fs.realpath(norm_trash_dir_path)) trash-cli-0.24.5.26/trashcli/put/trash_directories_finder.py000066400000000000000000000060061462460053300237620ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from typing import List from trashcli.fstab.volume_of import VolumeOf from trashcli.lib.environ import Environ from trashcli.lib.trash_dirs import ( volume_trash_dir1, volume_trash_dir2, home_trash_dir) from trashcli.put.core.candidate import Candidate from trashcli.put.core.check_type import NoCheck, TopTrashDirCheck from trashcli.put.core.path_maker_type import PathMakerType from trashcli.put.gate import Gate class TrashDirectoriesFinder: def __init__(self, fs, # type: VolumeOf ): self.fs = fs def possible_trash_directories_for(self, volume, # type: str specific_trash_dir, # type: str environ, # type: Environ uid, # type: int home_fallback, # type: bool ): # type: (...) -> List[Candidate] trash_dirs = [] def add_home_trash(path, volume, gate): trash_dirs.append( Candidate(trash_dir_path=path, volume=volume, path_maker_type=PathMakerType.AbsolutePaths, check_type=NoCheck, gate=gate)) def add_top_trash_dir(path, volume): trash_dirs.append( Candidate(trash_dir_path=path, volume=volume, path_maker_type=PathMakerType.RelativePaths, check_type=TopTrashDirCheck, gate=Gate.SameVolume)) def add_alt_top_trash_dir(path, volume): trash_dirs.append( Candidate(trash_dir_path=path, volume=volume, path_maker_type=PathMakerType.RelativePaths, check_type=NoCheck, gate=Gate.SameVolume)) if specific_trash_dir: path = specific_trash_dir volume = self.fs.volume_of(path) trash_dirs.append( Candidate(trash_dir_path=path, volume=volume, path_maker_type=PathMakerType.RelativePaths, check_type=NoCheck, gate=Gate.SameVolume)) else: for path, dir_volume in home_trash_dir(environ, self.fs): add_home_trash(path, dir_volume, Gate.SameVolume) for path, dir_volume in volume_trash_dir1(volume, uid): add_top_trash_dir(path, dir_volume) for path, dir_volume in volume_trash_dir2(volume, uid): add_alt_top_trash_dir(path, dir_volume) if home_fallback: for path, dir_volume in home_trash_dir(environ, self.fs): add_home_trash(path, dir_volume, Gate.HomeFallback) return trash_dirs trash-cli-0.24.5.26/trashcli/put/trash_put_cmd.py000066400000000000000000000032141462460053300215500ustar00rootroot00000000000000from typing import List from trashcli.lib.environ import Environ from trashcli.put.context import Context from trashcli.put.my_logger import LogData from trashcli.put.parser import ExitWithCode from trashcli.put.parser import Parser from trashcli.put.parser import Trash from trashcli.put.reporting.trash_put_reporter import TrashPutReporter from trashcli.put.trasher import Trasher class TrashPutCmd: def __init__(self, reporter, # type: TrashPutReporter trasher, # type: Trasher ): self.reporter = reporter self.trasher = trasher def run_put(self, argv, # type: List[str] environ, # type: Environ uid, # type: int ): # type: (...) -> int parser = Parser() parsed = parser.parse_args(argv) if isinstance(parsed, ExitWithCode): return parsed.exit_code elif isinstance(parsed, Trash): program_name = parsed.program_name log_data = LogData(program_name, parsed.verbose) context = Context(paths=parsed.files, user_trash_dir=parsed.trash_dir, mode=parsed.mode, forced_volume=parsed.forced_volume, home_fallback=parsed.home_fallback, program_name=program_name, log_data=log_data, environ=environ, uid=uid) result = context.trash_each(self.trasher) return self.reporter.exit_code(result) trash-cli-0.24.5.26/trashcli/put/trasher.py000066400000000000000000000050511462460053300203650ustar00rootroot00000000000000from trashcli.put.my_logger import LoggerBackend from trashcli.put.context import Context from trashcli.put.context import SingleTrasher from trashcli.put.core.trash_result import TrashResult from trashcli.put.core.trashee import should_skipped_by_specs from trashcli.put.file_trasher import FileTrasher from trashcli.put.fs.fs import Fs from trashcli.put.my_logger import MyLogger from trashcli.put.reporting.trash_put_reporter import TrashPutReporter from trashcli.put.user import User from trashcli.put.user import user_replied_no class Trasher(SingleTrasher): def __init__(self, file_trasher, # type: FileTrasher user, # type: User fs, # type: Fs backend, # type: LoggerBackend ): self.file_trasher = file_trasher self.user = user self.logger = MyLogger(backend) self.reporter = TrashPutReporter(fs) self.fs = fs def trash_single(self, path, # type: str context, # type: Context ): """ 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 should_skipped_by_specs(path): self.logger.log_put( self.reporter.unable_to_trash_dot_entries(path), context.log_data) return TrashResult.Failure if not self.fs.lexists(path): if context.mode.can_ignore_not_existent_path(): return TrashResult.Success else: self.logger.log_put( self.reporter.unable_to_trash_file_non_existent(path), context.log_data) return TrashResult.Failure if context.mode.should_we_ask_to_the_user(self.fs.is_accessible(path)): reply = self.user.ask_user_about_deleting_file(context.program_name, path) if reply == user_replied_no: return TrashResult.Success return self.file_trasher.trash_file(path, context) trash-cli-0.24.5.26/trashcli/put/user.py000066400000000000000000000014101462460053300176660ustar00rootroot00000000000000from trashcli.lib.my_input import Input from trashcli.put.describer import Describer class User: def __init__(self, my_input, # type: Input describer, # type: Describer ): self.input = my_input self.describer = describer def ask_user_about_deleting_file(self, program_name, path): reply = self.input.read_input( "%s: trash %s '%s'? " % (program_name, self.describer.describe(path), path)) return parse_user_reply(reply) user_replied_no = "user_replied_no" user_replied_yes = "user_replied_yes" def parse_user_reply(reply): return {False: user_replied_no, True: user_replied_yes}[reply.lower().startswith("y")] trash-cli-0.24.5.26/trashcli/restore/000077500000000000000000000000001462460053300172155ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/restore/__init__.py000066400000000000000000000000001462460053300213140ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/restore/args.py000066400000000000000000000006771462460053300205350ustar00rootroot00000000000000from enum import Enum from typing import NamedTuple, Optional from trashcli.lib.enum_repr import repr_for_enum class Sort(Enum): ByDate = "ByDate" ByPath = "ByPath" DoNot = "DoNot" def __repr__(self): return repr_for_enum(self) class RunRestoreArgs( NamedTuple('RunRestoreArgs', [ ('path', str), ('sort', Sort), ('trash_dir', Optional[str]), ('overwrite', bool), ])): pass trash-cli-0.24.5.26/trashcli/restore/file_system.py000066400000000000000000000043121462460053300221120ustar00rootroot00000000000000import os from abc import ABCMeta, abstractmethod from trashcli.compat import Protocol import six from trashcli import fs from trashcli.fs import FsMethods, RealListFilesInDir, ListFilesInDir, \ RealContentsOf class FileReader(Protocol): @abstractmethod def contents_of(self, path): raise NotImplementedError() class RealFileReader(RealContentsOf, FileReader): pass class FakeFileReader(FileReader): def __init__(self, contents=None): self.contents = contents def set_content(self, contents): self.contents = contents def contents_of(self, path): return self.contents @six.add_metaclass(ABCMeta) class RestoreReadFileSystem: @abstractmethod def path_exists(self, path): # type: (str) -> bool raise NotImplementedError() class RealRestoreReadFileSystem(RestoreReadFileSystem): def path_exists(self, path): return os.path.exists(path) @six.add_metaclass(ABCMeta) class RestoreWriteFileSystem: @abstractmethod def mkdirs(self, path): # type: (str) -> None raise NotImplementedError() @abstractmethod def move(self, path, dest): # type: (str, str) -> None raise NotImplementedError() @abstractmethod def remove_file(self, path): # type: (str) -> None raise NotImplementedError() class RealRestoreWriteFileSystem(RestoreWriteFileSystem): def mkdirs(self, path): return fs.mkdirs(path) def move(self, path, dest): return fs.move(path, dest) def remove_file(self, path): return fs.remove_file(path) @six.add_metaclass(ABCMeta) class ReadCwd: @abstractmethod def getcwd_as_realpath(self): # type: () -> str raise NotImplementedError() class RealReadCwd(ReadCwd): def getcwd_as_realpath(self): return os.path.realpath(os.curdir) class FakeReadCwd(ReadCwd): def __init__(self, default_cur_dir=None): self.default_cur_dir = default_cur_dir def chdir(self, path): self.default_cur_dir = path def getcwd_as_realpath(self): return self.default_cur_dir class ListingFileSystem(ListFilesInDir, Protocol): pass class RealListingFileSystem(ListingFileSystem, RealListFilesInDir): pass trash-cli-0.24.5.26/trashcli/restore/handler.py000066400000000000000000000042131462460053300212040ustar00rootroot00000000000000from __future__ import print_function from __future__ import unicode_literals from typing import List from trashcli.lib.my_input import Input from trashcli.restore.file_system import ReadCwd from trashcli.restore.output import Output from trashcli.restore.output_recorder import OutputRecorder from trashcli.restore.restore_asking_the_user import RestoreAskingTheUser from trashcli.restore.restorer import Restorer from trashcli.restore.run_restore_action import Handler from trashcli.restore.trashed_file import TrashedFile class HandlerImpl(Handler): def __init__(self, input, # type: Input cwd, # type: ReadCwd restorer, # type: Restorer output, # type: Output ): self.input = input self.cwd = cwd self.restorer = restorer self.output = output def handle_trashed_files(self, trashed_files, # type: List[TrashedFile] overwrite, # type: bool ): if not trashed_files: self.report_no_files_found(self.cwd.getcwd_as_realpath()) else: for i, trashed_file in enumerate(trashed_files): self.output.println("%4d %s %s" % (i, trashed_file.deletion_date, trashed_file.original_location)) self.restore_asking_the_user(trashed_files, overwrite) def restore_asking_the_user(self, trashed_files, overwrite=False): my_output = OutputRecorder() restore_asking_the_user = RestoreAskingTheUser(self.input, self.restorer, my_output) restore_asking_the_user.restore_asking_the_user(trashed_files, overwrite) my_output.apply_to(self.output) def report_no_files_found(self, directory): # type: (str) -> None self.output.println( "No files trashed from current dir ('%s')" % directory) trash-cli-0.24.5.26/trashcli/restore/index.py000066400000000000000000000002171462460053300206760ustar00rootroot00000000000000from typing import Union from trashcli.restore.range import Range from trashcli.restore.single import Single Sequence = Union[Range, Single] trash-cli-0.24.5.26/trashcli/restore/info_dir_searcher.py000066400000000000000000000017121462460053300232350ustar00rootroot00000000000000from typing import NamedTuple, Iterable, Optional from trashcli.restore.info_files import InfoFiles from trashcli.restore.trash_directories import TrashDirectories class InfoDirSearcher: def __init__(self, trash_directories, # type: TrashDirectories info_files, # type: InfoFiles ): self.trash_directories = trash_directories self.info_files = info_files def all_file_in_info_dir(self, trash_dir_from_cli, # type: Optional[str] ): # type: (...) -> Iterable[FileFound] for trash_dir_path, volume in self.trash_directories.list_trash_dirs( trash_dir_from_cli): for type, path in self.info_files.all_info_files(trash_dir_path): yield FileFound(type, path, volume) class FileFound(NamedTuple('Info', [ ('type', 'str'), ('path', 'str'), ('volume', 'str'), ])): pass trash-cli-0.24.5.26/trashcli/restore/info_files.py000066400000000000000000000012601462460053300217030ustar00rootroot00000000000000import os from trashcli.restore.file_system import ListingFileSystem class InfoFiles: def __init__(self, fs, # type: ListingFileSystem ): self.fs = fs def all_info_files(self, path): norm_path = os.path.normpath(path) info_dir = os.path.join(norm_path, 'info') try: for info_file in self.fs.list_files_in_dir(info_dir): if not os.path.basename(info_file).endswith('.trashinfo'): yield ('non_trashinfo', info_file) else: yield ('trashinfo', info_file) except OSError: # when directory does not exist pass trash-cli-0.24.5.26/trashcli/restore/main.py000066400000000000000000000026471462460053300205240ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy import os import sys import trashcli.trash from .file_system import RealRestoreReadFileSystem, \ RealRestoreWriteFileSystem, RealReadCwd, RealFileReader, \ RealListingFileSystem from .info_dir_searcher import InfoDirSearcher from .info_files import InfoFiles from .real_restore_logger import RealRestoreLogger from .restore_cmd import RestoreCmd from .trash_directories import TrashDirectoriesImpl from .trashed_files import TrashedFiles from ..fstab.volumes import RealVolumes from ..lib.logger import my_logger from ..lib.my_input import RealInput def main(): info_files = InfoFiles(RealListingFileSystem()) volumes = RealVolumes() trash_directories = TrashDirectoriesImpl(volumes, os.getuid(), os.environ) searcher = InfoDirSearcher(trash_directories, info_files) trashed_files = TrashedFiles(RealRestoreLogger(my_logger), RealFileReader(), searcher) RestoreCmd.make( stdout=sys.stdout, stderr=sys.stderr, exit=sys.exit, input=RealInput(), version=trashcli.trash.version, trashed_files=trashed_files, read_fs=RealRestoreReadFileSystem(), write_fs=RealRestoreWriteFileSystem(), read_cwd=RealReadCwd() ).run(sys.argv) trash-cli-0.24.5.26/trashcli/restore/output.py000066400000000000000000000010361462460053300211270ustar00rootroot00000000000000from abc import ABCMeta, abstractmethod import six from trashcli.restore.output_event import OutputEvent @six.add_metaclass(ABCMeta) class Output: @abstractmethod def quit(self): raise NotImplementedError @abstractmethod def die(self, msg): raise NotImplementedError @abstractmethod def println(self, msg): raise NotImplementedError @abstractmethod def append_event(self, event, # type: OutputEvent ): raise NotImplementedError trash-cli-0.24.5.26/trashcli/restore/output_event.py000066400000000000000000000004171462460053300223320ustar00rootroot00000000000000from typing import NamedTuple, Union Quit = NamedTuple('Quit', []) Die = NamedTuple('Die', [('msg', Union[str, Exception])]) Println = NamedTuple('Println', [('msg', str)]) Exiting = NamedTuple('Exiting', [('msg', str)]) OutputEvent = Union[Quit, Die, Println, Exiting] trash-cli-0.24.5.26/trashcli/restore/output_recorder.py000066400000000000000000000015171462460053300230200ustar00rootroot00000000000000from typing import List from trashcli.restore.output import Output from trashcli.restore.output_event import Quit, Die, Println, OutputEvent class OutputRecorder(Output): def __init__(self): # type: (...) -> None self.events = [] # type: List[OutputEvent] def quit(self): # type: () -> None self.append_event(Quit()) def die(self, msg): # type: (str) -> None self.append_event(Die(msg)) def println(self, msg): # type: (str) -> None self.append_event(Println(msg)) def append_event(self, event, # type: OutputEvent ): # type: (...) -> None self.events.append(event) def apply_to(self, output, # type: Output ): # type: (...) -> None for event in self.events: output.append_event(event) trash-cli-0.24.5.26/trashcli/restore/range.py000066400000000000000000000010221462460053300206560ustar00rootroot00000000000000from six.moves import range class Range: def __init__(self, start, stop): self.start = start self.stop = stop def __eq__(self, other): if type(other) != type(self): return False if self.start != other.start: return False if self.stop != other.stop: return False return True def __iter__(self): return iter(range(self.start, self.stop + 1)) def __repr__(self): return "Range(%s, %s)" % (self.start, self.stop) trash-cli-0.24.5.26/trashcli/restore/real_output.py000066400000000000000000000020651462460053300221350ustar00rootroot00000000000000from __future__ import print_function import six from trashcli.restore.output import Output from trashcli.restore.output_event import Println, Die, Quit, Exiting, \ OutputEvent class RealOutput(Output): def __init__(self, stdout, stderr, exit): self.stdout = stdout self.stderr = stderr self.exit = exit def quit(self): self.die('') def printerr(self, msg): print(six.text_type(msg), file=self.stderr) def println(self, line): print(six.text_type(line), file=self.stdout) def die(self, error): self.printerr(error) self.exit(1) def append_event(self, event, # type: OutputEvent ): if isinstance(event, Println): self.println(event.msg) elif isinstance(event, Die): self.die(event.msg) elif isinstance(event, Quit): self.quit() elif isinstance(event, Exiting): self.println(event.msg) else: raise Exception("Unknown call %s" % event) trash-cli-0.24.5.26/trashcli/restore/real_restore_logger.py000066400000000000000000000004711462460053300236160ustar00rootroot00000000000000from logging import Logger from trashcli.restore.restore_logger import RestoreLogger class RealRestoreLogger(RestoreLogger): def __init__(self, logger, # type: Logger ): self._logger = logger def warning(self, message): self._logger.warning(message) trash-cli-0.24.5.26/trashcli/restore/restore_arg_parser.py000066400000000000000000000050431462460053300234610ustar00rootroot00000000000000import os from typing import Union, List, cast from trashcli.lib.print_version import PrintVersionArgs from trashcli.restore.args import RunRestoreArgs, Sort from trashcli.shell_completion import add_argument_to, TRASH_FILES, TRASH_DIRS, \ complete_with Command = Union[PrintVersionArgs, RunRestoreArgs] class RestoreArgParser: def parse_restore_args(self, sys_argv, # type: List[str] curdir, # type: str ): # type: (...) -> Command import argparse parser = argparse.ArgumentParser( description='Restores from trash chosen file', formatter_class=argparse.ArgumentDefaultsHelpFormatter) add_argument_to(parser) complete_with( TRASH_FILES, parser.add_argument('path', default="", nargs='?', help='Restore files from given path instead of current ' 'directory' ) ) parser.add_argument('--sort', choices=['date', 'path', 'none'], default='date', help='Sort list of restore candidates by given field') complete_with( TRASH_DIRS, parser.add_argument('--trash-dir', action='store', dest='trash_dir', help=argparse.SUPPRESS )) parser.add_argument('--version', action='store_true', default=False) parser.add_argument('--overwrite', action='store_true', default=False, help='Overwrite existing files with files coming out of the trash') parsed = parser.parse_args(sys_argv[1:]) if parsed.version: return PrintVersionArgs(argv0=sys_argv[0]) else: path = os.path.normpath( os.path.join(curdir + os.path.sep, parsed.path)) return RunRestoreArgs(path=path, sort=cast(Sort, { 'path': Sort.ByPath, 'date': Sort.ByDate, 'none': Sort.DoNot }[parsed.sort]), trash_dir=parsed.trash_dir, overwrite=parsed.overwrite) trash-cli-0.24.5.26/trashcli/restore/restore_asking_the_user.py000066400000000000000000000126301462460053300245060ustar00rootroot00000000000000from typing import TypeVar, Generic, List, NamedTuple, Callable from six.moves import range from trashcli.lib.my_input import Input from trashcli.restore.index import Sequence from trashcli.restore.output import Output from trashcli.restore.output_event import Die, OutputEvent, Quit from trashcli.restore.output_event import Exiting from trashcli.restore.range import Range from trashcli.restore.sequences import Sequences from trashcli.restore.single import Single from trashcli.restore.trashed_file import TrashedFile SelectedFiles = NamedTuple('SelectedFiles', [ ('files_to_restore', List[TrashedFile]), ('overwrite', bool), ]) Context = NamedTuple('Context', [ ('trashed_files', List[TrashedFile]), ('overwrite', bool), ]) InputRead = NamedTuple('InputRead', [ ('user_input', str), ('trashed_files', List[TrashedFile]), ('overwrite', bool), ]) class RestoreAskingTheUser(object): def __init__(self, input, # type: Input restorer, output, # type: Output ): self.input = input self.restorer = restorer self.output = output def read_user_input(self, args, # type: Context ): # type: (...) -> Either[OutputEvent, InputRead] try: user_input = self.input.read_input( "What file to restore [0..%d]: " % ( len(args.trashed_files) - 1)) except KeyboardInterrupt: return Left(Quit()) except EOFError: return Left(Quit()) else: if user_input == "": return Left(Exiting("No files were restored")) else: return Right( InputRead(user_input, args.trashed_files, args.overwrite)) def restore_asking_the_user(self, trashed_files, overwrite): input = Right(Context(trashed_files, overwrite)) compose(input, [ self.read_user_input, trashed_files_to_restore, self.restore_selected_files, ]).on_error( lambda error: self.output.append_event(error)) def restore_selected_files(self, selected_files, # type: SelectedFiles ): # type: (...) -> Either[Die, None] try: for trashed_file in selected_files.files_to_restore: self.restorer.restore_trashed_file(trashed_file, selected_files.overwrite) return Right(None) except IOError as e: return Left(Die(e)) Error = TypeVar('Error') Value = TypeVar('Value') def compose(input, funcs): for f in funcs: input = input.apply(f) return input class Either(Generic[Error, Value]): def apply(self, f): # type: (Callable[[Value], Either[Error, OutputValue]]) -> Either[Error, OutputValue] raise NotImplementedError() class Left(Either, Generic[Error, Value]): def __init__(self, error, # type: Error ): self.error = error def apply(self, f): # type: (Callable[[Value], Either[Error, OutputValue]]) -> Either[Error, OutputValue] return self def on_error(self, f): return f(self.error) OutputValue = TypeVar('OutputValue') class Right(Either[Error, Value]): def __init__(self, value, # type: Value ): self.value = value def apply(self, f): # type: (Callable[[Value], Either[Error, OutputValue]]) -> Either[Error, OutputValue] return f(self.value) def on_error(self, f): return self def trashed_files_to_restore(input_read, # type: InputRead ): # type: (...) -> Either[Die, SelectedFiles] try: sequences = parse_indexes(input_read.user_input, len(input_read.trashed_files)) file_to_restore = [input_read.trashed_files[index] for index in sequences.all_indexes()] selected_files = SelectedFiles(file_to_restore, input_read.overwrite) return Right(selected_files) except InvalidEntry as e: return Left(Die("Invalid entry: %s" % e)) class InvalidEntry(Exception): pass def parse_indexes(user_input, # type: str len_trashed_files, # type: int ): # type: (...) -> Sequences indexes = user_input.split(',') sequences = [] # type: List[Sequence] for index in indexes: if "-" in index: first, last = index.split("-", 2) if first == "" or last == "": raise InvalidEntry("open interval: %s" % index) split = list(map(parse_int_index, (first, last))) sequences.append(Range(split[0], split[1])) else: int_index = parse_int_index(index) sequences.append(Single(int_index)) result = Sequences(sequences) acceptable_values = range(0, len_trashed_files) for index in result.all_indexes(): if not index in acceptable_values: raise InvalidEntry( "out of range %s..%s: %s" % (acceptable_values[0], acceptable_values[-1], index)) return result def parse_int_index(text): # type: (str) -> int try: return int(text) except ValueError: raise InvalidEntry("not an index: %s" % text) trash-cli-0.24.5.26/trashcli/restore/restore_cmd.py000066400000000000000000000045211462460053300220770ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from typing import TextIO, Callable from trashcli.lib.my_input import Input from trashcli.lib.print_version import PrintVersionAction, PrintVersionArgs from trashcli.restore.args import RunRestoreArgs from trashcli.restore.file_system import RestoreReadFileSystem, \ RestoreWriteFileSystem, ReadCwd from trashcli.restore.handler import HandlerImpl from trashcli.restore.real_output import RealOutput from trashcli.restore.restore_arg_parser import RestoreArgParser from trashcli.restore.restorer import Restorer from trashcli.restore.run_restore_action import RunRestoreAction, Handler from trashcli.restore.trashed_files import TrashedFiles class RestoreCmd(object): @staticmethod def make(stdout, # type: TextIO stderr, # type: TextIO exit, # type: Callable[[int], None] input, # type: Input version, # type: str trashed_files, # type: TrashedFiles read_fs, # type: RestoreReadFileSystem write_fs, # type: RestoreWriteFileSystem read_cwd, # type: ReadCwd ): # type: (...) -> RestoreCmd restorer = Restorer(read_fs, write_fs) output = RealOutput(stdout, stderr, exit) handler = HandlerImpl(input, read_cwd, restorer, output) return RestoreCmd(stdout, version, trashed_files, read_cwd, handler) def __init__(self, stdout, # type: TextIO version, # type: str trashed_files, # type: TrashedFiles read_cwd, # type: ReadCwd handler, # type: Handler ): self.read_cwd = read_cwd self.parser = RestoreArgParser() self.run_restore_action = RunRestoreAction(handler, trashed_files) self.print_version_action = PrintVersionAction(stdout, version) def run(self, argv): args = self.parser.parse_restore_args(argv, self.read_cwd.getcwd_as_realpath()) if isinstance(args, RunRestoreArgs): self.run_restore_action.run_action(args) elif isinstance(args, PrintVersionArgs): self.print_version_action.run_action(args) trash-cli-0.24.5.26/trashcli/restore/restore_logger.py000066400000000000000000000002101462460053300226020ustar00rootroot00000000000000from trashcli.compat import Protocol class RestoreLogger(Protocol): def warning(self, message): raise NotImplementedError trash-cli-0.24.5.26/trashcli/restore/restorer.py000066400000000000000000000023021462460053300214310ustar00rootroot00000000000000import os from trashcli.restore.file_system import RestoreWriteFileSystem, \ RestoreReadFileSystem from trashcli.restore.trashed_file import TrashedFile class Restorer: def __init__(self, read_fs, # type: RestoreReadFileSystem write_fs, # type: RestoreWriteFileSystem ): self.read_fs = read_fs self.write_fs = write_fs def restore_trashed_file(self, trashed_file, # type: TrashedFile overwrite, # type: bool ): """ If overwrite is enabled, then the restore functionality will overwrite an existing file """ if not overwrite and self.read_fs.path_exists(trashed_file.original_location): raise IOError( 'Refusing to overwrite existing file "%s".' % os.path.basename( trashed_file.original_location)) else: parent = os.path.dirname(trashed_file.original_location) self.write_fs.mkdirs(parent) self.write_fs.move(trashed_file.original_file, trashed_file.original_location) self.write_fs.remove_file(trashed_file.info_file) trash-cli-0.24.5.26/trashcli/restore/run_restore_action.py000066400000000000000000000036541462460053300235030ustar00rootroot00000000000000import os from abc import ABCMeta, abstractmethod import six from typing import Optional, Iterable from trashcli.restore.args import RunRestoreArgs from trashcli.restore.sort_method import sort_files from trashcli.restore.trashed_file import TrashedFile from trashcli.restore.trashed_files import TrashedFiles class RunRestoreAction: def __init__(self, handler, # type: 'Handler' trashed_files, # type: TrashedFiles ): self.handler = handler self.trashed_files = trashed_files def run_action(self, args, # type: RunRestoreArgs ): # type: (...) -> None trashed_files = self.all_files_trashed_from_path(args.path, args.trash_dir) trashed_files = sort_files(args.sort, trashed_files) self.handler.handle_trashed_files(trashed_files, args.overwrite) def all_files_trashed_from_path(self, path, # type: str trash_dir_from_cli, # type: Optional[str] ): # type: (...) -> Iterable[TrashedFile] for trashed_file in self.trashed_files.all_trashed_files( trash_dir_from_cli): if trashed_file.original_location_matches_path(path): yield trashed_file @six.add_metaclass(ABCMeta) class Handler: @abstractmethod def handle_trashed_files(self, trashed_files, overwrite, # type: bool ): raise NotImplementedError() def original_location_matches_path(trashed_file_original_location, path): if path == os.path.sep: return True if trashed_file_original_location.startswith(path + os.path.sep): return True return trashed_file_original_location == path trash-cli-0.24.5.26/trashcli/restore/sequences.py000066400000000000000000000004471462460053300215670ustar00rootroot00000000000000from typing import NamedTuple, List from trashcli.restore.index import Sequence class Sequences(NamedTuple('Sequences', [ ('sequences', List[Sequence]), ])): def all_indexes(self): for sequence in self.sequences: for index in sequence: yield index trash-cli-0.24.5.26/trashcli/restore/single.py000066400000000000000000000001461462460053300210510ustar00rootroot00000000000000from typing import NamedTuple class Single(NamedTuple('Single', [ ('index', int), ])): pass trash-cli-0.24.5.26/trashcli/restore/sort_method.py000066400000000000000000000027661462460053300221310ustar00rootroot00000000000000from abc import abstractmethod from typing import Callable, Any, Iterable from trashcli.compat import Protocol from trashcli.restore.args import Sort from trashcli.restore.trashed_file import TrashedFile def sort_files(sort, # type: Sort trashed_files, # type: Iterable[TrashedFile] ): # type: (...) -> Iterable[TrashedFile] return sorter_for(sort).sort_files(trashed_files) class Sorter(Protocol): @abstractmethod def sort_files(self, trashed_files, # type: Iterable[TrashedFile] ): # type: (...) -> Iterable[TrashedFile] raise NotImplementedError() class NoSorter(Sorter): def sort_files(self, trashed_files, # type: Iterable[TrashedFile] ): # type: (...) -> Iterable[TrashedFile] return trashed_files class SortFunction(Sorter): def __init__(self, sort_func): # type: (Callable[[TrashedFile], Any]) -> None self.sort_func = sort_func def sort_files(self, trashed_files, # type: Iterable[TrashedFile] ): # type: (...) -> Iterable[TrashedFile] return sorted(trashed_files, key=self.sort_func) def sorter_for(sort, # type: Sort ): # type (...) -> Sorter path_ranking = lambda x: x.original_location + str(x.deletion_date) date_rankking = lambda x: x.deletion_date return { Sort.ByPath: SortFunction(path_ranking), Sort.ByDate: SortFunction(date_rankking), Sort.DoNot: NoSorter, }[sort] trash-cli-0.24.5.26/trashcli/restore/trash_directories.py000066400000000000000000000047161462460053300233140ustar00rootroot00000000000000# Copyright (C) 2007-2023 Andrea Francia Trivolzio(PV) Italy from abc import abstractmethod, ABCMeta import six from typing import Optional from trashcli.fstab.volume_of import VolumeOf from trashcli.fstab.volumes import Volumes from trashcli.lib.environ import Environ from trashcli.lib.trash_dirs import ( volume_trash_dir1, volume_trash_dir2, home_trash_dir) @six.add_metaclass(ABCMeta) class TrashDirectories: @abstractmethod def list_trash_dirs(self, trash_dir_from_cli): raise NotImplementedError() class TrashDirectoriesImpl(TrashDirectories): def __init__(self, volumes, # type: Volumes uid, # type: int environ, ): trash_directories1 = TrashDirectories1(volumes, uid, environ) self.trash_directories2 = TrashDirectories2(volumes, trash_directories1) def list_trash_dirs(self, trash_dir_from_cli, # type: Optional[str] ): return self.trash_directories2.trash_directories_or_user( trash_dir_from_cli) class TrashDirectories2: def __init__(self, volume_of, # type: VolumeOf trash_directories, # type: TrashDirectories1 ): self.volume_of = volume_of self.trash_directories = trash_directories def trash_directories_or_user(self, trash_dir_from_cli, # type: Optional[str] ): if trash_dir_from_cli: return [(trash_dir_from_cli, self.volume_of.volume_of(trash_dir_from_cli))] return self.trash_directories.all_trash_directories() class TrashDirectories1: def __init__(self, volumes, # type: Volumes uid, # type: int environ, # type: Environ ): self.volumes = volumes self.uid = uid self.environ = environ def all_trash_directories(self): volumes_to_check = self.volumes.list_mount_points() for path1, volume1 in home_trash_dir(self.environ, self.volumes): yield path1, volume1 for volume in volumes_to_check: for path1, volume1 in volume_trash_dir1(volume, self.uid): yield path1, volume1 for path1, volume1 in volume_trash_dir2(volume, self.uid): yield path1, volume1 trash-cli-0.24.5.26/trashcli/restore/trashed_file.py000066400000000000000000000021041462460053300222150ustar00rootroot00000000000000import datetime import os from typing import NamedTuple, Optional class TrashedFile( NamedTuple('TrashedFile', [ ('original_location', str), ('deletion_date', Optional[datetime.datetime]), ('info_file', str), ('original_file', str), ])): """ Represent a trashed file. Each trashed file is persisted in two files: - $trash_dir/info/$id.trashinfo - $trash_dir/files/$id Properties: - path : the original path from where the file has been trashed - deletion_date : the time when the file has been trashed (instance of datetime) - info_file : the file that contains information (instance of Path) - original_file : the path where the trashed file has been placed after the trash operation (instance of Path) """ def original_location_matches_path(self, path): if path == os.path.sep: return True if self.original_location.startswith(path + os.path.sep): return True return self.original_location == path trash-cli-0.24.5.26/trashcli/restore/trashed_files.py000066400000000000000000000074011462460053300224050ustar00rootroot00000000000000from typing import Iterable from typing import NamedTuple from typing import Optional from typing import Union from trashcli.lib.path_of_backup_copy import path_of_backup_copy from trashcli.parse_trashinfo.parse_deletion_date import parse_deletion_date from trashcli.parse_trashinfo.parse_original_location import \ parse_original_location from trashcli.restore.file_system import FileReader from trashcli.restore.info_dir_searcher import InfoDirSearcher from trashcli.restore.restore_logger import RestoreLogger from trashcli.restore.trashed_file import TrashedFile class TrashedFiles: def __init__(self, logger, # type: RestoreLogger file_reader, # type: FileReader searcher, # type: InfoDirSearcher ): self.logger = logger self.file_reader = file_reader self.searcher = searcher def all_trashed_files(self, trash_dir_from_cli, # type: Optional[str] ): # type: (...) -> Iterable[TrashedFile] for event in self.all_trashed_files_internal(trash_dir_from_cli): if type(event) is NonTrashinfoFileFound: self.logger.warning("Non .trashinfo file in info dir") elif type(event) is NonParsableTrashInfo: self.logger.warning( "Non parsable trashinfo file: %s, because %s" % (event.path, event.reason)) elif type(event) is IOErrorReadingTrashInfo: self.logger.warning(str(event)) elif type(event) is TrashedFileFound: yield event.trashed_file else: raise RuntimeError() def all_trashed_files_internal(self, trash_dir_from_cli, # type: Optional[str] ): # type: (...) -> Iterable[Event] for info_file in self.searcher.all_file_in_info_dir(trash_dir_from_cli): if info_file.type == 'non_trashinfo': yield NonTrashinfoFileFound(info_file.path) elif info_file.type == 'trashinfo': try: contents = self.file_reader.contents_of(info_file.path) original_location = parse_original_location(contents, info_file.volume) deletion_date = parse_deletion_date(contents) backup_file_path = path_of_backup_copy(info_file.path) trashedfile = TrashedFile(original_location, deletion_date, info_file.path, backup_file_path) yield TrashedFileFound(trashedfile) except ValueError as e: yield NonParsableTrashInfo(info_file.path, e) except IOError as e: yield IOErrorReadingTrashInfo(info_file.path, str(e)) else: raise RuntimeError("Unexpected file type: %s: %s", info_file.type, info_file.path) class NonTrashinfoFileFound( NamedTuple('NonTrashinfoFileFound', [ ('path', str), ])): pass class TrashedFileFound( NamedTuple('TrashedFileFound', [ ('trashed_file', TrashedFile), ])): pass class NonParsableTrashInfo( NamedTuple('NonParsableTrashInfo', [ ('path', str), ('reason', Exception), ])): pass class IOErrorReadingTrashInfo( NamedTuple('IOErrorReadingTrashInfo', [ ('path', str), ('error', str), ])): pass Event = Union[ NonTrashinfoFileFound, TrashedFileFound, NonParsableTrashInfo, IOErrorReadingTrashInfo] trash-cli-0.24.5.26/trashcli/rm/000077500000000000000000000000001462460053300161505ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/rm/__init__.py000066400000000000000000000000001462460053300202470ustar00rootroot00000000000000trash-cli-0.24.5.26/trashcli/rm/cleanable_trashcan.py000066400000000000000000000011101462460053300223040ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy from trashcli.rm.file_remover import FileRemover from trashcli.lib.path_of_backup_copy import path_of_backup_copy class CleanableTrashcan: def __init__(self, file_remover, # type: FileRemover ): self._file_remover = file_remover def delete_trash_info_and_backup_copy(self, trash_info_path): backup_copy = path_of_backup_copy(trash_info_path) self._file_remover.remove_file_if_exists(backup_copy) self._file_remover.remove_file2(trash_info_path) trash-cli-0.24.5.26/trashcli/rm/file_remover.py000066400000000000000000000002411462460053300211750ustar00rootroot00000000000000from trashcli.fs import FsMethods class FileRemover: remove_file2 = FsMethods().remove_file2 remove_file_if_exists = FsMethods().remove_file_if_exists trash-cli-0.24.5.26/trashcli/rm/filter.py000066400000000000000000000006171462460053300200130ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy import fnmatch import os class Filter: def __init__(self, pattern): self.pattern = pattern def matches(self, original_location): basename = os.path.basename(original_location) subject = original_location if self.pattern[0] == '/' else basename return fnmatch.fnmatchcase(subject, self.pattern) trash-cli-0.24.5.26/trashcli/rm/list_trashinfo.py000066400000000000000000000031141462460053300215510ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy import os from abc import abstractmethod from trashcli.compat import Protocol from trashcli.fs import ContentsOf from trashcli.lib.dir_reader import DirReader from trashcli.lib.trash_dir_reader import TrashDirReader from trashcli.parse_trashinfo.parse_path import parse_path from trashcli.parse_trashinfo.parser_error import ParseError class FileContentReader(Protocol): @abstractmethod def contents_of(self, path): raise NotImplementedError() class ListTrashinfos: def __init__(self, file_content_reader, # type: ContentsOf trash_dir_reader, # type: TrashDirReader ): self.trash_dir_reader = trash_dir_reader self.file_content_reader = file_content_reader def list_from_volume_trashdir(self, trashdir_path, volume): for trashinfo_path in self.trash_dir_reader.list_trashinfo( trashdir_path): trashinfo = self.file_content_reader.contents_of(trashinfo_path) try: path = parse_path(trashinfo) except ParseError: yield 'unable_to_parse_path', trashinfo_path else: complete_path = os.path.join(volume, path) yield 'trashed_file', (complete_path, trashinfo_path) @staticmethod def make(file_content_reader, # type: ContentsOf dir_reader, # type: DirReader ): trash_dir_reader = TrashDirReader(dir_reader) return ListTrashinfos(file_content_reader, trash_dir_reader) trash-cli-0.24.5.26/trashcli/rm/main.py000066400000000000000000000017031462460053300174470ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy import os import sys from trashcli.fs import RealExists, RealIsStickyDir, RealIsSymLink, \ RealContentsOf, RealEntriesIfDirExists from trashcli.fstab.volume_listing import RealVolumesListing from trashcli.rm.rm_cmd import RmCmd, RmFileSystemReader def main(): volumes_listing = RealVolumesListing() cmd = RmCmd(environ=os.environ, getuid=os.getuid, volumes_listing=volumes_listing, stderr=sys.stderr, file_reader=RealRmFileSystemReader()) cmd.run(sys.argv, os.getuid()) return cmd.exit_code class RealRmFileSystemReader(RmFileSystemReader, RealExists, RealIsStickyDir, RealIsSymLink, RealContentsOf, RealEntriesIfDirExists, ): pass trash-cli-0.24.5.26/trashcli/rm/rm_cmd.py000066400000000000000000000057721462460053300177760ustar00rootroot00000000000000# Copyright (C) 2011-2021 Andrea Francia Bereguardo(PV) Italy from trashcli.compat import Protocol from trashcli.fs import ContentsOf from trashcli.lib.dir_checker import DirChecker from trashcli.lib.dir_reader import DirReader from trashcli.lib.user_info import SingleUserInfoProvider from trashcli.rm.cleanable_trashcan import CleanableTrashcan from trashcli.rm.file_remover import FileRemover from trashcli.rm.filter import Filter from trashcli.rm.list_trashinfo import ListTrashinfos from trashcli.trash_dirs_scanner import TrashDirsScanner, TopTrashDirRules, \ trash_dir_found class RmFileSystemReader(ContentsOf, DirReader, TopTrashDirRules.Reader, Protocol): pass class RmCmd: def __init__(self, environ, getuid, volumes_listing, stderr, file_reader, # type: RmFileSystemReader ): self.environ = environ self.getuid = getuid self.volumes_listing = volumes_listing self.stderr = stderr self.file_reader = file_reader def run(self, argv, uid): args = argv[1:] self.exit_code = 0 if not args: self.print_err('Usage:\n' ' trash-rm PATTERN\n' '\n' 'Please specify PATTERN.\n' 'trash-rm uses fnmatch.fnmatchcase to match patterns, see https://docs.python.org/3/library/fnmatch.html for more details.') self.exit_code = 8 return trashcan = CleanableTrashcan(FileRemover()) cmd = Filter(args[0]) listing = ListTrashinfos.make(self.file_reader, self.file_reader) user_info_provider = SingleUserInfoProvider() scanner = TrashDirsScanner(user_info_provider, self.volumes_listing, TopTrashDirRules(self.file_reader), DirChecker()) for event, args in scanner.scan_trash_dirs(self.environ, uid): if event == trash_dir_found: path, volume = args for type, arg in listing.list_from_volume_trashdir(path, volume): if type == 'unable_to_parse_path': self.unable_to_parse_path(arg) elif type == 'trashed_file': original_location, info_file = arg if cmd.matches(original_location): trashcan.delete_trash_info_and_backup_copy( info_file) def unable_to_parse_path(self, trashinfo): self.report_error('{}: unable to parse \'Path\''.format(trashinfo)) def report_error(self, error_msg): self.print_err('trash-rm: {}'.format(error_msg)) def print_err(self, msg): self.stderr.write(msg + '\n') trash-cli-0.24.5.26/trashcli/shell_completion.py000066400000000000000000000033571462460053300214540ustar00rootroot00000000000000import argparse from copy import copy from typing import Dict try: def convert_to_list(tuple): return [item for item in tuple] from shtab import add_argument_to, FILE, DIR # type: ignore defaults = convert_to_list(add_argument_to.__defaults__) defaults[-1] = { "zsh": r""" # https://github.com/zsh-users/zsh/blob/19390a1ba8dc983b0a1379058e90cd51ce156815/Completion/Unix/Command/_rm#L72-L74 _trash_files() { (( CURRENT > 0 )) && line[CURRENT]=() line=( ${line//(#m)[\[\]()\\*?#<>~\^\|]/\\$MATCH} ) _files -F line } """, } add_argument_to.__defaults__ = tuple(defaults) TRASH_FILES = copy(FILE) TRASH_DIRS = copy(DIR) def complete_with(completion, # type: Dict[str, str] action, # type: argparse.Action ): action.complete = completion # type: ignore except ImportError: from argparse import Action TRASH_FILES = TRASH_DIRS = {} class PrintCompletionAction(Action): def __call__(self, parser, namespace, values, option_string=None): print('Please install shtab firstly!') parser.exit(0) def add_argument_to(parser, *args, **kwargs): # type: ignore Action.complete = None # type: ignore parser.add_argument( '--print-completion', choices=['bash', 'zsh', 'tcsh'], action=PrintCompletionAction, help='print shell completion script', ) return parser def complete_with(completion, # type: Dict[str, str] action, # type: argparse.Action ): pass TRASH_FILES.update({"zsh": "_trash_files"}) TRASH_DIRS.update({"zsh": "(\\${\\$(trash-list --trash-dirs)#parent_*})"}) trash-cli-0.24.5.26/trashcli/trash.py000066400000000000000000000001241462460053300172220ustar00rootroot00000000000000# Copyright (C) 2007-2011 Andrea Francia Trivolzio(PV) Italy version = '0.24.5.26' trash-cli-0.24.5.26/trashcli/trash_dirs_scanner.py000066400000000000000000000076461462460053300217740ustar00rootroot00000000000000import os from typing import Iterable from trashcli.compat import Protocol from trashcli.fs import PathExists, IsStickyDir, IsSymLink from trashcli.fstab.volume_listing import VolumesListing from trashcli.lib.dir_checker import DirChecker from trashcli.lib.user_info import UserInfoProvider class MyEnum(str): def __repr__(self): return str(self) trash_dir_found = MyEnum('trash_dir_found') trash_dir_skipped_because_parent_not_sticky = \ MyEnum('trash_dir_skipped_because_parent_not_sticky') trash_dir_skipped_because_parent_is_symlink = \ MyEnum('trash_dir_skipped_because_parent_is_symlink') top_trash_dir_does_not_exist = MyEnum('top_trash_dir_does_not_exist') top_trash_dir_invalid_because_not_sticky = \ MyEnum('top_trash_dir_invalid_because_not_sticky') top_trash_dir_invalid_because_parent_is_symlink = \ MyEnum('top_trash_dir_invalid_because_parent_is_symlink') top_trash_dir_valid = MyEnum('top_trash_dir_valid') class TrashDir(tuple): @property def path(self): return self[0] @property def volume(self): return self[1] def __new__(cls, path, volume): return tuple.__new__(TrashDir, (path, volume)) def __repr__(self): return 'TrashDir(%r, %r)' % (self.path, self.volume) class TopTrashDirRules: class Reader(PathExists, IsStickyDir, IsSymLink, Protocol): pass def __init__(self, reader): # type: (Reader) -> None self.reader = reader def valid_to_be_read(self, path): parent_trashdir = os.path.dirname(path) if not self.reader.exists(path): return top_trash_dir_does_not_exist if not self.reader.is_sticky_dir(parent_trashdir): return top_trash_dir_invalid_because_not_sticky if self.reader.is_symlink(parent_trashdir): return top_trash_dir_invalid_because_parent_is_symlink else: return top_trash_dir_valid class TrashDirsScanner: def __init__(self, user_info_provider, # type: UserInfoProvider volumes_listing, # type: VolumesListing top_trash_dir_rules, # type: TopTrashDirRules dir_checker, # type: DirChecker ): self.user_info_provider = user_info_provider self.volumes_listing = volumes_listing # type: VolumesListing self.top_trash_dir_rules = top_trash_dir_rules self.dir_checker = dir_checker def scan_trash_dirs(self, environ, uid): for user_info in self.user_info_provider.get_user_info(environ, uid): for path in user_info.home_trash_dir_paths: yield trash_dir_found, TrashDir(path, '/') for volume in self.volumes_listing.list_volumes(environ): top_trash_dir_path = os.path.join(volume, '.Trash', str(user_info.uid)) result = self.top_trash_dir_rules.valid_to_be_read( top_trash_dir_path) if result == top_trash_dir_valid: yield trash_dir_found, TrashDir(top_trash_dir_path, volume) elif result == top_trash_dir_invalid_because_not_sticky: yield trash_dir_skipped_because_parent_not_sticky, ( top_trash_dir_path,) elif result == top_trash_dir_invalid_because_parent_is_symlink: yield trash_dir_skipped_because_parent_is_symlink, ( top_trash_dir_path,) alt_top_trash_dir = os.path.join(volume, '.Trash-%s' % user_info.uid) if self.dir_checker.is_dir(alt_top_trash_dir): yield trash_dir_found, TrashDir(alt_top_trash_dir, volume) def only_found(events, # type: Iterable[TrashDir] ): # type: (...) -> Iterable[TrashDir] for event, args in events: if event == trash_dir_found: yield args