pax_global_header00006660000000000000000000000064147516673770014541gustar00rootroot0000000000000052 comment=b7252411df818ddad357e02b515fa59dcc91cfb9 apt-offline-1.8.6/000077500000000000000000000000001475166737700137615ustar00rootroot00000000000000apt-offline-1.8.6/.github/000077500000000000000000000000001475166737700153215ustar00rootroot00000000000000apt-offline-1.8.6/.github/FUNDING.yml000066400000000000000000000001051475166737700171320ustar00rootroot00000000000000# These are supported funding model platforms github: [rickysarraf] apt-offline-1.8.6/.github/workflows/000077500000000000000000000000001475166737700173565ustar00rootroot00000000000000apt-offline-1.8.6/.github/workflows/main.yml000066400000000000000000000026561475166737700210360ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the master branch push: branches: [ master ] pull_request: branches: [ master ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 # Enable additonal repositories - name: Enable source repositories run: | sudo sed -i 's/^#.deb/deb/g' /etc/apt/sources.list # Install pre-requisites - name: Install pre-reqs run: sudo apt-get update; sudo apt-get -y install pyqt5-dev-tools man2html-base python3-pysimplesoap # Runs a single command using the runners shell - name: Build tests run: make # Runs a set of commands using the runners shell - name: Linux runtime tests run: | echo Running tests from tests/ folder ./tests/apt-offline-tests-github.sh apt-offline-1.8.6/.gitignore000066400000000000000000000002141475166737700157460ustar00rootroot00000000000000*.pyc .settings/ /apt_offline_gui/Ui_*.py /apt_offline_gui/resources_rc.py # pixi environments .pixi *.egg-info # magic environments .magic apt-offline-1.8.6/.mailmap000066400000000000000000000007351475166737700154070ustar00rootroot00000000000000Ritesh Raj Sarraf None <> Ritesh Raj Sarraf root <> Ritesh Raj Sarraf rrs@freebsd <> Ritesh Raj Sarraf riteshsarraf <> Ritesh Raj Sarraf GitHub Ritesh Raj Sarraf root Ritesh Raj Sarraf Ritesh Raj Sarraf Abhishek Mishra Abhishek Mishra apt-offline-1.8.6/.project000066400000000000000000000005551475166737700154350ustar00rootroot00000000000000 apt-offline org.python.pydev.PyDevBuilder org.python.pydev.pythonNature apt-offline-1.8.6/.pydevproject000066400000000000000000000007071475166737700165040ustar00rootroot00000000000000 /${PROJECT_DIR_NAME} python interpreter Default apt-offline-1.8.6/.travis.yml000066400000000000000000000005121475166737700160700ustar00rootroot00000000000000--- language: python branches: - master before_install: - sudo apt-get update -qq - sudo apt-get install -y python-magic python-soappy debian-archive-keyring script: - ./apt-offline get tests/set-update.uris --threads 5 1>/dev/null - sudo ./tests/apt-offline-tests.sh 1>/dev/null apt-offline-1.8.6/.vscode/000077500000000000000000000000001475166737700153225ustar00rootroot00000000000000apt-offline-1.8.6/.vscode/launch.json000066400000000000000000000020231475166737700174640ustar00rootroot00000000000000{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "apt-offline set command", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "args": ["set", "/var/tmp/set.uris"] }, { "name": "apt-offline get command", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "args": ["get", "/var/tmp/set.uris"] }, { "name": "apt-offline install command", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "args": ["install", "/var/tmp/set.uris"] }, ] }apt-offline-1.8.6/INSTALL000066400000000000000000000011431475166737700150110ustar00rootroot00000000000000## Under Linux # Pre-requisites Please refer to requirements.txt for dependencies that need to be pre-installed for apt-offline. Note: If using a package from your distribution, manual installation of dependencies should not be required # To build make build # To Install (You might need to be root) make install ## Under Microsoft Windows # Execute the file using the Python interpreter # Assuming python.exe is in your path #TODO: Find a windows native way to update/generate the Ui files # The equivalent of genui.sh # To build C:\> python3 setup.py build # To install C:\> python3 setup.py install apt-offline-1.8.6/LICENSE000066400000000000000000001045151475166737700147740ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. 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 them 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 prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. 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. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey 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; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. 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. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 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 state 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 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program 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, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU 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 Lesser General Public License instead of this License. But first, please read . apt-offline-1.8.6/Makefile000066400000000000000000000004751475166737700154270ustar00rootroot00000000000000all: build html gui: cd apt_offline_gui ; ./genui.sh || exit 1 build:gui python3 setup.py build install: python3 setup.py install html: man2html apt-offline.8 > apt-offline.html clean: rm -f apt_offline_gui/Ui_*.py rm -f apt_offline_gui/resources_rc.py rm -f *.pyc rm -rf build/ rm -f apt-offline.html apt-offline-1.8.6/README.md000066400000000000000000000030441475166737700152410ustar00rootroot00000000000000### apt-offline -- An Offline Package Manager (C) 2005 - 2022 Ritesh Raj Sarraf ![apt-offline-gui](https://camo.githubusercontent.com/867002ddf0a84bc99625d4221eb1f7f6779020e9/68747470733a2f2f6c68362e676f6f676c6575736572636f6e74656e742e636f6d2f5f5f6574717a2d79655034732f545a3778705772717965492f41414141414141414255552f66705a565f316f496e37342f733634302f6170742d6f66666c696e652d616476616e6365642d6f7074696f6e732e706e67) apt-offline is an offline package management tool written in the Python Programming Language. This program, as of now, is intended for people using Debian (And Debian based) systems. It can help you install/upgrade packages, and their dependencies, on a Debian box with no direct internet connection. It can also download full bug report (Debian only) for those packages. For Developers, it can help you download a source deb package, along with all its build dependencies. This program allows leveraging the power of Debian (more precisely APT) onto a completely disconnected machine. Most people with slow or no internet connection (mostly people from 3rd world countries), may not have considered using Debian (or Debian derived distributions), because Debian's real taste is experienced when it is connected to the internet. This utility is an attempt in making that problem eradicate. I hope this utility comes of use to you. I'd be eager to hear your comments/suggestions. Feel free to drop an email at rrs _AT_ researchut |DOT| com ### Dedication This software is dedicated in memory of my father Santosh Kumar Sarraf. We miss you a lot. apt-offline-1.8.6/THANKS000066400000000000000000000003071475166737700146740ustar00rootroot00000000000000These are, namely some of the people, who've helped me a lot in my process of learning programming. *) Peter Otten *) Duncan Booth *) Simon Forman *) Dennis Lee Bieber *) Any others whom I've missed apt-offline-1.8.6/_config.yml000066400000000000000000000000361475166737700161070ustar00rootroot00000000000000--- theme: jekyll-theme-slate apt-offline-1.8.6/apt-offline000077500000000000000000000030731475166737700161160ustar00rootroot00000000000000#!/usr/bin/env python3 # apt-offline # ############################################################################ # Copyright (C) 2005, 2017 Ritesh Raj Sarraf # # rrs@researchut.com # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation; either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the # # Free Software Foundation, Inc., # # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################ from apt_offline_core.AptOfflineCoreLib import main if __name__ == "__main__": main() apt-offline-1.8.6/apt-offline-gui000077500000000000000000000036541475166737700167050ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # apt-offline # ############################################################################ # Copyright (C) 2010 Manish Sinha # # mail@manishsinha.net # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation; either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the # # Free Software Foundation, Inc., # # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################ import sys import os from PyQt5 import QtCore, QtGui, QtWidgets from apt_offline_gui.AptOfflineQtMain import AptOfflineQtMain try: # This seems to be needed, at least of Debian os.putenv('QT_X11_NO_MITSHM', "1") except: sys.stderr.write("Couldn't set environment variable\n") if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) myapp = AptOfflineQtMain() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt-offline-gui-pkexec000077500000000000000000000002521475166737700201510ustar00rootroot00000000000000#!/bin/sh PKEXEC=`which pkexec`; if [ -x $PKEXEC ]; then echo "Exec with pkexec" $PKEXEC /usr/bin/apt-offline-gui else echo "No pkexec found" exit 1 fi apt-offline-1.8.6/apt-offline-gui.desktop000066400000000000000000000002401475166737700203360ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=APT Offline GenericName=APT Offline Comment=Offline APT Package Management Exec=apt-offline-gui-pkexec Categories=System; apt-offline-1.8.6/apt-offline.8000066400000000000000000000255221475166737700162640ustar00rootroot00000000000000.TH apt-offline 8 "February 08, 2025" "version 1.8.6" "USER COMMANDS" .SH NAME apt-offline \- Offline APT Package manager .SH SYNOPSIS .B apt-offline [ARGUMENTS] [OPTIONS] .SH DESCRIPTION apt-offline brings offline package management functionality to Debian based system. It can be used to download packages and its dependencies to be installed later on (or required to update) a disconnected machine. Packages can be downloaded from a different connected machine. .PP It can also fetch bug reports for the packages that are downloaded. .PP Unless the \-h/\-v or \-\-help/\-\-version options are given, one of the .B get, set or .B install commands must be provided. .SS set FILENAME [-h] [--install-packages PKG...PKG_N] [--install-src-packages PKG...PKG_N] --src-build-dep [--release release_name] [--update] [--upgrade] [--upgrade-type upgrade] .PP .B set generates a signature file with the data required to install on or update the disconnected machine. .B FILENAME {apt-offline.sig} is the signature file generated on the machine. This file will contain all the information necessary for apt. .TP \-\-simulate Developer only. Used for testing. .IP "\fB\-\-install\-packages [PKG...PKG_N]\fP" 10 Packages that need to be installed .IP "\fB\-\-install\-src\-packages [PKG...PKG_N]\fP" 10 Source packages that need to be installed .IP "\fB\-\-src\-build\-dep\fP" 10 Download Build Dependency packages for the source packages requested .IP "\fB\-\-release release_name\fP" 10 Release target to install packages from .IP "\fB\-\-apt\-backend apt-get\fP" 10 APT backend to use. One of: apt, apt-get, python-apt .IP "\fB\-\-generate\-changelog\fP" 10 Generate changelog of the version to be downloaded .IP "\fB\-\-update\fP" 10 Generate APT Database signature for an update. This is the equivalent of using .B "apt-get update" .IP "\fB\-\-upgrade\fP" 10 Generate APT Database signature for package upgrade. This is the equivalent of using .B "apt-get upgrade" .IP "\fB\-\-upgrade\-type {upgrade_type}\fP" 10 Type of upgrade you would like to perform. Default is .B "upgrade". Other valid types are .B "dist-upgrade" and "dselect-upgrade" .SS get FILENAME [-h] [--socket-timeout ] [-d / --download-dir] [-s / --cache-dir] [--no-checksum] [-t / --threads ] [--bundle] [--bug-reports] [--proxy-host] [--proxy-port] [--https-cert-file --https-key-file] [--http-basicauth] [--disable-cert-check] .PP .B get downloads APT data as per a signature file. .B FILENAME {apt-offline.sig} is the signature file required for details about data to be downloaded. This file should have been generated by the above .B set command. .IP "\fB\-d, \-\-download\-dir DIR_NAME\fP" 10 Download data to the specified DIR_NAME folder. If no folder name is specified, data is downloaded to a folder in the TEMPDIR path in the format .B apt-offline-download-$PID .IP "\fB\-s, \-\-cache\-dir DIR_NAME\fP" 10 Look for data in the cache before downloading it from the internet. If you are on a Debian box, you would want to specify .I /var/cache/apt/archives here. If the data is not available in the cache, the downloaded data is also copied to the cache. .IP "\fB\-\-no\-checksum\fP" 10 Enabling this option will bypass the checksum verification of each downloaded file thus losing integrity of the package. Usage of this option is highly discouraged .IP "\fB\-t, \-\-threads NUM_OF_THREADS\fP" 10 Number of threads to spawn for downloads. Default is 1. Using too many threads can overload the servers, hence it is advisable to keep the number low .IP "\fB\-\-bundle FILENAME\fP" 10 Create an archive file FILENAME. The file is archived in zip format .IP "\fB\-\-bug\-reports\fP" 10 Download bug reports for packages that are being downloaded. Currently only the Debian BTS is supported. .IP "\fB\-\-proxy\-host\fP" 10 Specify the Proxy Host to be used. .IP "\fB\-\-proxy\-port\fP" 10 Specify the Proxy port number to be used. .IP "\fB\-\-https\-cert\-file FILENAME\fP" 10 The Certificate-file to use if https-client-authentication is required on one or multiple hosts. .IP "\fB\-\-https\-key\-file FILENAME\fP" 10 The Certificate-key-file to use if https-client-authentication is required on one or multiple hosts. .IP "\fB\-\-disable-cert-check\fP" 10 Disable the check of https certificates on servers .IP "\fB\-\-http\-basicauth URL\fP" 10 A username/password encoded in the URL. Note\: Passwords with special characters should be percent encoded as per RFC-3986 section 2.1 .SS install FILE/FOLDER [-h] [--skip-bug-reports ] [--install-src-path PATH] [--allow-unauthenticated] .PP .B install installs APT data to the APT package database and updates it. .B FILE {archive_bundle_file} Install data from the archive (bundle) file. .B FOLDER {/folder/path} Install data from the folder path. Either FILE or FOLDER argument can be provided to the .B install command. .TP \-\-simulate Developer only. Used for testing. .IP "\fB\-\-skip\-bug\-reports\fP" 10 Skip listing of downloaded bug reports, if any. .IP "\fB\-\-skip\-changelog\fP" 10 Skip display of changelog, if any. .IP "\fB\-\-allow\-unauthenticated\fP" 10 Don't verify GPG signatures for the data to be installed to APT. Usage of this option is highly discouraged. .IP "\fB\-\-strict\-deb\-check\fP" 10 With this option enabled, apt-offline delegate's .deb package checksum validation to apt. While the .debs are already available, they are stored in the temporary apt cache, where apt validates its checksum, before considering it for further processing. Note: This does have the caveat that apt may need network availability even though it doesn't download anything over the network. But it does invoke the download routines and realizes that the payload is already available. It then further proceeds with checksum validation The default behavior is to .B not do strict checksum validation for .deb files. Instead, apt-offline copies the .deb files to apt's download location. apt still does size validation of the available .deb files and discards them in case there is a mismatch. .IP "\fB\-\-install\-src\-path PATH\fP" 10 Path to filesystem where we want the source packages to be installed to. Default will be a folder in your TEMPDIR. .SH GLOBAL OPTIONS .TP \-h, \-\-help Show help message .TP \-\-verbose Run in verbose mode .TP \-v, \-\-version Display the version of the program .SH EXAMPLES .TP .B NOTE: argument/option handling apt\-offline relies on argparse for argument/option parsing. To explicitly instruct apt\-offline about an argument, you can pass it with the \-\- delimiter. .B Ex. apt\-offline set \-\-update \-\-upgrade \-\-install\-packages wm2 \-\- foo.sig By specifying the .B \-\- delimiter, we instruct apt\-offline that foo.sig is an argument to the .B apt\-offline command and not to the .B \-\-install\-packages option. Otherwise, you could also use it positionally next to the set command .B Ex. apt\-offline set foo.sig \-\-update \-\-upgrade \-\-install\-packages wm2 .TP .B apt-offline set FILENAME This command will generate a signature file FILENAME for APT Package Database. To generate only the signature for updates, use the \-\-update option. To generate only the signature for package upgrades, use the \-\-upgrade option. .TP .B apt-offline get FILENAME This command will fetch the data required for APT Package Database as per the signature file FILENAME generated by .B apt-offline get. To download bug reports also use the \-\-bug\-reports option. Currently supported bug tracker is Debian BTS only. By default, if neither of \-d or \-\-bundle options are specified, apt-offline downloads data into a folder inside the TEMPDIR environment folder in the format apt\-offline\-downloads\-PID, where PID is the PID of the running apt\-offline process. Example on a Linux machine would be something like: /tmp/apt-offline-downloads-23242/ .TP .B apt-offline install FILE|FOLDER This command will sync the data downloaded by .B apt-offline get to the APT Package Database and update it. Depending on where the data was downloaded to or packed into, either the absolute FOLDER path or the archive FILE path can be specified. .B NOTE1: On a freshly installed box, that was installed without the network, the package database is null. In that case, you first need to run .B apt-offline with just the .B \-\-update option to ensure you have a meaningful package database .B Example: apt-offline set set.uris \-\-update .B NOTE2: On a fresh setup installed through CD/DVD, the default APT setting lists only the install media URLs. In such case, you need to add the default APT network repositories to the list. For example, for a fresh (DVD) installed Debian box, add the relevant repository to .I /etc/apt/sources.list.d/apt-offline.list or /etc/apt/sources.list deb https://deb.debian.org/debian stable main contrib .B (For Debian Stable) deb https://deb.debian.org/debian unstable main contrib .B (For Debian Unstable/Sid) deb https://deb.debian.org/debian stretch main contrib .B (For Debian Stretch) deb http://security.debian.org stable/updates main contrib .B (Security Updates for Debian Stable) deb http://security.debian.org testing-security/updates main contrib .B (Security Updates for Debian Testing) .TP Sequence 1: The following set of commands, when run in sequence, will update a disconnected machine. .B apt-offline set update.sig \-\-update (Generate the required data needed to update the APT database. Should be run on the disconnected machine) .B apt-offline get update.sig \-\-bundle update.zip (Download the required data needed to update the APT database. Should be run on a machine with internet connectivity) .B apt-offline install update.zip (Installs the data needed to update the APT database. Should be run on the disconnected machine) .TP Sequence 2: With successful completion of Sequence 1, the APT database on the disconnected machine will be up\-to\-date. Now, the following set of commands, when run in sequence, will upgrade a disconnected machine. .B apt-offline set upgrade.sig \-\-upgrade (Generate the required data needed to upgrade the upgradable packages. Should be run on the disconnected machine) .B apt-offline get upgrade.sig \-\-bundle upgrade.zip (Download the required data needed to upgrade the upgradable packages. Should be run on a machine with internet connectivity) .B apt-offline install upgrade.zip (Installs the data needed to upgrade the upgradable packages. Should be run on the disconnected machine) .TP After successful completion of .B Sequence 1 and .B Sequence 2 in order, further running .B apt-get upgrade will result in 0 bytes of additional download. .SH AUTHOR .B apt-offline is written by Ritesh Raj Sarraf (rrs@researchut.com) If you wish to report a bug in apt-offline, please see .B https://github.com/rickysarraf/apt-offline or else, send an email to me at .B rrs@researchut.com .SH SEE ALSO apt-get(8), apt-cache(8), dpkg(8), aptitude(8), .SH DEDICATION This software is dedicated to the memory of my father Santosh Kumar Sarraf. We miss you a lot. apt-offline-1.8.6/apt-offline.html000066400000000000000000000331161475166737700170570ustar00rootroot00000000000000Content-type: text/html; charset=UTF-8 Man page of apt-offline

apt-offline

Section: USER COMMANDS (8)
Updated: February 08, 2025
Index Return to Main Contents
 

NAME

apt-offline - Offline APT Package manager  

SYNOPSIS

apt-offline [ARGUMENTS] [OPTIONS]

 

DESCRIPTION

apt-offline brings offline package management functionality to Debian based system. It can be used to download packages and its dependencies to be installed later on (or required to update) a disconnected machine. Packages can be downloaded from a different connected machine.

It can also fetch bug reports for the packages that are downloaded.

Unless the -h/-v or --help/--version options are given, one of the get, set or install commands must be provided.

 

set FILENAME [-h] [--install-packages PKG...PKG_N] [--install-src-packages PKG...PKG_N] --src-build-dep [--release release_name] [--update] [--upgrade] [--upgrade-type upgrade]

set generates a signature file with the data required to install on or update the disconnected machine.

FILENAME {apt-offline.sig} is the signature file generated on the machine. This file will contain all the information necessary for apt.

--simulate
Developer only. Used for testing.

--install-packages [PKG...PKG_N]
Packages that need to be installed

--install-src-packages [PKG...PKG_N]
Source packages that need to be installed

--src-build-dep
Download Build Dependency packages for the source packages requested

--release release_name
Release target to install packages from

--apt-backend apt-get
APT backend to use. One of: apt, apt-get, python-apt

--generate-changelog
Generate changelog of the version to be downloaded

--update
Generate APT Database signature for an update. This is the equivalent of using apt-get update

--upgrade
Generate APT Database signature for package upgrade. This is the equivalent of using apt-get upgrade

--upgrade-type {upgrade_type}
Type of upgrade you would like to perform. Default is upgrade. Other valid types are dist-upgrade and dselect-upgrade

 

get FILENAME [-h] [--socket-timeout ] [-d / --download-dir] [-s / --cache-dir] [--no-checksum] [-t / --threads ] [--bundle] [--bug-reports] [--proxy-host] [--proxy-port] [--https-cert-file --https-key-file] [--http-basicauth] [--disable-cert-check]

get downloads APT data as per a signature file.

FILENAME {apt-offline.sig} is the signature file required for details about data to be downloaded. This file should have been generated by the above set command.

-d, --download-dir DIR_NAME
Download data to the specified DIR_NAME folder. If no folder name is specified, data is downloaded to a folder in the TEMPDIR path in the format apt-offline-download-$PID

-s, --cache-dir DIR_NAME
Look for data in the cache before downloading it from the internet. If you are on a Debian box, you would want to specify /var/cache/apt/archives here. If the data is not available in the cache, the downloaded data is also copied to the cache.

--no-checksum
Enabling this option will bypass the checksum verification of each downloaded file thus losing integrity of the package. Usage of this option is highly discouraged

-t, --threads NUM_OF_THREADS
Number of threads to spawn for downloads. Default is 1. Using too many threads can overload the servers, hence it is advisable to keep the number low

--bundle FILENAME
Create an archive file FILENAME. The file is archived in zip format

--bug-reports
Download bug reports for packages that are being downloaded. Currently only the Debian BTS is supported.

--proxy-host
Specify the Proxy Host to be used.

--proxy-port
Specify the Proxy port number to be used.

--https-cert-file FILENAME
The Certificate-file to use if https-client-authentication is required on one or multiple hosts.

--https-key-file FILENAME
The Certificate-key-file to use if https-client-authentication is required on one or multiple hosts.

--disable-cert-check
Disable the check of https certificates on servers

--http-basicauth URL
A username/password encoded in the URL. Note Passwords with special characters should be percent encoded as per RFC-3986 section 2.1

 

install FILE/FOLDER [-h] [--skip-bug-reports ] [--install-src-path PATH] [--allow-unauthenticated]

install installs APT data to the APT package database and updates it.

FILE {archive_bundle_file} Install data from the archive (bundle) file.

FOLDER {/folder/path} Install data from the folder path.

Either FILE or FOLDER argument can be provided to the install command.

--simulate
Developer only. Used for testing.

--skip-bug-reports
Skip listing of downloaded bug reports, if any.

--skip-changelog
Skip display of changelog, if any.

--allow-unauthenticated
Don't verify GPG signatures for the data to be installed to APT. Usage of this option is highly discouraged.

--strict-deb-check
With this option enabled, apt-offline delegate's .deb package checksum validation to apt. While the .debs are already available, they are stored in the temporary apt cache, where apt validates its checksum, before considering it for further processing. Note: This does have the caveat that apt may need network availability even though it doesn't download anything over the network. But it does invoke the download routines and realizes that the payload is already available. It then further proceeds with checksum validation

The default behavior is to not do strict checksum validation for .deb files. Instead, apt-offline copies the .deb files to apt's download location. apt still does size validation of the available .deb files and discards them in case there is a mismatch.

--install-src-path PATH
Path to filesystem where we want the source packages to be installed to. Default will be a folder in your TEMPDIR.

 

GLOBAL OPTIONS

-h, --help
Show help message

--verbose
Run in verbose mode

-v, --version
Display the version of the program

 

EXAMPLES

NOTE: argument/option handling
apt-offline relies on argparse for argument/option parsing. To explicitly instruct apt-offline about an argument, you can pass it with the -- delimiter.

Ex. apt-offline set --update --upgrade --install-packages wm2 -- foo.sig

By specifying the -- delimiter, we instruct apt-offline that foo.sig is an argument to the apt-offline command and not to the --install-packages option.

Otherwise, you could also use it positionally next to the set command

Ex. apt-offline set foo.sig --update --upgrade --install-packages wm2

apt-offline set FILENAME
This command will generate a signature file FILENAME for APT Package Database. To generate only the signature for updates, use the --update option. To generate only the signature for package upgrades, use the --upgrade option.

apt-offline get FILENAME
This command will fetch the data required for APT Package Database as per the signature file FILENAME generated by apt-offline get. To download bug reports also use the --bug-reports option. Currently supported bug tracker is Debian BTS only. By default, if neither of -d or --bundle options are specified, apt-offline downloads data into a folder inside the TEMPDIR environment folder in the format apt-offline-downloads-PID, where PID is the PID of the running apt-offline process. Example on a Linux machine would be something like: /tmp/apt-offline-downloads-23242/

apt-offline install FILE|FOLDER
This command will sync the data downloaded by apt-offline get to the APT Package Database and update it. Depending on where the data was downloaded to or packed into, either the absolute FOLDER path or the archive FILE path can be specified.

NOTE1: On a freshly installed box, that was installed without the network, the package database is null. In that case, you first need to run apt-offline with just the --update option to ensure you have a meaningful package database

Example: apt-offline set set.uris --update

NOTE2: On a fresh setup installed through CD/DVD, the default APT setting lists only the install media URLs. In such case, you need to add the default APT network repositories to the list. For example, for a fresh (DVD) installed Debian box, add the relevant repository to

/etc/apt/sources.list.d/apt-offline.list or /etc/apt/sources.list

deb https://deb.debian.org/debian stable main contrib

(For Debian Stable)

deb https://deb.debian.org/debian unstable main contrib

(For Debian Unstable/Sid)

deb https://deb.debian.org/debian stretch main contrib

(For Debian Stretch)

deb http://security.debian.org stable/updates main contrib

(Security Updates for Debian Stable)

deb http://security.debian.org testing-security/updates main contrib

(Security Updates for Debian Testing)

Sequence 1: The following set of commands, when run in sequence, will update a disconnected machine.

apt-offline set update.sig --update

(Generate the required data needed to update the APT database. Should be run on the disconnected machine)

apt-offline get update.sig --bundle update.zip

(Download the required data needed to update the APT database. Should be run on a machine with internet connectivity)

apt-offline install update.zip

(Installs the data needed to update the APT database. Should be run on the disconnected machine)

Sequence 2: With successful completion of Sequence 1, the APT database on the disconnected machine will be up-to-date. Now, the following set of commands, when run in sequence, will upgrade a disconnected machine.

apt-offline set upgrade.sig --upgrade

(Generate the required data needed to upgrade the upgradable packages. Should be run on the disconnected machine)

apt-offline get upgrade.sig --bundle upgrade.zip

(Download the required data needed to upgrade the upgradable packages. Should be run on a machine with internet connectivity)

apt-offline install upgrade.zip

(Installs the data needed to upgrade the upgradable packages. Should be run on the disconnected machine)

After successful completion of
Sequence 1 and Sequence 2 in order, further running apt-get upgrade will result in 0 bytes of additional download.

 

AUTHOR

apt-offline is written by Ritesh Raj Sarraf (rrs@researchut.com)

If you wish to report a bug in apt-offline, please see https://github.com/rickysarraf/apt-offline or else, send an email to me at rrs@researchut.com

 

SEE ALSO

apt-get(8), apt-cache(8), dpkg(8), aptitude(8),

 

DEDICATION

This software is dedicated to the memory of my father Santosh Kumar Sarraf. We miss you a lot.


 

Index

NAME
SYNOPSIS
DESCRIPTION
set FILENAME [-h] [--install-packages PKG...PKG_N] [--install-src-packages PKG...PKG_N] --src-build-dep [--release release_name] [--update] [--upgrade] [--upgrade-type upgrade]
get FILENAME [-h] [--socket-timeout ] [-d / --download-dir] [-s / --cache-dir] [--no-checksum] [-t / --threads ] [--bundle] [--bug-reports] [--proxy-host] [--proxy-port] [--https-cert-file --https-key-file] [--http-basicauth] [--disable-cert-check]
install FILE/FOLDER [-h] [--skip-bug-reports ] [--install-src-path PATH] [--allow-unauthenticated]
GLOBAL OPTIONS
EXAMPLES
AUTHOR
SEE ALSO
DEDICATION

This document was created by man2html, using the manual pages.
Time: 14:49:28 GMT, February 08, 2025 apt-offline-1.8.6/apt_offline_core/000077500000000000000000000000001475166737700172575ustar00rootroot00000000000000apt-offline-1.8.6/apt_offline_core/AptOfflineCoreLib.py000066400000000000000000003636761475166737700231450ustar00rootroot00000000000000############################################################################ # Copyright (C) 2005, 2025 Ritesh Raj Sarraf # # rrs@researchut.com # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation; either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the # # Free Software Foundation, Inc., # # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################ import urllib.parse import urllib.error import urllib.request from apt_offline_core import AptOfflineLib from apt_offline_core.AptOfflineLib import AptOfflineErrors, AptOfflineLibShutilError import zlib import argparse import traceback import pydoc import zipfile import random # to generate random directory names for installing multiple bundles in on go import tempfile import socket import subprocess import threading import queue import http.client import ssl import platform import shutil import sys import os FCNTL_LOCK = True try: import fcntl except ImportError: # Only available on platform Unix FCNTL_LOCK = False # On Debian, python-debianbts package provides this library DebianBTS = True try: import debianbts except ImportError: try: from apt_offline_core import AptOfflineDebianBtsLib as debianbts except ImportError: DebianBTS = False try: MagicLib = True from apt_offline_core import AptOfflineMagicLib except TypeError: """ On Windows, the file magic library does not work """ MagicLib = False except AttributeError: # On Linux, make sure libmagic is installed MagicLib = False # INFO: added to handle GUI interaction guiBool = False guiTerminateSignal = False # cancelling a download guiMetaCompleted = False totalSize = [0, 0] # total_size, current_total # INFO: Check if python-apt is installed PythonApt = False try: import apt import apt_pkg from apt.debfile import DebPackage PythonApt = True except ImportError: PythonApt = False # INFO: Set the default timeout to 30 seconds for the packages that are being downloaded. socket.setdefaulttimeout(30) # How many times should we retry on socket timeouts SOCKET_TIMEOUT_RETRY = 5 """This is the core module. It does the main job of downloading packages/update packages,\n figuring out if the packages are in the local cache, handling exceptions and many more stuff""" app_name = "apt-offline" version = "1.8.6" myCopyright = "(C) 2005 - 2025 Ritesh Raj Sarraf" terminal_license = "This program comes with ABSOLUTELY NO WARRANTY.\n\ This is free software, and you are welcome to redistribute it under\n\ the GNU GPL Version 3 (or later) License\n" errlist = [] supported_platforms = ["Linux", "GNU/kFreeBSD", "GNU"] apt_update_target_path = "/var/lib/apt/lists/partial" apt_update_final_path = "/var/lib/apt/lists/" apt_package_target_path = "/var/cache/apt/archives/partial/" apt_package_final_path = "/var/cache/apt/archives/" # Locks apt_lists_lock = "/var/lib/apt/lists/lock" apt_packages_lock = "/var/cache/apt/archives/lock" apt_bug_file_format = "__apt__bug__report" # These are spaces which will overwrite the progressbar left mess LINE_OVERWRITE_SMALL = " " * 10 LINE_OVERWRITE_MID = " " * 30 LINE_OVERWRITE_FULL = " " * 60 Bool_Verbose = False # Bool_TestWindows = True log = AptOfflineLib.Log(Bool_Verbose, lock=True) class FetchBugReports: def __init__( self, apt_bug_file_format, ArchiveFile=None, lock=False, DownloadDir=None ): self.bugsList = [] self.lock = lock self.apt_bug = apt_bug_file_format self.DownloadDir = DownloadDir self.ArchiveFile = ArchiveFile self.fileMgmt = AptOfflineLib.FileMgmt() def FetchBugsDebian(self, PackageName): """0 => False 1 => No Bug Reports 2 => True""" try: self.bugs_list = debianbts.get_bugs(package=PackageName) num_of_bugs = len(self.bugs_list) except Exception: log.err(traceback.format_exc()) log.err("Foreign exception raised in module debianbts\n") log.err("Failed to download bug report for package %s\n" % (PackageName)) return 0 if num_of_bugs: atleast_one_bug_report_downloaded = False for eachBug in self.bugs_list: # Fetch bug report.. # TODO: Handle exceptions later try: bugReport = debianbts.get_bug_log(eachBug) except Exception: # INFO: Some of these exceptions are sporadic. For example, this one was hit because of network timeout # And we don't want the entire operation to fail because of this log.warn("Foreign exception raised in module debianbts\n") log.warn( "Failed to download bug report for %s\nWill continue to download others\n" % (eachBug) ) log.err(traceback.format_exc()) continue # This tells us how many follow-ups for the bug report are present. bugReportLength = bugReport.__len__() writeBugReport = 0 self.fileName = os.path.join( tempfile.gettempdir(), PackageName + "{}" + str(eachBug) + "{}" + self.apt_bug, ) file_handle = open(self.fileName, "w", encoding="utf-8") # TODO: Can we manipulate these headers in a more efficient way??? for line in bugReport[writeBugReport]["header"].split("\n"): if line.startswith("Subject:"): file_handle.write(line) file_handle.write("\n") break while writeBugReport < bugReportLength: file_handle.write(bugReport[writeBugReport]["body"]) file_handle.write("\n\n") writeBugReport += 1 if writeBugReport < bugReportLength: file_handle.write("Follow-Up #%d\n\n" % writeBugReport) file_handle.flush() file_handle.close() # We're adding to an archive file here. if self.ArchiveFile: if self.AddToArchive(self.ArchiveFile, self.fileName): log.verbose( "%s added to file %s\n" % ( self.fileName, self.ArchiveFile) ) else: log.warn( "%s failed to be added to file %s\n" % (self.fileName, self.ArchiveFile) ) elif self.DownloadDir: try: if self.fileMgmt.move_file(self.fileName, self.DownloadDir): log.verbose( "%s added to download dir %s\n" % (self.fileName, self.DownloadDir) ) except AptOfflineLibShutilError as msg: log.warn("%s\n" % (msg)) atleast_one_bug_report_downloaded = True if atleast_one_bug_report_downloaded: return 2 else: return 1 else: # FIXME: When no bug reports are there, i.e. bug count is 0, we hit here # We shouldn't be returning False return 1 def AddToArchive(self, ArchiveFile, fileName): try: if self.compress_the_file(ArchiveFile, fileName): if self.file_possibly_deleted is not True: os.unlink(fileName) except AptOfflineErrors as message: log.warn("%s\n" % (message)) return True class ExecCmd: def __init__(self, Simulate=False): self.Simulate = Simulate def Simulate(self): if self.Simulate is True: pass def ExecSystemCmd(self, cmd, sigFile=None): """ Execute command 'cmd' with subprocess module Write stdout to sigFile, if provided """ if self.Simulate: return True if sigFile is None: # subprocess.call does take None as an arg fh = None else: try: fh = open(sigFile, "a") except Exception: log.verbose(traceback.format_exc()) return False if fh is not None: preState = fh.tell() log.verbose("Command is: %s\n" % (cmd)) p = subprocess.call(cmd, universal_newlines=True, stdout=fh) if fh is not None: fh.flush() if p != 0: # INFO: stderr will give us junk which our stripper() will not understand # So, under that condition, truncate the data so that at least, our # sig file is still usable if fh is not None: fh.truncate(preState) fh.flush() return False return True class AptManip(ExecCmd): def __init__( self, OutputFile, Simulate=False, AptType="apt-get", AptReinstall=False ): ExecCmd.__init__(self, Simulate) self.WriteTo = OutputFile self.AptReinstall = AptReinstall self.ShouldSimulate = Simulate if AptType == "apt": self.apt = "apt" elif AptType == "apt-get": self.apt = "apt-get" elif AptType == "aptitude": self.apt = "aptitude" elif AptType == "python-apt": self.apt = "python-apt" else: self.apt = "apt-get" def Update(self): log.verbose("APT Update Method is of type: %s\n" % self.apt) if self.apt == "apt-get": self.__AptGetUpdate() elif self.apt == "apt": self.__AptUpdate() elif self.apt == "aptitude": pass elif self.apt == "python-apt": self.__PythonAptUpdate() else: log.err("Method not supported") sys.exit(1) def Upgrade(self, UpgradeType="upgrade", ReleaseType=None): log.verbose("APT Upgrade Method is of type: %s\n" % self.apt) if self.apt == "apt-get": return self.__AptGetUpgrade(UpgradeType, ReleaseType) elif self.apt == "apt": return self.__AptUpgrade(UpgradeType, ReleaseType) elif self.apt == "aptitude": pass elif self.apt == "python-apt": # Upgrade is broken in python-apt # Hence for now, redirect to apt-get return self.__PythonAptUpgrade(UpgradeType, ReleaseType) else: log.err("Method not supported") sys.exit(1) def InstallPackages(self, PackageList, ReleaseType): log.verbose("APT Install Method is of type: %s\n" % self.apt) if self.apt == "apt-get": return self.__AptGetInstallPackage(PackageList, ReleaseType) elif self.apt == "apt": return self.__AptInstallPackage(PackageList, ReleaseType) elif self.apt == "python-apt": return self.__AptInstallPackage(PackageList, ReleaseType) else: log.err("Method not supported") sys.exit(1) def InstallSrcPackages(self, SrcPackageList, ReleaseType, BuildDependency): log.verbose("APT Install Source Method is of type: %s\n" % self.apt) if self.apt == "apt-get": return self.__AptGetInstallSrcPackages( SrcPackageList, ReleaseType, BuildDependency ) elif self.apt == "apt": return self.__AptInstallSrcPackages( SrcPackageList, ReleaseType, BuildDependency ) elif self.apt == "python-apt": return self.__AptInstallSrcPackages( SrcPackageList, ReleaseType, BuildDependency ) else: log.err("Method not supported") sys.exit(1) def __FixAptSigs(self): if self.ShouldSimulate is True: log.msg("In simulation mode, no changes applied\n") else: for localFile in os.listdir(apt_update_target_path): if localFile.endswith(".gpg.reverify"): sig_file = localFile.rstrip(".reverify") log.verbose("Recovering gpg signature %s.\n" % (localFile)) localFile = os.path.join(apt_update_target_path, localFile) os.rename(localFile, os.path.join( apt_update_final_path + sig_file)) def __AptUpdate(self): log.msg("Gathering details needed for 'update' operation\n") if ( self.ExecSystemCmd( ["/usr/bin/apt", "-qq", "--print-uris", "update"], self.WriteTo ) is False ): log.verbose("FATAL: Something is wrong with the apt system.\n") return False log.verbose("Calling __FixAptSigs to fix the apt sig problem") self.__FixAptSigs() def __AptGetUpdate(self): log.msg("Gathering details needed for 'update' operation\n") if ( self.ExecSystemCmd( ["/usr/bin/apt-get", "-q", "--print-uris", "update"], self.WriteTo ) is False ): log.verbose("FATAL: Something is wrong with the apt system.\n") return False log.verbose("Calling __FixAptSigs to fix the apt sig problem") self.__FixAptSigs() def __AptitudeUpdate(self): pass def __PythonAptUpdate(self): log.verbose("Open file %s for write" % self.WriteTo) try: self.writeFH = open(self.WriteTo, "a") except Exception: log.verbose(traceback.format_exc()) log.err("Failed to open file %s for write. Exiting\n" % (self.WriteTo)) sys.exit(1) log.msg("Gathering details needed for 'update' operation\n") log.verbose("\nUsing Python apt interface\n") apt_pkg.init_config() apt_pkg.init_system() acquire = apt_pkg.Acquire() slist = apt_pkg.SourceList() # Read the main list slist.read_main_list() # Add all indexes to the fetcher slist.get_indexes(acquire, True) # Now write the URI of every item for item in acquire.items: # INFO: For update files, there's no checksum present. # Also, their size is not determined. # Hence filesize is always returned '0' # And checksum is something I'm writing as ':' # We strip item.destfile because that's how apt-get had historically presented it to us destFile = item.destfile.split("/")[-1] self.writeFH.write( "'" + item.desc_uri + "'" + " " + destFile + " " + str(item.filesize) + " " + ":" + "\n" ) log.verbose( "Writing string %s %s %d %s to file %s\n" % (item.desc_uri, destFile, item.filesize, ":", self.WriteTo) ) self.writeFH.flush() self.writeFH.close() def __PythonAptUpgrade(self, UpgradeType="upgrade", ReleaseType=None): log.verbose("Open file %s for write\n" % self.WriteTo) try: self.writeFH = open(self.WriteTo, "a") except Exception: log.verbose(traceback.format_exc()) log.err("Failed to open file %s for write. Exiting\n") sys.exit(1) log.msg("Gathering details needed for 'upgrade' operation\n") log.warn("Option --upgrade-type not supported with this backend\n") log.verbose("\nUsing Python apt interface\n") cache = apt.Cache() cache.open(None) if UpgradeType == "dist-upgrade": cache.upgrade(dist_upgrade=True) elif UpgradeType == "upgrade": cache.upgrade(dist_upgrade=False) else: cache.upgrade() for pkg in cache.get_changes(): log.verbose( "Generable data is: %s %s %d %s\n" % ( pkg.candidate.uri, pkg.candidate.filename.split("/")[-1], pkg.candidate.size, pkg.candidate.md5, ) ) self.writeFH.write( "%s %s %d %s\n" % ( pkg.candidate.uri, pkg.candidate.filename.split("/")[-1], pkg.candidate.size, pkg.candidate.md5, ) ) self.writeFH.flush() self.writeFH.close() def __AptUpgrade(self, UpgradeType="upgrade", ReleaseType=None): self.ReleaseType = ReleaseType if ReleaseType is not None: cmd = ["/usr/bin/apt", "-qqq", "--print-uris", "-t"] cmd.append(self.ReleaseType) cmd.append(UpgradeType) else: cmd = ["/usr/bin/apt", "-qqq", "--print-uris"] cmd.append(UpgradeType) log.msg("Gathering details needed for '%s' operation\n" % (UpgradeType)) if self.ExecSystemCmd(cmd, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the APT system\n") return False def __AptGetUpgrade(self, UpgradeType="upgrade", ReleaseType=None): self.ReleaseType = ReleaseType if ReleaseType is not None: cmd = ["/usr/bin/apt-get", "-qq", "--print-uris", "-t"] cmd.append(self.ReleaseType) cmd.append(UpgradeType) else: cmd = ["/usr/bin/apt-get", "-qq", "--print-uris"] cmd.append(UpgradeType) log.msg("Gathering details needed for '%s' operation\n" % (UpgradeType)) if self.ExecSystemCmd(cmd, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the APT system\n") return False def __AptInstallPackage(self, PackageList=None, ReleaseType=None): self.ReleaseType = ReleaseType log.msg("Gathering installation details for package %s\n" % (PackageList)) if self.ReleaseType is not None: cmd = ["/usr/bin/apt", "-qqq", "--print-uris", "install", "-t"] cmd.append(self.ReleaseType) else: cmd = ["/usr/bin/apt", "-qqq", "--print-uris", "install"] for pkg in PackageList: cmd.append(pkg) if self.ExecSystemCmd(cmd, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the apt system.\n") return False def __AptInstallSrcPackages( self, SrcPackageList=None, ReleaseType=None, BuildDependency=False ): self.ReleaseType = ReleaseType log.msg( "Gathering installation details for source package %s\n" % ( SrcPackageList) ) if self.ReleaseType is not None: cmd = ["/usr/bin/apt", "-qqq", "--print-uris", "source", "-t"] cmd.append(self.ReleaseType) cmdBuildDep = ["/usr/bin/apt", "-qqq", "--print-uris", "build-dep", "-t"] cmdBuildDep.append(self.ReleaseType) else: cmd = ["/usr/bin/apt", "-qqq", "--print-uris", "source"] cmdBuildDep = ["/usr/bin/apt", "-qqq", "--print-uris", "build-dep"] for pkg in SrcPackageList: cmd.append(pkg) cmdBuildDep.append(pkg) if self.ExecSystemCmd(cmd, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the apt system.\n") return False if BuildDependency: log.msg( "Generating Build-Dependency for source packages %s.\n" % (SrcPackageList) ) if self.ExecSystemCmd(cmdBuildDep, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the apt system.\n") return False def __AptGetInstallPackage(self, PackageList=None, ReleaseType=None): self.ReleaseType = ReleaseType log.msg("Gathering installation details for package %s\n" % (PackageList)) if self.ReleaseType is not None: cmd = ["/usr/bin/apt-get", "-qq", "--print-uris", "install", "-t"] cmd.append(self.ReleaseType) else: cmd = ["/usr/bin/apt-get", "-qq", "--print-uris", "install"] for pkg in PackageList: cmd.append(pkg) if self.ExecSystemCmd(cmd, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the apt system.\n") return False def __AptGetInstallSrcPackages( self, SrcPackageList=None, ReleaseType=None, BuildDependency=False ): self.ReleaseType = ReleaseType log.msg( "Gathering installation details for source package %s\n" % ( SrcPackageList) ) if self.ReleaseType is not None: cmd = ["/usr/bin/apt-get", "-qq", "--print-uris", "source", "-t"] cmd.append(self.ReleaseType) cmdBuildDep = ["/usr/bin/apt-get", "-qq", "--print-uris", "build-dep", "-t"] cmdBuildDep.append(self.ReleaseType) else: cmd = ["/usr/bin/apt-get", "-qq", "--print-uris", "source"] cmdBuildDep = ["/usr/bin/apt-get", "-qq", "--print-uris", "build-dep"] for pkg in SrcPackageList: cmd.append(pkg) cmdBuildDep.append(pkg) if self.ExecSystemCmd(cmd, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the apt system.\n") return False if BuildDependency: log.msg( "Generating Build-Dependency for source packages %s.\n" % (SrcPackageList) ) if self.ExecSystemCmd(cmdBuildDep, self.WriteTo) is False: log.verbose("FATAL: Something is wrong with the apt system.\n") return False class APTVerifySigs(ExecCmd): def __init__(self, gpgv=None, keyring=None, Simulate=False): """ Initialize keyring based on environment Uses python-apt or apt-config """ ExecCmd.__init__(self, Simulate) self.defaultPaths = [] if PythonApt is True: apt_pkg.init() self.defaultPaths.append( apt_pkg.config.find_dir("Dir::Etc::trustedparts")) self.defaultPaths.append( apt_pkg.config.find_file("Dir::Etc::trusted")) else: command = b""" # Unset variables in case they are set already unset trusted unset trustedparts # Get the variables from apt eval $(apt-config shell trusted Dir::Etc::trusted/f) eval $(apt-config shell trustedparts Dir::Etc::trustedparts/d) # Securely pass the variables back to python-apt printf "%s\\0%s" "$trusted" "$trustedparts" """ process = subprocess.Popen( ["sh"], stdin=subprocess.PIPE, stdout=subprocess.PIPE ) output = process.communicate(input=command)[0] trusted, trustedparts = output.decode("utf-8").split("\x00") log.verbose( "trusted is %s and trustedparts is %s\n" % ( trusted, trustedparts) ) self.defaultPaths.append(trusted) self.defaultPaths.append(trustedparts) log.verbose("APT Signature verification path is: %s\n" % self.defaultPaths) if gpgv is None: self.gpgv = "/usr/bin/gpgv" else: self.gpgv = gpgv self.opts = [] self.opts.append("--ignore-time-conflict") for eachPath in self.defaultPaths: if os.path.isfile(eachPath): if eachPath.endswith(".asc"): self.DearmorSig(eachPath) elif eachPath.endswith(".gpg"): self.opts.extend(["--keyring", eachPath]) elif os.path.isdir(eachPath): for eachGPG in os.listdir(eachPath): if eachGPG.endswith(".asc"): eachGPG = os.path.join(eachPath, eachGPG) self.DearmorSig(eachGPG) elif eachGPG.endswith(".gpg"): eachGPG = os.path.join(eachPath, eachGPG) log.verbose( "Adding %s to the apt-offline keyring\n" % ( eachGPG) ) self.opts.extend(["--keyring", eachGPG]) if keyring: for eachFile in os.listdir(keyring): extraKeyringFile = os.path.join(keyring, eachFile) log.verbose("extraKeyringFile is %s" % extraKeyringFile) self.opts.extend( ["--keyring", extraKeyringFile, "--ignore-time-conflict"] ) if len(self.opts) == 1: log.err( "No valid keyring paths found in: %s\n" % ( ", ".join(self.defaultPaths)) ) def DearmorSig(self, asciiSig): gpgCmd = [] gpgKeyringFile = tempfile.mktemp() gpgCmd.append("/usr/bin/gpg") gpgCmd.extend(["--output", gpgKeyringFile]) gpgCmd.extend(["--dearmor", asciiSig]) if self.ExecSystemCmd(gpgCmd, None): self.opts.extend(["--keyring", gpgKeyringFile]) else: log.error("Failed to dearmor gpg signature file %s\n", (asciiSig)) def VerifySig(self, signature_file, signed_file): if not os.access(signature_file, os.F_OK): log.err("%s is bad. Can't proceed.\n" % (signature_file)) return False if not os.access(signed_file, os.F_OK): log.err("%s is bad. Can't proceed.\n" % (signed_file)) return False # INFO: Commands can escape and inject. So carefully craft the command # Thanks: Bernd Dietzel gpgvCmd = [] gpgvCmd.append(self.gpgv) gpgvCmd.extend(self.opts) gpgvCmd.append(signature_file) gpgvCmd.append(signed_file) return self.ExecSystemCmd(gpgvCmd, None) def VerifyFile(self, signed_file): # Verify a self signed file if not os.access(signed_file, os.F_OK): log.err("%s is bad. Can't proceed.\n" % (signed_file)) return False gpgvCmd = [] gpgvCmd.append(self.gpgv) gpgvCmd.extend(self.opts) gpgvCmd.append(signed_file) return self.ExecSystemCmd(gpgvCmd, None) class LockAPT: """Manipulate locks on the APT Database""" def __init__(self, lists, packages): try: self.listLock = os.open( lists, os.O_RDWR | os.O_TRUNC | os.O_CREAT, 0o640) self.pkgLock = os.open(packages, os.O_RDWR | os.O_TRUNC | os.O_CREAT, 0o640) except Exception: log.verbose(traceback.format_exc()) log.err("Couldn't open lockfile\n") sys.exit(1) def lockLists(self): try: fcntl.lockf(self.listLock, fcntl.LOCK_EX | fcntl.LOCK_NB) return True except Exception: log.verbose(traceback.format_exc()) return False def lockPackages(self): try: fcntl.lockf(self.pkgLock, fcntl.LOCK_EX | fcntl.LOCK_NB) return True except Exception: log.verbose(traceback.format_exc()) return False def unlockLists(self): try: fcntl.lockf(self.listLock, fcntl.LOCK_UN) return True except Exception: log.verbose(traceback.format_exc()) return False def unlockPackages(self): try: fcntl.lockf(self.pkgLock, fcntl.LOCK_UN) return True except Exception: log.verbose(traceback.format_exc()) return False class GenericDownloadFunction: def download_from_web(self, url, localFile, download_dir): """url = url to fetch localFile = file to save to donwload_dir = download path""" block_size = 4096 i = 0 counter = 0 os.chdir(download_dir) try: temp = urllib.request.urlopen(url) headers = temp.info() size = int(headers["Content-Length"]) # INFO: Add the download thread into the Global ProgressBar Thread self.addItem(size) except urllib.error.HTTPError as errstring: errfunc(errstring.code, errstring.reason, url) return False except urllib.error.URLError as errstring: # INFO: Weird. But in urllib2.URLError, I noticed that for # error type "timeouts", no errno was defined. # errstring.errno was listed as None # In my tests, wget categorized this behavior as: # 504: gateway timeout # So I am doing the same here. if errstring.errno is None: errfunc(504, errstring.reason, url) else: errfunc(errstring.errno, errstring.reason, url) return False except http.client.HTTPException as e: log.err("Type HTTPException occurred while processing url: %s\n" % (url)) log.err("Failed to download %s\n" % (localFile)) if type(e) is str: log.err(e) return False except http.client.BadStatusLine as e: # INFO: See Python Bug: https://bugs.python.org/issue8823 log.err("BadStatusLine exception: Python Bug 8823\n") log.err("Type BadStatusLine occurred while processing url: %s\n" % (url)) log.err("Failed to download %s\n" % (localFile)) if type(e) is str: log.err(e) return False except socket.timeout: log.err("Socket timeout. Skipping URL: %s\n" % (url)) return False data = open(localFile, "wb") socket_counter = 0 while i < size: socket_timeout = None try: data.write(temp.read(block_size)) except socket.timeout: socket_timeout = True socket_counter += 1 except socket.error: socket_timeout = True socket_counter += 1 if socket_counter == SOCKET_TIMEOUT_RETRY: errfunc( 101010, "Max timeout retry count reached. Discontinuing download.\n", url, ) # Clean the half downloaded file. data.close() os.unlink(localFile) return False if socket_timeout is True: errfunc(10054, "Socket Timeout. Retry - %d\n" % (socket_counter), url) continue increment = min(block_size, size - i) i += block_size counter += 1 self.updateValue(increment) # REAL_PROGRESS: update current total in totalSize if guiBool and not guiTerminateSignal: totalSize[1] += block_size if guiTerminateSignal: data.close() temp.close() return False self.completed() data.close() temp.close() return True # #FIXME: Find out optimal fix for this exception handling # except OSError as erret: # (errno, strerror) = erret # errfunc(errno, strerror, download_dir) # except IOError as e: # if hasattr(e, 'reason'): # log.err("%s\n" % (e.reason)) # if hasattr(e, 'code') and hasattr(e, 'reason'): # errfunc(e.code, e.reason, localFile) # except socket.timeout: # errfunc(10054, "Socket timeout.\n", url) class DownloadFromWebDisplay(AptOfflineLib.ProgressBar, GenericDownloadFunction): """Class for DownloadFromWeb This class also inherits progressbar functionalities from parent class, ProgressBar""" def __init__(self, width, total_items): """width = Progress Bar width""" AptOfflineLib.ProgressBar.__init__( self, width=width, total_items=total_items) class DownloadFromWebQuiet(AptOfflineLib.ProgressBar, GenericDownloadFunction): """Class for DownloadFromWeb This class also inherits progressbar functionalities from parent class, ProgressBar""" def __init__(self, width, total_items): """width = Progress Bar width""" AptOfflineLib.ProgressBar.__init__( self, width=width, total_items=total_items) def display(self): return def stripper(item): """Strips extra characters from "item". Breaks "item" into: url - The URL localFile - The actual package file size - The file size checksum - The checksum string and returns them.""" log.verbose("Item before split is: %s\n" % (item)) SplitItem = item.split(" ") url = SplitItem[0].strip("'").strip() localFile = SplitItem[1].strip("'").strip() size = SplitItem[2].strip("'").strip() try: checksum = SplitItem[3].strip("'").strip() except IndexError: checksum = None log.verbose("line %s is missing checksum entry" % (item)) log.verbose("Items after split is: %s\n" % (SplitItem)) # Convert size to integer size = int(size) if size.isdecimal() else 0 log.verbose("size of size is: %s\n" % (size)) return (url, localFile, size, checksum) def errfunc(errno, errormsg, filename): """We use errfunc to handler errors. There are some error codes (-3 and 13 as of now) which are temporary codes, they happen when there is a temporary resolution failure, for example. For such situations, we can't abort because the uri file might have other hosts also, which might be well accessible. This function does the job of behaving accordingly as per the error codes.""" retriable_error_codes = [-3, 13, 404, 403, 401, 429, 10060, 104, 101010] # 104, 'Connection reset by peer' # 504 is for gateway timeout # 429 Too Many Requests # 404 is for URL error. Page not found. # 401 is for Restricted pages # 10060 is for Operation Time out. There can be multiple reasons for this timeout # 101010 is for socket max retry count # 10054 is for Socket Timeout. Socket Timeout are seen during network congestion # TODO: Find out what these error codes are for # and better document them the next time you find it out. # 13 is for "Permission Denied" when you don't have privileges to access the destination if errno in retriable_error_codes: log.verbose( "%s - %s - %s %s\n" % (filename, errno, errormsg, LINE_OVERWRITE_FULL) ) log.verbose("Will still try with other package uris\n") elif errno == 10054: log.err("%s - %s - %s %s\n" % (filename, errno, errormsg, LINE_OVERWRITE_FULL)) elif errno == 504: log.err( "%s failed with error %s:%s %s\n" % (filename, errno, errormsg, LINE_OVERWRITE_FULL) ) errlist.append(filename) elif errno == 407 or errno == 2: # These, I believe are from OSError/IOError exception. # I'll document it as soon as I confirm it. log.err("%s\n" % (errormsg)) sys.exit(errno) elif errno == 1: log.err(errormsg) log.err("Explicit program termination %s\n" % (errno)) sys.exit(errno) else: log.err( "I don't understand this error code %s\nPlease file a bug report\n" % (errno) ) def fetcher(args): # get opts Str_GetArg = args.get Int_SocketTimeout = args.socket_timeout Str_DownloadDir = args.download_dir Str_CacheDir = args.cache_dir Bool_DisableMD5Check = args.disable_md5check Int_NumOfThreads = args.num_of_threads Str_BundleFile = args.bundle_file Str_ProxyHost = args.proxy_host Str_ProxyPort = args.proxy_port Str_HttpsCertFile = args.https_cert_file Str_HttpsKeyFile = args.https_key_file Str_HttpBasicAuth = args.http_basicauth Bool_DisableCertCheck = args.disable_cert_check Bool_BugReports = args.deb_bugs global guiTerminateSignal if Int_SocketTimeout: try: Int_SocketTimeout.__int__() socket.setdefaulttimeout(Int_SocketTimeout) log.verbose("Default timeout now is: %d.\n" % (socket.getdefaulttimeout())) except AttributeError: log.err("Incorrect value set for socket timeout.\n") sys.exit(1) if Str_ProxyHost: if Str_ProxyPort: log.verbose(Str_ProxyHost + ":" + str(Str_ProxyPort)) proxy_support = urllib.request.ProxyHandler( { "http": Str_ProxyHost + ":" + str(Str_ProxyPort), "https": Str_ProxyHost + ":" + str(Str_ProxyPort), } ) opener = urllib.request.build_opener(proxy_support) urllib.request.install_opener(opener) log.verbose( "Proxy successfully set up with Host %s and port %s\n" % (Str_ProxyHost, str(Str_ProxyPort)) ) else: proxy_support = urllib.request.ProxyHandler( {"http": Str_ProxyHost, "https": Str_ProxyHost} ) opener = urllib.request.build_opener(proxy_support) urllib.request.install_opener(opener) log.verbose( "Proxy successfully set up with Host %s and default port\n" % (Str_ProxyHost) ) if (Str_HttpsCertFile and Str_HttpsKeyFile) or Bool_DisableCertCheck: context = ssl.create_default_context() if Bool_DisableCertCheck: log.verbose("certificate checks for servers are ignored") context.check_hostname = False context.verify_mode = ssl.CERT_NONE if Str_HttpsCertFile and Str_HttpsKeyFile: log.verbose( "cert-file:" + Str_HttpsCertFile + " key-file:" + Str_HttpsKeyFile ) context.load_cert_chain(Str_HttpsCertFile, Str_HttpsKeyFile) opener = urllib.request.build_opener( urllib.request.HTTPSHandler(context=context) ) urllib.request.install_opener(opener) log.verbose( "SSL Client Authentication successfully set up with certificate file %s and key file %s\n" % (Str_HttpsCertFile, Str_HttpsKeyFile) ) if Str_HttpBasicAuth: # create a password manager password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() try: for authstr in Str_HttpBasicAuth: # Add the username and password. parsedUrl = urllib.parse.urlparse(authstr) username = parsedUrl.username password = urllib.parse.unquote(parsedUrl.password) top_level_url = parsedUrl.hostname password_mgr.add_password( None, top_level_url, username, password) log.verbose( "Added user %s with pass %s auth to domain %s\n" % (username, password, top_level_url) ) handler = urllib.request.HTTPBasicAuthHandler(password_mgr) # create "opener" (OpenerDirector instance) opener = urllib.request.build_opener(handler) # Install the opener. # Now all calls to urllib.request.urlopen use our opener. urllib.request.install_opener(opener) except: log.err("Incorrect value set for HttpBasicAuth.\n") sys.exit(1) # INFO: Python 2.5 has hashlib which supports sha256 # If we don't have Python 2.5, disable MD5/SHA256 checksum if AptOfflineLib.Python_2_5 is False: Bool_DisableMD5Check = True log.verbose( "\nMD5/SHA256 Checksum is being disabled. You need at least Python 2.5 to do checksum verification.\n" ) if Str_GetArg: if os.path.isfile(Str_GetArg): log.msg("\nFetching APT Data\n\n") else: log.err("File %s not present. Check path.\n" % (Str_GetArg)) sys.exit(1) if Str_CacheDir: if os.path.isdir(Str_CacheDir) is False: log.err( "WARNING: cache dir %s is incorrect. Did you give the full path ?\n" % (Str_CacheDir) ) sys.exit(1) if not Str_DownloadDir and not Str_BundleFile: log.err("Please provide a target download file/folder location.\n") sys.exit(1) if Str_DownloadDir: if os.path.exists(Str_DownloadDir): if os.path.isdir(Str_DownloadDir): if os.access(Str_DownloadDir, os.W_OK) is True: Str_DownloadDir = os.path.abspath(Str_DownloadDir) else: log.err("Cannot write to directory %s\n" % (Str_DownloadDir)) sys.exit(1) else: log.err("%s is not a directory\n" % (Str_DownloadDir)) sys.exit(1) else: os.mkdir(Str_DownloadDir) # INFO: Because the user may give in a relative path Str_DownloadDir = os.path.abspath(Str_DownloadDir) log.verbose("Creating directory %s\n" % (Str_DownloadDir)) else: tempdir = tempfile.gettempdir() if os.access(tempdir, os.W_OK): pidname = os.getpid() randomjunk = ( "".join(chr(random.randint(97, 122)) for x in range(5)) if guiBool else "" ) # 5 byte random junk to make mkdir possible multiple times # use-case -> download many sigs of different machines using one instance tempdir = os.path.join( tempdir, "apt-offline-downloads-" + str(pidname) + randomjunk ) os.mkdir(tempdir) Str_DownloadDir = os.path.abspath(tempdir) else: log.err("%s is not writable\n" % (tempdir)) errfunc(1, "", tempdir) if Str_BundleFile: Str_BundleFile = os.path.abspath(Str_BundleFile) if os.access(Str_BundleFile, os.F_OK): log.err("%s already present.\nRemove it first.\n" % (Str_BundleFile)) sys.exit(1) else: try: open(Str_BundleFile, "w") except IOError: log.err("Cannot write to file %s\n" % (Str_BundleFile)) sys.exit(1) else: Str_BundleFile = False if Bool_BugReports: if DebianBTS is True: Bool_BugReports = True else: log.err("Couldn't find debianbts module. Cannot fetch Bug Reports.\n") Bool_BugReports = False if args.quiet: DownloadFromWeb = DownloadFromWebQuiet else: DownloadFromWeb = DownloadFromWebDisplay class FetcherClass( DownloadFromWeb, AptOfflineLib.Archiver, AptOfflineLib.Checksum, AptOfflineLib.FileMgmt, FetchBugReports, ): def __init__(self, *args, **kwargs): DownloadFromWeb.__init__( self, width=kwargs.pop("width"), total_items=kwargs.pop("total_items") ) # ProgressBar.__init__(self, width) # self.width = width AptOfflineLib.Archiver.__init__(self, lock=kwargs.get("lock")) # self.lock = lock AptOfflineLib.FileMgmt.__init__(self) FetchBugReports.__init__( self, apt_bug_file_format, Str_BundleFile, lock=kwargs.get( "lock") ) # INFO: Bunch of important attributes self.CheckSum = Bool_DisableMD5Check self.BundleFile = Str_BundleFile self.BugReports = Bool_BugReports self.DownloadDir = Str_DownloadDir self.CacheDir = Str_CacheDir def verifyPayloadIntegrity(self, payload, checksum): """Verify the integrity of the payload against the checksum""" if self.CheckSum is True: return True if self.CheckHashDigest(payload, checksum): return True else: return False def writeData(self, data): """Write data to backend""" if self.BundleFile is not False: self.writeToArchive(data) else: self.writeToDir(data) def writeToDir(self, data): """Write data to directory""" self.copy_file(data, self.DownloadDir) def writeToArchive(self, data): """Write data to archive file""" try: self.compress_the_file(self.BundleFile, data) except AptOfflineErrors as message: log.warn("%s\n" % (message)) def writeToCache(self, data): """Write data to cacheDir""" if self.CacheDir: self.copy_file(data, self.CacheDir) def processBugReports(self, pkgName): """Process Bug Reports""" if not self.BugReports: return False log.msg("Fetching bug report for %s%s\n" % (pkgName, LINE_OVERWRITE_FULL)) # INFO: Payload is written to destination inside the method self.FetchBugsDebian(pkgName) log.success( "Fetched bug report for %s%s\n" % ( pkgName, LINE_OVERWRITE_FULL) ) def buildChangelog(self, pkgPath, installedVersion): """Return latest changes against installedVersion""" constChangelog = "changelog.Debian.gz" if PythonApt is not True: log.err("Cannot provide changelog feature\n") return False else: pkgHandle = DebPackage(pkgPath) for pkgFile in pkgHandle.filelist: if constChangelog in pkgFile: chlogFile = tempfile.NamedTemporaryFile( "r+", buffering=-1, encoding="utf-8", dir=None, delete=True ) pkgLogFile = open( os.path.join( tempfile.gettempdir(), pkgHandle.pkgname + ".changelog" ), "w", encoding="utf-8", ) # INFO: python-apt is able to read the data from the gzipped changelog dynamically try: chlogFile.writelines( pkgHandle.data_content(pkgFile)) except TypeError: log.warn( "Couldn't extract changelog for package %s\n" % (pkgHandle.pkgname) ) log.verbose(traceback.format_exc()) break chlogFile.flush() # Seek to beginning chlogFile.seek(0) if "Source" in pkgHandle: srcname = pkgHandle["Source"] else: srcname = pkgHandle.pkgname if " " in srcname: srcname = srcname.split(" ", 1)[0] installedVersion_changelog_line = "%s (%s) " % ( srcname, installedVersion, ) # FIXME: replace this with parsing the changelog using the Python debian module for eachLine in chlogFile.readlines(): if eachLine.startswith(installedVersion_changelog_line): break else: pkgLogFile.writelines(eachLine) pkgLogFile.flush() if self.ArchiveFile: if self.AddToArchive(self.ArchiveFile, pkgLogFile.name): log.verbose( "%s added to file %s\n" % (pkgLogFile.name, self.ArchiveFile) ) else: log.warn( "%s failed to be added to file %s\n" % (pkgLogFile.name, self.ArchiveFile) ) else: try: if self.move_file(pkgLogFile.name, self.DownloadDir): log.verbose( "%s added to download dir %s\n" % (pkgLogFile.name, self.DownloadDir) ) except AptOfflineLibShutilError as msg: log.warn("%s\n" % (msg)) chlogFile.close() pkgLogFile.close() break FetchData = {} # Info: Initialize an empty dictionary. # INFO: This key/val dict contains record of installed packages PackageInstalledVersion = {} # INFO: We don't distinguish in between what to fetch # We just rely on what a signature file lists us to get # It can be just debs or just package updates or both if Str_GetArg is not None: try: raw_data_list = open(Str_GetArg, "r").readlines() except IOError as e: (errno, strerror) = e.args log.err("%s %s %s\n" % (errno, strerror, Str_GetArg)) sys.exit(errno) FetchData["Item"] = [] for item in raw_data_list: if item.startswith("Changelog/"): (strConstant, pkgName, pkgVersion) = item.split("/") pkgVersion = pkgVersion.strip() PackageInstalledVersion[pkgName] = pkgVersion log.verbose( "Added package %s with version %s to dict\n" % ( pkgName, pkgVersion) ) else: # Interim fix for Debian bug #664654 # FIXME is this needed still with explicit install handling? # Will cause double syncing of files on install try: (ItemURL, ItemFile, ItemSize, ItemChecksum) = stripper(item) if not ItemURL.startswith(("http", "https", "ftp", "file")): log.err("This is a broken url: %s\n" % (ItemURL)) continue elif ItemURL.endswith("InRelease"): log.verbose("APT uses new InRelease auth mechanism\n") ExtraItemURL = ItemURL.rstrip(ItemURL.split("/")[-1]) GPGItemURL = "'" + ExtraItemURL + "Release.gpg" ReleaseItemURL = "'" + ExtraItemURL + "Release" ExtraItemFile = ItemFile.rstrip( ItemFile.split("_")[-1]) GPGItemFile = ExtraItemFile + "Release.gpg" ReleaseItemFile = ExtraItemFile + "Release" FetchData["Item"].append( GPGItemURL + " " + GPGItemFile + " " + str(ItemSize) ) log.verbose("Printing GPG URL/Files\n") log.verbose("%s %s" % (GPGItemURL, GPGItemFile)) FetchData["Item"].append( ReleaseItemURL + " " + ReleaseItemFile + " " + str(ItemSize) ) log.verbose("Printing Release URL/Files\n") log.verbose("%s %s" % (ReleaseItemURL, ReleaseItemFile)) FetchData["Item"].append(item) except IndexError: log.err( "Failed parisng file: %s, while processing line: %s\n" % (Str_GetArg, item) ) log.err(traceback.format_exc()) sys.exit(1) except ValueError: log.err("Cannot parse line: %s\n" % (item)) log.err(traceback.format_exc()) sys.exit(1) del raw_data_list # INFO: Let's get the total number of items. This will get the # correct total count in the progress bar. total_items = len(FetchData["Item"]) # BoolCheckSum=False, BoolBundleFile=False, BoolBugReports=False, BoolDownloadDir=False, BoolCacheDir=False): FetcherInstance = FetcherClass( Bool_DisableMD5Check, Str_BundleFile, Bool_BugReports, Str_DownloadDir, Str_CacheDir, total_items=total_items, width=30, lock=True, ) # INFO: Thread Support if Int_NumOfThreads > 2: log.msg("WARNING: If you are on a slow connection, it is good to\n") log.msg("WARNING: limit the number of threads to a low number like 2.\n") log.msg("WARNING: Else higher number of threads executed could cause\n") log.msg("WARNING: network congestion and timeouts.\n\n") def DataFetcher(request, response, func=FetcherInstance.find_first_match): """Get items from the request Queue, process them with func(), put the results along with the Thread's name into the response Queue. Stop running when item is None.""" # while 1: # tuple_item_key = request.get() # if tuple_item_key is None: # break # (key, item) = tuple_item_key (key, item) = request # INFO: Everything (url, pkgFile, download_size, checksum) = stripper(item) thread_name = threading.current_thread().name log.verbose("Thread is %s\n" % (thread_name)) if url.endswith(".deb"): try: PackageName = pkgFile.split("_")[0] except IndexError: log.err( "Not getting a package name here is problematic. Better bail out.\n" ) sys.exit(1) # INFO: For Package version, we don't want to fail try: PackageVersion = pkgFile.split("_")[1] except IndexError: PackageVersion = "NA" log.verbose( "Weird!! Package version not present. Is it really a deb file?\n" ) # INFO: find_first_match() returns False or a file name with absolute path full_file_path = func(Str_CacheDir, pkgFile) # INFO: If we find the file in the local Str_CacheDir, we'll execute this block. if full_file_path is not False: if FetcherInstance.verifyPayloadIntegrity(full_file_path, checksum): log.success( "%s found in cache%s\n" % ( PackageName, LINE_OVERWRITE_FULL) ) # INFO: When we copy the payload from the local cache, we need to update the progressbar # Hence we are doing it explicitly for local cache found files FetcherInstance.addItem(download_size) FetcherInstance.writeData(full_file_path) FetcherInstance.processBugReports(PackageName) FetcherInstance.updateValue(download_size) FetcherInstance.completed() if PackageName in list(PackageInstalledVersion.keys()): FetcherInstance.buildChangelog( full_file_path, PackageInstalledVersion[PackageName] ) else: log.verbose( "%s checksum mismatch. Skipping file %s\n" % (pkgFile, LINE_OVERWRITE_FULL) ) log.msg( "Downloading %s - %s %s\n" % ( PackageName, log.calcSize(download_size / 1024), LINE_OVERWRITE_FULL, ) ) if FetcherInstance.download_from_web(url, pkgFile, Str_DownloadDir): log.success("%s done %s\n" % (PackageName, LINE_OVERWRITE_FULL)) FetcherInstance.writeData(pkgFile) FetcherInstance.writeToCache(pkgFile) FetcherInstance.processBugReports(PackageName) FetcherInstance.updateValue(download_size) if PackageName in list(PackageInstalledVersion.keys()): FetcherInstance.buildChangelog( pkgFile, PackageInstalledVersion[PackageName] ) else: log.err("Failed to download %s\n" % (PackageName)) errlist.append(PackageName) else: log.msg( "Downloading %s - %s %s\n" % ( PackageName, log.calcSize(download_size / 1024), LINE_OVERWRITE_FULL, ) ) if FetcherInstance.download_from_web(url, pkgFile, Str_DownloadDir): log.success("%s done %s\n" % (PackageName, LINE_OVERWRITE_FULL)) FetcherInstance.writeData(pkgFile) FetcherInstance.writeToCache(pkgFile) FetcherInstance.processBugReports(PackageName) FetcherInstance.updateValue(download_size) if PackageName in list(PackageInstalledVersion.keys()): FetcherInstance.buildChangelog( pkgFile, PackageInstalledVersion[PackageName] ) else: log.err("Failed to download %s\n" % (PackageName)) errlist.append(PackageName) else: def DownloadPackages(PackageName, PackageFile): if FetcherInstance.download_from_web( PackageName, PackageFile, Str_DownloadDir ): log.success("%s done %s\n" % (PackageName, LINE_OVERWRITE_FULL)) FetcherInstance.writeData( os.path.join(Str_DownloadDir, PackageFile) ) FetcherInstance.updateValue(download_size) return True else: return False # INFO: Handle the multiple Packages formats. # See DTBS #583502 SupportedFormats = ["bz2", "gz", "xz", "lzma"] # INFO: We are a package update PackageName = url PackageFile = url.split("/")[-1] PackageFormat = PackageFile.split(".")[-1] if pkgFile.endswith(".gpg") or pkgFile.endswith("Release"): # Don't touch APT signature files and download them as is pkgFileWithType = pkgFile elif ( pkgFile.endswith(".dsc") or "orig.tar" in pkgFile or "debian.tar" in pkgFile ): # Same as above. # Don't touch Source Package's metadata files and download them as is pkgFileWithType = pkgFile else: # INFO: This whole circus needs to be fixed with something better # We do this because apt update's metadata can support different compression types on the remote repository pkgFileWithType = pkgFile + "." + \ url.split("/")[-1].split(".")[-1] if PackageFormat in SupportedFormats: # Remove the already tried format SupportedFormats.remove(PackageFormat) log.msg("Downloading %s %s\n" % (PackageName, LINE_OVERWRITE_FULL)) if ( DownloadPackages(PackageName, pkgFileWithType) is False and guiTerminateSignal is False ): # don't proceed retry if Ctrl+C in cli log.verbose( "%s failed. Retry with the remaining possible formats\n" % ( url) ) FetcherInstance.completed() # We could fail with the Packages format of what apt gave us. We can try the rest of the formats that apt or the archive could support reallyFailed = True for Format in SupportedFormats: NewPackageFile = ( pkgFileWithType.rstrip(pkgFileWithType.split(".")[-1]).rstrip( "." ) + "." + Format ) NewUrl = url.replace(PackageFormat, Format) log.verbose( "Retry download %s %s\n" % ( NewUrl, LINE_OVERWRITE_FULL) ) # INFO: Why are we doing this? # Because ProgressBar's total_item is fixed # And download_from_web's addItem() increases the active item upon every # cycle through apt's backend archive formats # This ends up resulting in active items being more than total_items # By increasing the counter, the active/total item list is reflected correctly FetcherInstance.items += 1 if DownloadPackages(NewUrl, NewPackageFile) is True: reallyFailed = False break else: log.verbose( "Failed with URL %s %s\n" % ( NewUrl, LINE_OVERWRITE_FULL) ) FetcherInstance.completed() if reallyFailed is True: log.verbose( "Giving up on URL %s %s\n" % ( NewUrl, LINE_OVERWRITE_FULL) ) # Create two Queues for the requests and responses requestQueue = queue.Queue() responseQueue = queue.Queue() # create size metadata for progress for key in list(FetchData.keys()): for item in FetchData.get(key): if guiBool: # REAL_PROGRESS: to calculate the total download size, NOTE: initially this was under the loop that Queued the items if guiTerminateSignal: break try: (url, pkgFile, download_size, checksum) = stripper(item) size = int(download_size) if size == 0: log.msg("MSG_START") temp = urllib.request.urlopen(url) headers = temp.info() size = int(headers["Content-Length"]) totalSize[0] += size except urllib.error.HTTPError as errstring: errfunc(errstring.code, errstring.reason, url) log.verbose(traceback.format_exc()) except urllib.error.URLError as errstring: if errstring.errno is None: errfunc(504, errstring.reason, url) else: errfunc(errstring.errno, errstring.reason, url) log.verbose(traceback.format_exc()) if not guiTerminateSignal: ConnectThread = AptOfflineLib.MyThread( DataFetcher, requestQueue, responseQueue, Int_NumOfThreads ) ConnectThread.startThreads() # Queue up the requests. # for item in raw_data_list: requestQueue.put(item) for key in list(FetchData.keys()): for item in FetchData.get(key): ConnectThread.populateQueue((key, item)) if guiBool: log.msg("MSG_END") guiMetaCompleted = True # For the sake of a responsive GUI while ConnectThread.threads_finished < ConnectThread.threads: # handle signals from gui here if guiTerminateSignal: # stop all ongoing work # TODO: find a way to stop those threads here ConnectThread.guiTerminateSignal = True for thread in ConnectThread.thread_pool: thread.guiTerminateSignal = True ConnectThread.stopThreads() ConnectThread.stopQueue(timeout=0.2) return ConnectThread.stopThreads() ConnectThread.stopQueue(timeout=0.2) # let them work for 0.2s log.msg("[%d/%d]" % (totalSize[1], totalSize[0])) else: # else go by the normal CLI way while ConnectThread.threads_finished < ConnectThread.threads: try: ConnectThread.stopThreads() ConnectThread.stopQueue(0.2) except KeyboardInterrupt: # user pressed Ctrl-c, signal all threads to exit guiTerminateSignal = True # this would signal download_from_web to stop ConnectThread.guiTerminateSignal = True for thread in ConnectThread.thread_pool: thread.guiTerminateSignal = True # tell all threads to exit ConnectThread.stopThreads() ConnectThread.stopQueue() log.err("\nInterrupted by user. Exiting!\n") sys.exit(0) if args.bundle_file: log.msg("\nDownloaded data to %s\n" % (Str_BundleFile)) else: log.msg("\nDownloaded data to %s\n" % (Str_DownloadDir)) # Print the failed files if len(errlist) > 0: log.err("Some items failed to download. Downloaded data may be incomplete\n") log.err("Please run in verbose mode to see details about failed items\n") log.msg("\n\n") log.verbose("The following files failed to be downloaded.\n") log.verbose( "Not all errors are fatal. For eg. Translation files are not present on all mirrors.\n" ) for error in errlist: log.err("%s failed.\n" % (error)) sys.exit(100) else: sys.exit(0) def installer(args): class InstallerClass( AptOfflineLib.Archiver, AptOfflineLib.Checksum, AptOfflineLib.FileMgmt, LockAPT ): def __init__(self, args): # install opts self.Str_InstallArg = args.install self.Bool_TestWindows = args.install_simulate self.Bool_SkipBugReports = args.skip_bug_reports self.Bool_Untrusted = args.allow_unauthenticated self.Str_InstallSrcPath = args.install_src_path self.Bool_SkipChangelog = args.skip_changelog self.tempdir = tempfile.gettempdir() self.Bool_StrictDebCheck = args.strict_deb_check self.extra_keyring = args.extra_keyring if not os.access(self.tempdir, os.W_OK): log.err( "Temporary path %s in not writable. Some functionality may fail\n" ) return False AptOfflineLib.Archiver.__init__(self) LockAPT.__init__(self, apt_lists_lock, apt_packages_lock) if MagicLib is False: log.err("Please ensure libmagic is installed\n") return False self.magicMIME = AptOfflineMagicLib.open( AptOfflineMagicLib.MAGIC_MIME_TYPE) if self.Str_InstallSrcPath is None: pidname = os.getpid() randomjunk = ( "".join(chr(random.randint(97, 122)) for x in range(5)) if guiBool else "" ) # 5 byte random junk to make mkdir possible multiple times # use-case -> installing multiple bundles with one dialog srcDownloadsPath = os.path.join( self.tempdir, "apt-offline-src-downloads-" + str(pidname) + randomjunk, ) os.mkdir(srcDownloadsPath) self.Str_InstallSrcPath = os.path.abspath(srcDownloadsPath) if not os.path.isdir(self.Str_InstallSrcPath): log.err("Not a folder.\n") return False if os.access(self.Str_InstallSrcPath, os.W_OK) is not True: log.err("%s is not writable.\n" % (self.Str_InstallSrcPath)) return False if self.Bool_StrictDebCheck: # Force copying of debs to apt_package_target_path, which should be the 'partial/' folder self.apt_package_path = apt_package_target_path else: self.apt_package_path = apt_package_final_path if self.Bool_TestWindows: pidname = os.getpid() tempdir = os.path.join( self.tempdir, "apt-package-target-path-" + str(pidname) ) log.verbose("apt-package-target-path is %s\n" % (tempdir)) os.mkdir(tempdir) self.apt_package_path = os.path.abspath(tempdir) tempdir = os.path.join( self.tempdir, "apt-update-target-path-" + str(pidname) ) log.verbose("apt-update-target-path is %s\n" % (tempdir)) os.mkdir(tempdir) self.apt_update_target_path = os.path.abspath(tempdir) tempdir = os.path.join( self.tempdir, "apt-update-final-path-" + str(pidname) ) log.verbose("apt-update-final-path is %s\n" % (tempdir)) os.mkdir(tempdir) self.apt_update_final_path = os.path.abspath(tempdir) else: self.apt_update_target_path = apt_update_target_path self.apt_update_final_path = apt_update_final_path try: if os.geteuid() != 0: log.err( "EACCES: You need superuser privileges to execute this option\n" ) sys.exit(13) except AttributeError: log.err( "EOPNOTSUPP: Unsupported platform: %s\n" % ( platform.platform()) ) sys.exit(95) def verifyPayloadIntegrity(self, payload, checksum, checksumType): """Verify the integrity of the payload against the checksum""" if self.HashMessageDigestAlgorithms(checksum, checksumType, payload): return True else: return False def cleanAptPartial(self, path): self.lockLists() for eachPath in os.listdir(path): eachPath = os.path.abspath(eachPath) os.unlink(eachPath) self.unlockLists() def display_options(self, dispType): log.msg("(Y) Yes. Proceed with installation\n") log.msg("(N) No, Abort.\n") if dispType == "BugReports": log.msg("(R) Redisplay the list of bugs.\n") log.msg( "(Bug Number) Display the bug report from the Offline Bug Reports.\n" ) elif dispType == "Chlog": log.msg("(C) Display changelog\n") log.msg("(?) Display this help message.\n") def get_response(self): response = input("What would you like to do next:\t (y, N, ?)") response = response.rstrip("\r") return response def list_bugs(self, dictList): """ Takes a dictionary of key,value pair where: key => filename value => subject string """ log.msg("\n\nFollowing are the list of bugs present.\n") sortedKeyList = list(dictList.keys()) sortedKeyList.sort() for each_bug in sortedKeyList: # INFO: '{}' is the bug split identifier - Used at another place also pkg_name = each_bug.split("{}")[-3].split("/")[-1] bug_num = each_bug.split("{}")[-2] bug_subject = dictList[each_bug] log.msg("%s\t%s\t%s\n" % (bug_num, pkg_name, bug_subject)) def magic_check_and_uncompress(self, archive_file=None, filename=None): self.magicMIME.load() retval = False if ( self.magicMIME.file(archive_file) == "application/x-bzip2" or self.magicMIME.file(archive_file) == "application/gzip" or self.magicMIME.file(archive_file) == "application/x-xz" ): temp_filename = os.path.join( self.apt_update_target_path, filename + app_name ) if self.magicMIME.file(archive_file) == "application/x-bzip2": retval = self.decompress_the_file( archive_file, temp_filename, "bzip2" ) filename = filename.rstrip(".bz2") filename = filename.rstrip(".bzip") elif self.magicMIME.file(archive_file) == "application/gzip": retval = self.decompress_the_file( archive_file, temp_filename, "gzip" ) filename = filename.rstrip(".tar.gz") filename = filename.rstrip(".gz") elif self.magicMIME.file(archive_file) == "application/x-xz": retval = self.decompress_the_file( archive_file, temp_filename, "xz") filename = filename.rstrip(".xz") else: log.verbose("No filetype match for %s\n" % (filename)) retval = False filename = os.path.join(self.apt_update_final_path, filename) if retval is True: os.rename(temp_filename, filename) os.chmod(filename, 0o644) log.verbose("Synchronized file to %s\n" % (filename)) else: log.err("Failed to sync file %s\n" % (filename)) try: os.unlink(temp_filename) except OSError: log.warn( "Failed to unlink temporary file %s. Check respective decompressor library support\n" % (temp_filename) ) elif ( self.magicMIME.file( archive_file) == "application/x-gnupg-keyring" or self.magicMIME.file(archive_file) == "application/pgp-signature" or self.magicMIME.file(archive_file) == "text/PGP" ): gpgFile = os.path.join(self.apt_update_final_path, filename) shutil.copy2(archive_file, gpgFile) os.chmod(gpgFile, 0o644) # PGP armored data should be bypassed log.verbose("File is %s, hence 'True'.\n" % (filename)) retval = True elif ( self.magicMIME.file(archive_file) == "application/vnd.debian.binary-package" or self.magicMIME.file(archive_file) == "application/x-debian-package" ): debFile = os.path.join(self.apt_package_path, filename) if os.access(self.apt_package_path, os.W_OK): shutil.copy2(archive_file, debFile) os.chmod(debFile, 0o644) log.success("%s file synced.\n" % (filename)) log.verbose("%s file synced.\n" % (debFile)) retval = True else: log.err( "Cannot write to target path %s\n" % ( self.apt_package_path) ) sys.exit(1) elif filename.endswith(apt_bug_file_format): pass elif self.magicMIME.file(archive_file) == "text/plain": txtFile = os.path.join(self.apt_update_final_path, filename) if os.access(self.apt_update_final_path, os.W_OK): shutil.copy(archive_file, txtFile) os.chmod(txtFile, 0o644) retval = True else: log.err( "Cannot write to target path %s\n" % (self.apt_update_final_path) ) sys.exit(1) else: log.err( "I couldn't understand file %s of type %s.\n" % (filename, self.magicMIME.file(archive_file)) ) # INFO: Close the handle and conserve precious memory # self.magicMIME.close() if retval: # CHANGE: track progress totalSize[0] += 1 if guiBool: log.msg("[%d/%d]" % (totalSize[0], totalSize[1])) # ENDCHANGE log.verbose("%s file synced to APT.\n" % (filename)) def displayChangelog(self, dataType=None): """Takes file or directory as input""" chlogFile = tempfile.NamedTemporaryFile() chlogPresent = False if os.path.isdir(dataType): for eachItem in os.listdir(dataType): eachItem = os.path.join(dataType, eachItem) if eachItem.endswith(".changelog"): eachFile = open(eachItem, "rb") chlogFile.write(eachFile.read()) chlogPresent = True elif os.path.isfile(dataType): zipLogFile = zipfile.ZipFile(dataType) for filename in zipLogFile.namelist(): if filename.endswith(".changelog"): chlogFile.write(zipLogFile.read(filename)) chlogPresent = True else: return False if chlogPresent is False: log.verbose("No changelog available\n") response = "y" else: chlogFile.seek(0) pydoc.pager(chlogFile.read().decode("utf-8")) self.display_options("Chlog") response = self.get_response() while True: if response == "?": self.display_options("Chlog") response = self.get_response() elif response.startswith("C") or response.startswith("c"): chlogFile.seek(0) pydoc.pager(chlogFile.read().decode("utf-8")) self.display_options("Chlog") response = self.get_response() elif response.startswith("y") or response.startswith("Y"): log.msg("Proceeding with installation\n") break else: log.err("Aborting installation, on user request\n") sys.exit(1) def displayBugs(self, dataType=None): """Takes keywords "file" or "dir" as type input""" if dataType is None: return False # Display the list of bugs self.list_bugs(bugs_number) self.display_options("BugReports") response = self.get_response() while True: if response == "?": self.display_options("BugReports") response = self.get_response() elif response.startswith("y") or response.startswith("Y"): if dataType == "file": for filename in zipBugFile.namelist(): # INFO: Take care of Src Pkgs found = False for item in list(SrcPkgDict.keys()): if filename in SrcPkgDict[item]: found = True break data = tempfile.NamedTemporaryFile() try: data.file.write(zipBugFile.read(filename)) data.file.flush() except (zipfile.BadZipfile, zlib.error): log.warn( "Failed to read archive file: %s\nContinuing with the rest\n" % (filename) ) log.verbose(traceback.format_exc()) continue # INFO: We can't ranosm the entire payload for a bad CRC for individual files. # The same zip archive, if unarchived with plain unix unizp, works file. # On the internet, there are many bug reports of Python's zipfile having certain bugs. # Hence we continue hoping to milk the possible payloads from the archive archive_file = data.name if ( found is True ): # found is True. That means this is a src package shutil.copy2( archive_file, os.path.join( self.Str_InstallSrcPath, filename), ) log.msg( "Installing src package file %s to %s.\n" % (filename, self.Str_InstallSrcPath) ) continue if self.Bool_TestWindows: log.verbose( "In simulate mode. No locking required\n") elif self.lockPackages() is False: log.err( "Couldn't acquire lock on %s\nIs another apt process running?\n" % (archive_file) ) sys.exit(1) self.magic_check_and_uncompress( archive_file, filename) if self.Bool_TestWindows: log.verbose( "In simulate mode. No locking required\n") else: self.unlockLists() data.file.close() sys.exit(0) if dataType == "dir": if DirInstallPackages(self.Str_InstallArg) is True: sys.exit(0) else: log.err( "Failed during install operation on %s.\n" % (self.Str_InstallArg) ) sys.exit(1) elif response.startswith("n") or response.startswith("N"): log.err("Exiting gracefully on user request.\n\n") sys.exit(0) elif response.isdigit() is True: found = False for full_bug_file_name in bugs_number: full_bug_number = full_bug_file_name.split("{}")[1] if response == full_bug_number: bug_file_to_display = full_bug_file_name found = True break if found == False: log.err("Incorrect bug number %s provided.\n" % (response)) response = self.get_response() if found: if dataType == "file": pydoc.pager( zipBugFile.read( bug_file_to_display).decode("utf-8") ) # Redisplay the menu # FIXME: See a pythonic possibility of cleaning the screen at this stage response = self.get_response() if dataType == "dir": tempFile = open(bug_file_to_display, "r") pydoc.pager(tempFile.read()) # Redisplay the menu # FIXME: See a pythonic possibility of cleaning the screen at this stage response = self.get_response() elif response.startswith("r") or response.startswith("R"): self.list_bugs(bugs_number) response = self.get_response() else: log.err("Incorrect choice. Exiting\n") sys.exit(1) def verifyAptFileIntegrity(self, FileList): self.AptSecure = APTVerifySigs( keyring=InstallerInstance.extra_keyring, Simulate=InstallerInstance.Bool_TestWindows, ) self.lFileList = FileList self.lFileList.sort() self.lVerifiedWhitelist = [] self.checksumList = [] self.checksumHeader = "SHA256:" # Dictionary of header lines mapped to supported checksum types self.releaseHeaders = {"SHA256:": "sha256", "SHA512:": "sha512"} for localFile in self.lFileList: if localFile.endswith(".gpg"): log.verbose("%s\n" % (localFile)) gpgFile = os.path.abspath(localFile) if self.AptSecure.VerifySig(gpgFile, gpgFile.rstrip(".gpg")): self.lVerifiedWhitelist.append(gpgFile) self.lVerifiedWhitelist.append(gpgFile.rstrip(".gpg")) log.verbose("%s is gpg clean\n" % (gpgFile.rstrip(".gpg"))) # Let's build a list of all checksums whose gpg signature was clean # INFO: We take the Release file and mark the start with keyword "SHA256Sum:" # and we keep track of the lines (' checksum') # We declare the end as soon as we don't find the line in the format: (' checksum') releaseFile = open(gpgFile.rstrip(".gpg"), "r") for line in releaseFile.readlines(): if line.startswith(self.checksumHeader): startTrack = True if ( not line.startswith(" ") and self.checksumHeader not in line ): startTrack = False if startTrack: try: (checksum, size, files) = line.split() except: log.verbose( "This error should be ignored\n") continue aptPkgFile = gpgFile.rstrip("Release.gpg") # Remove the trailing _ underscore aptPkgFile = aptPkgFile[:-1] aptPkgFile = aptPkgFile.split("/")[-1] aptPkgFile = aptPkgFile + "_" + \ files.replace("/", "_") self.checksumList.append( [checksum, size, aptPkgFile, "sha256"] ) else: # Bad sig. log.err( "%s bad signature. Not syncing because in strict mode.\n" % (localFile) ) elif localFile.endswith("InRelease"): # TODO any value in parsing multiple header sections? absFile = os.path.abspath(localFile) # File should be self signed, check if self.AptSecure.VerifyFile(absFile): log.verbose("%s is gpg clean\n" % (absFile)) self.lVerifiedWhitelist.append(absFile) # Parse list of packages and their checksum releaseFile = open(absFile, "r") for line in releaseFile.readlines(): # Check if the line starts with a supported header foundHeader = False for header in self.releaseHeaders.keys(): if line.startswith(header): checksumType = self.releaseHeaders[header] startTrack = True foundHeader = True break if not foundHeader and not line.startswith(" "): startTrack = False if startTrack: try: (checksum, size, files) = line.split() except: log.verbose( "This error should be ignored\n") continue # And add the package file to list to checksum aptPkgFile = absFile.rstrip("InRelease") # Remove the trailing _ underscore aptPkgFile = aptPkgFile[:-1] aptPkgFile = aptPkgFile.split("/")[-1] aptPkgFile = aptPkgFile + "_" + \ files.replace("/", "_") self.checksumList.append( [checksum, size, aptPkgFile, checksumType] ) else: # Bad sig. log.err( "%s bad signature. Not syncing because in strict mode.\n" % (localFile) ) # INFO: It is inefficient and being run twice, but we need to do so to do a match # for the proper checksummed files for localFile in self.lFileList: for checksumItem in self.checksumList: if os.path.basename(localFile) == checksumItem[2]: if InstallerInstance.verifyPayloadIntegrity( localFile, checksumItem[0], checksumItem[3] ): log.verbose( "localFile %s Integrity with checksum %s matches\n" % (localFile, checksumItem[0]) ) self.lVerifiedWhitelist.append(localFile) else: log.verbose( "localFile %s integrity doesn't match to checksum %s\n" % (localFile, checksumItem[0]) ) return self.lVerifiedWhitelist def installVerifiedList(self, verifiedWhiteList, masterList): # INFO: Above, when verifying the integritiy of the Release Files, we built a list of the names of aptPkgFiles # which were gpg clean. # And in lFileList, we have the full list of aptPkgFiles that have been downloaded # Below, we loop through both the list's items and match out gpg clean list item's name to be present in # lFileList's items for whitelist_item in verifiedWhiteList: for final_item in masterList: if whitelist_item == final_item: self.magic_check_and_uncompress( final_item, os.path.basename(final_item) ) log.success("%s synced.\n" % (os.path.basename(final_item))) return True def listdir_fullpath(self, d): return [os.path.join(d, f) for f in os.listdir(d)] InstallerInstance = InstallerClass(args) installPath = InstallerInstance.Str_InstallArg if os.path.isfile(installPath): # INFO: For now, we support zip bundles only try: zipBugFile = zipfile.ZipFile(installPath, "r") except zipfile.BadZipfile: log.err("File %s is not a valid zip file\n" % (installPath)) sys.exit(1) # CHANGE: for progress tracking totalSize[1] = len(zipBugFile.namelist()) totalSize[0] = 0 # ENDCHANGE # INFO: Handle source packages with care. # Build a dict and populate its files based on details in .dsc SrcPkgDict = {} # TODO: Refactor this loop for filename in zipBugFile.namelist(): if filename.endswith(".dsc"): SrcPkgName = filename.split("_")[0] temp = tempfile.NamedTemporaryFile() temp.file.write(zipBugFile.read(filename)) temp.file.flush() temp.file.seek(0) # Let's go back to the start of the file SrcPkgDict[SrcPkgName] = [] marker = None for SrcPkgIdentifier in temp.file.readlines(): SrcPkgIdentifier = SrcPkgIdentifier.decode("utf-8") if SrcPkgIdentifier.startswith("Files:"): marker = True continue if SrcPkgIdentifier.startswith( "\n" ) or not SrcPkgIdentifier.startswith(" "): marker = False continue if marker is True: SrcPkgData = SrcPkgIdentifier.split()[2].rstrip("\n") if SrcPkgData in SrcPkgDict[SrcPkgName]: break else: SrcPkgDict[SrcPkgName].append(SrcPkgData) SrcPkgDict[SrcPkgName].append(filename) temp.file.close() # Let's display changelog if InstallerInstance.Bool_SkipChangelog: log.verbose("Skipping display of changelog as requested\n") else: InstallerInstance.displayChangelog( InstallerInstance.Str_InstallArg) bugs_number = {} if InstallerInstance.Bool_SkipBugReports: log.verbose("Skipping bug report check as requested") else: for filename in zipBugFile.namelist(): if filename.endswith(apt_bug_file_format): temp = tempfile.NamedTemporaryFile() temp.file.write(zipBugFile.read(filename)) temp.file.flush() temp.file.seek(0) # Let's go back to the start of the file for bug_subject_identifier in temp.file.readlines(): bug_subject_identifier = bug_subject_identifier.decode( "utf-8") if bug_subject_identifier.startswith("Subject:"): subject = bug_subject_identifier.lstrip( bug_subject_identifier.split(":")[0] ) subject = subject.rstrip("\n") break bugs_number[filename] = subject temp.file.close() log.verbose(str(bugs_number) + "\n") if bugs_number: InstallerInstance.displayBugs(dataType="file") else: log.verbose( "Great!!! No bugs found for all the packages that were downloaded.\n\n" ) FileList = [] tempDir = tempfile.mkdtemp() InstallerInstance.magicMIME.load() for filename in zipBugFile.namelist(): # INFO: Take care of Src Pkgs found = False for item in list(SrcPkgDict.keys()): if filename in SrcPkgDict[item]: found = True break tempZipFile = os.path.join(tempDir, filename) data = open(tempZipFile, "wb") data.write(zipBugFile.read(filename)) data.flush() archive_file = tempZipFile if ( found is True ): # We are src packages. And don't need a lock on the APT Database shutil.copy2( archive_file, os.path.join( InstallerInstance.Str_InstallSrcPath, filename), ) log.msg( "Installing src package file %s to %s.\n" % (filename, InstallerInstance.Str_InstallSrcPath) ) continue FileList.append(archive_file) # INFO: Integrity of the .deb packages is delegated to apt on the target system # We just copy the files to partial/ and apt will only use it if it meets all integrity checks if filename.endswith(".deb"): if ( InstallerInstance.magicMIME.file(archive_file) == "application/vnd.debian.binary-package" or InstallerInstance.magicMIME.file(archive_file) == "application/x-debian-package" ): debFile = os.path.join( InstallerInstance.apt_package_path, filename ) if os.access(InstallerInstance.apt_package_path, os.W_OK): shutil.copy2(archive_file, debFile) os.chmod(debFile, 0o644) log.success("%s file synced.\n" % (filename)) log.verbose("%s file synced.\n" % (debFile)) # InstallerInstance.magicMIME.close() if InstallerInstance.Bool_Untrusted: log.err( "Disabling apt gpg check can risk your machine to compromise.\n" ) if not InstallerInstance.installVerifiedList(FileList, FileList): log.err("Failed to synchronize the APT payload\n") sys.exit(1) else: verifiedList = InstallerInstance.verifyAptFileIntegrity( FileList) if not InstallerInstance.installVerifiedList(verifiedList, FileList): log.err("Failed to synchronize the APT payload\n") sys.exit(1) elif os.path.isdir(installPath): def DirInstallPackages(InstallDirPath): for eachfile in os.listdir(InstallDirPath): filename = eachfile FullFileName = os.path.abspath( os.path.join(InstallDirPath, eachfile)) if os.path.isdir(FullFileName): log.verbose("Skipping!! %s is a directory\n" % (FullFileName)) continue # INFO: Take care of Src Pkgs found = False for item in list(SrcPkgDict.keys()): if filename in SrcPkgDict[item]: found = True break if found is True: shutil.copy2( FullFileName, InstallerInstance.Str_InstallSrcPath) log.msg( "Installing src package file %s to %s.\n" % (filename, InstallerInstance.Str_InstallSrcPath) ) continue # INFO: Take care of all remaining deb packages if eachfile.endswith(".deb"): InstallerInstance.magic_check_and_uncompress( FullFileName, filename) return True # TODO: Needs refactoring with the previous common code SrcPkgDict = {} for filename in os.listdir(installPath): if filename.endswith(".dsc"): SrcPkgName = filename.split("_")[0] SrcPkgDict[SrcPkgName] = [] Tempfile = os.path.join(installPath, filename) temp = open(Tempfile, "r") marker = None for SrcPkgIdentifier in temp.readlines(): if SrcPkgIdentifier.startswith("Files:"): marker = True continue if SrcPkgIdentifier.startswith( "\n" ) or not SrcPkgIdentifier.startswith(" "): marker = False continue if marker is True: SrcPkgData = SrcPkgIdentifier.split()[2].rstrip("\n") if SrcPkgData in SrcPkgDict[SrcPkgName]: break else: SrcPkgDict[SrcPkgName].append(SrcPkgData) SrcPkgDict[SrcPkgName].append(filename) temp.close() # Let's display changelog if InstallerInstance.Bool_SkipChangelog: log.verbose("Skipping display of changelog as requested\n") else: InstallerInstance.displayChangelog( InstallerInstance.Str_InstallArg) bugs_number = {} if InstallerInstance.Bool_SkipBugReports: log.verbose("Skipping bug report check as requested") else: for filename in os.listdir(installPath): if filename.endswith(apt_bug_file_format): filename = os.path.join(installPath, filename) temp = open(filename, "r") for bug_subject_identifier in temp.readlines(): if bug_subject_identifier.startswith("Subject:"): subject = bug_subject_identifier.lstrip( bug_subject_identifier.split(":")[0] ) subject = subject.rstrip("\n") break bugs_number[filename] = subject temp.close() log.verbose(str(bugs_number) + "\n") if bugs_number: InstallerInstance.displayBugs(dataType="dir") else: log.verbose( "Great!!! No bugs found for all the packages that were downloaded.\n\n" ) if InstallerInstance.Bool_Untrusted: log.err("Disabling apt gpg check can risk your machine to compromise.\n") for x in os.listdir(installPath): if x.endswith(apt_bug_file_format) or x.endswith(".deb"): pass else: x = os.path.join(installPath, x) # Do we do a move ?? shutil.copy2(x, InstallerInstance.apt_update_final_path) log.verbose( "%s synced to %s\n" % (x, InstallerInstance.apt_update_final_path) ) log.success("%s synced.\n" % (x)) else: lFileList = InstallerInstance.listdir_fullpath(installPath) verifiedList = InstallerInstance.verifyAptFileIntegrity(lFileList) if not InstallerInstance.installVerifiedList(verifiedList, lFileList): log.err("Failed to synchronize the APT payload\n") sys.exit(1) # Call for processing the debs and source package metadata DirInstallPackages(installPath) else: log.err("Invalid path argument specified: %s\n" % (installPath)) sys.exit(1) def setter(args): # log.verbose(str(args)) # commented to keep setter UI sane for time Str_SetArg = args.set List_SetInstallPackages = args.set_install_packages List_SetInstallSrcPackages = args.set_install_src_packages Str_SetInstallRelease = args.set_install_release Bool_SetUpdate = args.set_update Bool_SetUpgrade = args.set_upgrade Str_SetUpgradeType = args.upgrade_type Bool_SrcBuildDep = args.src_build_dep Bool_TestWindows = args.set_simulate Bool_Changelog = args.generate_changelog if ( Bool_SetUpdate is False and Bool_SetUpgrade is False and List_SetInstallPackages is None and List_SetInstallSrcPackages is None ): log.err("No option specified to set command\n") sys.exit(1) # FIXME: We'll use python-apt library to make it cleaner. # For now, we need to set markers using shell variables. if os.path.isfile(Str_SetArg): try: os.unlink(Str_SetArg) except OSError: log.err("Cannot remove file %s.\n" % (Str_SetArg)) # Pick apt backend based on what option the user chose AptInst = AptManip(Str_SetArg, Simulate=Bool_TestWindows, AptType=args.apt_backend) if Bool_SetUpdate: if platform.system() in supported_platforms: if not Bool_TestWindows and os.geteuid() != 0: log.err( "This option requires super-user privileges. Execute as root or use sudo/su\n" ) sys.exit(1) else: if AptInst.Update() is False: sys.exit(1) else: log.err( "This argument is supported only on Unix like systems with apt installed\n" ) sys.exit(1) if Bool_SetUpgrade: if platform.system() in supported_platforms: if not Bool_TestWindows and os.geteuid() != 0: log.err( "This option requires super-user privileges. Execute as root or use sudo/su" ) sys.exit(1) # TODO: Use a more Pythonic way for it if Str_SetUpgradeType == "upgrade": if ( AptInst.Upgrade( "upgrade", ReleaseType=Str_SetInstallRelease) is False ): sys.exit(1) elif Str_SetUpgradeType == "dist-upgrade": if ( AptInst.Upgrade( "dist-upgrade", ReleaseType=Str_SetInstallRelease) is False ): sys.exit(1) elif Str_SetUpgradeType == "dselect-upgrade": if ( AptInst.Upgrade( "dselect-upgrade", ReleaseType=Str_SetInstallRelease ) is False ): sys.exit(1) else: log.err( "Invalid upgrade argument type selected\nPlease use one of, upgrade/dist-upgrade/dselect-upgrade\n" ) else: log.err( "This argument is supported only on Unix like systems with apt installed\n" ) sys.exit(1) if List_SetInstallPackages != None and List_SetInstallPackages != []: if platform.system() in supported_platforms: if not Bool_TestWindows and os.geteuid() != 0: log.err( "This option requires super-user privileges. Execute as root or use sudo/su\n" ) sys.exit(1) if ( AptInst.InstallPackages( List_SetInstallPackages, Str_SetInstallRelease) is False ): sys.exit(1) else: log.err( "This argument is supported only on Unix like systems with apt installed\n" ) sys.exit(1) if List_SetInstallSrcPackages != None and List_SetInstallSrcPackages != []: if platform.system() in supported_platforms: if ( AptInst.InstallSrcPackages( List_SetInstallSrcPackages, Str_SetInstallRelease, Bool_SrcBuildDep ) is False ): sys.exit(1) else: log.err( "This argument is supported only on Unix like systems with apt installed\n" ) sys.exit(1) if Bool_Changelog: if not PythonApt: # INFO: No crude ways. Will only work with python-apt log.err("Cannot provide changelog without proper backend support\n") sys.exit(1) log.verbose("Initializing apt cache\n") aptCache = apt.Cache() aptCache.open() try: sigFile = open(Str_SetArg, "r+") except Exception: log.err(traceback.format_exc()) for eachLine in sigFile.readlines(): (pkgUrl, pkgFile, pkgSize, pkgChecksum) = stripper(eachLine) pkgName = pkgFile.split("_")[0] try: pkgMeta = aptCache[pkgName] pkgInstalledVersion = pkgMeta.installed.version except AttributeError: log.verbose("Package %s is not installed. Thus no changelog\n") continue except KeyError: # INFO: For --update, we'll get an obvious key error because those aren't # packages, but just source URLs. log.verbose( "Cannot find package %s in package cache\n" % (pkgName)) continue except Exception: log.err(traceback.format_exc()) raise # INFO: '/' will be the delimiter log.verbose( "Writing to Changelog, pkgName: %s, pkgInstalledVersion %s\n" % (pkgName, pkgInstalledVersion) ) sigFile.writelines("Changelog/%s/%s\n" % (pkgName, pkgInstalledVersion)) if not Bool_TestWindows: if os.path.getsize(Str_SetArg) == 0: log.err("Generated signature file %s is of 0 bytes\n" % (Str_SetArg)) log.err( "This is usually the case when the underneath apt system has no payload to download\n" ) sys.exit(1) def main(): """Here we basically do the sanity checks, some validations and then accordingly call the corresponding functions. Contains most of the variables that are required by the application to run. Also does command-line option parsing and variable validation.""" # INFO: One way to handle global options in argparse so that they are available to # subparsers also # Global options global_options = argparse.ArgumentParser(add_help=False) global_options.add_argument( "--verbose", dest="verbose", help="Enable verbose messages", action="store_true" ) global_options.add_argument( "--quiet", dest="quiet", help="Enable quiet mode", action="store_true" ) if float(argparse.__version__) >= 1.1: parser = argparse.ArgumentParser( prog=app_name, description="Offline APT Package Manager" + " - " + version, epilog=myCopyright + " - " + terminal_license, parents=[global_options], ) parser.add_argument("-v", "--version", action="version", version=version) else: # Remain backward compatible with older argparse versions parser = argparse.ArgumentParser( prog=app_name, version=app_name + " - " + version, description="Offline APT Package Manager", epilog=myCopyright + " - " + terminal_license, parents=[global_options], ) # We need subparsers for set/get/install subparsers = parser.add_subparsers() # SET command options # parser_set = subparsers.add_parser("set", parents=[global_options]) parser_set.set_defaults(func=setter) parser_set.add_argument( "set", help="Generate a signature file", action="store", type=str, metavar="apt-offline.sig", default="apt-offline.sig", ) parser_set.add_argument( "--simulate", dest="set_simulate", help="Just simulate. Very helpful when debugging", action="store_true", ) # TODO: Handle nargs here. parser_set.add_argument( "--install-packages", dest="set_install_packages", help="Packages that need to be installed", action="store", type=str, nargs="*", metavar="PKG", ) parser_set.add_argument( "--install-src-packages", dest="set_install_src_packages", help="Source Packages that need to be installed", action="store", type=str, nargs="*", metavar="SOURCE PKG", ) parser_set.add_argument( "--src-build-dep", dest="src_build_dep", help="Install Build Dependency packages for requested source packages", action="store_true", ) parser_set.add_argument( "--release", dest="set_install_release", help="Release target to install packages from", action="store", type=str, metavar="release_name", ) parser_set.add_argument( "--update", dest="set_update", help="Generate Signature to update APT Database", action="store_true", ) parser_set.add_argument( "--upgrade", dest="set_upgrade", help="Generate Signature of packages to be upgraded", action="store_true", ) parser_set.add_argument( "--upgrade-type", dest="upgrade_type", help="Type of upgrade to do. Use one of upgrade, dist-upgrade, dselect-ugprade", action="store", type=str, metavar="upgrade", default="upgrade", ) parser_set.add_argument( "--generate-changelog", dest="generate_changelog", help="Generate changelog of the version to be downloaded", action="store_true", ) parser_set.add_argument( "--apt-backend", dest="apt_backend", help="APT backend to use. One of: apt, apt-get, python-apt", action="store", type=str, metavar="apt-get", default="apt-get", ) # GET command options parser_get = subparsers.add_parser("get", parents=[global_options]) # INFO: When get option is called, call the fetcher() function parser_get.set_defaults(func=fetcher) parser_get.add_argument( "get", help="Get apt-offline data", action="store", type=str, metavar="apt-offline.sig", default="apt-offline.sig", ) parser_get.add_argument( "--socket-timeout", dest="socket_timeout", help="Set Socket Timeout", action="store", type=int, metavar="30", default=30, ) parser_get.add_argument( "-d", "--download-dir", dest="download_dir", help="Folder path to save files to", action="store", type=str, metavar="apt-downloads", ) parser_get.add_argument( "-s", "--cache-dir", dest="cache_dir", help="Cache folder to search for", action="store", type=str, metavar="/var/cache/apt/archives/", ) parser_get.add_argument( "--no-checksum", dest="disable_md5check", help="Do not validate checksum of downloaded files", action="store_true", ) parser_get.add_argument( "-t", "--threads", dest="num_of_threads", help="Number of threads to spawn", action="store", type=int, metavar="1", default=1, ) parser_get.add_argument( "--bundle", dest="bundle_file", help="Bundle output data to a file", action="store", type=str, metavar="apt-offline-bundle.zip", ) parser_get.add_argument( "--bug-reports", dest="deb_bugs", help="Fetch bug reports from the BTS", action="store_true", ) parser_get.add_argument( "--proxy-host", dest="proxy_host", help="Proxy Host to use", type=str, default=None, ) parser_get.add_argument( "--proxy-port", dest="proxy_port", help="Proxy port number to use", type=int, default=None, ) parser_get.add_argument( "--https-cert-file", dest="https_cert_file", help="Certificate file for https client authentication", type=str, default=None, ) parser_get.add_argument( "--https-key-file", dest="https_key_file", help="Certificate key for https client authentication", type=str, default=None, ) parser_get.add_argument( "--http-basicauth", dest="http_basicauth", action="append", help="A user/password encoded URL. Passwords with special characters should be percent encoded", type=str, default=[], metavar="https://username:password@hostname.com/", ) parser_get.add_argument( "--disable-cert-check", dest="disable_cert_check", help="Disable Certificate check on https connections", action="store_true", ) # INSTALL command options parser_install = subparsers.add_parser("install", parents=[global_options]) parser_install.set_defaults(func=installer) parser_install.add_argument( "install", help="Install apt-offline data, a bundle file or a directory", action="store", type=str, metavar="apt-offline-download.zip | apt-offline-download/", ) parser_install.add_argument( "--simulate", dest="install_simulate", help="Just simulate. Very helpful when debugging", action="store_true", ) parser_install.add_argument( "--install-src-path", dest="install_src_path", help="Install src packages to specified path.", default=None, ) parser_install.add_argument( "--extra-keyring", dest="extra_keyring", help="Extra Keyring path to include for.", metavar="/etc/apt/keyring/", default=None, ) parser_install.add_argument( "--skip-bug-reports", dest="skip_bug_reports", help="Skip the bug report check", action="store_true", ) parser_install.add_argument( "--skip-changelog", dest="skip_changelog", help="Skip display of changelog", action="store_true", ) parser_install.add_argument( "--allow-unauthenticated", dest="allow_unauthenticated", help="Ignore apt gpg signatures mismatch", action="store_true", ) parser_install.add_argument( "--strict-deb-check", dest="strict_deb_check", help="Perform strict checksum validaton for downloaded .deb files", action="store_true", ) if len(sys.argv) <= 1: sys.argv.append("--help") args = parser.parse_args() try: global log if args.quiet: class Log(AptOfflineLib.Log): def msg(self, msg): return def success(self, msg): return log = Log(args.verbose, lock=True) else: log = AptOfflineLib.Log(args.verbose, lock=True) log.verbose(str(args) + "\n") args.func(args) except KeyboardInterrupt: log.err("\nInterrupted by user. Exiting!\n") sys.exit(0) apt-offline-1.8.6/apt_offline_core/AptOfflineDebianBtsLib.py000066400000000000000000000437241475166737700240750ustar00rootroot00000000000000#!/usr/bin/env python """ Query Debian's Bug Tracking System (BTS). This module provides a layer between Python and Debian's BTS. It provides methods to query the BTS using the BTS' SOAP interface, and the Bugreport class which represents a bugreport from the BTS. """ import base64 from distutils.version import LooseVersion import email.feedparser import email.policy from datetime import datetime import os import sys import logging import pysimplesoap from pysimplesoap.client import SoapClient from pysimplesoap.simplexml import SimpleXMLElement logger = logging.getLogger(__name__) # Support running from Debian infrastructure ca_path = '/etc/ssl/ca-debian' if os.path.isdir(ca_path): os.environ['SSL_CERT_DIR'] = ca_path PYSIMPLESOAP_1_16_2 = (LooseVersion(pysimplesoap.__version__) >= LooseVersion('1.16.2')) # Setup the soap server # Default values URL = 'https://bugs.debian.org/cgi-bin/soap.cgi' NS = 'Debbugs/SOAP/V1' BTS_URL = 'https://bugs.debian.org/' # Max number of bugs to send in a single get_status request BATCH_SIZE = 500 SEVERITIES = { 'critical': 7, 'grave': 6, 'serious': 5, 'important': 4, 'normal': 3, 'minor': 2, 'wishlist': 1, } class Bugreport(object): """Represents a bugreport from Debian's Bug Tracking System. A bugreport object provides all attributes provided by the SOAP interface. Most of the attributes are strings, the others are marked. Attributes ---------- bug_num : int The bugnumber severity : str Severity of the bugreport tags : list of strings Tags of the bugreport subject : str The subject/title of the bugreport originator : str Submitter of the bugreport mergedwith : list of ints List of bugnumbers this bug was merged with package : str Package of the bugreport source : str Source package of the bugreport date : datetime Date of bug creation log_modified : datetime Date of update of the bugreport done : boolean Is the bug fixed or not done_by : str or None Name and Email or None archived : bool Is the bug archived or not unarchived : bool Was the bug unarchived or not fixed_versions : list of strings List of versions, can be empty even if bug is fixed found_versions : list of strings List of version numbers where bug was found forwarded : str A URL or email address blocks: list of ints List of bugnumbers this bug blocks blockedby : list of int List of bugnumbers which block this bug pending : str Either 'pending' or 'done' msgid : str Message ID of the bugreport owner : str Who took responsibility for fixing this bug location : str Either 'db-h' or 'archive' affects : list of str List of Packagenames summary : str Arbitrary text """ def __init__(self): self.originator = None self.date = None self.subject = None self.msgid = None self.package = None self.tags = None self.done = None self.forwarded = None self.mergedwith = None self.severity = None self.owner = None self.found_versions = None self.fixed_versions = None self.blocks = None self.blockedby = None self.unarchived = None self.summary = None self.affects = None self.log_modified = None self.location = None self.archived = None self.bug_num = None self.source = None self.pending = None # The ones below are also there but not used # self.fixed = None # self.found = None # self.fixed_date = None # self.found_date = None # self.keywords = None # self.id = None def __str__(self): s = '\n'.join('{}: {}'.format(key, value) for key, value in self.__dict__.items()) return s + '\n' def __lt__(self, other): """Compare a bugreport with another. The more open and urgent a bug is, the greater the bug is: outstanding > resolved > archived critical > grave > serious > important > normal > minor > wishlist. Openness always beats urgency, eg an archived bug is *always* smaller than an outstanding bug. This sorting is useful for displaying bugreports in a list and sorting them in a useful way. """ return self._get_value() < other._get_value() def __le__(self, other): return not self.__gt__(other) def __gt__(self, other): return self._get_value() > other._get_value() def __ge__(self, other): return not self.__lt__(other) def __eq__(self, other): return self._get_value() == other._get_value() def __ne__(self, other): return not self.__eq__(other) def _get_value(self): if self.archived: # archived and done val = 0 elif self.done: # not archived and done val = 10 else: # not done val = 20 val += SEVERITIES[self.severity] return val def get_status(nrs, *additional): """Returns a list of Bugreport objects. Given a list of bugnumbers this method returns a list of Bugreport objects. Parameters ---------- nrs : int or list of ints The bugnumbers additional : int Deprecated! The remaining positional arguments are treated as bugnumbers. This is deprecated since 2.10.0, please use the `nrs` parameter instead. Returns ------- bugs : list of Bugreport objects """ if not isinstance(nrs, (list, tuple)): nrs = [nrs] # backward compatible with <= 2.10.0 if additional: logger.warning('Calling get_status with bugnumbers as positional' ' arguments is deprecated, please use a list instead.') nrs.extend(additional) # Process the input in batches to avoid hitting resource limits on # the BTS soap_client = _build_soap_client() bugs = [] for i in range(0, len(nrs), BATCH_SIZE): slice_ = nrs[i:i + BATCH_SIZE] # I build body by hand, pysimplesoap doesn't generate soap Arrays # without using wsdl method_el = SimpleXMLElement('') _build_int_array_el('arg0', method_el, slice_) reply = soap_client.call('get_status', method_el) for bug_item_el in reply('s-gensym3').children() or []: bug_el = bug_item_el.children()[1] bugs.append(_parse_status(bug_el)) return bugs def get_usertag(email, tags=None, *moretags): """Get buglists by usertags. Parameters ---------- email : str tags : list of strings If tags are given the dictionary is limited to the matching tags, if no tags are given all available tags are returned. moretags : str Deprecated! The remaining positional arguments are treated as tags. This is deprecated since 2.10.0, please use the `tags` parameter instead. Returns ------- mapping : dict a mapping of usertag -> buglist """ if tags is None: tags = [] # backward compatible with <= 2.10.0 if not isinstance(tags, (list, tuple)): tags = [tags] if moretags: logger.warning('Calling get_getusertag with tags as positional' ' arguments is deprecated, please use a list instead.') tags.extend(moretags) reply = _soap_client_call('get_usertag', email, *tags) map_el = reply('s-gensym3') mapping = {} # element in response can have standard type # xsi:type=apachens:Map (example, for email debian-python@lists.debian.org) # OR no type, in this case keys are the names of child elements and # the array is contained in the child elements type_attr = map_el.attributes().get('xsi:type') if type_attr and type_attr.value == 'apachens:Map': for usertag_el in map_el.children() or []: tag = str(usertag_el('key')) buglist_el = usertag_el('value') mapping[tag] = [int(bug) for bug in buglist_el.children() or []] else: for usertag_el in map_el.children() or []: tag = usertag_el.get_name() mapping[tag] = [int(bug) for bug in usertag_el.children() or []] return mapping def get_bug_log(nr): """Get Buglogs. A buglog is a dictionary with the following mappings: * "header" => string * "body" => string * "attachments" => list * "msg_num" => int * "message" => email.message.Message Parameters ---------- nr : int the bugnumber Returns ------- buglogs : list of dicts """ reply = _soap_client_call('get_bug_log', nr) items_el = reply('soapenc:Array') buglogs = [] for buglog_el in items_el.children(): buglog = {} buglog["header"] = _parse_string_el(buglog_el("header")) buglog["body"] = _parse_string_el(buglog_el("body")) buglog["msg_num"] = int(buglog_el("msg_num")) # server always returns an empty attachments array ? buglog["attachments"] = [] mail_parser = email.feedparser.BytesFeedParser( policy=email.policy.SMTP ) mail_parser.feed(buglog["header"].encode()) mail_parser.feed("\n\n".encode()) mail_parser.feed(buglog["body"].encode()) buglog["message"] = mail_parser.close() buglogs.append(buglog) return buglogs def newest_bugs(amount): """Returns the newest bugs. This method can be used to query the BTS for the n newest bugs. Parameters ---------- amount : int the number of desired bugs. E.g. if `amount` is 10 the method will return the 10 latest bugs. Returns ------- bugs : list of int the bugnumbers """ reply = _soap_client_call('newest_bugs', amount) items_el = reply('soapenc:Array') return [int(item_el) for item_el in items_el.children() or []] def get_bugs(*key_value, **kwargs): """Get list of bugs matching certain criteria. The conditions are defined by the keyword arguments. Arguments --------- key_value : str Deprecated! The positional arguments are treated as key-values. This is deprecated since 2.10.0, please use the `kwargs` parameters instead. kwargs : Possible keywords are: * "package": bugs for the given package * "submitter": bugs from the submitter * "maint": bugs belonging to a maintainer * "src": bugs belonging to a source package * "severity": bugs with a certain severity * "status": can be either "done", "forwarded", or "open" * "tag": see http://www.debian.org/Bugs/Developer#tags for available tags * "owner": bugs which are assigned to `owner` * "bugs": takes single int or list of bugnumbers, filters the list according to given criteria * "correspondent": bugs where `correspondent` has sent a mail to * "archive": takes a string: "0" (unarchived), "1" (archived) or "both" (un- and archived). if omitted, only returns un-archived bugs. Returns ------- bugs : list of ints the bugnumbers Examples -------- >>> get_bugs(package='gtk-qt-engine', severity='normal') [12345, 23456] """ # flatten kwargs to list: # {'foo': 'bar', 'baz': 1} -> ['foo', 'bar','baz', 1] args = [] for k, v in kwargs.items(): args.extend([k, v]) # previous versions also accepted # get_bugs(['package', 'gtk-qt-engine', 'severity', 'normal']) # if key_value is a list in a one elemented tuple, remove the # wrapping list if len(key_value) == 1 and isinstance(key_value[0], list): key_value = tuple(key_value[0]) if key_value: logger.warning('Calling get_bugs with positional arguments is' ' deprecated, please use keyword arguments instead.') args.extend(key_value) # pysimplesoap doesn't generate soap Arrays without using wsdl # I build body by hand, converting list to array and using standard # pysimplesoap marshalling for other types method_el = SimpleXMLElement('') for arg_n, kv in enumerate(args): arg_name = 'arg' + str(arg_n) if isinstance(kv, (list, tuple)): _build_int_array_el(arg_name, method_el, kv) else: method_el.marshall(arg_name, kv) soap_client = _build_soap_client() reply = soap_client.call('get_bugs', method_el) items_el = reply('soapenc:Array') return [int(item_el) for item_el in items_el.children() or []] def _parse_status(bug_el): """Return a bugreport object from a given status xml element""" bug = Bugreport() # plain fields for field in ('originator', 'subject', 'msgid', 'package', 'severity', 'owner', 'summary', 'location', 'source', 'pending', 'forwarded'): setattr(bug, field, _parse_string_el(bug_el(field))) bug.date = datetime.utcfromtimestamp(float(bug_el('date'))) bug.log_modified = datetime.utcfromtimestamp(float(bug_el('log_modified'))) bug.tags = [tag for tag in str(bug_el('tags')).split()] bug.done = _parse_bool(bug_el('done')) bug.done_by = _parse_string_el(bug_el('done')) if bug.done else None bug.archived = _parse_bool(bug_el('archived')) bug.unarchived = _parse_bool(bug_el('unarchived')) bug.bug_num = int(bug_el('bug_num')) bug.mergedwith = [int(i) for i in str(bug_el('mergedwith')).split()] bug.blockedby = [int(i) for i in str(bug_el('blockedby')).split()] bug.blocks = [int(i) for i in str(bug_el('blocks')).split()] bug.found_versions = [str(el) for el in bug_el('found_versions').children() or []] bug.fixed_versions = [str(el) for el in bug_el('fixed_versions').children() or []] affects = [_f for _f in str(bug_el('affects')).split(',') if _f] bug.affects = [a.strip() for a in affects] # Also available, but unused or broken # bug.keywords = [keyword for keyword in # str(bug_el('keywords')).split()] # bug.fixed = _parse_crappy_soap(tmp, "fixed") # bug.found = _parse_crappy_soap(tmp, "found") # bug.found_date = \ # [datetime.utcfromtimestamp(i) for i in tmp["found_date"]] # bug.fixed_date = \ # [datetime.utcfromtimestamp(i) for i in tmp["fixed_date"]] return bug # to support python 3.4.3, when using httplib2 as pysimplesoap transport # we must work around a bug in httplib2, which uses # http.client.HTTPSConnection with check_hostname=True, but with an # empty ssl context that prevents the certificate verification. Passing # `cacert` to httplib2 through pysimplesoap permits to create a valid # ssl context. _soap_client_kwargs = { 'location': URL, 'action': '', 'namespace': NS, 'soap_ns': 'soap' } if sys.version_info.major == 3 and sys.version_info < (3, 4, 3): try: from httplib2 import CA_CERTS except ImportError: pass else: _soap_client_kwargs['cacert'] = CA_CERTS def set_soap_proxy(proxy_arg): """Set proxy for SOAP client. You must use this method after import to set the proxy. Parameters ---------- proxy_arg : str """ _soap_client_kwargs['proxy'] = proxy_arg def set_soap_location(url): """Set location URL for SOAP client You may use this method after import to override the default URL. Parameters ---------- url : str """ _soap_client_kwargs['location'] = url def get_soap_client_kwargs(): return _soap_client_kwargs def _build_soap_client(): """Factory method that creates a SoapClient. For thread-safety we create SoapClients on demand instead of using a module-level one. Returns ------- sc : SoapClient instance """ return SoapClient(**_soap_client_kwargs) def _convert_soap_method_args(*args): """Convert arguments to be consumed by a SoapClient method Soap client required a list of named arguments: >>> _convert_soap_method_args('a', 1) [('arg0', 'a'), ('arg1', 1)] """ soap_args = [] for arg_n, arg in enumerate(args): soap_args.append(('arg' + str(arg_n), arg)) return soap_args def _soap_client_call(method_name, *args): """Wrapper to call SoapClient method""" # a new client instance is built for threading issues soap_client = _build_soap_client() soap_args = _convert_soap_method_args(*args) # if pysimplesoap version requires it, apply a workaround for # https://github.com/pysimplesoap/pysimplesoap/issues/31 if PYSIMPLESOAP_1_16_2: return getattr(soap_client, method_name)(*soap_args) else: return getattr(soap_client, method_name)(soap_client, *soap_args) def _build_int_array_el(el_name, parent, list_): """build a soapenc:Array made of ints called `el_name` as a child of `parent`""" el = parent.add_child(el_name) el.add_attribute('xmlns:soapenc', 'http://schemas.xmlsoap.org/soap/encoding/') el.add_attribute('xsi:type', 'soapenc:Array') el.add_attribute('soapenc:arrayType', 'xsd:int[{:d}]'.format(len(list_))) for item in list_: item_el = el.add_child('item', str(item)) item_el.add_attribute('xsi:type', 'xsd:int') return el def _parse_bool(el): """parse a boolean value from a xml element""" value = str(el) return not value.strip() in ('', '0') def _parse_string_el(el): """read a string element, maybe encoded in base64""" value = str(el) el_type = el.attributes().get('xsi:type') if el_type and el_type.value == 'xsd:base64Binary': value = base64.b64decode(value) value = value.decode('utf-8', errors='replace') return value apt-offline-1.8.6/apt_offline_core/AptOfflineLib.py000066400000000000000000000577311475166737700223240ustar00rootroot00000000000000############################################################################ # Copyright (C) 2005, 2015 Ritesh Raj Sarraf # # rrs@researchut.com # # # # 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. # ############################################################################ import signal from array import array import os import sys import threading import zipfile import bz2 import gzip import errno import shutil import warnings # LZMA is not native to Python in 2.x modLZMA = True try: import lzma except ImportError: modLZMA = False sys.stderr.write("WARN: lzma module unavailable\n") sys.stderr.write( "WARN: Please install Python lzma module for APT lzma backend\n") WindowColor = True try: import WConio except ImportError: WindowColor = False # INFO: They aren't on Windows try: from fcntl import ioctl import termios except ImportError: pass # INFO: Python 2.5 introduces hashlib. # This module supports many hash/digest algorithms # We do this check till Python 2.5 becomes widely used. Python_2_5 = True try: import hashlib except ImportError: Python_2_5 = False class Checksum: def HashMessageDigestAlgorithms(self, checksum, HashType, checksumFile): try: data = open(checksumFile, "rb") if HashType == "sha256": Hash = self.sha256(data) elif HashType == "sha512": Hash = self.sha512(data) elif HashType == "md5" or HashType == "md5sum": Hash = self.md5(data) else: Hash = None except IOError: return False data.close() if Hash == checksum: return True return False def sha256(self, data): sha256 = hashlib.sha256() sha256.update(data.read()) return sha256.hexdigest() def sha512(self, data): sha512 = hashlib.sha512() sha512.update(data.read()) return sha512.hexdigest() def md5(self, data): md5hash = hashlib.md5() md5hash.update(data.read()) return md5hash.hexdigest() def CheckHashDigest(self, checksumFile, checksum): """Return Bool against file and its checksum""" checksumType = checksum.split(":")[0] checksumType = checksumType.lower() checksum = checksum.split(":")[1] return self.HashMessageDigestAlgorithms(checksum, checksumType, checksumFile) class Log: """ To display color on Windows CMD, we have optional dependency on WConio WConio can provide simple coloring mechanism for Microsoft Windows console Color Codes: Black = 0 Green = 2 Red = 4 White = 15 Light Red = 12 Light Cyan = 11 #FIXME: The Windows Command Interpreter does support colors natively. I think that support has been since Win2k. That's all for Windows Command Interpreter. As for ANSI Compliant Terminals (which most Linux/Unix Terminals are.)..... I think the ANSI Color Codes would be good enough for my requirements to print colored text on an ANSI compliant terminal. The ANSI Terminal Specification gives programs the ability to change the text color or background color. An ansi code begins with the ESC character [^ (ascii 27) followed by a number (or 2 or more separated by a semicolon) and a letter. In the case of colour codes, the trailing letter is "m"... So as an example, we have ESC[31m ... this will change the foreground colour to red. The codes are as follows: For Foreground Colors 1m - Hicolour (bold) mode 4m - Underline (doesn't seem to work) 5m - BLINK!! 8m - Hidden (same colour as bg) 30m - Black 31m - Red 32m - Green 33m - Yellow 34m - Blue 35m - Magenta 36m - Cyan 37m - White For Background Colors 40m - Change Background to Black 41m - Red 42m - Green 43m - Yellow 44m - Blue 45m - Magenta 46m - Cyan 47m - White 7m - Change to Black text on a White bg 0m - Turn off all attributes. Now for example, say I wanted blinking, yellow text on a magenta background... I'd type ESC[45;33;5m """ def __init__(self, verbose, lock=None): self.VERBOSE = bool(verbose) self.color_syntax = "\033[1;" if lock is True: self.DispLock = threading.Lock() self.lock = True else: self.DispLock = False self.lock = False if os.name == "posix": self.platform = "posix" self.color = { "Red": "31m", "Black": "30m", "Green": "32m", "Yellow": "33m", "Blue": "34m", "Magenta": "35m", "Cyan": "36m", "White": "37m", "Bold_Text": "1m", "Underline": "4m", "Blink": "5m", "SwitchOffAttributes": "0m", } elif os.name in ["nt", "dos"]: self.platform = None if WindowColor is True: self.platform = "microsoft" self.color = { "Red": 4, "Black": 0, "Green": 2, "White": 15, "Cyan": 11, "SwitchOffAttributes": 15, } else: self.platform = None self.color = None def set_color(self, color): """Check the platform and set the color""" if self.platform == "posix": sys.stdout.write(self.color_syntax + self.color[color]) sys.stderr.write(self.color_syntax + self.color[color]) elif self.platform == "microsoft": WConio.textcolor(self.color[color]) def msg(self, msg): """Print general messages. If locking is available use them.""" if self.lock: self.DispLock.acquire() # self.set_color( 'White' ) sys.stdout.write(msg) sys.stdout.flush() # self.set_color( 'SwitchOffAttributes' ) if self.lock: self.DispLock.release() def warn(self, msg): """Print messages with a warning. If locking is available use them.""" if self.lock: self.DispLock.acquire() self.set_color("Magenta") sys.stderr.write("WARN: " + msg) sys.stderr.flush() self.set_color("SwitchOffAttributes") if self.lock: self.DispLock.release() def err(self, msg): """Print messages with an error. If locking is available use them.""" if self.lock: self.DispLock.acquire() self.set_color("Red") sys.stderr.write("ERROR: " + msg) sys.stderr.flush() self.set_color("SwitchOffAttributes") if self.lock: self.DispLock.release() def success(self, msg): """Print messages with a success. If locking is available use them.""" if self.lock: self.DispLock.acquire() self.set_color("Green") sys.stdout.write(msg) sys.stdout.flush() self.set_color("SwitchOffAttributes") if self.lock: self.DispLock.release() # For the rest, we need to check the options also def verbose(self, msg): """Print verbose messages. If locking is available use them.""" if self.lock: self.DispLock.acquire() if self.VERBOSE is True: self.set_color("Magenta") sys.stdout.write("VERBOSE: " + msg) sys.stdout.flush() self.set_color("SwitchOffAttributes") if self.lock: self.DispLock.release() def calcSize(self, size): """Takes number of kB and returns a string of proper size. Like if > 1024, return a megabyte""" if size > 1024: size = size // 1024 if size > 1024: size = size // 1024 return "%d GiB" % (size) return "%d MiB" % (size) return "%d KiB" % (size) class ProgressBar(object): def __init__( self, minValue=0, maxValue=0, width=None, total_items=None, fd=sys.stderr ): # width does NOT include the two places for [] markers self.min = minValue self.max = maxValue self.span = float(self.max - self.min) if self.span == 0: self.span = 1 self.fd = fd self.signal_set = False if width is None: try: self.handle_resize(None, None) signal.signal(signal.SIGWINCH, self.handle_resize) self.signal_set = True except: self.width = 79 # The standard else: self.width = width self.value = self.min if total_items is None or total_items <= 0: self.items = 0 # count of items being tracked self.items_update = True else: self.items = total_items self.items_update = False self.complete = 0 def handle_resize(self, signum, frame): h, w = array("h", ioctl(self.fd, termios.TIOCGWINSZ, "\0" * 8))[:2] self.width = w def updateValue(self, newValue): # require caller to supply a value! newValue is the increment from last call self.value = max(self.min, min(self.max, self.value + newValue)) self.display() def completed(self): self.complete = self.complete + 1 if self.signal_set: signal.signal(signal.SIGWINCH, signal.SIG_DFL) self.display() def addItem(self, maxValue): self.max = self.max + maxValue self.span = float(self.max - self.min) if self.items_update is True: self.items = self.items + 1 self.display() def display(self): sys.stdout.write( "\r%3s / %3s items: %s\r" % (self.complete, self.items, str(self)) ) def __str__(self): # compute display fraction percentFilled = (self.value - self.min) / self.span widthFilled = int(self.width * percentFilled + 0.5) return ( "[" + "#" * widthFilled + " " * (self.width - widthFilled) + "]" + " %5.1f%% of %s" % (percentFilled * 100.0, self.__numStr__(self.max / 1024)) ) def __numStr__(self, size): if size > 1024: size = size / 1024 if size > 1024: size = size / 1024 return "%d GiB" % (size) return "%d MiB" % (size) return "%d KiB" % (size) class AptOfflineErrors(Exception): def __init__(self, message): Exception.__init__(self, message) class AptOfflineLibShutilError(Exception): def __init__(self, message): Exception.__init__(self, message) class Archiver: def __init__(self, lock=None): if lock is None or lock != 1: self.ZipLock = False else: self.ZipLock = threading.Lock() self.lock = True # INFO: Needed for bug reports in multiple threads. Because you can have multiple bug reports # for the same src package # https://github.com/rickysarraf/apt-offline/issues/44 self.file_possibly_deleted = False def TarGzipBZ2_Uncompress(self, SourceFileHandle, TargetFileHandle): try: TargetFileHandle.write(SourceFileHandle.read()) except EOFError: pass except IOError: # TODO: What constitutes an "IOError: invalid data stream" ??? # Couldn't find much from the docs. Needs to be investigated. # Answer: # A BZ2 file corruption is seen during file creation only. # Perhaps it has to do with the bad network, loss of packets et cetera # The safest bet at the moment is to simply discard such files, which were # downloaded in damaged form. return False return True def compress_the_file(self, zip_file_name, files_to_compress): """Condenses all the files into one single file for easy transfer""" try: if self.lock: self.ZipLock.acquire(True) filename = zipfile.ZipFile(zip_file_name, "a") fileOpened = True except IOError: # INFO: By design zipfile throws an IOError exception when you open # in "append" mode and the file is not present. fileOpened = False try: filename = zipfile.ZipFile(zip_file_name, "w") fileOpened = True except IOError: fileOpened = False if fileOpened: # Supported from Python 2.5 ?? # INFO: We could get duplicate files being writted to the zip archive. # See explanation below in the exception warnings.filterwarnings("error") try: filename.write( files_to_compress, os.path.basename(files_to_compress), zipfile.ZIP_DEFLATED, ) except OSError as e: if e.errno == errno.ENOENT: # INFO: We could be here, because in another thread (amd64), it completed, i.e. wrote to the archive and removed # And by the time this thread got a chance, the file was deleted by the previous thread # # A more ideal fix will be to check for files_to_compress's presence in zipfile at this stage self.file_possibly_deleted = True raise AptOfflineErrors( "Ignoring err ENOENT, multiarch package name clash %s\n" % (files_to_compress) ) except UserWarning as e: raise AptOfflineErrors("Ignoring err type %s\n" % (e.args)) finally: filename.close() if self.lock: self.ZipLock.release() return True else: if self.lock: self.ZipLock.release() return False def decompress_the_file(self, archive_file, target_file, archive_type): """Extracts all the files from a single condensed archive file""" if archive_type == "bzip2" or archive_type == "gzip" or archive_type == "xz": if archive_type == "bzip2": try: read_from = bz2.BZ2File(archive_file, "r") except IOError: return False elif archive_type == "gzip": try: read_from = gzip.GzipFile(archive_file, "r") except IOError: return False elif archive_type == "xz": if modLZMA is True: try: read_from = lzma.LZMAFile(archive_file, "rb") except IOError: return False else: return False else: return False try: write_to = open(target_file, "wb") except IOError: return False if self.TarGzipBZ2_Uncompress(read_from, write_to) != True: # INFO: Return False for the stream that failed. return False write_to.close() read_from.close() return True elif archive_type == "zip": # INFO: We will never reach here. # Package data from Debian is usually served only in bz2 or gzip format # Plain zip is something we might never see. # Leaving it here just like that. Maybe we will use it in the future # FIXME: This looks odd. Where are we writing to a file ??? try: zip_file = zipfile.ZipFile(archive_file, "r") except IOError: return False # FIXME: for filename in zip_file.namelist(): try: write_to = open(filename, "wb") except IOError: return False write_to.write(zip_file.read(filename)) write_to.flush() write_to.close() zip_file.close() return True else: return False class FileMgmt(object): def __init__(self): self.duplicate_files = [] def files(self, root): for path, folders, files in os.walk(root): for f in files: yield path, f def find_first_match(self, cache_dir=None, filename=None): """Return the full path of the filename if a match is found Else Return False""" if cache_dir is None: return False elif filename is None: return False elif os.path.isdir(cache_dir) is False: return False else: for path, f in self.files(cache_dir): if f == filename: return os.path.join(path, f) return False def rename_file(self, orig, new): """Rename file from orig to new""" if not os.path.isfile(orig): return False os.rename(orig, new) return True def remove_file(self, src): """Remvoe the given src file.""" try: os.unlink(src) except IOError: return False def move_file(self, src, dest): """Move file from src to dest.""" if not os.path.isdir(dest): return False try: shutil.move(src, dest) except IOError: return False except: # INFO: These errors happen when, say, you have duplicate files # shutil didn't show a nice exception handle # So we propagate the failure and warn out loud in the caller raise AptOfflineLibShutilError( "Possbile duplicate file %s already present in %s\n" % ( src, dest) ) return True def copy_file(self, src, dest): """Copy file from src to dest""" try: # INFO: If src and dest are the same, it is effectively opening the same file # in read and write modes, which leads to NULL data corruption destFile = os.path.join(dest, os.path.basename(src)) srcFile = os.path.abspath(src) # print(srcFile, destFile) if srcFile == destFile: return True SFH = open(srcFile, "rb") DFH = open(destFile, "wb") DFH.write(SFH.read()) DFH.flush() DFH.close() SFH.close() except IOError: SFH.close() return False def move_folder(self, src, dest): """Move folder from src to dest.""" if os.path.isdir(dest): try: os.rename(src, dest + "/" + os.path.basename(src)) except IOError: return False def find_dup(self, dir): """ "dir" will be the directory within which duplicate files are searched Returns a list with the duplicates""" # TODO: This is buggy currently for xpath, yfile in dir: for path, file in dir: if file == yfile: if not (xpath + "/" + yfile == path + "/" + file): if [ xpath + "/" + yfile, path + "/" + file, ] in self.duplicate_files: break else: self.duplicate_files += [ [xpath + "/" + yfile, path + "/" + file] ] # self.duplicate_files = set(self.duplicate_files) len = self.duplicate_files.__len__() print(len) for x in range(len): self.duplicate_files[x].sort() self.duplicate_files.sort() num = 0 number = 0 for x, y in self.duplicate_files: while number < len - 1: if ( x in self.duplicate_files[number] or y in self.duplicate_files[number] ): num += 1 print(num) if num > 1: print("Num went 2") self.duplicate_files.pop(number) num -= 0 number += 1 return self.duplicate_files class MyThread(threading.Thread): """My thread class""" def __init__( self, WorkerFunction, requestQueue=None, responseQueue=None, NumOfThreads=1 ): # Pool of NUMTHREADS Threads that run run(). self.requestQueue = requestQueue self.responseQueue = responseQueue self.threads = NumOfThreads self.threads_finished = 0 # used by gui to understand if things got over self.guiTerminateSignal = False self.WorkerFunction = WorkerFunction self.thread_pool = [ threading.Thread(target=self.run, args=()) for i in range(self.threads) ] for thread in self.thread_pool: thread.guiTerminateSignal = False def startThreads(self): for thread in self.thread_pool: thread.start() def stopThreads(self): """Shut down the threads after all requests end. (Put one None "sentinel" for each thread.)""" for thread in self.thread_pool: self.requestQueue.put(None) def populateQueue(self, item): self.requestQueue.put(item) def stopQueue(self, timeout=0): """Don't end the program prematurely. (Note that because Queue.get() is blocking by default this isn't strictly necessary. But if you were, say, handling responses in another thread, you'd want something like this in your main thread.)""" if timeout != 0: self.threads_finished = 0 # recount finished threads if gui handler needs for thread in self.thread_pool: if timeout == 0: thread.join() else: # let threads also lookout for gui signals of cancellation thread.join(timeout) if not thread.is_alive(): self.threads_finished += 1 def run(self, item=None): while True: if threading.current_thread().guiTerminateSignal: break if self.requestQueue is not None: item = self.requestQueue.get() if item is None: break thread_name = threading.current_thread().name if self.responseQueue is not None: self.responseQueue.put(self.WorkerFunction(item, thread_name)) exit_status = self.responseQueue.get() else: self.WorkerFunction(item, thread_name) apt-offline-1.8.6/apt_offline_core/AptOfflineMagicLib.py000066400000000000000000000150101475166737700232450ustar00rootroot00000000000000#!/usr/bin/env python3 ''' Python bindings for libmagic ''' import ctypes from ctypes import * from ctypes.util import find_library def _init(): """ Loads the shared library through ctypes and returns a library L{ctypes.CDLL} instance """ return ctypes.cdll.LoadLibrary(find_library('magic')) _libraries = {} _libraries['magic'] = _init() # Flag constants for open and setflags MAGIC_NONE = NONE = 0 MAGIC_DEBUG = DEBUG = 1 MAGIC_SYMLINK = SYMLINK = 2 MAGIC_COMPRESS = COMPRESS = 4 MAGIC_DEVICES = DEVICES = 8 MAGIC_MIME_TYPE = MIME_TYPE = 16 MAGIC_CONTINUE = CONTINUE = 32 MAGIC_CHECK = CHECK = 64 MAGIC_PRESERVE_ATIME = PRESERVE_ATIME = 128 MAGIC_RAW = RAW = 256 MAGIC_ERROR = ERROR = 512 MAGIC_MIME_ENCODING = MIME_ENCODING = 1024 MAGIC_MIME = MIME = 1040 MAGIC_APPLE = APPLE = 2048 MAGIC_NO_CHECK_COMPRESS = NO_CHECK_COMPRESS = 4096 MAGIC_NO_CHECK_TAR = NO_CHECK_TAR = 8192 MAGIC_NO_CHECK_SOFT = NO_CHECK_SOFT = 16384 MAGIC_NO_CHECK_APPTYPE = NO_CHECK_APPTYPE = 32768 MAGIC_NO_CHECK_ELF = NO_CHECK_ELF = 65536 MAGIC_NO_CHECK_TEXT = NO_CHECK_TEXT = 131072 MAGIC_NO_CHECK_CDF = NO_CHECK_CDF = 262144 MAGIC_NO_CHECK_TOKENS = NO_CHECK_TOKENS = 1048576 MAGIC_NO_CHECK_ENCODING = NO_CHECK_ENCODING = 2097152 MAGIC_NO_CHECK_BUILTIN = NO_CHECK_BUILTIN = 4173824 class magic_set(Structure): pass magic_set._fields_ = [] magic_t = POINTER(magic_set) _open = _libraries['magic'].magic_open _open.restype = magic_t _open.argtypes = [c_int] _close = _libraries['magic'].magic_close _close.restype = None _close.argtypes = [magic_t] _file = _libraries['magic'].magic_file _file.restype = c_char_p _file.argtypes = [magic_t, c_char_p] _descriptor = _libraries['magic'].magic_descriptor _descriptor.restype = c_char_p _descriptor.argtypes = [magic_t, c_int] _buffer = _libraries['magic'].magic_buffer _buffer.restype = c_char_p _buffer.argtypes = [magic_t, c_void_p, c_size_t] _error = _libraries['magic'].magic_error _error.restype = c_char_p _error.argtypes = [magic_t] _setflags = _libraries['magic'].magic_setflags _setflags.restype = c_int _setflags.argtypes = [magic_t, c_int] _load = _libraries['magic'].magic_load _load.restype = c_int _load.argtypes = [magic_t, c_char_p] _compile = _libraries['magic'].magic_compile _compile.restype = c_int _compile.argtypes = [magic_t, c_char_p] _check = _libraries['magic'].magic_check _check.restype = c_int _check.argtypes = [magic_t, c_char_p] _list = _libraries['magic'].magic_list _list.restype = c_int _list.argtypes = [magic_t, c_char_p] _errno = _libraries['magic'].magic_errno _errno.restype = c_int _errno.argtypes = [magic_t] class Magic(object): def __init__(self, ms): self._magic_t = ms def close(self): """ Closes the magic database and deallocates any resources used. """ _close(self._magic_t) def file(self, filename): """ Returns a textual description of the contents of the argument passed as a filename or None if an error occurred and the MAGIC_ERROR flag is set. A call to errno() will return the numeric error code. """ try: # attempt python3 approach first if isinstance(filename, bytes): bi = filename else: bi = bytes(filename, 'utf-8') return str(_file(self._magic_t, bi), 'utf-8') except: return _file(self._magic_t, filename.encode('utf-8')) def descriptor(self, fd): """ Like the file method, but the argument is a file descriptor. """ return _descriptor(self._magic_t, fd) def buffer(self, buf): """ Returns a textual description of the contents of the argument passed as a buffer or None if an error occurred and the MAGIC_ERROR flag is set. A call to errno() will return the numeric error code. """ try: # attempt python3 approach first return str(_buffer(self._magic_t, buf, len(buf)), 'utf-8') except: return _buffer(self._magic_t, buf, len(buf)) def error(self): """ Returns a textual explanation of the last error or None if there was no error. """ try: # attempt python3 approach first return str(_error(self._magic_t), 'utf-8') except: return _error(self._magic_t) def setflags(self, flags): """ Set flags on the magic object which determine how magic checking behaves; a bitwise OR of the flags described in libmagic(3), but without the MAGIC_ prefix. Returns -1 on systems that don't support utime(2) or utimes(2) when PRESERVE_ATIME is set. """ return _setflags(self._magic_t, flags) def load(self, filename=None): """ Must be called to load entries in the colon separated list of database files passed as argument or the default database file if no argument before any magic queries can be performed. Returns 0 on success and -1 on failure. """ return _load(self._magic_t, filename) def compile(self, dbs): """ Compile entries in the colon separated list of database files passed as argument or the default database file if no argument. Returns 0 on success and -1 on failure. The compiled files created are named from the basename(1) of each file argument with ".mgc" appended to it. """ return _compile(self._magic_t, dbs) def check(self, dbs): """ Check the validity of entries in the colon separated list of database files passed as argument or the default database file if no argument. Returns 0 on success and -1 on failure. """ return _check(self._magic_t, dbs) def list(self, dbs): """ Check the validity of entries in the colon separated list of database files passed as argument or the default database file if no argument. Returns 0 on success and -1 on failure. """ return _list(self._magic_t, dbs) def errno(self): """ Returns a numeric error code. If return value is 0, an internal magic error occurred. If return value is non-zero, the value is an OS error code. Use the errno module or os.strerror() can be used to provide detailed error information. """ return _errno(self._magic_t) def open(flags): """ Returns a magic object on success and None on failure. Flags argument as for setflags. """ return Magic(_open(flags)) apt-offline-1.8.6/apt_offline_core/__init__.py000066400000000000000000000000001475166737700213560ustar00rootroot00000000000000apt-offline-1.8.6/apt_offline_gui/000077500000000000000000000000001475166737700171135ustar00rootroot00000000000000apt-offline-1.8.6/apt_offline_gui/AptOfflineQtAbout.py000066400000000000000000000022761475166737700230230ustar00rootroot00000000000000import os import sys from PyQt5 import QtCore, QtGui, QtWidgets from apt_offline_gui.Ui_AptOfflineQtAbout import Ui_AboutAptOffline class AptOfflineQtAbout(QtWidgets.QDialog): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_AboutAptOffline() self.ui.setupUi(self) self.setupLicense() def setupLicense(self): ''' LICENSE is looked for in - 1. Current directory (dev / possibly windows) 2. /usr/local/share/doc/apt-offline (source install) 3. /usr/share/doc/apt-offline (package install) TODO: to resolve location on window ''' filename = 'LICENSE' locations = ['.', '/usr/local/share/doc/apt-offline/', '/usr/share/doc/apt-offline'] for l in locations: filepath = os.path.join(l,filename) if os.path.isfile(filepath): f = open(filepath,"r") self.ui.licenseText.setPlainText(f.read()) f.close() return if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtAbout() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtAbout.ui000066400000000000000000000227271475166737700230130ustar00rootroot00000000000000 AboutAptOffline Qt::ApplicationModal 0 0 526 378 0 0 526 378 526 378 About Apt-Offline 12 30 511 21 16 Apt-Offline Qt::AlignCenter 7 90 510 241 0 About 10 20 491 31 apt-offline is an Offline APT Package Manager for Debian and derivatives. Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true 10 60 481 111 apt-offline can fully update/upgrade your disconnected Debian box without the need of connecting it to the network. This is a Graphical User Interface which exposes the functionality of apt-offline. true true Author 10 10 111 16 Written by: 30 30 271 16 Ritesh Raj Sarraf <rrs@researchut.com> 10 60 131 16 GUI written by: 30 80 261 16 Manish Sinha <mail@manishsinha.net> true 30 100 271 16 Abhishek Mishra <ideamonk@gmail.com> true 30 120 280 20 Ritesh Raj Sarraf <rrs@researchut.com> true Thanks To 10 10 221 16 Peter Otten 10 30 141 16 Duncan Booth 10 50 161 16 Simon Forman 10 70 161 16 Dennis Lee Bieber 10 110 471 51 The awesome Directi people for their office space required for the mini hackfests true true License 4 4 490 203 10 false true false true GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. apt-offline is Copyright (C) - Ritesh Raj Sarraf 10 60 511 16 A GUI for apt-offline - an offline APT Package Manager Qt::AlignCenter 416 340 101 31 Close :/icons/icons/dialog-cancel.png:/icons/icons/dialog-cancel.png tabWidget licenseText pushButton pushButton clicked() AboutAptOffline close() 258 322 161 178 apt-offline-1.8.6/apt_offline_gui/AptOfflineQtCommon.py000066400000000000000000000012721475166737700231740ustar00rootroot00000000000000# -*- coding: utf-8 -*- styles = { 'red': "#", 'orange': "#", 'green': "#", 'green_fin': "#" } def style(text, style_type): try: return styles[style_type].replace("#",text) except: return text def updateInto(myobject,text): # sanitize coloring if ('[1;' in text): return if ("ERROR" in text or "FATAL" in text): text = style(text,'red') if ("Completed" in text): text = style(text,'green_fin') myobject.append (text)apt-offline-1.8.6/apt_offline_gui/AptOfflineQtCreateProfile.py000066400000000000000000000210611475166737700244660ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys,os from PyQt5 import QtCore, QtGui, QtWidgets from apt_offline_gui.Ui_AptOfflineQtCreateProfile import Ui_CreateProfile from apt_offline_gui.UiDataStructs import SetterArgs from apt_offline_gui import AptOfflineQtCommon as guicommon import apt_offline_core.AptOfflineCoreLib class AptOfflineQtCreateProfile(QtWidgets.QDialog): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_CreateProfile() self.ui.setupUi(self) # Connect the clicked signal of the Browse button to it's slot #QtCore.QObject.connect(self.ui.browseFilePathButton, QtCore.SIGNAL("clicked()"), # self.popupDirectoryDialog ) self.ui.browseFilePathButton.clicked.connect(self.popupDirectoryDialog) # Connect the clicked signal of the Save to it's Slot - accept #QtCore.QObject.connect(self.ui.createProfileButton, QtCore.SIGNAL("clicked()"), # self.CreateProfile ) self.ui.createProfileButton.clicked.connect(self.CreateProfile) # Connect the clicked signal of the Cancel to it's Slot - reject #QtCore.QObject.connect(self.ui.cancelButton, QtCore.SIGNAL("clicked()"), # self.reject ) self.ui.cancelButton.clicked.connect(self.reject) # Disable or Enable the Package List field #QtCore.QObject.connect(self.ui.installPackagesCheckBox, QtCore.SIGNAL("toggled(bool)"), # self.PackageListFieldStatus ) self.ui.installPackagesCheckBox.toggled.connect(self.PackageListFieldStatus) #QtCore.QObject.connect(self.ui.installSrcPackagesCheckBox, QtCore.SIGNAL("toggled(bool)"), # self.SrcPackageListFieldStatus ) self.ui.installSrcPackagesCheckBox.toggled.connect(self.SrcPackageListFieldStatus) #QtCore.QObject.connect(self.ui.srcBuildDeps, QtCore.SIGNAL("toggled(bool)"), # self.SrcPackageListFieldStatus ) self.ui.srcBuildDeps.toggled.connect(self.SrcPackageListFieldStatus) #QtCore.QObject.connect(self.ui.targetReleaseCheckBox, QtCore.SIGNAL("toggled(bool)"), # self.TargetReleaseFieldStatus ) self.ui.targetReleaseCheckBox.toggled.connect(self.TargetReleaseFieldStatus) #QtCore.QObject.connect(self.ui.upgradePackagesCheckBox, QtCore.SIGNAL("toggled(bool)"), # self.UpgradeCheckStatus ) self.ui.upgradePackagesCheckBox.toggled.connect(self.UpgradeCheckStatus) def UpgradeCheckStatus(self): self.isFieldChecked = self.ui.upgradePackagesCheckBox.isChecked() self.ui.targetReleaseCheckBox.setEnabled(self.isFieldChecked) self.ui.generateChangelog.setEnabled(self.isFieldChecked) self.ui.upgradeTaskComboBox.setEnabled(self.isFieldChecked) def TargetReleaseFieldStatus(self): # If Install Packages Box is selected self.isFieldChecked = self.ui.targetReleaseCheckBox.isChecked() self.ui.targetReleaseTextInput.setEnabled(self.isFieldChecked) def SrcPackageListFieldStatus(self): # If Install Packages Box is selected self.isFieldChecked = self.ui.installSrcPackagesCheckBox.isChecked() self.ui.srcPackageList.setEnabled(self.isFieldChecked) self.ui.srcBuildDeps.setEnabled(self.isFieldChecked) self.ui.targetReleaseCheckBox.setEnabled(self.isFieldChecked) self.ui.upgradeTaskComboBox.setEnabled(self.isFieldChecked) def PackageListFieldStatus(self): # If Install Packages Box is selected self.isFieldChecked = self.ui.installPackagesCheckBox.isChecked() self.ui.packageList.setEnabled(self.isFieldChecked) self.ui.targetReleaseCheckBox.setEnabled(self.isFieldChecked) self.ui.generateChangelog.setEnabled(self.isFieldChecked) self.ui.upgradeTaskComboBox.setEnabled(self.isFieldChecked) def CreateProfile(self): # Is the Update requested self.updateChecked = self.ui.updateCheckBox.isChecked() # Is Upgrade requested self.upgradeChecked = self.ui.upgradePackagesCheckBox.isChecked() # Is Install Requested self.installChecked = self.ui.installPackagesCheckBox.isChecked() self.installSrcChecked = self.ui.installSrcPackagesCheckBox.isChecked() self.chlogChecked = self.ui.generateChangelog.isChecked() self.aptBackend = self.ui.aptBackendComboBox.currentText() self.upgradeType = self.ui.upgradeTaskComboBox.currentText() self.releaseBtnChecked = self.ui.targetReleaseCheckBox.isChecked() # Clear the consoleOutputHolder self.ui.consoleOutputHolder.setText("") self.filepath = str(self.ui.profileFilePath.text()) if os.path.exists(os.path.dirname(self.filepath)) == False: if (len(self.filepath) == 0): self.ui.consoleOutputHolder.setText ( \ guicommon.style("Please select a file to store the signature!",'red')) else: self.ui.consoleOutputHolder.setText ( \ guicommon.style("Could not access %s" % self.filepath,'red')) return # If at least one is requested if self.updateChecked or self.upgradeChecked or self.installChecked or self.installSrcChecked: if self.installChecked: self.packageList = str(self.ui.packageList.text()).split(",") else: self.packageList = None if self.installSrcChecked: self.srcPackageList = str(self.ui.srcPackageList.text()).split(",") self.srcBuildDeps = self.ui.srcBuildDeps else: self.srcPackageList = None self.srcBuildDeps = False if self.releaseBtnChecked: self.release = str(self.ui.targetReleaseTextInput.text()) else: self.release = None # setup i/o redirects before call sys.stdout = self sys.stderr = self args = SetterArgs(filename=self.filepath, update=self.updateChecked, upgrade=self.upgradeChecked, install_packages=self.packageList, \ install_src_packages=self.srcPackageList, src_build_dep=self.srcBuildDeps, changelog=self.chlogChecked, \ release=self.release, apt_backend=self.aptBackend, simulate=False) returnStatus = apt_offline_core.AptOfflineCoreLib.setter(args) if(returnStatus != False): # right now it returns None, I think it doesn't return at all but sys.exits on failure # TODO ^ fixup this behaviour guicommon.updateInto(self.ui.consoleOutputHolder, guicommon.style("Completed.","green_fin")) self.ui.createProfileButton.setEnabled(False) self.ui.cancelButton.setText("Finish") self.ui.cancelButton.setIcon(QtGui.QIcon()) else: pass def popupDirectoryDialog(self): # Popup a Directory selection box signatureFilePath = os.path.join (os.path.expanduser("~"), "/Desktop/"+"apt-offline.sig") directory, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'Select a filename to save the signature', signatureFilePath, "apt-offline Signatures (*.sig)") # Show the selected file path in the field marked for showing directory path self.ui.profileFilePath.setText(directory) def write(self, text): # redirects console output to our consoleOutputHolder text=text.strip() if (len(text)>2): guicommon.updateInto(self.ui.consoleOutputHolder,text) def flush(self): ''' nothing to do :D ''' def resetUI(self): self.ui.updateCheckBox.setChecked(False) self.ui.upgradePackagesCheckBox.setChecked(False) self.ui.installPackagesCheckBox.setChecked(False) self.ui.cancelButton.setText("Close") self.ui.createProfileButton.setEnabled(True) self.ui.consoleOutputHolder.setText("") self.ui.profileFilePath.setText("") self.ui.packageList.setText("") self.ui.aptBackendComboBox.setCurrentIndex(0) self.ui.generateChangelog.setChecked(False) self.ui.srcBuildDeps.setChecked(False) self.ui.installSrcPackagesCheckBox.setChecked(False) self.ui.srcPackageList.setText("") self.ui.targetReleaseCheckBox.setChecked(False) self.ui.targetReleaseTextInput.setText("") if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtCreateProfile() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtCreateProfile.ui000066400000000000000000000347431475166737700244660ustar00rootroot00000000000000 CreateProfile Qt::ApplicationModal 0 0 443 500 0 0 443 400 443 500 Qt::NoFocus Generate Signature false 11 230 131 25 Qt::StrongFocus Check to also include the Build Dependencies for the Source Packages Src Build Deps 164 276 110 30 Qt::StrongFocus Choose APT backend. Default backend is the most tested and known to work under all scenarios apt-get apt python-apt false 330 276 100 30 Qt::StrongFocus Choose upgrade type to perform upgrade dist-upgrade dselect-upgrade false 11 123 143 25 Qt::StrongFocus Check to set the target release. If not specified, system default is picked Target Release false 164 121 265 30 0 30 Qt::StrongFocus Please specify valid target release name. If not specified, system default is picked false true 290 281 40 20 Choose upgrade type to perform Task 62 281 100 20 Choose APT backend. Default backend is the most tested and known to work under all scenarios APT Backend false 160 230 111 25 Qt::StrongFocus Check to generate changelog for the packages Changelog 224 451 100 32 100 30 Cancel :/icons/icons/dialog-cancel.png:/icons/icons/dialog-cancel.png true 72 451 100 32 100 30 Create :/icons/icons/dialog-ok-apply.png:/icons/icons/dialog-ok-apply.png 2 313 116 20 Console Output: 2 339 427 100 0 0 0 100 16777215 100 Qt::NoFocus QTextEdit::AutoAll true false Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 30 83 131 20 New filename to save the signature data Save Signature As true 167 80 170 30 0 0 0 30 Qt::StrongFocus New filename to save the signature data true 341 80 85 30 0 30 Qt::StrongFocus Browse false 164 193 265 30 0 30 Qt::StrongFocus List of Source packages to install, separated by space false true 11 195 147 25 Qt::StrongFocus Check to install Source Packages Source Packages false 164 157 265 30 0 30 Qt::StrongFocus List of Binary packages to install, separated by space false true 11 159 143 25 Qt::StrongFocus Check to install Binary packages Binary Packages 150 10 81 25 Qt::StrongFocus Check to include APT Package Database Updates Update 260 10 159 25 Qt::StrongFocus Check to include Package Upgrades Upgrade Packages 3 10 117 20 16777215 16777215 Check all tasks that apply. Eg. Update, Upgrade Installation Type Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop updateCheckBox upgradePackagesCheckBox profileFilePath browseFilePathButton targetReleaseCheckBox targetReleaseTextInput installPackagesCheckBox packageList installSrcPackagesCheckBox srcPackageList srcBuildDeps generateChangelog aptBackendComboBox upgradeTaskComboBox createProfileButton cancelButton apt-offline-1.8.6/apt_offline_gui/AptOfflineQtFetch.py000066400000000000000000000330031475166737700227720ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os, sys from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import QMessageBox from apt_offline_gui.Ui_AptOfflineQtFetch import Ui_AptOfflineQtFetch from apt_offline_gui.UiDataStructs import GetterArgs from apt_offline_gui import AptOfflineQtCommon as guicommon import apt_offline_core.AptOfflineCoreLib from apt_offline_gui.AptOfflineQtFetchOptions import AptOfflineQtFetchOptions class Worker(QtCore.QThread): output = QtCore.pyqtSignal(str) progress = QtCore.pyqtSignal(str, str) status = QtCore.pyqtSignal(str) finished = QtCore.pyqtSignal() terminated = QtCore.pyqtSignal() def __init__(self, parent = None): QtCore.QThread.__init__(self, parent) self.parent = parent self.exiting = False #INFO: Qt5 Signal and Slots self.output.emit('') self.progress.emit('','') self.status.emit('') self.finished.emit() self.terminated.emit() def __del__(self): self.exiting = True self.wait() def run(self): # setup i/o redirects before call sys.stdout = self sys.stderr = self apt_offline_core.AptOfflineCoreLib.fetcher(self.args) def setArgs (self,args): self.args = args def write(self, text): # redirects console output to our consoleOutputHolder # extract details from text if apt_offline_core.AptOfflineCoreLib.guiTerminateSignal: # ^ so artificial, the threads still remain frozen in time I suppose return if ("MSG_START" in text): self.status.emit("Fetching missing meta data...") elif ("MSG_END" in text): self.status.emit("Downloading packages ...") elif ("WARNING" in text): self.output.emit("%s" % (guicommon.style(text,"red"))) elif ("Downloading" in text): self.output.emit("%s" % (guicommon.style(text,"orange"))) elif ("done." in text): self.output.emit("%s" % (guicommon.style(text,"green"))) elif ("[" in text and "]" in text): try: # no more splits, we know the exact byte count now progress = str(apt_offline_core.AptOfflineCoreLib.totalSize[1]) total = str(apt_offline_core.AptOfflineCoreLib.totalSize[0]) self.progress.emit(progress,total) except: ''' nothing to do ''' else: self.output.emit(text.strip()) def flush(self): ''' nothing to do :D ''' def quit(self): self.finished.emit() class AptOfflineQtFetch(QtWidgets.QDialog): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_AptOfflineQtFetch() self.ui.setupUi(self) self.advancedOptionsDialog = AptOfflineQtFetchOptions() # Connect the clicked signal of the Signature File Browse button to it's slot self.ui.browseFilePathButton.clicked.connect(self.popupDirectoryDialog) # Connect the clicked signal of the Zip File Browse button to it's slot self.ui.browseZipFileButton.clicked.connect(self.popupZipFileDialog) # Connect the clicked signal of the Save to it's Slot - accept self.ui.startDownloadButton.clicked.connect(self.StartDownload) # Connect the clicked signal of the Cancel to it's Slot - reject self.ui.cancelButton.clicked.connect(self.handleCancel) self.ui.profileFilePath.textChanged.connect(self.controlStartDownloadBox) self.ui.zipFilePath.textChanged.connect(self.controlStartDownloadBox) self.ui.advancedOptionsButton.clicked.connect(self.showAdvancedOptions) self.worker = Worker(parent=self) self.worker.output.connect(self.updateLog) self.worker.progress.connect(self.updateProgress) self.worker.status.connect(self.updateStatus) self.worker.finished.connect(self.finishedWork) self.worker.terminated.connect(self.finishedWork) #INFO: inform CLI that it's a gui app apt_offline_core.AptOfflineCoreLib.guiBool = True # Reduce extra line gaps in CLI o/p apt_offline_core.AptOfflineCoreLib.LINE_OVERWRITE_SMALL="" apt_offline_core.AptOfflineCoreLib.LINE_OVERWRITE_MID="" apt_offline_core.AptOfflineCoreLib.LINE_OVERWRITE_FULL="" def showAdvancedOptions(self): self.advancedOptionsDialog.show() def popupDirectoryDialog(self): # Popup a Directory selection box directory, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Select the signature file') # Show the selected file path in the field marked for showing directory path self.ui.profileFilePath.setText(directory) self.controlStartDownloadBox() def popupZipFileDialog(self): if self.ui.saveDatacheckBox.isChecked() is True: filename, _ = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select the folder to save downloads to') else: # Popup a Zip File selection box filename, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'Select the zip file to save downloads') # Show the selected file path in the field marked for showing directory path self.ui.zipFilePath.setText(filename) self.controlStartDownloadBox() def StartDownload(self): # Do all the download related work here and then close # Clear the consoleOutputHolder self.ui.rawLogHolder.setText("") self.filepath = str(self.ui.profileFilePath.text()) if os.path.isfile(self.filepath) == False: if (len(self.filepath) == 0): self.ui.rawLogHolder.setText ( \ guicommon.style("Please select a signature file!",'red')) else: self.ui.rawLogHolder.setText ( \ guicommon.style("%s does not exist." % self.filepath,'red')) return self.zipfilepath = str(self.ui.zipFilePath.text()) # First we need to determine if the input is a file path or a directory path if self.ui.saveDatacheckBox.isChecked() is not True: #Input is a file path # if file already exists if os.path.exists(self.zipfilepath): ret = QMessageBox.warning(self, "Replace archive file?", "The file %s already exists.\n" "Do you want to overwrite it?" % self.zipfilepath, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if ret == QMessageBox.Yes: # delete the file try: #TODO: If "/" is the path, then os.unlink quietly fails crashing the GUI os.unlink(self.zipfilepath) except: guicommon.updateInto (self.ui.rawLogHolder, guicommon.style("Couldn't write to %s!" % self.zipfilepath,'red')) else: return else: if not os.access(os.path.dirname(self.zipfilepath), os.W_OK): guicommon.updateInto (self.ui.rawLogHolder, guicommon.style("%s does not have write access." % self.zipfilepath,'red')) return targetFilePath = self.zipfilepath targetDirPath = None else: if os.path.exists(self.zipfilepath): if os.access(self.zipfilepath, os.W_OK) == False: guicommon.updateInto (self.ui.rawLogHolder, guicommon.style("%s does not have write access." % self.zipfilepath,'red')) return else: ret = QMessageBox.warning(self, "No such directory", "No such directory %s\n" "Do you want to create it?" % self.zipfilepath, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if ret == QMessageBox.Yes: # delete the file try: os.mkdir(self.zipfilepath) except: guicommon.updateInto (self.ui.rawLogHolder, guicommon.style("Couldn't create directory %s!" % self.zipfilepath,'red')) return else: return targetFilePath = None targetDirPath = self.zipfilepath args = GetterArgs(filename=self.filepath, bundle_file=targetFilePath, progress_bar=self.ui.statusProgressBar, progress_label=self.ui.progressStatusDescription, proxy_host=self.advancedOptionsDialog.proxy_host, proxy_port=self.advancedOptionsDialog.proxy_port, num_of_threads=self.advancedOptionsDialog.num_of_threads, socket_timeout=self.advancedOptionsDialog.socket_timeout, cache_dir=self.advancedOptionsDialog.cache_dir, download_dir=targetDirPath, disable_md5check=self.advancedOptionsDialog.disable_md5check, deb_bugs=self.advancedOptionsDialog.deb_bugs) #returnStatus = apt_offline_core.AptOfflineCoreLib.fetcher(args) # TODO: deal with return status laters self.ui.cancelButton.setText("Cancel") self.disableAction() self.disableAtDownload() self.worker.setArgs (args) self.worker.start() #if (returnStatus): ''' TODO: do something with self.zipfilepath ''' # TODO to be implemented later # self.accept() def updateLog(self,text): if not ('[' in text and ']' in text): if ('Downloaded data ' in text): guicommon.updateInto (self.ui.rawLogHolder, guicommon.style(text,'green_fin')) self.ui.progressStatusDescription.setText('Finished.') self.finishedWork() else: guicommon.updateInto (self.ui.rawLogHolder,text) def updateStatus(self,text): self.ui.progressStatusDescription.setText(text) def updateProgress(self,progress,total): try: # try parsing numbers and updating progressBar percent = (float(progress)/float(total))*100 self.ui.statusProgressBar.setValue (percent) except: ''' nothing to do ''' def controlStartDownloadBox(self): if not self.ui.profileFilePath.text(): self.disableAction() if not self.ui.zipFilePath.text(): self.disableAction() else: self.enableAction() def handleCancel(self): if self.ui.cancelButton.text() == "Cancel": if self.worker.isRunning(): # Download is still in progress ret = QMessageBox.warning(self, "Cancel current downloads?", "A download is already in progress.\nDo you want to cancel it?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if ret == QMessageBox.Yes: # we can't just stop threads, we need to pass message apt_offline_core.AptOfflineCoreLib.guiTerminateSignal=True self.updateStatus(guicommon.style("Download aborted","red")) self.enableAtStop() self.ui.cancelButton.setText("Close") else: self.reject() else: self.reject() def resetUI(self): apt_offline_core.AptOfflineCoreLib.guiTerminateSignal=False apt_offline_core.AptOfflineCoreLib.guiMetaCompleted=False apt_offline_core.AptOfflineCoreLib.errlist = [] apt_offline_core.AptOfflineCoreLib.totalSize = [0,0] self.ui.profileFilePath.setText("") self.ui.zipFilePath.setText("") self.ui.rawLogHolder.setText("") self.ui.statusProgressBar.setValue(0) self.updateStatus("Ready") self.enableAction() self.enableAtStop() def disableAction(self): self.ui.startDownloadButton.setEnabled(False) def disableAtDownload(self): self.ui.advancedOptionsButton.setEnabled(False) self.ui.browseZipFileButton.setEnabled(False) self.ui.browseFilePathButton.setEnabled(False) self.ui.zipFilePath.setEnabled(False) self.ui.profileFilePath.setEnabled(False) self.ui.saveDatacheckBox.setEnabled(False) def enableAction(self): self.ui.startDownloadButton.setEnabled(True) def enableAtStop(self): self.ui.advancedOptionsButton.setEnabled(True) self.ui.browseZipFileButton.setEnabled(True) self.ui.browseFilePathButton.setEnabled(True) self.ui.zipFilePath.setEnabled(True) self.ui.profileFilePath.setEnabled(True) self.ui.saveDatacheckBox.setEnabled(True) def finishedWork(self): ''' do nothing ''' self.ui.cancelButton.setText("Close") if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtFetch() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtFetch.ui000066400000000000000000000211501475166737700227570ustar00rootroot00000000000000 AptOfflineQtFetch Qt::ApplicationModal 0 0 468 483 0 0 468 475 468 495 Fetch Packages or Updates 30 35 270 30 Path to signature file true 320 35 110 30 Browse false 179 148 120 30 Start download Download :/icons/icons/go-down.png:/icons/icons/go-down.png false false false 319 148 110 30 Close window Close :/icons/icons/application-exit.png:/icons/icons/application-exit.png 30 5 200 30 10 Select the signature file 30 214 410 20 0 32 194 70 16 10 Status: 82 194 341 16 10 Ready 30 252 411 211 10 false QAbstractScrollArea::AdjustToContentsOnFirstShow QTextEdit::AutoAll false true <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Cantarell'; font-size:10pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;"><br /></p></body></html> 80 Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 320 98 110 30 Browse 30 68 200 30 10 Save data as 30 98 270 30 Data save location file/directory true true 31 148 120 30 Additional options for download Options :/icons/icons/configure.png:/icons/icons/configure.png false false false 321 80 110 18 Click if target save location is a directory Is Directory profileFilePath browseFilePathButton zipFilePath saveDatacheckBox browseZipFileButton advancedOptionsButton startDownloadButton cancelButton rawLogHolder apt-offline-1.8.6/apt_offline_gui/AptOfflineQtFetchOptions.py000066400000000000000000000073541475166737700243600ustar00rootroot00000000000000import sys, os from PyQt5 import QtCore, QtGui, QtWidgets from apt_offline_gui.Ui_AptOfflineQtFetchOptions import Ui_downloadOptionsDialog class AptOfflineQtFetchOptions(QtWidgets.QDialog): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_downloadOptionsDialog() self.ui.setupUi(self) # Connect the clicked signal of the Ok button to it's slot #QtCore.QObject.connect(self.ui.downloadOptionDialogOkButton, QtCore.SIGNAL("clicked()"), # self.validateOptions ) self.ui.downloadOptionDialogOkButton.clicked.connect(self.validateOptions) #QtCore.QObject.connect(self.ui.useProxyCheckBox, QtCore.SIGNAL("toggled(bool)"), # self.toggleProxyControls ) self.ui.useProxyCheckBox.toggled.connect(self.toggleProxyControls) #QtCore.QObject.connect(self.ui.cacheDirBrowseButton, QtCore.SIGNAL("clicked()"), # self.populateCacheDir ) self.ui.cacheDirBrowseButton.clicked.connect(self.populateCacheDir) # defaults self.num_of_threads = 1 self.socket_timeout = 30 self.cache_dir = None self.disable_md5check = False self.deb_bugs = False self.proxy_host = None self.proxy_port = None def storeOptions(self): self._num_of_threads = self.ui.spinThreads.value() self._socket_timeout = self.ui.spinTimeout.value() self._cache_dir = str(self.ui.cacheDirLineEdit.text() ) self._disable_md5check = self.ui.disableChecksumCheckBox.isChecked() self._deb_bugs = self.ui.fetchBugReportsCheckBox.isChecked() if self.ui.useProxyCheckBox.isChecked(): self._proxy_host = str(self.ui.proxyHostLineEdit.text() ) self._proxy_port = str(self.ui.proxyPortLineEdit.text() ) else: self._proxy_host = None self._proxy_port = None def validateOptions(self): self.storeOptions() if len(self._cache_dir) > 0 and not (os.access(self._cache_dir, os.W_OK) or os.access(self._cache_dir, os.R_OK) ): QtWidgets.QMessageBox.critical(self, "Error", "Could not locate cache directory") return if self._proxy_port: try: int(self._proxy_port) except: QtWidgets.QMessageBox.critical(self, "Error", "Invalid Proxy Port Number") return self.applyOptionValues() self.hide() def applyOptionValues(self): self.num_of_threads = self._num_of_threads self.socket_timeout = self._socket_timeout self.cache_dir = self._cache_dir self.disable_md5check = self._disable_md5check self.deb_bugs = self._deb_bugs self.proxy_host = self._proxy_host self.proxy_port = self._proxy_port def toggleProxyControls(self): if self.ui.useProxyCheckBox.isChecked(): self.ui.proxyHostLineEdit.setEnabled(True) self.ui.proxyPortLineEdit.setEnabled(True) else: self.ui.proxyHostLineEdit.setEnabled(False) self.ui.proxyPortLineEdit.setEnabled(False) def populateCacheDir(self): directory = QtWidgets.QFileDialog.getExistingDirectory(None, 'Provide path to APT\'s Cache Dir') self.ui.cacheDirLineEdit.setText(directory) self._cache_dir = directory if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtFetchOptions() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtFetchOptions.ui000066400000000000000000000250651475166737700243440ustar00rootroot00000000000000 downloadOptionsDialog 0 0 443 304 0 0 443 304 443 304 Advanced options for download :/icons/icons/configure.png:/icons/icons/configure.png false true 255 10 80 30 10 Number of parallel connections Use threads 338 9 90 30 0 0 Number of parallel connections 1 10 350 260 81 31 Ok :/icons/icons/dialog-ok-apply.png:/icons/icons/dialog-ok-apply.png 10 8 110 30 10 Network timeout in seconds Network Timeout 119 7 100 30 0 0 Network timeout in seconds 10 100 30 120 60 221 31 0 0 Cache folder to search for 350 60 81 31 0 0 Browse 10 60 91 30 0 0 10 Cache folder to search for Cache Directory 10 120 151 31 0 0 Disables checksum verification of downloaded items. Enable only if you know what you are doing Disable Checksum 180 120 151 31 0 0 Fetch Bug Reports Fetch Bug Reports 9 169 421 81 0 0 true false 25 39 238 31 0 0 Proxy Server Host/IP Address false 280 39 61 31 0 0 Proxy Server Port Address 0 0 89 21 0 0 75 true Check if using a Proxy server Proxy 290 186 138 30 10 Cache folder to search for Port 35 186 138 30 10 Cache folder to search for Host spinTimeout spinThreads cacheDirLineEdit cacheDirBrowseButton disableChecksumCheckBox fetchBugReportsCheckBox useProxyCheckBox proxyHostLineEdit proxyPortLineEdit downloadOptionDialogOkButton apt-offline-1.8.6/apt_offline_gui/AptOfflineQtInstall.py000066400000000000000000000163671475166737700233650ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os, sys from PyQt5 import QtCore, QtGui, QtWidgets from apt_offline_gui.Ui_AptOfflineQtInstall import Ui_AptOfflineQtInstall from apt_offline_gui.UiDataStructs import InstallerArgs from apt_offline_gui import AptOfflineQtCommon as guicommon import apt_offline_core.AptOfflineCoreLib from apt_offline_gui.AptOfflineQtInstallBugList import AptOfflineQtInstallBugList from apt_offline_gui.AptOfflineQtInstallChangelog import AptOfflineQtInstallChangelog class Worker(QtCore.QThread): output = QtCore.pyqtSignal(str) progress = QtCore.pyqtSignal(str, str) status = QtCore.pyqtSignal(str) finished = QtCore.pyqtSignal() terminated = QtCore.pyqtSignal() def __init__(self, parent = None): QtCore.QThread.__init__(self, parent) self.parent = parent self.exiting = False def __del__(self): self.exiting = True self.wait() def run(self): # setup i/o redirects before call sys.stdout = self sys.stderr = self apt_offline_core.AptOfflineCoreLib.installer(self.args) def setArgs (self,args): self.args = args def write(self, text): # redirects console output to our consoleOutputHolder # extract details from text if ('.deb' in text and 'synced' in text): try: text = guicommon.style("Package : ",'orange') + guicommon.style(text.split("/")[-1],'green') except: pass self.output.emit(text) elif ('apt/lists' in text): try: # this part is always done on a Linux system so we can hardcode / for a while text = guicommon.style("Update : ",'orange') + guicommon.style(text.split("/")[-1],'green') except: # let the text be original otherwise pass self.output.emit(text) elif ('[' in text and ']' in text): try: progress = str(apt_offline_core.AptOfflineCoreLib.totalSize[0]) total = str(apt_offline_core.AptOfflineCoreLib.totalSize[1]) self.progress.emit("%s%s" % (progress, total)) except: ''' nothing to do ''' else: self.output.emit(text) def flush(self): ''' nothing to do :D ''' def quit(self): self.finished.emit() class AptOfflineQtInstall(QtWidgets.QDialog): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_AptOfflineQtInstall() self.ui.setupUi(self) # Connect the clicked signal of the Browse button to it's slot self.ui.browseFilePathButton.clicked.connect(self.popupDirectoryDialog) # Connect the clicked signal of the Save to it's Slot - accept self.ui.startInstallButton.clicked.connect(self.StartInstall) # Connect the clicked signal of the Cancel to it's Slot - reject self.ui.cancelButton.clicked.connect(self.reject) self.ui.bugReportsButton.clicked.connect(self.showBugReports) self.ui.changelogButton.clicked.connect(self.showChangelog) self.ui.zipFilePath.editingFinished.connect(self.ControlStartInstallBox) self.ui.zipFilePath.textChanged.connect(self.ControlStartInstallBox) self.worker = Worker(parent=self) self.worker.output.connect(self.updateLog) self.worker.progress.connect(self.updateProgress) self.worker.status.connect(self.updateStatus) self.worker.finished.connect(self.finishedWork) self.worker.terminated.connect(self.finishedWork) def StartInstall(self): # gui validation # Clear the consoleOutputHolder self.ui.rawLogHolder.setText("") self.filepath = str(self.ui.zipFilePath.text()) # parse args args = InstallerArgs(filename=self.filepath, progress_bar=self.ui.statusProgressBar, progress_label=self.ui.progressStatusDescription ) self.disableActions() self.ui.progressStatusDescription.setText("Syncing updates") self.worker.setArgs (args) self.worker.start() def showBugReports(self): self.filepath = str(self.ui.zipFilePath.text()) self.bugReportsDialog = AptOfflineQtInstallBugList(self.filepath) self.bugReportsDialog.filepath= self.filepath self.bugReportsDialog.show() def showChangelog(self): self.filepath = str(self.ui.zipFilePath.text()) self.changelogDialog = AptOfflineQtInstallChangelog(self.filepath) self.changelogDialog.filepath = self.filepath self.changelogDialog.show() def popupDirectoryDialog(self): # Popup a Directory selection box if self.ui.browseFileFoldercheckBox.isChecked() is True: directory = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select the folder') else: directory = QtWidgets.QFileDialog.getOpenFileName(self, 'Select the Zip File') # Show the selected file path in the field marked for showing directory path self.ui.zipFilePath.setText(directory) self.ui.zipFilePath.setFocus() def ControlStartInstallBox(self): if os.path.isdir(self.ui.zipFilePath.text()) or os.path.isfile(self.ui.zipFilePath.text() ): self.ui.startInstallButton.setEnabled(True) self.ui.bugReportsButton.setEnabled(True) self.ui.changelogButton.setEnabled(True) else: self.ui.startInstallButton.setEnabled(False) self.ui.bugReportsButton.setEnabled(False) self.ui.changelogButton.setEnabled(False) def updateLog(self,text): guicommon.updateInto (self.ui.rawLogHolder,text) def updateStatus(self,text): # status handler self.ui.progressStatusDescription.setText(text) def updateProgress(self,progress,total): try: # try parsing numbers and updating progressBar percent = (float(progress)/float(total))*100 self.ui.statusProgressBar.setValue (percent) except: ''' nothing to do ''' def finishedWork(self): self.enableActions() guicommon.updateInto (self.ui.rawLogHolder, guicommon.style("Finished syncing updates/packages","green_fin")) self.ui.progressStatusDescription.setText("Finished Syncing") self.ui.cancelButton.setText("Close") def disableActions(self): self.ui.browseFileFoldercheckBox.setEnabled(False) self.ui.cancelButton.setEnabled(False) self.ui.startInstallButton.setEnabled(False) self.ui.bugReportsButton.setEnabled(False) self.ui.browseFilePathButton.setEnabled(False) self.ui.zipFilePath.setEnabled(False) self.ui.changelogButton.setEnabled(False) def enableActions(self): self.ui.browseFileFoldercheckBox.setEnabled(True) self.ui.cancelButton.setEnabled(True) self.ui.startInstallButton.setEnabled(True) self.ui.bugReportsButton.setEnabled(True) self.ui.browseFilePathButton.setEnabled(True) self.ui.zipFilePath.setEnabled(True) self.ui.changelogButton.setEnabled(True) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtInstall() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtInstall.ui000066400000000000000000000147011475166737700233400ustar00rootroot00000000000000 AptOfflineQtInstall Qt::ApplicationModal 0 0 466 400 0 0 466 400 466 400 Install Packages 8 32 310 30 Path to apt-offline data file/dir true 340 32 110 30 Browse false 340 110 110 30 Install :/icons/icons/dialog-ok-apply.png:/icons/icons/dialog-ok-apply.png 340 70 110 30 Close :/icons/icons/application-exit.png:/icons/icons/application-exit.png 8 146 440 20 0 8 2 190 30 10 Specify file or folder path 12 120 60 16 10 Status: 68 120 53 15 10 Ready 8 180 441 200 QTextEdit::AutoAll true false true false false 10 75 130 30 Offline Bug Reports Bug Reports :/icons/icons/help-about.png:/icons/icons/help-about.png 342 10 110 18 Check to enable apt-offline data directory Is Directory false 170 75 140 30 Display Changelog Changelog :/icons/icons/help-about.png:/icons/icons/help-about.png zipFilePath browseFileFoldercheckBox browseFilePathButton bugReportsButton changelogButton cancelButton startInstallButton rawLogHolder apt-offline-1.8.6/apt_offline_gui/AptOfflineQtInstallBugList.py000066400000000000000000000127331475166737700246500ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os,sys from PyQt5 import QtCore, QtGui, QtWidgets import zipfile, tempfile from apt_offline_gui.Ui_AptOfflineQtInstallBugList import Ui_AptOfflineQtInstallBugList import apt_offline_core.AptOfflineCoreLib as AptOfflineCoreLib class AptOfflineQtInstallBugList(QtWidgets.QDialog): output = QtCore.pyqtSignal(str) progress = QtCore.pyqtSignal(str, str) status = QtCore.pyqtSignal(str) finished = QtCore.pyqtSignal() terminated = QtCore.pyqtSignal() def __init__(self, filepath, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_AptOfflineQtInstallBugList() self.bugList = {} self.filepath = filepath self.ui.setupUi(self) self.populateBugList(self.filepath) self.ui.bugListViewWindow.itemSelectionChanged.connect(self.populateBugListPlainTextEdit) # Connect the clicked signal of the Browse button to it's slot #QtCore.QObject.connect(self.ui.closeButton, QtCore.SIGNAL("clicked()"), # self.reject ) self.ui.closeButton.clicked.connect(self.reject) def populateBugListPlainTextEdit(self): self.ui.bugListplainTextEdit.clear() textItem = str(self.ui.bugListViewWindow.currentItem().text() ) extractedText = self.bugList[textItem] self.ui.bugListplainTextEdit.appendPlainText((b" ".join(extractedText)).decode('utf-8')) myCursor = self.ui.bugListplainTextEdit.textCursor() myCursor.movePosition(myCursor.Start) self.ui.bugListplainTextEdit.setTextCursor(myCursor) def noBugPopulateBugListPlainTextEdit(self): self.ui.bugListplainTextEdit.clear() self.ui.bugListplainTextEdit.appendPlainText("No Bug Reports Found") def populateBugList(self, path): if os.path.isfile(path): zipFile = zipfile.ZipFile(path, "r") for filename in zipFile.namelist(): if filename.endswith( AptOfflineCoreLib.apt_bug_file_format ): #INFO: The splitter is use is "{}". Also used at other places bugNumber = filename.split("{}")[1] temp = tempfile.NamedTemporaryFile() temp.file.write( zipFile.read( filename ) ) temp.file.flush() temp.file.seek( 0 ) #Let's go back to the start of the file for bug_subject_identifier in temp.file.readlines(): bug_subject_identifier = bug_subject_identifier.decode('utf-8') print(bug_subject_identifier) if bug_subject_identifier.startswith( 'Subject:' ): bug_subject_identifier = str(bugNumber) + ": " + bug_subject_identifier.lstrip("Subject:") bug_subject_identifier = bug_subject_identifier.rstrip("\n") temp.file.seek(0) self.bugList[bug_subject_identifier] = temp.file.readlines() break temp.file.close() elif os.path.isdir(path): for filename in os.listdir( path ): if filename.endswith( AptOfflineCoreLib.apt_bug_file_format ): bugNumber = filename.split("{}")[1] filename = os.path.join(path, filename) temp = open(filename, 'r') for bug_subject_identifier in temp.readlines(): bug_subject_identifier = bug_subject_identifier.decode('utf-8') if bug_subject_identifier.startswith( 'Subject:' ): bug_subject_identifier = str(bugNumber) + ": " + bug_subject_identifier.lstrip("Subject:") bug_subject_identifier = bug_subject_identifier.rstrip("\n") temp.seek(0) self.bugList[bug_subject_identifier] = temp.readlines() break temp.close() else: print("Invalid Path") return False if len(list(self.bugList.keys())) == 0: self.noBugPopulateBugListPlainTextEdit() else: for eachItem in list(self.bugList.keys()): item = QtWidgets.QListWidgetItem(eachItem) self.ui.bugListViewWindow.addItem(item) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtInstallBugList() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtInstallBugList.ui000066400000000000000000000075371475166737700246430ustar00rootroot00000000000000 AptOfflineQtInstallBugList 0 0 642 674 0 0 642 674 642 674 List of Bugs :/icons/icons/help-about.png:/icons/icons/help-about.png true 30 30 581 121 10 Bug List Bug List Bug List QFrame::WinPanel false QListView::Adjust true 30 10 231 16 List of bugs 30 170 581 461 10 false Bug Report Content Bug Report Content Bug Report Content QFrame::Panel QFrame::Sunken true false true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 510 640 100 28 Close this window Close :/icons/icons/application-exit.png:/icons/icons/application-exit.png apt-offline-1.8.6/apt_offline_gui/AptOfflineQtInstallChangelog.py000066400000000000000000000052161475166737700251640ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os,sys from PyQt5 import QtCore, QtGui, QtWidgets import zipfile, tempfile from apt_offline_gui.Ui_AptOfflineQtInstallChangelog import Ui_AptOfflineQtInstallChangelog class AptOfflineQtInstallChangelog(QtWidgets.QDialog): output = QtCore.pyqtSignal(str) progress = QtCore.pyqtSignal(str, str) status = QtCore.pyqtSignal(str) finished = QtCore.pyqtSignal() terminated = QtCore.pyqtSignal() def __init__(self, filepath, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_AptOfflineQtInstallChangelog() self.filepath = filepath self.ui.setupUi(self) self.populateChangelog(self.filepath) # Connect the clicked signal of the Browse button to it's slot #QtCore.QObject.connect(self.ui.closeButton, QtCore.SIGNAL("clicked()"), # self.reject ) self.ui.closeButton.clicked.connect(self.reject) def populateChangelog(self, path): self.chlogFile = tempfile.NamedTemporaryFile('r+', buffering=-1, encoding='utf-8', dir=None, delete=True) self.chlogPresent = False if os.path.isdir(path): for eachItem in os.listdir(path): eachItem = os.path.join(path, eachItem) if eachItem.endswith(".changelog"): eachFile = open(eachItem, 'r') self.chlogFile.write(eachFile.read()) self.chlogPresent = True elif os.path.isfile(path): zipLogFile = zipfile.ZipFile(path) for filename in zipLogFile.namelist(): if filename.endswith(".changelog"): self.chlogFile.write(zipLogFile.read(filename)) self.chlogPresent = True else: return False if self.chlogPresent is False: self.ui.changelogPlainTextEdit.clear() self.ui.changelogPlainTextEdit.appendPlainText('No changelog present') else: self.ui.changelogPlainTextEdit.clear() self.chlogFile.seek(0) self.ui.changelogPlainTextEdit.appendPlainText(self.chlogFile.read()) myCursor = self.ui.changelogPlainTextEdit.textCursor() myCursor.movePosition(myCursor.Start) self.ui.changelogPlainTextEdit.setTextCursor(myCursor) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtInstallChangelog() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtInstallChangelog.ui000066400000000000000000000036611475166737700251530ustar00rootroot00000000000000 AptOfflineQtInstallChangelog 0 0 670 675 0 0 Dialog true 540 620 100 28 Close this window Close :/icons/icons/application-exit.png:/icons/icons/application-exit.png 20 50 621 561 true false true 20 20 81 20 Changelog apt-offline-1.8.6/apt_offline_gui/AptOfflineQtMain.py000066400000000000000000000135511475166737700226330ustar00rootroot00000000000000import os from PyQt5 import QtCore, QtGui, QtWidgets from apt_offline_gui.Ui_AptOfflineQtMain import Ui_AptOfflineMain from apt_offline_gui.AptOfflineQtCreateProfile import AptOfflineQtCreateProfile from apt_offline_gui.AptOfflineQtFetch import AptOfflineQtFetch from apt_offline_gui.AptOfflineQtInstall import AptOfflineQtInstall from apt_offline_gui.AptOfflineQtAbout import AptOfflineQtAbout class AptOfflineQtMain(QtWidgets.QMainWindow): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.ui = Ui_AptOfflineMain() self.ui.setupUi(self) # Configure the various actions self.ConfigureCreateProfile() self.ConfigureDownload() self.ConfigureInstall() self.ConfigureAbout() self.ConfigureMenuExit() # Configure Hover over Buttons for Help self.CreateButtonHoverHelp() def ConfigureCreateProfile(self): #QtCore.QObject.connect(self.ui.menuCreateProfile, QtCore.SIGNAL("triggered()"), self.CreateProfile) #QtCore.QObject.connect(self.ui.createProfileButton,QtCore.SIGNAL("clicked()"), self.CreateProfile) self.ui.menuCreateProfile.triggered.connect(self.CreateProfile) self.ui.createProfileButton.clicked.connect(self.CreateProfile) # Create an object and do not show it self.createProfileDialog = AptOfflineQtCreateProfile() # setup hover hack self.ui.createProfileButton.installEventFilter(self) def ConfigureDownload(self): #QtCore.QObject.connect(self.ui.menuDownload, QtCore.SIGNAL("triggered()"), self.DownloadPackagesUpgrades) #QtCore.QObject.connect(self.ui.downloadButton, QtCore.SIGNAL("clicked()"), self.DownloadPackagesUpgrades) self.ui.menuDownload.triggered.connect(self.DownloadPackagesUpgrades) self.ui.downloadButton.clicked.connect(self.DownloadPackagesUpgrades) # Create an object for download dialog self.createDownloadDialog = AptOfflineQtFetch() # setup hover hack self.ui.downloadButton.installEventFilter(self) def ConfigureInstall(self): #QtCore.QObject.connect(self.ui.menuInstall, QtCore.SIGNAL("triggered()"), self.InstallPackagesUpgrades) #QtCore.QObject.connect(self.ui.restoreButton, QtCore.SIGNAL("clicked()"), self.InstallPackagesUpgrades) self.ui.menuInstall.triggered.connect(self.InstallPackagesUpgrades) self.ui.restoreButton.clicked.connect(self.InstallPackagesUpgrades) # Create an object for Install dialog self.createInstallDialog = AptOfflineQtInstall() # setup hover hack self.ui.restoreButton.installEventFilter(self) def ConfigureAbout(self): #QtCore.QObject.connect(self.ui.menuAbout, QtCore.SIGNAL("triggered()"), self.ShowAbout) #QtCore.QObject.connect(self.ui.menuHelp_2, QtCore.SIGNAL("triggered()"), self.ShowHelp) self.ui.menuHelp_2.triggered.connect(self.ShowAbout) self.ui.menuAbout.triggered.connect(self.ShowAbout) # Create an object for About Dialog self.createAboutDialog = AptOfflineQtAbout() def ConfigureMenuExit(self): #QtCore.QObject.connect(self.ui.menuExit, QtCore.SIGNAL("triggered()"), self.ExitApp) #QtCore.QObject.connect(self.ui.exitButton, QtCore.SIGNAL("clicked()"), self.ExitApp) self.ui.menuExit.triggered.connect(self.ExitApp) self.ui.exitButton.clicked.connect(self.ExitApp) def eventFilter(self,target,event): # hover hack for 3 buttons if event.type() == QtCore.QEvent.HoverEnter: if target.objectName() == 'createProfileButton': self.ui.descriptionField.setText("Click here to generate a signature of this machine.") if target.objectName() == 'downloadButton': self.ui.descriptionField.setText("Once you are on an internet connected machine, use this to download packages as per your signature file.") if target.objectName() == 'restoreButton': self.ui.descriptionField.setText("Once you've downloaded all the packages, click here to install them on the offline machine.") if event.type() == QtCore.QEvent.HoverLeave: self.ui.descriptionField.setText("Hover your mouse over the buttons to get the description.") return False def CreateProfile(self): try: if os.geteuid() != 0: QtWidgets.QMessageBox.critical(self, "Error", "You need to run with root privileges!") return except AttributeError: QtWidgets.QMessageBox.critical(self, "Error", "This is supported only on Unix like systems with apt installed.") return # Code for creating Modal Dialog for Create Profile self.createProfileDialog.resetUI() self.createProfileDialog.show() def DownloadPackagesUpgrades(self): # Code for creating Modal Dialog for Downloading Packages/Upgrades self.createDownloadDialog.resetUI() self.createDownloadDialog.show() def InstallPackagesUpgrades(self): try: if os.geteuid() != 0: QtWidgets.QMessageBox.critical(self, "Error", "You need to run with root privileges!") return except AttributeError: QtWidgets.QMessageBox.critical(self, "Error", "This is supported only on Unix like systems with apt installed.") return # Code for creating Modal Dialog for Installing Packages/Upgrades self.createInstallDialog.show() def ShowAbout(self): # Code for showing Model Dialog for About Application self.createAboutDialog.show() def ShowHelp(self): QtWidgets.QMessageBox.information(self, "Info", "Please refer to the apt-offline(8) man page") def CreateButtonHoverHelp(self): pass def ExitApp(self): self.close() apt-offline-1.8.6/apt_offline_gui/AptOfflineQtMain.ui000066400000000000000000000176321475166737700226240ustar00rootroot00000000000000 AptOfflineMain 0 0 432 544 0 0 432 544 432 544 APT Offline Qt::ToolButtonIconOnly true 30 20 371 40 12 true Generate Signature :/icons/icons/contact-new.png:/icons/icons/contact-new.png 30 80 371 41 12 50 false Download Packages or Updates :/icons/icons/go-down.png:/icons/icons/go-down.png 30 140 371 41 12 Install Packages or Updates :/icons/icons/install.png:/icons/icons/install.png 30 220 371 211 10 QFrame::Box QFrame::Plain 1 0 0 371 211 Hover your mouse over the buttons to get the description Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true 10 280 450 121 41 10 Exit :/icons/icons/application-exit.png:/icons/icons/application-exit.png 30 200 91 21 10 Description 0 0 432 28 Operations Help :/icons/icons/contact-new.png:/icons/icons/contact-new.png Generate Signature Ctrl+N :/icons/icons/go-down.png:/icons/icons/go-down.png Download Packages or Updates Ctrl+O :/icons/icons/install.png:/icons/icons/install.png Install Packages or Updates Ctrl+I :/icons/icons/application-exit.png:/icons/icons/application-exit.png Exit Ctrl+Q :/icons/icons/help-contents.png:/icons/icons/help-contents.png Help F1 :/icons/icons/help-about.png:/icons/icons/help-about.png About apt-offline apt-offline-1.8.6/apt_offline_gui/AptOfflineQtSaveZip.py000066400000000000000000000024671475166737700233340ustar00rootroot00000000000000import sys from PyQt5 import QtCore, QtGui from apt_offline_gui.Ui_AptOfflineQtSaveZip import Ui_SaveZipFile class AptOfflineQtSaveZip(QtGui.QDialog): def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self.ui = Ui_SaveZipFile() self.ui.setupUi(self) # Connect the clicked signal of the Browse button to it's slot QtCore.QObject.connect(self.ui.browseFilePathButton, QtCore.SIGNAL("clicked()"), self.popupDirectoryDialog ) # Connect the clicked signal of the Save to it's Slot - accept QtCore.QObject.connect(self.ui.saveButton, QtCore.SIGNAL("clicked()"), self.accept ) # Connect the clicked signal of the Cancel to it's Slot - reject QtCore.QObject.connect(self.ui.cancelButton, QtCore.SIGNAL("clicked()"), self.reject ) def popupDirectoryDialog(self): # Popup a Directory selection box directory = QtGui.QFileDialog.getExistingDirectory(self, 'Open Directory') # Show the selected file path in the field marked for showing directory path self.ui.zipFilePath.setText(directory) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) myapp = AptOfflineQtSaveZip() myapp.show() sys.exit(app.exec_()) apt-offline-1.8.6/apt_offline_gui/AptOfflineQtSaveZip.ui000066400000000000000000000045571475166737700233230ustar00rootroot00000000000000 SaveZipFile Qt::WindowModal 0 0 445 176 Packages downloaded successfully 20 70 291 31 20 0 401 71 Success! The packages have been downloaded. Please specify the location where you want the zip file of the packages to be saved true 330 70 101 31 Browse 90 120 121 31 Save :/icons/icons/document-save.png:/icons/icons/document-save.png 240 120 121 31 Cancel :/icons/icons/dialog-cancel.png:/icons/icons/dialog-cancel.png apt-offline-1.8.6/apt_offline_gui/QtProgressBar.py000066400000000000000000000056351475166737700222340ustar00rootroot00000000000000class QtProgressBar( object ): def __init__( self, progressbar=None,label =None, minValue=0, maxValue=0, total_items=None): self.min = minValue self.max = maxValue self.span = float( self.max - self.min ) self.fd = fd self.signal_set = False # This field stores QProgessBar self.progressBar = progressbar # This field stores the Label self.progressLabel = label self.value = self.min if total_items is None or total_items <= 0: self.items = 0 #count of items being tracked self.items_update = True else: self.items = total_items self.items_update = False self.complete = 0 def handle_resize( self, signum, frame ): h, w = array( 'h', ioctl( self.fd, termios.TIOCGWINSZ, '\0' * 8 ) )[:2] self.width = w def updateValue( self, newValue ): #require caller to supply a value! newValue is the increment from last call self.value = max( self.min, min( self.max, self.value + newValue ) ) self.display() def completed( self ): self.complete = self.complete + 1 #if self.signal_set: #signal.signal( signal.SIGWINCH, signal.SIG_DFL ) self.display() def addItem( self, maxValue ): self.max = self.max + maxValue self.span = float( self.max - self.min ) if self.items_update is True: self.items = self.items + 1 self.display() def display( self ): #print "\r%3s /%3s items: %s\r" % ( self.complete, self.items, str( self ) ), self.progressBar.setValue(int(self.__str__())) progressText = "%3s /%3s Size: %s" % ( self.complete, self.items, self.__numStr__( self.max / 1024 )) self.progressLabel.setText(progressText) def __str__( self ): #compute display fraction percentFilled = ( ( self.value - self.min ) / self.span ) #widthFilled = int( self.width * percentFilled + 0.5 ) #return ( "[" + "#"*widthFilled + " " * ( self.width - widthFilled ) + "]" + " %5.1f%% of %s" % ( percentFilled * 100.0, self.__numStr__( self.max / 1024 ) ) ) return percentFilled def __numStr__( self, size ): if size > 1024: size = size / 1024 if size > 1024: size = size / 1024 return ( "%d GiB" % ( size ) ) return ( "%d MiB" % ( size ) ) return ( "%d KiB" % ( size ) ) apt-offline-1.8.6/apt_offline_gui/UiDataStructs.py000066400000000000000000000066351475166737700222360ustar00rootroot00000000000000# -*- coding: utf-8 -*- class SetterArgs(): def __init__(self, filename, update, upgrade, install_packages, install_src_packages, \ src_build_dep, changelog, release, apt_backend, simulate=False): self.set = filename # self.set_update is of type boolean self.set_update = update # self.set_upgrade can be either True or False self.set_upgrade = upgrade self.upgrade_type = "upgrade" # Should be set to None for disabling or Tuple for activating self.set_install_packages = install_packages # To be implemented later self.src_build_dep = src_build_dep self.set_install_src_packages = install_src_packages self.set_install_release = release self.apt_backend = apt_backend self.set_simulate=simulate self.generate_changelog = changelog def __str__(self): print("self.set=",self.set) print("self.set_update=",self.set_update) print("self.set_upgrade=",self.set_upgrade) print("self.upgrade_type=",self.upgrade_type) print("self.set_install_packages=",self.set_install_packages) print("self.set_simulate=", self.set_simulate) return "" class GetterArgs(): def __init__(self, filename=None, bundle_file=None, socket_timeout=30, \ num_of_threads=1, disable_md5check=True, deb_bugs=False, download_dir=None, cache_dir=None, proxy_host=None, proxy_port=None, progress_bar=None, progress_label=None): self.get = filename # TODO: to be implemented in next revision self.socket_timeout = socket_timeout self.num_of_threads = num_of_threads self.bundle_file = bundle_file self.disable_md5check = disable_md5check self.deb_bugs = deb_bugs self.download_dir = download_dir self.cache_dir = cache_dir self.proxy_host = proxy_host self.proxy_port = proxy_port self.progress_bar = progress_bar self.progress_label = progress_label def __str__(self): print("self.get=",self.get) print("self.filename=",self.filename) print("self.bundle_file=",self.bundle_file) print("self.socket_timeout=",self.socket_timeout) print("self.num_of_threads=",self.num_of_threads) print("self.disable_md5_check=",self.disable_md5check) print("self.deb_bugs=",self.deb_bugs) print("self.download_dir=",self.download_dir) print("self.cache_dir=",self.cache_dir) return "" ''' # install opts Str_InstallArg = args.install Bool_TestWindows = args.install_simulate Bool_SkipBugReports = args.skip_bug_reports Bool_Untrusted = args.allow_unauthenticated Str_InstallSrcPath = args.install_src_path ''' class InstallerArgs(): def __init__(self, filename=None, skip_bug_reports=True, skip_changelog=True, allow_unauthenticated=False, install_src_path=None, progress_bar=None, progress_label=None, simulate = False): self.install = filename # TODO: to be implemented in next revision self.install_simulate = simulate self.skip_bug_reports = skip_bug_reports self.skip_changelog = skip_changelog self.allow_unauthenticated = allow_unauthenticated self.install_src_path = install_src_path self.progress_bar = progress_bar self.progress_label = progress_label apt-offline-1.8.6/apt_offline_gui/__init__.py000066400000000000000000000000001475166737700212120ustar00rootroot00000000000000apt-offline-1.8.6/apt_offline_gui/genui.sh000077500000000000000000000006461475166737700205670ustar00rootroot00000000000000#!/bin/sh # Todo : after adding a new UI file to dialog, also add # its corresponding Ui_ script generator here # echo "Compiling Ui files" for each_file in *.ui; do filename=$(echo $each_file | cut -d "." -f1) echo "Compiling file $each_file into Ui_$filename.py"; pyuic5 --from-imports $each_file -o Ui_$filename.py; done echo "Compiling Resources files" pyrcc5 -o resources_rc.py resources.qrc echo "Done" apt-offline-1.8.6/apt_offline_gui/icons/000077500000000000000000000000001475166737700202265ustar00rootroot00000000000000apt-offline-1.8.6/apt_offline_gui/icons/application-exit.png000066400000000000000000000033401475166737700242060ustar00rootroot00000000000000PNG  IHDR szz pHYs^tIME kbKGDmIDATxW[lTU]ޙR>JUbĆ"HH0 ["9=g̙ kSY+͋AMSLhU:;3}}f9-I4Zuu eԨ}'O&ޡ rzM0^]h65 Pшn6\߉D]=R |֏P!|uh‘A'͜938S"? ep;MJ29g{[1b*;Vś_@ h`#obrV$WO j (_{O}E,)ą!b6=IΙqX)d>[kBaB&ۖSU,bz {w ;UD2Il/&e"RD[Oq XKY(Uasze 'WQ-lm%e1pgU1P, w׆h zZ̲yPH" Ȥ !@@{RA|ϞGDLop:>  ?͓"*8c1!oj07:,Gb /pC>Z2YDÜ.ЭWh4W{Q#(Ep%MsШ x .1=6tl,Ϧ {t ENXqitDu8>Ў0n/ye{f7vDhIENDB`apt-offline-1.8.6/apt_offline_gui/icons/configure.png000066400000000000000000000026521475166737700227220ustar00rootroot00000000000000PNG  IHDR szzsRGB pHYs^tIME)%FUbKGD*IDATxڽV[PSW5h"be4"2EBCx"Ũ!!ommVU:-XEZ|ؖ)Y?3k}]{a#\]gX+Tx+ߴ;_[{~6c wX]ߏALxxNdɉZVr{zLx^PkT֕exl%0j^S]x%hA%pX+˷ogx\ֻg|dm~DUH^~?NY,cPyF7}ϋ=\il*R+֌&aUm<R{kw?K&^C孺sƝϻIG; U؜ۛ-?XL}4g 5]Pռpb9 ^fZi91NHJ8%zދ|QW_gNn(; V3D{6ZDeuo?3g0w\D?6T|Yj6B:u-tK LH0W=vMFgY.򱯩9`2 hE a}ޭ5DD"kx[P^(b gJ3p8D[EIpVwOn"x׏44DigƲbQCG܎Gq#k~/q* #0m4T@&˿FX%%ӕ?]j%asj}fɢ%* ˅Bxzy".>e.zp<ٰٔqR S ((sppHa<+ ާǏ6bBD vvv* =+~ya 77BJj D$i^{Ξ'$V:h2 3T b"߅ȊD\7> r]ZT`Qkr7 t|V$$&@* "CA5-2\TkG0`^$ 'Px1[TʷhEѱSJھ}!Q{mmmAwG_:4ǩץBG?4Gv/<4\_22:cbm6JA?;B$KXڼ`f<+fQ 84DwOveA}gSel74w_p56"c#I3*ʧWy|f_ TULe C&nҮCEiRCHĆ1S7'F'l qp i>yZЙ 0v5@R7 ~@[^Ęklځ>-,sN5^M@`B G>:').aDst 0xr)v8@a5L‚ 30sa0^\ DLJАٔA 4J̒zS !y!"׆! $Z9;+9[w`a}5 A[XZln\zoѡLSSUV 7y|qHꇲ 8IB ,w>פ‘ap"!^NnMcb G)]VuqgәC\|LP641q,?ƇwH&th:Zbq3qmxisHSkplOX4\JcV*JgEwSuzV4s<{C, {C)*<% %$-e%ë^w-`ppzRH{>B`FK)j8Ir=0G$yZtc,NucOϼrD  O _vec}>n΅R=V`Iպ!hU/8v5G>E3iO?\>P.7W-1Ӡ9jbx/ӈVP~SݽSB }ۺ듓G%'ťWli΄`\~U?Z.` n"Z&#F۳TIENDB`apt-offline-1.8.6/apt_offline_gui/icons/dialog-cancel.png000066400000000000000000000042371475166737700234240ustar00rootroot00000000000000PNG  IHDR szzbKGD pHYs^tIME (x,IDATx՗kl[9;v8vnBӥkM*QFWtih4nӐ&}&1&m(c+m)Z'4t6$M\\v|n;= QS5\ĤiG-yϣhu O4Ųfӝ?{{gg >\| ?vٳzz~/:i*p?aDobD7oX Ũ#94hD(誊IsH!zeϞ+ka¦50QgGqr9[G߻`IWyMU Q }|Z/oX5bZu!z 0v씩3N^"w|$A~`e^߷Zx†K50KN Sywّz0kH |a-PDcZp dGUx&,c՗ZwW.;`iހKDoC؟Hy|wY 6.VIE>ùe&)XzaҊp3oVH݊wӰE(|:3B8\ɩ3Uy&-YB<| ;%n/. 4+'ct o6~=t(A>W0&.J (n,Y%Lt~]8Be)$!n7\>7tꀯTeD*Le2J5Cfu"5ʌƤI+6MgV27/c_T^+H@3T,$F-%dZxR G>藛7=Ҿi9jt6Tzrd) @5=?vWjcr1Q@[Mib( o, xYeE xMmrɰHv=@G h[ʽ w9R8Ͼxj:%ޠ\^nZ4yeD*[*,8!.LKDZ-|k J O.+Fdx+F 'O@"m𯁡\֏1F }Y }6}K'쮪Ѩ @vKL 1_蓚x.+@\[eZhI X{*+n4,ɜXr68|$R EIކs+_j~fEяnLQ쪝NDy I -HVGP- r RypE-3!6Ѱ`M/ޮ2x- -p1p`,lds&e{F<˦Y MJ!noW=zqy1x3i  GiZߚ5AEz$ , eѣ̌/K6\pClBHq r3@rj#oa:ϣIENDB`apt-offline-1.8.6/apt_offline_gui/icons/dialog-ok-apply.png000066400000000000000000000023021475166737700237220ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<?IDATxmLuǿk)]kR,V!l&,qE3c6I M$_,."fD^LB$al"lS@Z JJ>7\~}H U_zwuGu91Rt+q0-ݫtF:%666n+oyfTmBÆ#ѓ %eؗzM&ʒkA0EB 32@v@_1֚]Ν -Y<rJL%+vnkrG[Ҧ~om_cɳo+xP$HI͸gp)lOfڮF !9[-HV (fH5N\v  $7Ֆɖ ,M$XqIZ҈bjb`:85rT;+y9hgW7׮% Brɀ"0HjHl"b(8> s O׌dCi{LeσBHR EQ(K0DY1i5W I u3OZq3ׇ. V ڇ-H0o Ej KNcƹ_/|EN2JL=h}'r ) W:8{AM $WBø}"T `1qLIMODBu|nX}l|{n 8p##\S:zVi4i܍jh ڊu.oIENDB`apt-offline-1.8.6/apt_offline_gui/icons/document-save.png000066400000000000000000000023571475166737700235150ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<lIDATxVoEmvbv " 7gԗ T*oE4""Lq[fFzwBM3w775 @_D]λΛ, KxgHlQ {?iֹo~4ý:8߹ۯ:ȍYN,~/S6Hf$IVc웠yOgy~`n3@s -n,zž&D9a4Kh74 a pm V`5ɨC1S o%\(.qRCQә1<:WJP>4$Ȃ PъOtSq1u+ ^]1.Bi5' "ڎZ]Y95}Qw<2-HZMU.BpL5WDd<`u!9 hQt%a 'C׃K/0S]['=jO~z ŠV+8[I}k@X^?):50Mjқο0\`ⱊ/+0.saST, ,_ CZŗtܸyV(>2o3 ON( KS((N)x QKD-(dg0!#jj:   l@G$ +L"({4tࢢ8>|q$ +¢K $9_BdEdM FX_/z`V ǁ1Z2a]4M/I |9ߞv>46˗.}P־RU-E&ωP( Y+2Mv,^lULƣ?766~gx0 9=BjDП>(h*]2ѐC4wh4<"+츮빎(]a'/8"06}۱gxX`:Лap 壻f;4GTOD#K@4յ^xψȝL&5pgߚғCIENDB`apt-offline-1.8.6/apt_offline_gui/icons/go-down.png000066400000000000000000000026351475166737700223140ustar00rootroot00000000000000PNG  IHDR szz pHYs^tIME  ;+Ck?@/ * F yٖ7mt<~|d|&L`ќsMK}SCHJT-hc;{fQY{}3- hnfD"V_Ns:2}hΐcȭLYض):ɸEh5+Ʀhp Q NdMs H߇3ЀMo@]j䉣*GN7ei%LH3Պ(~2'ϵQBSd/fR9ӂ˽h@Npu±BUTSs#APqr>fn&䍡|_pmF8x(‘"B]uM7YZm@VDdS&k,nN"$:z!NGv| ` ̅isT$'[7ڿ%fR e+*ʡpaJ d0ȺC ڪ,(=} *%vy;H]d @J>,EQ՞;hO92AF I;i>"{ț4Yԍ+Vc!CQ N *RG۩CnuT @bh&ܦOQQ ZWKu.w~fO< z%bCU Q`\38wH޻ =~3<(.t 4d͗dQ|39drjՍs]\ׅ8Ǹws'J ;խR}@Ԍn?;]?&CT5#hz_>bΦoi-O6z{钵zLmA(B5MGN}"$P38֏tLD߇5RLR}ӵIENDB`apt-offline-1.8.6/apt_offline_gui/icons/help-about.png000066400000000000000000000027231475166737700230000ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<PIDATxŗ_U?sf{]][5ET,jTfH_֊z!zAY% %!!LEBP衤V(T1 kg=9x9s眙k- {t~^dÈBXu|IbR[ ߬Ǫ ,sưc~kK\^$0NK@$n#mWomlCԪϚeX0YY0,*\e^ŁS,TATOru=}+PjUSV^x!>TTJ!=}ݠ* ?iƁf7gҜ@$n|;+i#O l"6?\a\blbIA*bnv\I䳞\nՀM>LĂP(HÜM'5N.bT \UPqՂzJBU0P!krVcKY0.[-ך6 ŪzJpI,@{0\q4#Ǭli 1l%ٳ$fV$ f$8!w !w,F|x=L-J:!z^H1B5"P/4P~/u\[)u:$vBQkQa隇 <uXV#3`.:*v\/SW`rNڮ=@ {ڂt*2Z9edd?~l[`)`-CmC`mb1ۺ{iC٣ MuOsa 4RЏUo^^Rõ[yaZwƲGiXًHk%4Ua3Ԑ.|wԎ%(bAĬg vWzY:ap" M`xjF!:Am~;*X3cRnnuFfv * ,m@.d6#gF̞pOׯ(%JG( +DqM$DpL E5F L@*r_UIENDB`apt-offline-1.8.6/apt_offline_gui/icons/help-contents.png000066400000000000000000000030771475166737700235260ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxW͏EUu|- +, q#~  I8p LL7Op7z0xMEID #3Ntw^uRٕdϮ՛Y#LСC 0b☚{ Ȅ)RkM$[@9ҊPvM_z>'g˞WN>\[_ҵ90vx#lw<=$jX]hA RP3X^RJw?֔Ppuv%s L c4s\R ]W[k@J&k?ı-/4.mHaynDPk#Yxx$I| Lc{Oncka`ǓHKTmXTjTbg1rL?]KS5lK-O)u&L hݿ<R<DSƎ-9] 3|G+xq|C sxsNJP(/ӂFU#MLM$8wQo=pyo|pr?k97Ymp>vՍ(` "xm "Ё({`;mO|^ĈyvN$0 2_h~)厛3TЉrg!@p|P&1E(/…6oQM$Ձ$X#'`e)*DAe^R@14ZS)B֖:tw ?ut2m@% O0"fT DTct{W͵b>CZ \kaõ,, J#56\jbǜdΤf kCh;t<{+9fw>`ju xSWN*j׊S0pY^clw]ggBjD~Y,P:Ќ2YC. DW. FD1pp ل0*W$JGjQohpȸiه؏>$S2s̟"!@/^!hx!22Vŕ@Tx2]C+Ր(Uk>FٕL8CU(d z$6pzrځ; i% McOzhMJ|1]2 x \D58kJ1J^SߎͲ̾}34"/²~.x µ}CnKS[i"/"k}:vޱ["ֺ?99lgJ) TB)Xc׃ p) E);Lۏ' nN w0Fǥ b\bj @]ffԌ5XkoEU8˽:K?!h3A IENDB`apt-offline-1.8.6/apt_offline_gui/icons/install.png000066400000000000000000000035701475166737700224070ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxŗ PU²v d]G"52PBE_ddXJ!4 AU†NδfQ;|sKCOtrrZ>{{?mO//o.ruh3bUX~[Q, g4-=4q}P;WMmL_kl!+ۡS0H9JNg |%:1)IIIc[jk'`'`"cillL,Ǡ='<<| tvqXTɜc70_11<Cd[k,;IdddƍNTeB\B%'AF6NGA`rUEݩHmOBܓ;%xapz_δ$gϦx-ZǂBKyfd= [,7-CqS 9er=o84&(S}ݾ)YҭO~K BZ{hV@ՎjO[(? x~xx|d+S1 euЙ" weNw ;}G%%An E?qWcWgquytnֵͷ/G[ᰧ<7/=Rb.uqqDzݎ@Pŋ@xIq QOO ~~~OQ@ g}\:fl+`WdBxRh`)L&訲377E!""|k5&` FGcD$à>puǸ*E4cSw6G(&K])t<Tm&g|@7\7M] vb1CQk1 "88a.y.Y~0~,hB#R I>puu[[6/ZPpF))k ' 0uT);c3:511!W0!1`eeE;cb BUgk*#CP .NcZʂIENDB`apt-offline-1.8.6/apt_offline_gui/resources.qrc000066400000000000000000000007031475166737700216340ustar00rootroot00000000000000 icons/configure.png icons/help-about.png icons/application-exit.png icons/contact-new.png icons/dialog-cancel.png icons/dialog-ok-apply.png icons/document-save.png icons/go-down.png icons/help-contents.png icons/install.png apt-offline-1.8.6/bash_completion/000077500000000000000000000000001475166737700171275ustar00rootroot00000000000000apt-offline-1.8.6/bash_completion/apt-offline000066400000000000000000000120021475166737700212510ustar00rootroot00000000000000# Debian apt-offline(8) completion -*- shell-script -*- _apt_offline() { local cur prev words cword _init_completion || return local special i for (( i=0; i < ${#words[@]}-1; i++ )); do if [[ ${words[i]} == @(set|get|install|--install-packages|--install-src-packages) ]]; then special=${words[i]} fi done if [[ -n $special ]]; then case $special in --install-src-packages) COMPREPLY=( $( apt-cache --no-generate pkgnames "$cur" \ 2> /dev/null ) $( apt-cache dumpavail | \ command grep "^Source: $cur" | sort -u | cut -f2 -d" " ) ) return 0 ;; --install-packages) COMPREPLY=( $( apt-cache --no-generate pkgnames "$cur" \ 2> /dev/null ) ) return 0 ;; get) case $prev in -d|--download-dir|-s|--cache-dir) _filedir -d return 0 ;; --bundle) _filedir return 0 ;; --https-cert-file) _filedir return 0 ;; --https-key-file) _filedir return 0 ;; --http-basicauth) _http_auth_url return 0 ;; --proxy-host) _ip_addresses return 0 ;; --proxy-port) _port return 0 ;; --socket-timeout) COMPREPLY=( $( compgen -W '10 30 100 300 1000' -- "$cur" ) ) return 0 ;; -t|--threads) COMPREPLY=( $( compgen -W '2 3 4 5 6 7 8 9 10' -- "$cur" ) ) return 0 ;; esac if [[ "$cur" == -* || -e $prev ]]; then COMPREPLY=( $( compgen -W '-h --help -v --verbose --version --simulate --socket-timeout -d --download-dir -s --cache-dir --no-checksum -t --threads --bundle --bug-reports --proxy-host --proxy-port --https-key-file --https-cert-file --disable-cert-check --http-basicauth' -- "$cur" ) ) else _filedir fi return 0 ;; install) case $prev in --install-src-path) _filedir -d return 0 ;; esac if [[ "$cur" == -* || -e $prev ]]; then COMPREPLY=( $( compgen -W '-h --help -v --verbose --version --simulate --install-src-path --skip-bug-reports --allow-unauthenticated --skip-changelog --strict-deb-check' -- "$cur" ) ) else _filedir fi return 0 ;; set) case $prev in --release) COMPREPLY=( $( apt-cache policy | \ command grep "release.o=Debian,a=$cur" | \ sed -e "s/.*a=\(\w*\).*/\1/" | uniq 2> /dev/null) ) return 0 ;; --upgrade-type) COMPREPLY=( $( compgen -W 'upgrade dist-upgrade dselect-ugprade' -- "$cur" ) ) return 0 ;; --apt-backend) COMPREPLY=( $( compgen -W 'apt apt-get python-apt' -- "$cur" ) ) return 0 ;; esac if [[ "$cur" == -* || -e $prev ]]; then COMPREPLY=( $( compgen -W '-h --help -v --verbose --version --simulate --install-packages --install-src-packages --src-build-dep --release --update --upgrade --upgrade-type --generate-changelog' -- "$cur" ) ) else _filedir fi return 0 ;; esac fi if [[ "$cur" == -* ]]; then COMPREPLY=( $( compgen -W '-h --help -v --verbose --version --simulate' -- "$cur" ) ) else COMPREPLY=( $( compgen -W 'get install set' -- "$cur" ) ) fi return 0 } && complete -F _apt_offline apt-offline # ex: ts=4 sw=4 et filetype=sh apt-offline-1.8.6/org.debian.apt.aptoffline.policy000066400000000000000000000015361475166737700221300ustar00rootroot00000000000000 Authentication is required to run Offline APT Package Manager package-x-generic auth_admin auth_admin auth_admin /usr/bin/apt-offline-gui-pkexec /usr/bin/apt-offline-gui true apt-offline-1.8.6/release_setps000066400000000000000000000011131475166737700165360ustar00rootroot000000000000001. Complete you all code changes and test properly 2. Bump version in AptOfflineCore.py, manpage and setup.py 3. Execute make "html" target to create html docs from the manpage 4. Perhaps now, go and do a `git tag -s rel_name` git archive --format=zip --prefix=apt-offline/ v1.6 > apt-offline-1.6.zip git archive --format=tar.gz --prefix=apt-offline/ v1.6 > apt-offline-1.6.tar.gz For a Windows platform release * unzip apt-offline zip archive generated from above git archive command * cd to unpacked folder * `make` * cd .. * zip -r apt-offline-VERSION-windows.zip apt-offline apt-offline-1.8.6/requirements.txt000066400000000000000000000005151475166737700172460ustar00rootroot00000000000000# In case users use it directly from source, please ensure following modules # are installed argparse magic soappy lzma pysimplesoap # There's also a bunch of packages that need to installed # On Debian systems, please ensure following packages, or their equivalent # are installed pyqt5-dev-tools man2html-base python3-debianbts apt-offline-1.8.6/setup.cfg000066400000000000000000000001511475166737700155770ustar00rootroot00000000000000[flake8] exclude = .git,.github max-line-length = 119 show-source = yes #showpep8 = yes statistics = yes apt-offline-1.8.6/setup.py000066400000000000000000000044631475166737700155020ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright 2009 Ritesh Raj Sarraf # # Licensed under the GNU General Public License v3 and later import textwrap import distutils.core distutils.core.setup( name='apt-offline', version='1.8.6', author='Ritesh Raj Sarraf', author_email='rrs@researchut.com', url='https://github.com/rickysarraf/apt-offline', #packages = [ 'apt_offline_core' ], description='Offline APT Package Manager', long_description = textwrap.dedent("""\ apt-offline is an Offline APT Package Manager for Debian and derivatives """), license='GPL', platforms = 'Posix; MacOS X; Windows', classifiers=[ 'Development Status :: 3 - Testing/Beta', 'Environment :: Console', 'Intended Audience :: End Users', 'License :: OSI Approved :: GPL', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Package Management', ], py_modules=['apt_offline_core.AptOfflineCoreLib', 'apt_offline_core.AptOfflineDebianBtsLib', 'apt_offline_core.AptOfflineLib', 'apt_offline_core.AptOfflineMagicLib', 'apt_offline_gui.AptOfflineQtAbout', 'apt_offline_gui.AptOfflineQtCommon', 'apt_offline_gui.AptOfflineQtCreateProfile', 'apt_offline_gui.AptOfflineQtFetch', 'apt_offline_gui.AptOfflineQtFetchOptions', 'apt_offline_gui.AptOfflineQtInstall', 'apt_offline_gui.AptOfflineQtInstallBugList', 'apt_offline_gui.AptOfflineQtInstallChangelog', 'apt_offline_gui.AptOfflineQtMain', 'apt_offline_gui.AptOfflineQtSaveZip', 'apt_offline_gui.__init__', 'apt_offline_gui.QtProgressBar', 'apt_offline_gui.resources_rc', 'apt_offline_gui.Ui_AptOfflineQtAbout', 'apt_offline_gui.Ui_AptOfflineQtCreateProfile', 'apt_offline_gui.Ui_AptOfflineQtFetch', 'apt_offline_gui.Ui_AptOfflineQtFetchOptions', 'apt_offline_gui.Ui_AptOfflineQtInstall', 'apt_offline_gui.Ui_AptOfflineQtInstallBugList', 'apt_offline_gui.Ui_AptOfflineQtInstallChangelog', 'apt_offline_gui.Ui_AptOfflineQtMain', 'apt_offline_gui.Ui_AptOfflineQtSaveZip', 'apt_offline_gui.UiDataStructs',], scripts=['apt-offline', 'apt-offline-gui', 'apt-offline-gui-pkexec'], ) apt-offline-1.8.6/tests/000077500000000000000000000000001475166737700151235ustar00rootroot00000000000000apt-offline-1.8.6/tests/apt-offline-testcase.bat000066400000000000000000000014171475166737700216330ustar00rootroot00000000000000@echo pypt-offline Test Cases python.exe apt-offline get C:\signature.uris --threads 5 -d C:\test rmdir /q /s C:\test ping -n 10 localhost 1>null python.exe apt-offline get C:\signature.uris --threads 5 --bundle C:\apt-offline-bundle.zip rmdir /q /s C:\apt-offline-bundle.zip ping -n 10 localhost 1>null python.exe apt-offline get C:\signature.uris --threads 5 ping -n 10 localhost 1>null python.exe apt-offline get C:\signature.uris --bundle C:\apt-offline-bundle.zip rmdir /q /s C:\apt-offline-bundle.zip ping -n 10 localhost 1>null python.exe apt-offline get C:\signature.uris -d C:\test rmdir /q /s C:\test ping -n 10 localhost 1>null python.exe apt-offline get C:\signature.uris --threads 5 --bug-reports --download C:\test rmdir /q /s C:\test ping -n 10 localhost 1>null apt-offline-1.8.6/tests/apt-offline-tests-github.sh000077500000000000000000000102321475166737700223040ustar00rootroot00000000000000#!/bin/sh DISLIKED_PACKAGES="gcc golang gnome-terminal" DISLIKED_SRC_PACKAGES="glibc gcc golang gnome-terminal" RELEASE=`lsb_release -c -s` DIR="$(mktemp --tmpdir --directory apt-offline-tests-XXXXXXXX)" #cleanup () { rm --recursive --force "$DIR"; } #trap cleanup EXIT URI="$DIR/set.uris" CACHE_DIR="/var/cache/apt/archives" DOWNLOAD_DIR="$DIR/download-dir" BUNDLE_FILE="$DIR/bundle-file.zip" THREADS=5 APT_OFFLINE="./apt-offline " run() { echo "Executing command: $1" $1 } set_features () { if [ ! -z $1 ]; then URI=$1 fi # Needs root APT_OFFLINE="sudo $APT_OFFLINE" run "sudo apt update" run "$APT_OFFLINE set $URI --simulate --update --upgrade" run "$APT_OFFLINE set $URI --update" run "$APT_OFFLINE set $URI --upgrade" run "$APT_OFFLINE set $URI --update --upgrade" run "$APT_OFFLINE set $URI --update --upgrade --upgrade-type upgrade" run "$APT_OFFLINE set $URI --update --upgrade --upgrade-type upgrade --release $RELEASE" run "$APT_OFFLINE set $URI --install-packages $DISLIKED_PACKAGES" run "$APT_OFFLINE set $URI --install-packages $DISLIKED_PACKAGES --release $RELEASE" run "$APT_OFFLINE set $URI --install-src-packages $DISLIKED_SRC_PACKAGES" run "$APT_OFFLINE set $URI --install-src-packages $DISLIKED_SRC_PACKAGES --release $RELEASE" run "$APT_OFFLINE set $URI --src-build-dep --install-src-packages $DISLIKED_SRC_PACKAGES" run "$APT_OFFLINE set $URI --src-build-dep --install-src-packages $DISLIKED_SRC_PACKAGES --release $RELEASE" } get_features () { if [ ! -z $1 ]; then URI=$1 fi run "sudo apt update" run "$APT_OFFLINE get $URI --quiet --bundle $BUNDLE_FILE" rm -f $BUNDLE_FILE run "$APT_OFFLINE get $URI --quiet --threads $THREADS --bundle $BUNDLE_FILE" rm -f $BUNDLE_FILE run "$APT_OFFLINE get $URI --quiet --threads $THREADS --socket-timeout 30 --bundle $BUNDLE_FILE --bug-reports" run "$APT_OFFLINE get $URI --quiet --threads $THREADS -d $DOWNLOAD_DIR" rm -rf $DOWNLOAD_DIR run "$APT_OFFLINE get $URI --quiet --threads $THREADS -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR" rm -rf $DOWNLOAD_DIR run "$APT_OFFLINE get $URI --quiet --no-checksum -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR" rm -rf $DOWNLOAD_DIR run "$APT_OFFLINE get $URI --quiet --threads $THREADS --bug-reports -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR" } install_features () { if [ ! -z $1 ]; then DOWNLOAD_DIR=$1 BUNDLE_FILE=$1 fi # Needs root APT_OFFLINE="sudo $APT_OFFLINE" run "sudo apt update" run "$APT_OFFLINE install $DOWNLOAD_DIR --skip-bug-reports" run "$APT_OFFLINE install $DOWNLOAD_DIR --skip-bug-reports --allow-unauthenticated" run "$APT_OFFLINE install $BUNDLE_FILE --skip-bug-reports" run "$APT_OFFLINE install $BUNDLE_FILE --skip-bug-reports --allow-unauthenticated" } install_features_prompt () { if [ ! -z $1 ]; then DOWNLOAD_DIR=$1 BUNDLE_FILE=$1 fi # Needs root APT_OFFLINE="sudo $APT_OFFLINE" run "sudo apt update" run "$APT_OFFLINE install $DOWNLOAD_DIR" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate --allow-unauthenticated" run "$APT_OFFLINE install $BUNDLE_FILE" run "$APT_OFFLINE install $BUNDLE_FILE --simulate" run "$APT_OFFLINE install $BUNDLE_FILE --simulate" run "$APT_OFFLINE install $BUNDLE_FILE --simulate --allow-unauthenticated" } all_features () { echo "Executing function set_features" set_features echo "Executing function get_features" get_features echo "Executing function install_features" install_features } case $1 in "set") if [ ! -z $2 ]; then set_features $2 else set_features fi ;; "get") if [ ! -z $2 ]; then get_features $2 else get_features fi ;; "install_features_promptless") if [ ! -z $2 ]; then install_features $2 else install_features fi ;; "install") # With prompts for bug reports if [ ! -z $2 ]; then install_features_prompt $2 else install_features_prompt fi ;; "--help") echo "$0 [set || get || install_features_promptless || install]" exit 0; ;; "-h") echo "$0 [set || get || install_features_promptless || install]" exit 0; ;; *) all_features ;; esac apt-offline-1.8.6/tests/apt-offline-tests.sh000077500000000000000000000100131475166737700210210ustar00rootroot00000000000000#!/bin/sh DISLIKED_PACKAGES="lxde icewm eclipse" RELEASE=`lsb_release -c -s` DIR="$(mktemp --tmpdir --directory apt-offline-tests-XXXXXXXX)" cleanup () { rm --recursive --force "$DIR"; } trap cleanup EXIT URI="$DIR/set.uris" CACHE_DIR="/var/cache/apt/archives" DOWNLOAD_DIR="$DIR/download-dir" BUNDLE_FILE="$DIR/bundle-file.zip" THREADS=5 APT_OFFLINE="./apt-offline " run() { echo "Executing command: $1" $1 } set_features () { if [ ! -z $1 ]; then URI=$1 fi run "$APT_OFFLINE set $URI" run "$APT_OFFLINE set $URI --simulate" run "$APT_OFFLINE set $URI --update" run "$APT_OFFLINE set $URI --upgrade" run "$APT_OFFLINE set $URI --update --upgrade" run "$APT_OFFLINE set $URI --update --upgrade --upgrade-type upgrade" run "$APT_OFFLINE set $URI --update --upgrade --upgrade-type upgrade --release $RELEASE" run "$APT_OFFLINE set $URI --install-packages $DISLIKED_PACKAGES" run "$APT_OFFLINE set $URI --install-packages $DISLIKED_PACKAGES --release $RELEASE" run "$APT_OFFLINE set $URI --install-src-packages $DISLIKED_PACKAGES" run "$APT_OFFLINE set $URI --install-src-packages $DISLIKED_PACKAGES --release $RELEASE" run "$APT_OFFLINE set $URI --src-build-dep --install-src-packages $DISLIKED_PACKAGES" run "$APT_OFFLINE set $URI --src-build-dep --install-src-packages $DISLIKED_PACKAGES --release $RELEASE" } get_features () { if [ ! -z $1 ]; then URI=$1 fi run "$APT_OFFLINE get $URI" run "$APT_OFFLINE get $URI --threads $THREADS" run "$APT_OFFLINE get $URI --threads $THREADS --socket-timeout 30" run "$APT_OFFLINE get $URI --threads $THREADS -d $DOWNLOAD_DIR" run "$APT_OFFLINE get $URI --threads $THREADS -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR" run "$APT_OFFLINE get $URI --no-checksum -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR" run "$APT_OFFLINE get $URI --threads $THREADS --bug-reports -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR" run "$APT_OFFLINE get $URI --threads $THREADS --bug-reports -d $DOWNLOAD_DIR --cache-dir $CACHE_DIR --bundle $BUNDLE_FILE" } install_features () { if [ ! -z $1 ]; then DOWNLOAD_DIR=$1 BUNDLE_FILE=$1 fi run "$APT_OFFLINE install $DOWNLOAD_DIR --skip-bug-reports" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate --skip-bug-reports" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate --skip-bug-reports" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate --skip-bug-reports --allow-unauthenticated" run "$APT_OFFLINE install $BUNDLE_FILE --skip-bug-reports" run "$APT_OFFLINE install $BUNDLE_FILE --simulate --skip-bug-reports" run "$APT_OFFLINE install $BUNDLE_FILE --simulate --skip-bug-reports" run "$APT_OFFLINE install $BUNDLE_FILE --simulate --skip-bug-reports --allow-unauthenticated" } install_features_prompt () { if [ ! -z $1 ]; then DOWNLOAD_DIR=$1 BUNDLE_FILE=$1 fi run "$APT_OFFLINE install $DOWNLOAD_DIR" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate" run "$APT_OFFLINE install $DOWNLOAD_DIR --simulate --allow-unauthenticated" run "$APT_OFFLINE install $BUNDLE_FILE" run "$APT_OFFLINE install $BUNDLE_FILE --simulate" run "$APT_OFFLINE install $BUNDLE_FILE --simulate" run "$APT_OFFLINE install $BUNDLE_FILE --simulate --allow-unauthenticated" } all_features () { echo "Executing function set_features" set_features echo "Executing function get_features" get_features echo "Executing function install_features" install_features } case $1 in "set") if [ ! -z $2 ]; then set_features $2 else set_features fi ;; "get") if [ ! -z $2 ]; then get_features $2 else get_features fi ;; "install_features_promptless") if [ ! -z $2 ]; then install_features $2 else install_features fi ;; "install") # With prompts for bug reports if [ ! -z $2 ]; then install_features_prompt $2 else install_features_prompt fi ;; "--help") echo "$0 [set || get || install_features_promptless || install]" exit 0; ;; "-h") echo "$0 [set || get || install_features_promptless || install]" exit 0; ;; *) all_features ;; esac apt-offline-1.8.6/tests/set-update.uris000066400000000000000000000237411475166737700201110ustar00rootroot00000000000000'http://mirrors.kernel.org/debian/dists/testing/main/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_testing_main_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/testing/contrib/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_testing_contrib_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/testing/non-free/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_testing_non-free_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/testing/main/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_testing_main_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/testing/contrib/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_testing_contrib_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/testing/non-free/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_testing_non-free_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/testing/contrib/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_testing_contrib_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/testing/main/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_testing_main_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/testing/non-free/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_testing_non-free_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/testing/InRelease' mirrors.kernel.org_debian_dists_testing_InRelease 0 'http://mirrors.kernel.org/debian/dists/unstable/main/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_unstable_main_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/unstable/contrib/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_unstable_contrib_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/unstable/non-free/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_unstable_non-free_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/unstable/main/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_unstable_main_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/unstable/contrib/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_unstable_contrib_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/unstable/non-free/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_unstable_non-free_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/unstable/contrib/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_unstable_contrib_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/unstable/main/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_unstable_main_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/unstable/non-free/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_unstable_non-free_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/unstable/InRelease' mirrors.kernel.org_debian_dists_unstable_InRelease 0 'http://mirrors.kernel.org/debian/dists/experimental/main/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_experimental_main_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/experimental/contrib/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_experimental_contrib_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/experimental/non-free/binary-amd64/Packages.bz2' mirrors.kernel.org_debian_dists_experimental_non-free_binary-amd64_Packages 0 : 'http://mirrors.kernel.org/debian/dists/experimental/main/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_experimental_main_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/experimental/contrib/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_experimental_contrib_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/experimental/non-free/binary-i386/Packages.bz2' mirrors.kernel.org_debian_dists_experimental_non-free_binary-i386_Packages 0 : 'http://mirrors.kernel.org/debian/dists/experimental/contrib/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_experimental_contrib_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/experimental/main/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_experimental_main_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/experimental/non-free/i18n/Translation-en.bz2' mirrors.kernel.org_debian_dists_experimental_non-free_i18n_Translation-en 0 : 'http://mirrors.kernel.org/debian/dists/experimental/InRelease' mirrors.kernel.org_debian_dists_experimental_InRelease 0 'http://ftp.debian.org/debian/dists/testing/main/source/Sources.bz2' ftp.debian.org_debian_dists_testing_main_source_Sources 0 : 'http://ftp.debian.org/debian/dists/testing/contrib/source/Sources.bz2' ftp.debian.org_debian_dists_testing_contrib_source_Sources 0 : 'http://ftp.debian.org/debian/dists/testing/non-free/source/Sources.bz2' ftp.debian.org_debian_dists_testing_non-free_source_Sources 0 : 'http://ftp.debian.org/debian/dists/testing/main/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_testing_main_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/testing/contrib/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_testing_contrib_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/testing/non-free/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_testing_non-free_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/testing/main/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_testing_main_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/testing/contrib/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_testing_contrib_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/testing/non-free/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_testing_non-free_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/testing/contrib/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_testing_contrib_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/testing/main/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_testing_main_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/testing/non-free/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_testing_non-free_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/testing/InRelease' ftp.debian.org_debian_dists_testing_InRelease 0 'http://ftp.debian.org/debian/dists/unstable/main/source/Sources.bz2' ftp.debian.org_debian_dists_unstable_main_source_Sources 0 : 'http://ftp.debian.org/debian/dists/unstable/contrib/source/Sources.bz2' ftp.debian.org_debian_dists_unstable_contrib_source_Sources 0 : 'http://ftp.debian.org/debian/dists/unstable/non-free/source/Sources.bz2' ftp.debian.org_debian_dists_unstable_non-free_source_Sources 0 : 'http://ftp.debian.org/debian/dists/unstable/main/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_unstable_main_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/unstable/contrib/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_unstable_contrib_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/unstable/non-free/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_unstable_non-free_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/unstable/main/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_unstable_main_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/unstable/contrib/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_unstable_contrib_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/unstable/non-free/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_unstable_non-free_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/unstable/contrib/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_unstable_contrib_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/unstable/main/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_unstable_main_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/unstable/non-free/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_unstable_non-free_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/unstable/InRelease' ftp.debian.org_debian_dists_unstable_InRelease 0 'http://ftp.debian.org/debian/dists/experimental/main/source/Sources.bz2' ftp.debian.org_debian_dists_experimental_main_source_Sources 0 : 'http://ftp.debian.org/debian/dists/experimental/contrib/source/Sources.bz2' ftp.debian.org_debian_dists_experimental_contrib_source_Sources 0 : 'http://ftp.debian.org/debian/dists/experimental/non-free/source/Sources.bz2' ftp.debian.org_debian_dists_experimental_non-free_source_Sources 0 : 'http://ftp.debian.org/debian/dists/experimental/main/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_experimental_main_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/experimental/contrib/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_experimental_contrib_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/experimental/non-free/binary-amd64/Packages.bz2' ftp.debian.org_debian_dists_experimental_non-free_binary-amd64_Packages 0 : 'http://ftp.debian.org/debian/dists/experimental/main/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_experimental_main_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/experimental/contrib/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_experimental_contrib_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/experimental/non-free/binary-i386/Packages.bz2' ftp.debian.org_debian_dists_experimental_non-free_binary-i386_Packages 0 : 'http://ftp.debian.org/debian/dists/experimental/contrib/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_experimental_contrib_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/experimental/main/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_experimental_main_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/experimental/non-free/i18n/Translation-en.bz2' ftp.debian.org_debian_dists_experimental_non-free_i18n_Translation-en 0 : 'http://ftp.debian.org/debian/dists/experimental/InRelease' ftp.debian.org_debian_dists_experimental_InRelease 0