├── .gitignore ├── LICENSE ├── README.md ├── aur ├── PKGBUILD └── README.txt ├── buttermanager ├── bm_main.py ├── buttermanager │ ├── __init__.py │ ├── buttermanager.py │ ├── exception │ │ ├── __init__.py │ │ └── exception.py │ ├── filesystem │ │ ├── __init__.py │ │ ├── filesystem.py │ │ └── snapshot.py │ ├── images │ │ ├── accept_16px_icon.png │ │ ├── add_16px_icon.png │ │ ├── buttermanager.svg │ │ ├── buttermanager50.png │ │ ├── edit_16px_icon.png │ │ ├── exchange_arrows_16px_icon.png │ │ ├── folder_16px_icon.png │ │ ├── lock_24px_icon.png │ │ ├── remove_16px_icon.png │ │ └── view_24px_icon.png │ ├── manager │ │ ├── __init__.py │ │ └── upgrader.py │ ├── ui │ │ ├── ConsolidateSnapshotWindow.ui │ │ ├── GeneralInfoWindow.ui │ │ ├── InfoWindow.ui │ │ ├── LogViewWindow.ui │ │ ├── MainWindow.ui │ │ ├── PasswordWindow.ui │ │ ├── ProblemsFoundWindow.ui │ │ ├── SnapshotWindow.ui │ │ ├── SubvolumeWindow.ui │ │ └── UpdatesWindow.ui │ ├── util │ │ ├── __init__.py │ │ ├── settings.py │ │ └── utils.py │ └── window │ │ ├── __init__.py │ │ └── windows.py └── main.py ├── doc ├── README.md ├── screen-1.png ├── screen-10.png ├── screen-11.png ├── screen-12.png ├── screen-13.png ├── screen-14.png ├── screen-15.png ├── screen-16.png ├── screen-2.png ├── screen-3.png ├── screen-4.png ├── screen-5.png ├── screen-6.png ├── screen-7.png ├── screen-8.png └── screen-9.png ├── install ├── native_install.sh ├── uninstall.sh └── venv_install.sh ├── packaging ├── buttermanager.desktop ├── buttermanager.svg └── buttermanager_venv.desktop ├── requirements.txt ├── rpm ├── README.txt └── buttermanager.spec ├── setup.py └── version.txt /.gitignore: -------------------------------------------------------------------------------- 1 | #PyCharm configuration 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *.cover 44 | .hypothesis/ 45 | .pytest_cache/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | db.sqlite3 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | env.bak/ 88 | venv.bak/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ButterManager 2 | 3 | ## Summary 4 | ButterManager is a BTRFS tool for managing snapshots, balancing filesystems and upgrading the system safetly. 5 | 6 | ## Version 7 | 2.5.2 8 | 9 | ## Wiki 10 | You can find documentation and more info in the [wiki of the project](https://github.com/egara/buttermanager/wiki) 11 | 12 | ## Caveats 13 | - For using ButterManager, it is important to have a **good BTRFS structure in your filesystem**. If you want some tips and more information, you can [read 14 | this](https://github.com/egara/arch-btrfs-installation). 15 | 16 | - ButterManager works with **Debian**, **Ubuntu / derivatives (Linux Mint, KDE Neon, ElementaryOS, Zorin, Deepin...)**, **OpenSUSE / SUSE**, **RHEL / Fedora** and **Arch Linux** so far. 17 | 18 | ## Main Functionalities 19 | 20 | ### Managing snapshots 21 | You will be able to define all the subvolumes you want to create snapshots and the path for their storage. Then, using ButterManager you will create and delete snapshots of those subvolumes at your will. 22 | 23 | ### Integration with GRUB 24 | ButterManager is integrated with [grub-btrfs](https://github.com/Antynea/grub-btrfs) and you will be able to boot your system from any snapshot directly from the GRUB menu. 25 | 26 | ### Balancing BTRFS filesystems 27 | As new snapshots are created in the system, the free space of the filesystem decreases and it is necessary to perform a system balancing regularly. With ButterManager you can perform these balances at any time and visualize the real space that is occupied. 28 | 29 | ### Upgrading the system 30 | You will be able to upgrade your system and create new snapshots automatically when this operation is performed. Doing so, if something goes wrong, you will have a snapshot before the upgrade you can use to go back. You will be able to set the maximum number of snapshots in your system and ButterManager will maintain this number with every upgrade. 31 | 32 | ### Saving the logs 33 | Everytime your system is upgraded using ButterManager, you could save the log indepently. This way, you would be able to see the packages that have been updated in every snapshot if you wish. 34 | 35 | ## Installation 36 | You can install ButterManager in different ways. 37 | 38 | ### From the source code 39 | In order to install ButterManager manually, you have to install these packages (all the packages described below are for **Arch Linux**. Please, take into account that maybe the name is different in your distribution and you have to install them for python3 version): 40 | 41 | - Python 3 42 | - **python-setuptool** (f.i. the name of the package in Ubuntu is **python3-setuptools**). 43 | - **python-virtualenv** (f.i. the name of the package in Ubuntu is **python3-venv**). This package will only be needed if you use **venv_install.sh** script (see below). 44 | - **grub-btrfs**. Please, go to its GitHub repository [https://github.com/Antynea/grub-btrfs](https://github.com/Antynea/grub-btrfs) and follow the instructions to install it if the package is not in the official repository of your distribution. 45 | - **libxinerama**: This depency has been reported by some users (thanks Adam!) who install ButterManager on Ubuntu 20.04 (proper) (the name of the package in Ubuntu is **libxcb-xinerama0**) 46 | - **tk** (f.i. the name of the package in Ubuntu is **python3-tk**) 47 | 48 | Once you meet all the requirements, follow these steps: 49 | 50 | 1. Clone the repository (install **git** if it is needed first) 51 | 52 | ``` 53 | git clone https://github.com/egara/buttermanager.git 54 | 55 | ``` 56 | 57 | 1. Install ButterManager using one of the following installation scripts: 58 | 1. **Native Installation**: This is the preferred method. It is slimmer because no virtual environment is created in order to execute ButterManager. This installation method will install the dependencies needed in your system natively and create an executable script for running the application. You will be able to execute ButterManager from the terminal typing **buttermanager** or directly via a shortcut created. In order to install ButterManager just open a terminal and execute: 59 | 60 | ``` 61 | cd buttermanager 62 | cd install 63 | ./native_install.sh 64 | 65 | ``` 66 | 67 | 1. **Venv Installation**: If the first method doesn't run ButterManager properly, please try this second one. The installation process will create a virtual environment with all the dependencies needed and a desktop launcher to run ButterManager directly. In order to install ButterManager just open a terminal and type: 68 | 69 | ``` 70 | cd buttermanager 71 | cd install 72 | ./venv_install.sh 73 | ``` 74 | 75 | 1. If you want to uninstall ButterManager: 76 | 77 | ``` 78 | cd buttermanager 79 | cd install 80 | ./uninstall.sh 81 | ``` 82 | 83 | ### From AUR 84 | If you are an Arch Linux user or your distribution is a derivative (Manjaro, EndevourOS...), ButterManager is in AUR. Depending on your package manager for AUR, type: 85 | 86 | yaourt -S buttermanager 87 | 88 | Or 89 | 90 | trizen -S buttermanager 91 | 92 | Or 93 | 94 | yay -S buttermanager 95 | 96 | Those are only examples. Use the package manager you have installed for AUR. Once ButterManager is installed, you will be able to run it using the icon created in the main menu. 97 | 98 | ### From Nix 99 | ButterManager is packaged in Nix [and included in the official repo](https://search.nixos.org/packages?channel=24.05&from=0&size=50&sort=relevance&type=packages&query=buttermanager) 100 | 101 | ### From RPM 102 | ButterManager is packaged in Fedora but it is outdated. [This is the official package](https://packages.fedoraproject.org/pkgs/buttermanager/buttermanager/) 103 | 104 | ## Changelog 105 | 106 | ### Version 2.5.2 107 | - Issue #56. Cancel button has been re-implemented within the consolidation window and now it closes this window when it is clicked. 108 | - All ButterManager windows have been renamed to ButterManager. New labels in some windows have been added. 109 | 110 | ### Version 2.5.1 111 | - Issue #52. A bug that prevented ButterManager to start normally (reported by Fedora users) has been fixed. 112 | - ButterManager main window title is now displayed. 113 | - Some missing buttons and different elements from tabs have been included within enable and disable buttons methods. 114 | 115 | ### Version 2.5.0 116 | - Flatpak support implemented. 117 | - Windows layout reimplemented. All windows are resizeable now. 118 | - Font size customization implemented. 119 | - Issue #29 fixed. Now, mobile users should be able to rescale the application to fit their displays. 120 | 121 | ### Version 2.4.3 122 | - Issue #31 fixed. Preventing errors when calculating diffs against root directly. 123 | - Issue #33 fixed. Refreshing filesystem statistics when user changes filesystem within combobox. 124 | - Issue #45 fixed. Changing yaml.load method to yaml.safe_load because pyyaml library requires it since version 5.4. 125 | 126 | ### Version 2.4.2 127 | - Minor changes in order to improve the package and preparing all to publish ButterManager in Fedora. Thanks Michel Alexandre Salim! 128 | 129 | ### Version 2.4.1 130 | - Issue #32 fixed. ButterManager crashed when it was installed for the first time. 131 | 132 | ### Version 2.4 133 | - Delete log icon button fixed. 134 | - Issue #28 fixed. There is an error for Plasma Desktop (and PyQT5) when the file explorer is opened and the user tries to select a directory when setting subvolumes up. Only for this case, TK will be used as workaround. 135 | - 'Don't remove snapshots' and 'Snapshots to keep' parameters are not global anymore. The user will be able to configure them per subvolume. 136 | - Now, when user deletes a specific snapshot, the log related to it will be removed too if it exists. 137 | 138 | ### Version 2.3 139 | - Thanks to Fedora guys (Neal Gompa @Conan-Kudo and Michel Alexandre Salim @michel-slm) ButterManager has been restructured in order to be packaged for Fedora. Because of that, now the application won't need to be installed within a virtual environment so the package installation footprint will be very much smaller. 140 | - Two new installation methods have been created for users who wants to install ButterManager from source code: [native and venv](https://github.com/egara/buttermanager/tree/master/install). The first one is the recommended but the second one is still supported just in case native installation doesn't work properly. 141 | - Issue #26 fixed: Now, the **Upgrade with snapshots** button creates snapshots and removes the old ones if needed. 142 | - All the fast action buttons are disabled when a critical operation is executing. 143 | 144 | ### Version 2.2 145 | - More stability. Some bugs have been fixed and ButterManager should not crashed after upgrading the system. 146 | - Other operations are now Fast actions. 147 | - New fast actions implemented: Upgrade system with/without snapshots and Take snapshots. 148 | 149 | ### Version 2.1 150 | - Delete icon has been redesigned. 151 | - New button to open a file explorer within a specific snapshot has been implemented. 152 | - New feature to calculate differences between current snapshot and a specified one has been implemented. 153 | - User will be able to calculate full differences (it will take some time to complete) included files modified and files only present in one of the snapshots. 154 | - User will be able to calculate partial differences that will be faster but it will only inlcude files modified. 155 | 156 | ### Version 2.0 157 | - Now, the installer creates ~/.local/share/applications directory if it didn't exist in order to allocate the ButterManager desktop launcher. 158 | - The method of calculating the default original subvolume to consolidate the system has been reimplemented and improved to avoid some bugs detected. 159 | - Now, the allocation of the original snapshot is stored in fstab instead of the mounted point for every snapshot of root created. 160 | - grub-mkconfig will run after consolidating deafult root subvolume 161 | - Consolidation process will check the original path of the default subvolume for root and will use it in fstab. 162 | 163 | ### Version 1.9 164 | - Buttermanager has been integrated with **grub-btrfs** package. It means that, for all these users who use GRUB will be able to boot its system from any snapshot created with this version of the application and above. This integration will be optional and configurable from **Settings** tab. 165 | 166 | ### Version 1.8 ### 167 | - Font autoscaling implemented in order to let the GUI adapts to the current screen resolution. 168 | - It has been created a new tab for **Documentation** and a **Wiki** at Github repository. 169 | - The text of some tooltips have been fixed. 170 | 171 | ### Version 1.7 ### 172 | - Bug fixed for Arch Linux and derivatives installing the package from AUR. Now, the application will be installed and run from a virtual environmen with all the modules needed. This causes that the size of the package buttermanager has increased 173 | 174 | ### Version 1.6 ### 175 | - Logs generated during the upgrade process can be stored 176 | - Logs management implemented 177 | - Version checker implemented. When a new version of ButterManagger is released, a new info window will be display to warn the user 178 | 179 | ### Version 1.5 ### 180 | - Labels refreshing after balancing the filesystem has been fixed 181 | - Values to show certaing labels have been recalculated 182 | 183 | ### Version 1.4 ### 184 | - System progress bar has been removed because it doesn't provide any relevant information 185 | - A new button has been implemented in order to upgrade the system whithout managing snapshots 186 | - All the ButterManager windows and dialogs have been reconfigured using fixed pixels in order to avoid the resizing 187 | - The ButterManager icon has been assigned to all windows and dialogs 188 | - When ButterManager is installed for the first time, the updates checker is not checked by default 189 | - A new option added to yay command to check for AUR packages only 190 | - New messages implemented in the main window in order to warn the user about the space of the filesystem 191 | - Internet connection will be checked during 5 minutes. If there is no Internet connection, then the updates checker process will be cancelled 192 | 193 | ### Version 1.3 ### 194 | - The updates checker is executed in other thread, so now the GUI is not freezed during the process. 195 | 196 | ### Version 1.2 ### 197 | - Some layouts and texts fixed to fit properly 198 | - New window to list the packages to be updated implemented 199 | - Fixed a problem in the snap package for Wayland environments 200 | 201 | ### Version 1.1 ### 202 | 203 | - RHEL / Fedora, OpenSUSE / SUSE support added. 204 | - New window implemented for displaying serious problems related to the proper functioning of ButterManager. 205 | 206 | ### Version 1.0 ### 207 | 208 | - Initial release. 209 | - It supports Arch Linux, Debian, Ubuntu and derivatives. 210 | - Safely system upgrade. 211 | - BTRFS filesystems detection and visualization. 212 | - Snapshots management. 213 | - BTRFS filesystems balancing. 214 | - Application packaged in AUR. 215 | - Ubuntu Snap Package implemented for universal use in the rest of Linux distributions. 216 | 217 | ## Contact ## 218 | If you want to contact me, you can do it using this e-mail address . 219 | -------------------------------------------------------------------------------- /aur/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Eloy Garcia Almaden 2 | pkgname=buttermanager 3 | pkgver=2.5.2 4 | pkgrel=0 5 | epoch= 6 | pkgdesc="Graphical tool to create BTRFS snapshots, balance filesystems and upgrade the system safetly" 7 | arch=('x86_64') 8 | url="https://github.com/egara/buttermanager" 9 | license=('GPL') 10 | groups=() 11 | depends=('btrfs-progs' 'python>=3' 'grub-btrfs' 'python-setuptools' 'python-pyaml' 'python-pyqt5' 'tk') 12 | makedepends=('python>=3' 'git') 13 | checkdepends=() 14 | optdepends=() 15 | provides=() 16 | conflicts=() 17 | replaces=() 18 | backup=() 19 | options=() 20 | install= 21 | changelog= 22 | # Local source if user wants to build the package locally once the git repo has been cloned 23 | source=('git+file:////home/egarcia/Development/git/buttermanager/') 24 | # Remote source 25 | # Master branch 26 | # source=('git+https://github.com/egara/buttermanager#branch=master') 27 | # Tagged version 28 | # source=('https://github.com/egara/buttermanager/archive/"$pkgver".tar.gz') 29 | noextract=() 30 | md5sums=('SKIP') 31 | validpgpkeys=() 32 | 33 | build() { 34 | cd "$pkgname" 35 | python setup.py build 36 | } 37 | 38 | package() { 39 | cd "$pkgname" 40 | # Creating destination directory 41 | install -dm755 "$pkgdir/opt/$pkgname" 42 | 43 | # Installing ButterManager using python-setuptools 44 | echo -e "\n Installing ButterManager. Please wait..." 45 | python setup.py install --root="$pkgdir" --optimize=1 --skip-build 46 | 47 | # Copying .desktop file and icon 48 | echo -e "\n Creating desktop icon. Finishing the installation" 49 | install -Dm644 "$srcdir/$pkgname/packaging/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop" 50 | install -Dm644 "$srcdir/$pkgname/packaging/$pkgname.svg" "$pkgdir/opt/$pkgname/gui/$pkgname.svg" 51 | } 52 | -------------------------------------------------------------------------------- /aur/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains all the files needed to package ButterManager application for Arch Linux 2 | -------------------------------------------------------------------------------- /buttermanager/bm_main.py: -------------------------------------------------------------------------------- 1 | from .buttermanager.buttermanager import PasswordWindow 2 | from PyQt5.QtWidgets import QApplication 3 | import sys 4 | 5 | 6 | def main(): 7 | """Main wrapper for starting ButterManager 8 | 9 | This script is only for packaging purposes. If you are developing ButterManager, then use main.py as initial 10 | script. This script will be copied from setup.py using setuptools and it will be invoked from the script 11 | created within /usr/bin/buttermanager once the application is installed via sudo python setup.py install 12 | 13 | """ 14 | # Creating application instance 15 | application = QApplication(sys.argv) 16 | # Creating main window instance 17 | PasswordWindow(None) 18 | # Launching the application 19 | application.exec_() 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 Eloy García Almadén 2 | # 3 | # This file is part of buttermanager. 4 | # 5 | # This program is free software: you can redistribute it and / or modify it 6 | # under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation, version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | name = "buttermanager" 17 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/exception/__init__.py: -------------------------------------------------------------------------------- 1 | #Copyright 2018-2019 Eloy García Almadén 2 | # 3 | # This file is part of buttermanager. 4 | # 5 | # This program is free software: you can redistribute it and / or modify it 6 | # under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation, version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . -------------------------------------------------------------------------------- /buttermanager/buttermanager/exception/exception.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018-2019 Eloy García Almadén 4 | # 5 | # This file is part of buttermanager. 6 | # 7 | # This program is free software: you can redistribute it and / or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation, version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """This module gathers all the custom exceptions rised in buttermanager application. 20 | 21 | """ 22 | 23 | 24 | # Classes 25 | class NoCommandFound(Exception): 26 | """Exception raised when a needed program is not installed in the system. 27 | 28 | """ 29 | pass 30 | 31 | 32 | class BtrfsSnapshotDeletion(Exception): 33 | """Exception raised when a subvolume can't be deleted because it is not empty. 34 | A subvolume is not empty when there are other subvolumes within it 35 | 36 | """ 37 | def __init__(self, *args): 38 | if args: 39 | self.message = args[0] 40 | else: 41 | self.message = None 42 | 43 | def __str__(self): 44 | print('calling str') 45 | if self.message: 46 | return 'MyCustomError, {0} '.format(self.message) 47 | else: 48 | return 'MyCustomError has been raised' 49 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/filesystem/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 Eloy García Almadén 2 | # 3 | # This file is part of buttermanager. 4 | # 5 | # This program is free software: you can redistribute it and / or modify it 6 | # under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation, version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/filesystem/filesystem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018-2019 Eloy García Almadén 4 | # 5 | # This file is part of buttermanager. 6 | # 7 | # This program is free software: you can redistribute it and / or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation, version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """This module gathers all the operations related to BTRFS filesystems. 20 | 21 | It provides also Filesystem class. 22 | """ 23 | from ..exception import exception 24 | from ..window import windows 25 | import sys 26 | from ..util import utils 27 | from PyQt5.QtCore import QThread, pyqtSignal 28 | 29 | # Constants 30 | DEVID = "devid" 31 | LABEL = "Label" 32 | UUID = "uuid:" 33 | DEVICE_SIZE = "Device size:" 34 | DEVICE_ALLOCATED = "Device allocated:" 35 | DATA = "Data," 36 | METADATA = "Metadata," 37 | SYSTEM = "System," 38 | SIZE = "Size:" 39 | USED = "Used:" 40 | BTRFS_SHOW_COMMAND = "sudo -S btrfs filesystem show" 41 | FINDMT_COMMAND = "sudo -S findmnt -nt btrfs" 42 | BTRFS_USAGE_COMMAND = "sudo -S btrfs filesystem usage" 43 | BTRFS_BALANCE_COMMAND = "sudo -S btrfs balance start" 44 | BTRFS_BALANCE_DATA_USAGE_FILTER = "dusage" 45 | BTRFS_BALANCE_METADATA_USAGE_FILTER = "musage" 46 | 47 | 48 | # Classes 49 | class Filesystem: 50 | """BTRFS Filesystem. 51 | 52 | """ 53 | # Constructor 54 | def __init__(self, uuid): 55 | 56 | self.__uuid = uuid 57 | self.__devices = self.__get_devices() 58 | self.__mounted_points = self.__get_mounted_points() 59 | filesystem_info = self.__get_filesystem_info(self.mounted_points[0]) 60 | self.__total_size = filesystem_info['total_size'] 61 | self.__total_allocated = filesystem_info['total_allocated'] 62 | self.__data_size = filesystem_info['data_size'] 63 | self.__data_used = filesystem_info['data_used'] 64 | self.__data_percentage = filesystem_info['data_percentage'] 65 | self.__metadata_size = filesystem_info['metadata_size'] 66 | self.__metadata_used = filesystem_info['metadata_used'] 67 | self.__metadata_percentage = filesystem_info['metadata_percentage'] 68 | self.__system_size = filesystem_info['system_size'] 69 | self.__system_used = filesystem_info['system_used'] 70 | self.__system_percentage = filesystem_info['system_percentage'] 71 | 72 | # Private attributes 73 | # UUID 74 | @property 75 | def uuid(self): 76 | return self.__uuid 77 | 78 | # Devices 79 | @property 80 | def devices(self): 81 | return self.__devices 82 | 83 | # Mounted points 84 | @property 85 | def mounted_points(self): 86 | return self.__mounted_points 87 | 88 | # Total size 89 | @property 90 | def total_size(self): 91 | return self.__total_size 92 | 93 | # Total allocated 94 | @property 95 | def total_allocated(self): 96 | return self.__total_allocated 97 | 98 | # Data size 99 | @property 100 | def data_size(self): 101 | return self.__data_size 102 | 103 | # Data used 104 | @property 105 | def data_used(self): 106 | return self.__data_used 107 | 108 | # Data percentage 109 | @property 110 | def data_percentage(self): 111 | return self.__data_percentage 112 | 113 | # Metadata size 114 | @property 115 | def metadata_size(self): 116 | return self.__metadata_size 117 | 118 | # Metadata used 119 | @property 120 | def metadata_used(self): 121 | return self.__metadata_used 122 | 123 | # Metadata percentage 124 | @property 125 | def metadata_percentage(self): 126 | return self.__metadata_percentage 127 | 128 | # System size 129 | @property 130 | def system_size(self): 131 | return self.__system_size 132 | 133 | # System used 134 | @property 135 | def system_used(self): 136 | return self.__system_used 137 | 138 | # System percentage 139 | @property 140 | def system_percentage(self): 141 | return self.__system_percentage 142 | 143 | # Methods 144 | # Private methods 145 | def __get_devices(self): 146 | """Retrieves all the devices which the BTRFS filesystem is composed. 147 | 148 | Returns: 149 | list (:obj:`list` of :obj:`str`): devices. 150 | """ 151 | try: 152 | devices = [] 153 | commandline_output = utils.execute_command(BTRFS_SHOW_COMMAND, root=True) 154 | filesystem_found = False 155 | 156 | for line in commandline_output.split("\n"): 157 | if UUID in line: 158 | if filesystem_found: 159 | break 160 | else: 161 | if self.uuid in line: 162 | filesystem_found = True 163 | continue 164 | 165 | if filesystem_found: 166 | # The loop is inside the chosen BTRFS filesystem 167 | # It is necessary to find devid to retrieve all the filesystem paths 168 | if DEVID in line: 169 | path_init = line.find('/') 170 | # The device path is appended to the list 171 | devices.append(line[path_init:len(line)]) 172 | 173 | return devices 174 | 175 | except exception.NoCommandFound as no_command_found_exception: 176 | raise no_command_found_exception 177 | 178 | def __get_mounted_device(self): 179 | """Retrieves the device tha contains the BTRFS filesystem and it is mounted. 180 | 181 | Returns: 182 | string: device path. 183 | """ 184 | try: 185 | mounted_device = '' 186 | commandline_output = utils.execute_command(FINDMT_COMMAND) 187 | for device in self.devices: 188 | if device in commandline_output: 189 | mounted_device = device 190 | break 191 | 192 | return mounted_device 193 | 194 | except exception.NoCommandFound as no_command_found_exception: 195 | raise no_command_found_exception 196 | 197 | def __get_mounted_points(self): 198 | """Retrieves all the mounted points of the BTRFS filesystem. 199 | 200 | Returns: 201 | list (:obj:`list` of :obj:`str`): mounted points. 202 | """ 203 | try: 204 | mounted_points = [] 205 | device = self.__get_mounted_device() 206 | command = "{command} {device}".format(command=FINDMT_COMMAND, device=device) 207 | commandline_output = utils.execute_command(command) 208 | 209 | for line in commandline_output.split("\n"): 210 | if len(line) > 0: 211 | mounted_points.append(line.split(" ")[0].strip()) 212 | 213 | return mounted_points 214 | 215 | except exception.NoCommandFound as no_command_found_exception: 216 | raise no_command_found_exception 217 | 218 | def __get_filesystem_info(self, mounted_point): 219 | """Retrieves all the information of the BTRFS filesystem. 220 | 221 | Returns: 222 | dictionary (key=:obj:'string', value=:obj:'str' or obj:'int'): all the info. The keys of the dictionary 223 | will be: 224 | - total_size: Device size 225 | - total_allocated: Device allocated 226 | - data_size: Data size 227 | - data_used: Data used 228 | - data_percentage: Percentage of data used 229 | - metadata_size: Metadata size 230 | - metadata_used: Metadata used 231 | - metadata_percentage: Percentage of metadata used 232 | - system_size: System size 233 | - system_used: System used 234 | - system_percentage: Percentage of system used 235 | """ 236 | filesystem_info = {'total_size': '0', 'total_allocated': '0', 237 | 'data_size': '0', 'data_used': '0', 'data_percentage': 0, 238 | 'metadata_size': '0', 'metadata_used': '0', 'metadata_percentage': 0, 239 | 'system_size': '0', 'system_used': '0', 'system_percentage': 0} 240 | command = "{command} {point}".format(command=BTRFS_USAGE_COMMAND, point=mounted_point) 241 | commandline_output = utils.execute_command(command, root=True) 242 | 243 | for line in commandline_output.split("\n"): 244 | if DEVICE_SIZE in line: 245 | filesystem_info['total_size'] = line.split(DEVICE_SIZE)[1].strip() 246 | elif DEVICE_ALLOCATED in line: 247 | filesystem_info['total_allocated'] = line.split(DEVICE_ALLOCATED)[1].strip() 248 | elif DATA in line: 249 | data_size = line.split(SIZE)[1].split(',')[0].strip() 250 | filesystem_info['data_size'] = data_size 251 | data_used = line.split(USED)[1].strip() 252 | if '(' in data_used: 253 | # New versions of btrfs-progs already include the percentage 254 | data_used = data_used.split()[0].strip() 255 | filesystem_info['data_used'] = data_used 256 | filesystem_info['data_percentage'] = utils.get_percentage(filesystem_info['data_size'], 257 | filesystem_info['data_used']) 258 | elif METADATA in line: 259 | metadata_size = line.split(SIZE)[1].split(',')[0].strip() 260 | filesystem_info['metadata_size'] = metadata_size 261 | metadata_used = line.split(USED)[1].strip() 262 | if '(' in metadata_used: 263 | # New versions of btrfs-progs already include the percentage 264 | metadata_used = metadata_used.split()[0].strip() 265 | filesystem_info['metadata_used'] = metadata_used 266 | filesystem_info['metadata_percentage'] = utils.get_percentage(filesystem_info['metadata_size'], 267 | filesystem_info['metadata_used']) 268 | elif SYSTEM in line: 269 | system_size = line.split(SIZE)[1].split(',')[0].strip() 270 | filesystem_info['system_size'] = system_size 271 | filesystem_info['system_used'] = line.split(USED)[1].strip() 272 | filesystem_info['system_percentage'] = utils.get_percentage(filesystem_info['system_size'], 273 | filesystem_info['system_used']) 274 | 275 | return filesystem_info 276 | 277 | # Public methods 278 | def __str__(self): 279 | """Reimplementation of the str method inherited from object class. 280 | 281 | Returns: 282 | string: String representation of the Filesystem object. 283 | """ 284 | return "BTRFS Filesystem -> UUID: {0}; Devices: {1}; Mounted Points: {2}".format(self.uuid, self.devices, 285 | self.mounted_points) 286 | 287 | 288 | # Module's methods 289 | def get_btrfs_filesystems(mounted=True): 290 | """Retrieves all the BTRFS filesystems. 291 | 292 | Keyword arguments: 293 | mounted (bool): Only mounted filesystems will be retrieved (default True). 294 | 295 | Returns: 296 | list (:obj:`list` of :obj:`str`): filesystems UUID. 297 | """ 298 | 299 | filesystems = [] 300 | command = BTRFS_SHOW_COMMAND 301 | 302 | if mounted: 303 | command += " --mounted" 304 | 305 | commandline_output = utils.execute_command(command, root=True) 306 | 307 | for line in commandline_output.split("\n"): 308 | if UUID in line: 309 | # The uuid is appended to the list 310 | filesystems.append(line.split(UUID)[1].strip()) 311 | 312 | return filesystems 313 | 314 | 315 | def balance_filesystem(filter, percentage, mounted_point): 316 | """Balances a specific filesystem. 317 | 318 | Arguments: 319 | filter (string): filter. 320 | percentage (int): usage filter. 321 | mounted_point: path to balance. 322 | """ 323 | # Logger 324 | logger = utils.Logger(sys.modules['__main__'].__file__).get() 325 | logger.info("Balancing {mounted_point} using filter {filter} and " 326 | "percentage {percentage}".format(mounted_point=mounted_point, 327 | filter=filter, 328 | percentage=percentage)) 329 | 330 | command = "{command} -{filter}={percentage} {mounted_point}".format(command=BTRFS_BALANCE_COMMAND, 331 | filter=filter, 332 | percentage=percentage, 333 | mounted_point=mounted_point) 334 | logger.info("Command executed {command}".format(command=command)) 335 | commandline_output = utils.execute_command(command, root=True) 336 | for line in commandline_output.split("\n"): 337 | logger.info(line) 338 | 339 | 340 | class BalanceManager(QThread): 341 | """Independent thread that will run the filesystem balancing process. 342 | 343 | """ 344 | # Attributes 345 | # pyqtSignal that will be emitted when this class requires to display 346 | # a single information window on the screen 347 | show_one_window = pyqtSignal('bool') 348 | 349 | # pyqtSignal that will be emitted when this class requires that main 350 | # window refreshes current filesystem statistics 351 | refresh_filesystem_statistics = pyqtSignal() 352 | 353 | # Constructor 354 | def __init__(self, data_percentage, metadata_percentage, mounted_point): 355 | QThread.__init__(self) 356 | self.__data_percentage = data_percentage 357 | self.__metadata_percentage = metadata_percentage 358 | self.__mounted_point = mounted_point 359 | 360 | # Methods 361 | def run(self): 362 | # Main window will be hidden 363 | self.on_show_one_window(True) 364 | info_dialog = windows.InfoWindow(None, "Balancing '{mounted_point}' mounted point. \n" 365 | "This window will be closed automatically \n" 366 | "when the operation is done. \n \n" 367 | "Please wait...".format(mounted_point=self.__mounted_point)) 368 | # Displaying info window 369 | info_dialog.show() 370 | 371 | # Balances the filesystem 372 | self.__balance_filesystem() 373 | 374 | # Hiding info window 375 | info_dialog.hide() 376 | 377 | # Main window will be shown again 378 | self.on_show_one_window(False) 379 | 380 | # Refreshing current filesystem statistics 381 | self.on_refresh_filesystem_statistics() 382 | 383 | def __balance_filesystem(self): 384 | """Wraps all the operations to balance the filesystem. 385 | 386 | """ 387 | # Balancing data 388 | balance_filesystem( 389 | BTRFS_BALANCE_DATA_USAGE_FILTER, 390 | self.__data_percentage, 391 | self.__mounted_point) 392 | # Balancing metadata 393 | balance_filesystem( 394 | BTRFS_BALANCE_METADATA_USAGE_FILTER, 395 | self.__metadata_percentage, 396 | self.__mounted_point) 397 | 398 | def on_show_one_window(self, one_window): 399 | """Emits a QT Signal to hide or show the rest of application windows. 400 | 401 | Arguments: 402 | one_window (boolean): Information window should be unique?. 403 | """ 404 | self.show_one_window.emit(one_window) 405 | 406 | def on_refresh_filesystem_statistics(self): 407 | """Emits a QT Signal to refresh filesystem statistics in main window. 408 | 409 | """ 410 | self.refresh_filesystem_statistics.emit() 411 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/filesystem/snapshot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018-2019 Eloy García Almadén 4 | # 5 | # This file is part of buttermanager. 6 | # 7 | # This program is free software: you can redistribute it and / or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation, version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """This module gathers all the operations related to BTRFS snapshots. 20 | 21 | It provides also Snapshot class. 22 | """ 23 | from ..exception import exception 24 | from ..util import settings, utils 25 | from ..window import windows 26 | import glob 27 | import os 28 | import shutil 29 | import sys 30 | import subprocess 31 | import time 32 | from PyQt5.QtCore import QThread, pyqtSignal 33 | 34 | 35 | # Constants 36 | BTRFS_CREATE_SNAPSHOT_R_COMMAND = "sudo -S btrfs subvolume snapshot -r" 37 | BTRFS_CREATE_SNAPSHOT_RW_COMMAND = "sudo -S btrfs subvolume snapshot" 38 | BTRFS_DELETE_SNAPSHOT_COMMAND = "sudo -S btrfs subvolume delete" 39 | BTRFS_FIND_NEW_COMMAND = "sudo -S btrfs subvolume find-new" 40 | GRUB_BTRFS_COMMAND = "sudo -S grub-mkconfig -o /boot/grub/grub.cfg" 41 | 42 | 43 | # Classes 44 | class Subvolume: 45 | """BTRFS Snapshot. 46 | 47 | """ 48 | # Constructor 49 | def __init__(self, subvolume_origin, subvolume_dest, snapshot_name, snapshots_to_keep): 50 | """ Constructor. 51 | 52 | Arguments: 53 | subvolume_origin (str): Full path to the subvolume. 54 | subvolume_dest (str): Full path to the subvolume where all of the subvolumes created from origin are going 55 | to be stored. 56 | snapshot_name (str): Prefix for all the subvolumes created from origin 57 | snapshots_to_keep (str): Number of snapshots to keep for this subvolume 58 | """ 59 | # Logger 60 | self.__logger = utils.Logger(self.__class__.__name__).get() 61 | self.subvolume_origin = subvolume_origin if subvolume_origin[-1] == '/' else subvolume_origin + '/' 62 | self.subvolume_dest = subvolume_dest if subvolume_dest[-1] == '/' else subvolume_dest + '/' 63 | self.snapshot_name = snapshot_name 64 | self.snapshots_to_keep = int(snapshots_to_keep) 65 | self.__current_date = time.strftime('%Y%m%d') 66 | 67 | # Methods 68 | # Private methods 69 | 70 | # Public methods 71 | def create_snapshot(self): 72 | """Creates a snapshot. 73 | 74 | """ 75 | info_message = "Creating a read-only snapshot of {subvolume_origin} in {subvolume_dest}. " \ 76 | "Please wait...".format(subvolume_origin=self.subvolume_origin, 77 | subvolume_dest=self.subvolume_dest) 78 | self.__logger.info(info_message) 79 | 80 | # Checking how many snapshots are with the same name 81 | snapshot_full_name = "{snapshot_name}-{current_date}".format(snapshot_name=self.snapshot_name, 82 | current_date=self.__current_date) 83 | snapshots_with_same_name = [file for file in os.listdir(self.subvolume_dest) if snapshot_full_name in file] 84 | 85 | # Adding number to the full name 86 | snapshot_full_name = "{snapshot_full_name}-{number}".format(snapshot_full_name=snapshot_full_name, 87 | number=len(snapshots_with_same_name)) 88 | # Checks if grub-btrfs integration is enabled 89 | if settings.properties_manager.get_property("grub_btrfs"): 90 | # Checks if /etc/fstab is in subvolume_origin 91 | fstab_path = self.subvolume_origin + 'etc/fstab' 92 | if os.path.isfile(fstab_path): 93 | everything_ok = True 94 | # /etc/fstab is in the subvolume, so it is necessary to 95 | # modify it and add the snapshot's name 96 | # grep -rnw '/etc/fstab' -e "/_active/rootvol" 97 | # First, it is necessary to obtain the original subvolume 98 | # for / which is mounted in the system (subvolume_origin_real) 99 | subvolume_origin_real = None 100 | command_string = """sudo btrfs subvolume show /""" 101 | command = [command_string] 102 | commandline_output = None 103 | try: 104 | commandline_output = subprocess.check_output(command, shell=True) 105 | except subprocess.CalledProcessError as called_process_error_exception: 106 | self.__logger.error("Error retrieving the real and mounted subvolume for / . Reason: " + 107 | str(called_process_error_exception.reason)) 108 | everything_ok = False 109 | if everything_ok: 110 | commandline_output = commandline_output.decode('utf-8') 111 | for line_output in commandline_output.split("\n"): 112 | # The original subvolume mounted for / will be always the first line 113 | # of the output 114 | subvolume_origin_real = line_output 115 | break 116 | 117 | # Creating the snapshot in rw mode 118 | command = "{command} {subvolume_origin} {subvolume_dest}{snapshot_full_name}".format( 119 | command=BTRFS_CREATE_SNAPSHOT_RW_COMMAND, 120 | subvolume_origin=self.subvolume_origin, 121 | subvolume_dest=self.subvolume_dest, 122 | snapshot_full_name=snapshot_full_name 123 | ) 124 | utils.execute_command(command, console=True, root=True) 125 | 126 | # Obtaining the real subvolume for the new snapshot created 127 | subvolume_snapshot_created_real = None 128 | command_string = """sudo btrfs subvolume show {subvolume_dest}{snapshot_full_name}""".format( 129 | subvolume_dest=self.subvolume_dest, 130 | snapshot_full_name=snapshot_full_name 131 | ) 132 | command = [command_string] 133 | commandline_output = None 134 | try: 135 | commandline_output = subprocess.check_output(command, shell=True) 136 | except subprocess.CalledProcessError as called_process_error_exception: 137 | self.__logger.error("Error retrieving the real subvolume for snapshot created. Reason: " + 138 | str(called_process_error_exception.reason)) 139 | 140 | commandline_output = commandline_output.decode('utf-8') 141 | for line_output in commandline_output.split("\n"): 142 | # The original subvolume mounted for / will be always the first line 143 | # of the output 144 | subvolume_snapshot_created_real = line_output 145 | break 146 | 147 | # Getting the line in fstab where it is necessary to substitute the subvolume which is going to be 148 | # mounted as root. It is necessary to discard all the lines with comments in fstab starting with '#' 149 | command_string = """grep -rnw '{fstab_path}' -e '{subvolume_origin_real}'""".format( 150 | fstab_path=fstab_path, subvolume_origin_real=subvolume_origin_real) 151 | command = [command_string] 152 | commandline_output = None 153 | try: 154 | commandline_output = subprocess.check_output(command, shell=True) 155 | except subprocess.CalledProcessError: 156 | pass 157 | commandline_output = commandline_output.decode('utf-8') 158 | # line will be the line where grep has matched subvolume_origin_real. All lines commented with '#' 159 | # will be discarded 160 | line = "0" 161 | for line_output in commandline_output.split("\n"): 162 | line_splitted = line_output.split(':') 163 | # First element of the list will be the line where subvolume_origin_real has been matched by 164 | # grep command. If the second element is '#', this line is commented in fstab and it won't be 165 | # taken into account 166 | if not line_splitted[1].startswith('#'): 167 | line = line_splitted[0] 168 | break 169 | 170 | command_string = """sudo -S sed -i '{line}s|{subvolume_origin_real}|{subvolume_snapshot_created_real}|g' {subvolume_dest}{snapshot_full_name}/etc/fstab""".format( 171 | line=line, 172 | subvolume_origin_real=subvolume_origin_real, 173 | subvolume_snapshot_created_real=subvolume_snapshot_created_real, 174 | subvolume_dest=self.subvolume_dest, 175 | snapshot_full_name=snapshot_full_name 176 | ) 177 | command = [command_string] 178 | try: 179 | subprocess.check_output(command, shell=True) 180 | except subprocess.CalledProcessError as called_process_error_exception: 181 | self.__logger.error("Error trying to substitute the root's path in fstab with the " 182 | "path of the new snapshot created. Reason: " + 183 | str(called_process_error_exception.reason)) 184 | everything_ok = False 185 | if everything_ok: 186 | # subvolume_origin_real will be stored in configuration file in order to let the 187 | # user to consolidate the system's rollback to any snapshot different from the main one 188 | settings.properties_manager.set_property('path_to_consolidate_root_snapshot', 189 | subvolume_origin_real) 190 | 191 | # Run grub-btrfs in order to regenerate GRUB entries 192 | self.__logger.info("Regenerating GRUB entries. Please wait...") 193 | utils.execute_command(GRUB_BTRFS_COMMAND, console=True, root=True) 194 | 195 | else: 196 | # The original subvolume mounted for / couldn't be found 197 | # Snapshot won't be created 198 | self.__logger.error("The original subvolume mounted for / couldn't be found. " 199 | "Snapshot won't be created: ") 200 | pass 201 | 202 | else: 203 | command = "{command} {subvolume_origin} {subvolume_dest}{snapshot_full_name}".format( 204 | command=BTRFS_CREATE_SNAPSHOT_R_COMMAND, 205 | subvolume_origin=self.subvolume_origin, 206 | subvolume_dest=self.subvolume_dest, 207 | snapshot_full_name=snapshot_full_name 208 | ) 209 | utils.execute_command(command, console=True, root=True) 210 | else: 211 | command = "{command} {subvolume_origin} {subvolume_dest}{snapshot_full_name}".format( 212 | command=BTRFS_CREATE_SNAPSHOT_R_COMMAND, 213 | subvolume_origin=self.subvolume_origin, 214 | subvolume_dest=self.subvolume_dest, 215 | snapshot_full_name=snapshot_full_name 216 | ) 217 | utils.execute_command(command, console=True, root=True) 218 | 219 | def delete_snapshots(self): 220 | """Deletes (or not if user has defined it) all the snapshots needed to keep the desired number set by the user. 221 | It will delete the related logs if they exist 222 | 223 | """ 224 | info_message = "Deleting snapshot of {subvolume_origin} in {subvolume_dest}. " \ 225 | "Please wait...".format(subvolume_origin=self.subvolume_origin, 226 | subvolume_dest=self.subvolume_dest) 227 | self.__logger.info(info_message) 228 | 229 | # If user has selected not delete any snapshot, this operation won't be done 230 | if self.snapshots_to_keep > -1: 231 | # Checking how many snapshots are with the same name ordered by date 232 | snapshots = self.get_all_snapshots_with_the_same_name() 233 | 234 | # Removing all the snapshots needed starting with the oldest one until reach 235 | # the limit defined by the user 236 | snapshots_to_delete = len(snapshots) - self.snapshots_to_keep 237 | index = 0 238 | while snapshots_to_delete > 0: 239 | # Deletes the snapshot 240 | command = "{command} {snapshot}".format(command=BTRFS_DELETE_SNAPSHOT_COMMAND, 241 | snapshot=snapshots[index]) 242 | utils.execute_command(command, console=True, root=True) 243 | info_message = "Snapshot {snapshot} deleted.\n".format(snapshot=snapshots[index]) 244 | self.__logger.info(info_message) 245 | # Deletes the log if it exists 246 | snapshot_name = snapshots[index].split("/")[-1] 247 | log = "{snapshot_name}-{index}.txt".format(snapshot_name=snapshot_name.split("-")[-2], 248 | index=snapshot_name.split("-")[-1]) 249 | log_path = os.path.join(settings.logs_path, log) 250 | if os.path.exists(log_path): 251 | try: 252 | os.remove(log_path) 253 | info_message = "Log {log} deleted.\n".format(log=log) 254 | self.__logger.info(info_message) 255 | except OSError as os_error_exception: 256 | info_message = "Error deleting log {log}. Error {exception}\n".format(log=log, 257 | exception=str( 258 | os_error_exception)) 259 | self.__logger.info(info_message) 260 | else: 261 | info_message = "Log {log} doesn't exist. Skipping...deleted.\n".format(log=log) 262 | self.__logger.info(info_message) 263 | 264 | snapshots_to_delete -= 1 265 | index += 1 266 | 267 | # Checks if grub-btrfs integration is enabled 268 | if settings.properties_manager.get_property("grub_btrfs"): 269 | # Run grub-btrfs in order to regenerate GRUB entries 270 | utils.execute_command(GRUB_BTRFS_COMMAND, console=True, root=True) 271 | 272 | def delete_origin(self): 273 | """Deletes the original subvolume, i.e. the subvolume in subvolume_origin 274 | 275 | """ 276 | info_message = "Deleting subvolume from origin {subvolume_origin}. " \ 277 | "Please wait...".format(subvolume_origin=self.subvolume_origin) 278 | self.__logger.info(info_message) 279 | errors = False 280 | 281 | # Deletes the subvolume 282 | command_string = "{command} {snapshot}".format(command=BTRFS_DELETE_SNAPSHOT_COMMAND, 283 | snapshot=self.subvolume_origin) 284 | command = [command_string] 285 | commandline_output = None 286 | try: 287 | # This is a special way for executing a command using Python: 288 | # shell=True allows the execution of complex shell commands 289 | # stdout=subprocess.PIPE captures the output generated by the command 290 | # stderr=subprocess.STDOUT captures the errors generated by the command and redirects them to stdout 291 | commandline_output = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 292 | commandline_output = commandline_output.stdout.decode('utf-8') 293 | except subprocess.CalledProcessError: 294 | pass 295 | 296 | for line in commandline_output.split("\n"): 297 | if 'Directory not empty' in line: 298 | errors = True 299 | break 300 | if errors: 301 | # Subvolume is not empty so it can't be deleted. Exception is thrown 302 | raise exception.BtrfsSnapshotDeletion("Error: {snapshot} is not empty.\n". 303 | format(snapshot=self.subvolume_origin)) 304 | else: 305 | info_message = "Snapshot {snapshot} deleted.\n".format(snapshot=self.subvolume_origin) 306 | self.__logger.info(info_message) 307 | 308 | def get_all_snapshots_with_the_same_name(self): 309 | """Retrieves all the snapshots with name self.snapshot_name stored within self.subvolume_dest. 310 | 311 | Returns: 312 | list (:obj:`list` of :obj:`str`): paths to the snapshots. 313 | """ 314 | # Checking how many snapshots are with the same name ordered by date 315 | snapshots = glob.glob("{snapshot_directory}/*".format(snapshot_directory=self.subvolume_dest)) 316 | snapshots.sort(key=os.path.getmtime) 317 | snapshots_whit_same_name = [file for file in snapshots if self.snapshot_name in file] 318 | 319 | return snapshots_whit_same_name 320 | 321 | 322 | class RootSnapshotChecker: 323 | """Checks if the current snapshot used for root is the default or the user has booted the system from 324 | an alternate snapshot. 325 | 326 | """ 327 | def __init__(self, parent_window): 328 | # Attributes 329 | self.__snapshot_to_clone_in_root_full_path = None 330 | self.__root_subvolume = None 331 | # Logger 332 | self.__logger = utils.Logger(self.__class__.__name__).get() 333 | self.__logger.info("Checking if the current snapshot used for root is the default. Please wait...") 334 | self.__parent_window = parent_window 335 | 336 | def check_root_snapshot(self): 337 | """Checks if the current snapshot used for root is the default or the user has booted the system from 338 | an alternate snapshot. 339 | 340 | Returns: 341 | boolean: true if current snapshot used for root is the default or this paramenter has not been stored yet; 342 | false otherwise. 343 | """ 344 | # First, it is necessary to check if path_to_consolidate_root_snapshot is defined 345 | if settings.properties_manager.get_property("path_to_consolidate_root_snapshot") != 0: 346 | # Obtaining the mounted subvolume for root partition 347 | # mount | grep 'on / ' | grep -o 'subvol=/.*' | cut -f2- -d= 348 | command_string = """mount | grep 'on / ' | grep -o 'subvol=/.*' | cut -f2- -d=""" 349 | command = [command_string] 350 | mounted_snapshot_raw = None 351 | try: 352 | mounted_snapshot_raw = subprocess.check_output(command, shell=True) 353 | except subprocess.CalledProcessError: 354 | pass 355 | if mounted_snapshot_raw: 356 | # Removing the last two characters (a /n and a ")") 357 | mounted_snapshot_raw = mounted_snapshot_raw[:-2] 358 | # Converting bytes into string 359 | mounted_snapshot_raw = mounted_snapshot_raw.decode("utf-8") 360 | # Removing first / if path_to_consolidate_root_snapshot doesn't start with / 361 | if not settings.properties_manager.get_property("path_to_consolidate_root_snapshot")\ 362 | .startswith("/"): 363 | mounted_snapshot_raw = mounted_snapshot_raw[1:] 364 | if mounted_snapshot_raw != settings.properties_manager. \ 365 | get_property("path_to_consolidate_root_snapshot"): 366 | # If mounted snapshot is different from the supposed default root subvolume 367 | # it means that user has booted the system using an alternate snapshot from GRUB. 368 | # ButterManager will ask to consolidate the current snapshot as the default root 369 | # subvolume 370 | 371 | # Obtaining the snapshot mounted 372 | mounted_snapshot_full_path = None 373 | while mounted_snapshot_full_path is None: 374 | for subvolume in settings.subvolumes: 375 | snapshots = settings.subvolumes[subvolume].get_all_snapshots_with_the_same_name() 376 | for snapshot in snapshots: 377 | if mounted_snapshot_raw in snapshot: 378 | mounted_snapshot_full_path = snapshot 379 | break 380 | if mounted_snapshot_full_path is not None: 381 | break 382 | 383 | self.__snapshot_to_clone_in_root_full_path = mounted_snapshot_full_path 384 | self.__root_subvolume = settings.subvolumes[subvolume] 385 | return False 386 | else: 387 | return True 388 | else: 389 | # Path to consolidate root snapshot hasn't been defined yet so this check is skipped 390 | return True 391 | 392 | def open_consolidate_snapshot_window(self): 393 | """Checks if the current snapshot used for root is the default or the user has booted the system from 394 | an alternate snapshot. 395 | 396 | Returns: 397 | QDialog: The dialog window to consolidate the root snapshot. 398 | """ 399 | info_window = windows.ConsolidateSnapshotWindow(self.__parent_window, 400 | self.__snapshot_to_clone_in_root_full_path, 401 | self.__root_subvolume) 402 | return info_window 403 | 404 | 405 | class Differentiator(QThread): 406 | """Independent thread that will calculate the differences between a snapshot and its current state. 407 | 408 | """ 409 | # Constants 410 | DIFFS_DIR = "diffs" 411 | DIFF_COMMAND = "sudo -S diff -qr" 412 | MODIFIED_FILE = "modified.txt" 413 | OPERATION_FULL = "full_operation" 414 | OPERATION_PARTIAL = "partial_operation" 415 | 416 | # Attributes 417 | # pyqtSignal that will be emitted when this class requires to display 418 | # a single information window on the screen 419 | show_one_window = pyqtSignal('bool') 420 | 421 | # Constructor 422 | def __init__(self, snapshot_full_path, operation_type): 423 | QThread.__init__(self) 424 | self.__snapshot_full_path = snapshot_full_path 425 | self.__snapshot_name = snapshot_full_path.split("/")[-1] 426 | self.__operation_type = operation_type 427 | 428 | # Methods 429 | def run(self): 430 | # Main window will be hidden 431 | self.on_show_one_window(True) 432 | info_dialog = windows.InfoWindow(None, "Calculating differences in\n" 433 | "'{snapshot_name}'.\n" 434 | "Please, be patient. This process\n" 435 | "can take several minutes. This\n" 436 | "window will be closed when the\n" 437 | "operation is done. Calculating..." 438 | .format(snapshot_name=self.__snapshot_full_path)) 439 | # Displaying info window 440 | info_dialog.show() 441 | 442 | # Calculates differences 443 | self.__calculate_differences() 444 | 445 | # Hiding info window 446 | info_dialog.hide() 447 | 448 | # Main window will be shown again 449 | self.on_show_one_window(False) 450 | 451 | def __calculate_differences(self): 452 | """Wraps all the operations to calculate differences. 453 | 454 | """ 455 | # Gets the subvolume of the snapshot 456 | subvolume = get_subvolume_by_snapshot_name(self.__snapshot_full_path) 457 | 458 | if subvolume: 459 | # Creating a directory to store all the diff files generated if it doesn't exist 460 | # or removing and creating it if it existed 461 | diffs_path = os.path.join(settings.application_path, self.DIFFS_DIR, self.__snapshot_name) 462 | 463 | if os.path.exists(diffs_path): 464 | shutil.rmtree(diffs_path) 465 | 466 | os.makedirs(diffs_path) 467 | 468 | # Getting the current subvolune name. This subvolume is the current one which is going to be 469 | # used for the comparison. It is needed to remove all the empty strings within the list 470 | subvolume_name_list = subvolume.subvolume_origin.split("/") 471 | cleaned_subvolume_name_list = list(filter(None, subvolume_name_list)) 472 | if not cleaned_subvolume_name_list: 473 | subvolume_name = "original" 474 | else: 475 | subvolume_name = cleaned_subvolume_name_list[-1] 476 | 477 | if self.__operation_type == self.OPERATION_FULL: 478 | # Full operation 479 | # Creating 3 different files to store differences 480 | files_only_in_dir1_path = os.path.join(diffs_path, "{file_name}.txt".format(file_name=subvolume_name)) 481 | files_only_in_dir2_path = os.path.join(diffs_path, "{file_name}.txt".format( 482 | file_name=self.__snapshot_name)) 483 | files_in_both_modified_path = os.path.join(diffs_path, self.MODIFIED_FILE) 484 | files_only_in_dir1 = open(files_only_in_dir1_path, "w+") 485 | files_only_in_dir2 = open(files_only_in_dir2_path, "w+") 486 | files_in_both_modified = open(files_in_both_modified_path, "w+") 487 | 488 | files_in_both_modified.write("Files in both snapshots that have been modified" + "\r\n\r\n") 489 | files_only_in_dir1.write("Files only in ${dir}".format(dir=subvolume.subvolume_origin) + "\r\n\r\n") 490 | files_only_in_dir2.write("Files only in ${dir}".format(dir=self.__snapshot_full_path) + "\r\n\r\n") 491 | 492 | # Calculating differences 493 | command = "{command} {dir1} {dir2}".format(command=self.DIFF_COMMAND, dir1=subvolume.subvolume_origin, 494 | dir2=self.__snapshot_full_path) 495 | echo = subprocess.Popen(['echo', settings.user_password], stdout=subprocess.PIPE) 496 | result = subprocess.Popen(command.split(), stdin=echo.stdout, stdout=subprocess.PIPE) 497 | 498 | for line in iter(result.stdout.readline, b''): 499 | line_decoded = line.decode('utf-8') 500 | if " differ" in line_decoded: 501 | file_modified_splitted = line_decoded.split( 502 | "Files {path}".format(path=subvolume.subvolume_origin)) 503 | file_modified = "/" + file_modified_splitted[1].split(" ")[0] 504 | files_in_both_modified.write(file_modified + "\r\n") 505 | elif "Only in {dir1}".format(dir1=subvolume.subvolume_origin) in line_decoded: 506 | file_only_in_dir1_splitted = line_decoded.split("Only in {path}".format( 507 | path=subvolume.subvolume_origin)) 508 | file_only_in_dir1_splitted = file_only_in_dir1_splitted[1].split(":") 509 | file_name = file_only_in_dir1_splitted[1].strip() 510 | file_only_in_dir1 = "/" + file_only_in_dir1_splitted[0] + "/" + file_name 511 | files_only_in_dir1.write(file_only_in_dir1 + "\r\n") 512 | elif "Only in {dir2}".format(dir2=self.__snapshot_full_path) in line_decoded: 513 | file_only_in_dir2_splitted = line_decoded.split("Only in {path}".format( 514 | path=self.__snapshot_full_path)) 515 | file_only_in_dir2_splitted = file_only_in_dir2_splitted[1].split(":") 516 | file_name = file_only_in_dir2_splitted[1].strip() 517 | file_only_in_dir2 = file_only_in_dir2_splitted[0] + "/" + file_name 518 | files_only_in_dir2.write(file_only_in_dir2 + "\r\n") 519 | # Closing files 520 | files_only_in_dir1.close() 521 | files_only_in_dir2.close() 522 | files_in_both_modified.close() 523 | 524 | # Opening the file with the default application installed in the OS 525 | # Warning, xdg-open is not working executing the code from PyCharm so 526 | # it seems it doesn't work but it is really working 527 | subprocess.call(['xdg-open', files_only_in_dir1_path]) 528 | subprocess.call(['xdg-open', files_only_in_dir2_path]) 529 | subprocess.call(['xdg-open', files_in_both_modified_path]) 530 | 531 | else: 532 | # Partial operation 533 | # Creating only one file to store differences 534 | temp_sorted_modified_path = os.path.join(diffs_path, "tmp.txt") 535 | temp_sorted_modified = open(temp_sorted_modified_path, "w+") 536 | 537 | # Calculating differences 538 | # sudo btrfs subvolume find-new /mnt/defvol/_snapshots/root-20201021-0/ '9999999' 539 | # sudo btrfs subvolume find-new /mnt/defvol/_active/rootvol/ 463579 | sed '$d' | cut -f17- -d' ' | 540 | # sort | uniq 541 | 542 | # First, old transid is calculated. This ID will be used as a reference for the comparison 543 | transid = "9999999" 544 | command = "{command} {dir1} {transid}".format(command=BTRFS_FIND_NEW_COMMAND, 545 | dir1=self.__snapshot_full_path, transid=transid) 546 | echo = subprocess.Popen(['echo', settings.user_password], stdout=subprocess.PIPE) 547 | result = subprocess.Popen(command.split(), stdin=echo.stdout, stdout=subprocess.PIPE) 548 | 549 | for line in iter(result.stdout.readline, b''): 550 | line_decoded = line.decode('utf-8') 551 | line_splitted = line_decoded.split(" ") 552 | transid = line_splitted[-1].strip() 553 | 554 | # Then, the differences are obtained using transid 555 | command = "{command} {dir1} {transid}".format( 556 | command=BTRFS_FIND_NEW_COMMAND, 557 | dir1=subvolume.subvolume_origin, 558 | transid=transid) 559 | echo = subprocess.Popen(['echo', settings.user_password], stdout=subprocess.PIPE) 560 | result = subprocess.Popen(command.split(), stdin=echo.stdout, stdout=subprocess.PIPE) 561 | 562 | temp_sorted_modified.write("- Files in both snapshots that have been modified" + "\r\n") 563 | 564 | for line in iter(result.stdout.readline, b''): 565 | line_decoded = line.decode('utf-8') 566 | line_splitted = line_decoded.split(" ") 567 | file_modified = "/" + line_splitted[-1].strip() 568 | temp_sorted_modified.write(file_modified + "\r\n") 569 | 570 | # Closing file 571 | temp_sorted_modified.close() 572 | 573 | # Sorting file 574 | files_in_both_modified_path = os.path.join(diffs_path, self.MODIFIED_FILE) 575 | files_in_both_modified = open(files_in_both_modified_path, "w+") 576 | command = "sort {file}".format(file=temp_sorted_modified_path) 577 | subprocess.Popen(command.split(), stdout=files_in_both_modified) 578 | 579 | # Opening the file with the default application installed in the OS 580 | # Warning, xdg-open is not working executing the code from PyCharm so 581 | # it seems it doesn't work but it is really working 582 | subprocess.call(['xdg-open', files_in_both_modified_path]) 583 | 584 | def on_show_one_window(self, one_window): 585 | """Emits a QT Signal to hide or show the rest of application windows. 586 | 587 | Arguments: 588 | one_window (boolean): Information window should be unique?. 589 | """ 590 | self.show_one_window.emit(one_window) 591 | 592 | 593 | # Module's methods 594 | def delete_specific_snapshot(snapshot_full_path): 595 | """Deletes a specific snapshot. 596 | It will delete the specific log related if it exists too. 597 | 598 | Arguments: 599 | snapshot_full_path (string): path to the snapshot that user wants to delete. 600 | 601 | """ 602 | # Logger 603 | logger = utils.Logger(sys.modules['__main__'].__file__).get() 604 | info_message = "Deleting snapshot {snapshot}".format(snapshot=snapshot_full_path) 605 | logger.info(info_message) 606 | 607 | command = "{command} {snapshot}".format(command=BTRFS_DELETE_SNAPSHOT_COMMAND, snapshot=snapshot_full_path) 608 | utils.execute_command(command, root=True) 609 | info_message = "Snapshot {snapshot} deleted.\n".format(snapshot=snapshot_full_path) 610 | logger.info(info_message) 611 | 612 | # Checks if grub-btrfs integration is enabled 613 | if settings.properties_manager.get_property("grub_btrfs"): 614 | # Run grub-btrfs in order to regenerate GRUB entries 615 | utils.execute_command(GRUB_BTRFS_COMMAND, console=True, root=True) 616 | info_message = "Regenerating GRUB entries. Please wait..." 617 | logger.info(info_message) 618 | 619 | # Deletes the log if it exists 620 | snapshot_name = snapshot_full_path.split("/")[-1] 621 | log = "{snapshot_name}-{index}.txt".format(snapshot_name=snapshot_name.split("-")[-2], 622 | index=snapshot_name.split("-")[-1]) 623 | log_path = os.path.join(settings.logs_path, log) 624 | if os.path.exists(log_path): 625 | try: 626 | os.remove(log_path) 627 | info_message = "Log {log} deleted.\n".format(log=log) 628 | logger.info(info_message) 629 | except OSError as os_error_exception: 630 | info_message = "Error deleting log {log}. Error {exception}\n".format(log=log, 631 | exception=str(os_error_exception)) 632 | logger.info(info_message) 633 | else: 634 | info_message = "Log {log} doesn't exist. Skipping...deleted.\n".format(log=log) 635 | logger.info(info_message) 636 | 637 | 638 | def get_subvolume_by_snapshot_name(snapshot_name): 639 | """Gets a subvolume object using the name of the snapshot. 640 | 641 | Arguments: 642 | snapshot_name (string): name of the snapshot. 643 | 644 | Returns: 645 | Subvolume: The subvolume which belongs the snapshot. None if subvolume was not found. 646 | """ 647 | # Logger 648 | logger = utils.Logger(sys.modules['__main__'].__file__).get() 649 | info_message = "Getting subvolume from snapshot's name {snapshot_name}".format(snapshot_name=snapshot_name) 650 | logger.info(info_message) 651 | 652 | subvolume_found = None 653 | 654 | for subvolume_key in settings.subvolumes: 655 | subvolume = settings.subvolumes[subvolume_key] 656 | subvolume_snapshots_prefix = "{subvolume_dest}{subvolume_prefix}".format( 657 | subvolume_dest=subvolume.subvolume_dest, 658 | subvolume_prefix=subvolume.snapshot_name) 659 | if snapshot_name.startswith(subvolume_snapshots_prefix): 660 | info_message = "Found subvolume {subvolume}".format(subvolume=subvolume.subvolume_origin) 661 | logger.info(info_message) 662 | subvolume_found = subvolume 663 | return subvolume_found 664 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/accept_16px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/accept_16px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/add_16px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/add_16px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/buttermanager.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 44 | 48 | 52 | 54 | 58 | 62 | 63 | 65 | 66 | 68 | 69 | 71 | 72 | 74 | 75 | 77 | 78 | 80 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 92 | 93 | 95 | 96 | 98 | 99 | 101 | 102 | 104 | 105 | 107 | 108 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/buttermanager50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/buttermanager50.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/edit_16px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/edit_16px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/exchange_arrows_16px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/exchange_arrows_16px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/folder_16px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/folder_16px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/lock_24px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/lock_24px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/remove_16px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/remove_16px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/images/view_24px_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/images/view_24px_icon.png -------------------------------------------------------------------------------- /buttermanager/buttermanager/manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/manager/__init__.py -------------------------------------------------------------------------------- /buttermanager/buttermanager/manager/upgrader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018-2019 Eloy García Almadén 4 | # 5 | # This file is part of buttermanager. 6 | # 7 | # This program is free software: you can redistribute it and / or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation, version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """This module gathers all the managers built for the application. 20 | 21 | """ 22 | from .. import manager 23 | from ..util import settings, utils 24 | import sys 25 | import urllib.request 26 | from PyQt5.QtCore import QThread, pyqtSignal 27 | from urllib.error import URLError 28 | 29 | # Constants 30 | ARCH_PACMAN_REFRESH_REPOSITORIES = "sudo -S pacman -Sy" 31 | ARCH_PACMAN_CHECK_UPDATES = "sudo -S pacman -Qu" 32 | ARCH_PACMAN_UPGRADE_COMMAND = "sudo -S pacman -Syu --noconfirm" 33 | DEBIAN_APT_UPDATE_COMMAND = "sudo -S apt update" 34 | DEBIAN_APT_UPGRADE_COMMAND = "sudo -S apt upgrade -y" 35 | DEBIAN_APT_CHECK_UPDATES = "sudo -S apt list --upgradable" 36 | ARCH_YAOURT_UPGRADE_COMMAND = "yaourt -Syua --noconfirm" 37 | ARCH_YAOURT_COMMAND = "yaourt" 38 | ARCH_YAY_UPGRADE_COMMAND = "yay -Syua --noconfirm" 39 | ARCH_YAY_COMMAND = "yay" 40 | ARCH_TRIZEN_UPGRADE_COMMAND = "trizen -Syua --noconfirm" 41 | ARCH_TRIZEN_COMMAND = "trizen" 42 | SNAP_COMMAND = "snap" 43 | FLATPAK_COMMAND = "flatpak" 44 | SNAP_UPGRADE_COMMAND = "sudo -S snap refresh" 45 | FLATPAK_UPGRADE_COMMAND = "flatpak update -y" 46 | SUSE_ZYPPER_UPGRADE_COMMAND = "sudo -S zypper -n update" 47 | SUSE_ZYPPER_CHECK_UPDATES = "sudo -S zypper list-updates" 48 | FEDORA_DNF_UPGRADE_COMMAND = "sudo -S dnf upgrade --refresh --assumeyes" 49 | FEDORA_DNF_CHECK_UPDATES = "sudo -S dnf check-update" 50 | 51 | 52 | class Upgrader(QThread): 53 | """Independent thread that will run the system upgrading process. 54 | 55 | """ 56 | # Attributes 57 | 58 | # pyqtSignal that will be emitted when this class requires that main 59 | # window disables all the buttons 60 | disable_buttons = pyqtSignal() 61 | 62 | # pyqtSignal that will be emitted when this class requires that main 63 | # window enables all the buttons 64 | enable_buttons = pyqtSignal() 65 | 66 | # pyqtSignal that will be emitted when this class requires that main 67 | # window refreshes GUI 68 | refresh_gui = pyqtSignal() 69 | 70 | # Constructor 71 | def __init__(self, include_aur, include_snap, include_flatpak, snapshots): 72 | QThread.__init__(self) 73 | # Logger 74 | self.__logger = utils.Logger(self.__class__.__name__).get() 75 | # Include AUR packages upgrade 76 | self.__include_aur = include_aur 77 | # Include snap packages upgrade 78 | self.__include_snap = include_snap 79 | # Include flatpak packages upgrade 80 | self.__include_flatpak = include_flatpak 81 | # Create and delete snapshots 82 | self.__snapshots = snapshots 83 | 84 | # Methods 85 | def run(self): 86 | # Upgrading the system 87 | self.__upgrade_system() 88 | 89 | def __upgrade_system(self): 90 | """Wraps all the operations to upgrade the system. 91 | 92 | """ 93 | # Check for updates 94 | if check_updates(): 95 | # There are system updates 96 | # Starting the upgrading process. Disabling all the buttons. 97 | self.on_disable_gui_buttons() 98 | 99 | sys.stdout.write("\n") 100 | sys.stdout.write("--------") 101 | sys.stdout.write("\n") 102 | self.__logger.info("Starting system upgrading process.") 103 | sys.stdout.write("Starting system upgrading process. Please wait...") 104 | sys.stdout.write("\n") 105 | 106 | # Creates all the snapshots needed before upgrading the system 107 | # only if it is needed 108 | if self.__snapshots: 109 | sys.stdout.write("\n") 110 | sys.stdout.write("--------") 111 | sys.stdout.write("\n") 112 | sys.stdout.write("Creating snapshots and updating GRUB entries if it is necessary...") 113 | sys.stdout.write("\n") 114 | sys.stdout.write("--------") 115 | sys.stdout.write("\n") 116 | for snapshot in settings.subvolumes: 117 | try: 118 | settings.subvolumes[snapshot].create_snapshot() 119 | except Exception as exception: 120 | sys.stdout.write("\n") 121 | sys.stdout.write("--------") 122 | sys.stdout.write("\n") 123 | sys.stdout.write("Error creating the snapshot " + 124 | settings.subvolumes[snapshot].subvolume_origin) 125 | sys.stdout.write("\n") 126 | sys.stdout.write("Error: " + str(exception)) 127 | sys.stdout.write("\n") 128 | sys.stdout.write("--------") 129 | sys.stdout.write("\n") 130 | 131 | # Upgrades the system 132 | upgrading_command = "" 133 | if settings.user_os == utils.OS_ARCH: 134 | upgrading_command = ARCH_PACMAN_UPGRADE_COMMAND 135 | elif settings.user_os == utils.OS_DEBIAN: 136 | # First, it is necessary to update the system 137 | sys.stdout.write("\n") 138 | sys.stdout.write("--------") 139 | sys.stdout.write("\n") 140 | sys.stdout.write("Updating the system. Please wait...") 141 | sys.stdout.write("\n") 142 | utils.execute_command(DEBIAN_APT_UPDATE_COMMAND, console=True) 143 | sys.stdout.write("\n") 144 | upgrading_command = DEBIAN_APT_UPGRADE_COMMAND 145 | elif settings.user_os == utils.OS_SUSE: 146 | upgrading_command = SUSE_ZYPPER_UPGRADE_COMMAND 147 | elif settings.user_os == utils.OS_FEDORA: 148 | upgrading_command = FEDORA_DNF_UPGRADE_COMMAND 149 | 150 | if upgrading_command: 151 | try: 152 | sys.stdout.write("Upgrading the system. Please wait...") 153 | sys.stdout.write("\n") 154 | utils.execute_command(upgrading_command, console=True) 155 | except Exception as exception: 156 | sys.stdout.write("\n") 157 | sys.stdout.write("--------") 158 | sys.stdout.write("\n") 159 | sys.stdout.write("Error upgrading the system") 160 | sys.stdout.write("\n") 161 | sys.stdout.write("Error: " + str(exception)) 162 | sys.stdout.write("\n") 163 | sys.stdout.write("--------") 164 | sys.stdout.write("\n") 165 | 166 | # Upgrades AUR if distro is ArchLinux or derivatives 167 | if settings.user_os == utils.OS_ARCH: 168 | if self.__include_aur: 169 | try: 170 | sys.stdout.write("\n") 171 | sys.stdout.write("--------") 172 | sys.stdout.write("\n") 173 | sys.stdout.write("Updating AUR packages if it is needed. Please wait...") 174 | sys.stdout.write("\n") 175 | if utils.exist_program(ARCH_YAY_COMMAND): 176 | utils.execute_command(ARCH_YAY_UPGRADE_COMMAND, console=True) 177 | elif utils.exist_program(ARCH_TRIZEN_COMMAND): 178 | utils.execute_command(ARCH_TRIZEN_UPGRADE_COMMAND, console=True) 179 | elif utils.exist_program(ARCH_YAOURT_COMMAND): 180 | utils.execute_command(ARCH_YAOURT_UPGRADE_COMMAND, console=True) 181 | except Exception as exception: 182 | sys.stdout.write("\n") 183 | sys.stdout.write("--------") 184 | sys.stdout.write("\n") 185 | sys.stdout.write("Error upgrading AUR packages") 186 | sys.stdout.write("\n") 187 | sys.stdout.write("Error: " + str(exception)) 188 | sys.stdout.write("\n") 189 | sys.stdout.write("--------") 190 | sys.stdout.write("\n") 191 | 192 | # Upgrades snap packages 193 | if self.__include_snap: 194 | if utils.exist_program(SNAP_COMMAND): 195 | try: 196 | sys.stdout.write("\n") 197 | sys.stdout.write("--------") 198 | sys.stdout.write("\n") 199 | sys.stdout.write("Updating snap applications. Please wait...") 200 | sys.stdout.write("\n") 201 | utils.execute_command(SNAP_UPGRADE_COMMAND, console=True) 202 | except Exception as exception: 203 | sys.stdout.write("\n") 204 | sys.stdout.write("--------") 205 | sys.stdout.write("\n") 206 | sys.stdout.write("Error upgrading snap packages") 207 | sys.stdout.write("\n") 208 | sys.stdout.write("Error: " + str(exception)) 209 | sys.stdout.write("\n") 210 | sys.stdout.write("--------") 211 | sys.stdout.write("\n") 212 | 213 | # Upgrades flatpak packages 214 | if self.__include_flatpak: 215 | if utils.exist_program(FLATPAK_COMMAND): 216 | try: 217 | sys.stdout.write("\n") 218 | sys.stdout.write("--------") 219 | sys.stdout.write("\n") 220 | sys.stdout.write("Updating flatpak applications. Please wait...") 221 | sys.stdout.write("\n") 222 | utils.execute_command(FLATPAK_UPGRADE_COMMAND, console=True) 223 | except Exception as exception: 224 | sys.stdout.write("\n") 225 | sys.stdout.write("--------") 226 | sys.stdout.write("\n") 227 | sys.stdout.write("Error upgrading flatpak packages") 228 | sys.stdout.write("\n") 229 | sys.stdout.write("Error: " + str(exception)) 230 | sys.stdout.write("\n") 231 | sys.stdout.write("--------") 232 | sys.stdout.write("\n") 233 | 234 | # Removes all the snapshots not needed any more it is needed 235 | if self.__snapshots: 236 | sys.stdout.write("\n") 237 | sys.stdout.write("--------") 238 | sys.stdout.write("\n") 239 | sys.stdout.write("Removing old snapshots if it is needed and updating GRUB entries. Please wait...") 240 | sys.stdout.write("\n") 241 | for snapshot in settings.subvolumes: 242 | try: 243 | settings.subvolumes[snapshot].delete_snapshots() 244 | except Exception as exception: 245 | sys.stdout.write("\n") 246 | sys.stdout.write("--------") 247 | sys.stdout.write("\n") 248 | sys.stdout.write("Error deleting the snapshot " + 249 | settings.subvolumes[snapshot].subvolume_origin) 250 | sys.stdout.write("\n") 251 | sys.stdout.write("Error: " + str(exception)) 252 | sys.stdout.write("\n") 253 | sys.stdout.write("--------") 254 | sys.stdout.write("\n") 255 | 256 | sys.stdout.write("\n") 257 | sys.stdout.write("--------") 258 | sys.stdout.write("\n") 259 | self.__logger.info("System upgrading process finished.") 260 | sys.stdout.write("System upgrading process finished. You can close the terminal output now.") 261 | sys.stdout.write("\n") 262 | sys.stdout.write("\n") 263 | 264 | # Refreshing GUI 265 | self.on_refresh_gui() 266 | else: 267 | # There are not system updates 268 | self.__logger.info("Your system is up to date.") 269 | sys.stdout.write("Your system is up to date. You can close the terminal output now.") 270 | sys.stdout.write("\n") 271 | sys.stdout.write("\n") 272 | 273 | # Finishing the upgrading process. Enabling all the buttons. 274 | self.on_enable_gui_buttons() 275 | 276 | def on_disable_gui_buttons(self): 277 | """Emits a QT Signal to disable all the buttons in main window. 278 | 279 | """ 280 | self.disable_buttons.emit() 281 | 282 | def on_enable_gui_buttons(self): 283 | """Emits a QT Signal to enable all the buttons in main window. 284 | 285 | """ 286 | self.enable_buttons.emit() 287 | 288 | def on_refresh_gui(self): 289 | """Emits a QT Signal to refresh filesystem statistics in main window. 290 | 291 | """ 292 | self.refresh_gui.emit() 293 | 294 | 295 | class UpdatesChecker(QThread): 296 | """Independent thread that will run the system checking for updates. 297 | 298 | """ 299 | # Attributes 300 | 301 | # pyqtSignal that will be emitted when this class requires that main 302 | # window shows the updates window. The signal will emit an 'object' that, 303 | # in hits case, will be a list of strings. 304 | show_updates_window = pyqtSignal(object) 305 | 306 | # Constructor 307 | def __init__(self): 308 | QThread.__init__(self) 309 | # Logger 310 | self.__logger = utils.Logger(self.__class__.__name__).get() 311 | 312 | # Methods 313 | def run(self): 314 | # Checks for updates 315 | self.__check_updates() 316 | 317 | def __check_updates(self): 318 | """Wraps all the operations to check updates. 319 | 320 | First, it will check Internet connectivity for doing the operation. 321 | Emits a signal with the packages found. Otherwise, it won't emit this signal and 322 | nothing will happen. 323 | 324 | """ 325 | # Checking Internet connection for 5 minutes 326 | tries = 0 327 | internet_available = self.__internet_available() 328 | 329 | while (not internet_available) & (tries < 60): 330 | self.__logger.info("Trying to reach Internet again. If there is no Internet connection in 5 minutes, this" 331 | "operation will be canceled") 332 | self.sleep(5) 333 | internet_available = self.__internet_available() 334 | tries += 1 335 | 336 | # Checking updates only if Internet connection is available 337 | if internet_available: 338 | # Checking updates only if the user selected the option 339 | if settings.check_at_startup == 1: 340 | # Emmiting the signal only if there are updates 341 | if manager.upgrader.check_updates(): 342 | commandline_output = [] 343 | if settings.user_os == utils.OS_ARCH: 344 | refresh_repositories_command = manager.upgrader.ARCH_PACMAN_REFRESH_REPOSITORIES 345 | utils.execute_command(refresh_repositories_command) 346 | check_for_updates_command = manager.upgrader.ARCH_PACMAN_CHECK_UPDATES 347 | commandline_output = utils.execute_command(check_for_updates_command) 348 | 349 | elif settings.user_os == utils.OS_DEBIAN: 350 | check_for_updates_command = manager.upgrader.DEBIAN_APT_CHECK_UPDATES 351 | commandline_output = utils.execute_command(check_for_updates_command) 352 | 353 | elif settings.user_os == utils.OS_SUSE: 354 | check_for_updates_command = manager.upgrader.SUSE_ZYPPER_CHECK_UPDATES 355 | commandline_output = utils.execute_command(check_for_updates_command) 356 | 357 | elif settings.user_os == utils.OS_FEDORA: 358 | check_for_updates_command = manager.upgrader.FEDORA_DNF_CHECK_UPDATES 359 | commandline_output = utils.execute_command(check_for_updates_command) 360 | 361 | # If there are updates, emits the signal thta will be captured in buttermanager.py 362 | self.show_updates_window.emit(commandline_output) 363 | else: 364 | self.__logger.error("Timeout. Checking updates process has been cancelled because there is no Intenert" 365 | " connection") 366 | 367 | def __internet_available(self): 368 | """Checks Internet connection. 369 | 370 | Returns: 371 | boolean: true if there is Internet connection available; false otherwise. 372 | """ 373 | self.__logger.info("Checking Internet connection. Please wait...") 374 | try: 375 | urllib.request.urlopen('https://www.google.com', timeout=20) 376 | self.__logger.info("Internet connection is available!") 377 | return True 378 | except urllib.error.URLError as error: 379 | self.__logger.error("Internet connection is not available... Error: {error}".format(error=error)) 380 | return False 381 | 382 | 383 | # Module's methods 384 | def check_updates(): 385 | """Checks for updates. 386 | 387 | Returns: 388 | boolean: true if there are updates; false otherwise. 389 | """ 390 | # Logger 391 | logger = utils.Logger(sys.modules['__main__'].__file__).get() 392 | logger.info("Checking for system updates.") 393 | sys.stdout.write("Checking for system updates.") 394 | sys.stdout.write("\n") 395 | sys.stdout.write("--------") 396 | sys.stdout.write("\n") 397 | 398 | updates = False 399 | if settings.user_os == utils.OS_ARCH: 400 | refresh_repositories_command = ARCH_PACMAN_REFRESH_REPOSITORIES 401 | utils.execute_command(refresh_repositories_command) 402 | check_for_updates_command = ARCH_PACMAN_CHECK_UPDATES 403 | commandline_output = utils.execute_command(check_for_updates_command) 404 | 405 | for line in commandline_output.split("\n"): 406 | if line: 407 | updates = True 408 | 409 | elif settings.user_os == utils.OS_DEBIAN: 410 | check_for_updates_command = DEBIAN_APT_CHECK_UPDATES 411 | commandline_output = utils.execute_command(check_for_updates_command) 412 | lines = commandline_output.split("\n") 413 | if len(lines) > 2: 414 | updates = True 415 | 416 | elif settings.user_os == utils.OS_SUSE: 417 | check_for_updates_command = SUSE_ZYPPER_CHECK_UPDATES 418 | commandline_output = utils.execute_command(check_for_updates_command) 419 | lines = commandline_output.split("\n") 420 | if len(lines) > 4: 421 | updates = True 422 | 423 | elif settings.user_os == utils.OS_FEDORA: 424 | check_for_updates_command = FEDORA_DNF_CHECK_UPDATES 425 | commandline_output = utils.execute_command(check_for_updates_command) 426 | lines = commandline_output.split("\n") 427 | if len(lines) > 2: 428 | updates = True 429 | 430 | else: 431 | updates = True 432 | 433 | return updates 434 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/ConsolidateSnapshotWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 424 10 | 300 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 10 21 | 22 | 23 | 10 24 | 25 | 26 | 10 27 | 28 | 29 | 10 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Qt::AlignCenter 38 | 39 | 40 | 41 | 42 | 43 | 44 | Qt::Horizontal 45 | 46 | 47 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/GeneralInfoWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 420 10 | 285 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 10 21 | 22 | 23 | 10 24 | 25 | 26 | 10 27 | 28 | 29 | 10 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Qt::AlignCenter 38 | 39 | 40 | 41 | 42 | 43 | 44 | Qt::Horizontal 45 | 46 | 47 | QDialogButtonBox::Close 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/InfoWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 320 10 | 240 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 10 21 | 22 | 23 | 10 24 | 25 | 26 | 10 27 | 28 | 29 | 10 30 | 31 | 32 | 33 | 34 | Qt::LeftToRight 35 | 36 | 37 | false 38 | 39 | 40 | TextLabel 41 | 42 | 43 | Qt::AlignCenter 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/LogViewWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LogViewWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 767 10 | 442 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 21 | 10 22 | 23 | 24 | 10 25 | 26 | 27 | 10 28 | 29 | 30 | 10 31 | 32 | 33 | 34 | 35 | Log details 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/PasswordWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PasswordWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 330 10 | 195 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 21 | Qt::Vertical 22 | 23 | 24 | 25 | 20 26 | 40 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 10 35 | 36 | 37 | 10 38 | 39 | 40 | 41 | 42 | 43 | 44 | Qt::Horizontal 45 | 46 | 47 | 48 | 40 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | false 59 | 60 | 61 | 62 | ArrowCursor 63 | 64 | 65 | 66 | 67 | 68 | false 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | 78 | 79 | 80 | 81 | Qt::Horizontal 82 | 83 | 84 | 85 | 40 86 | 20 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Qt::Vertical 97 | 98 | 99 | 100 | 20 101 | 40 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Please. type your password: 110 | 111 | 112 | 113 | 114 | 115 | 116 | QLineEdit::Password 117 | 118 | 119 | 120 | 121 | 122 | 123 | Qt::Vertical 124 | 125 | 126 | 127 | 20 128 | 40 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 10 139 | 140 | 141 | 10 142 | 143 | 144 | 145 | 146 | Qt::Horizontal 147 | 148 | 149 | 150 | 40 151 | 20 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Ok 160 | 161 | 162 | 163 | 164 | 165 | 166 | Cancel 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/ProblemsFoundWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SnapshotWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 420 10 | 285 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 21 | 10 22 | 23 | 24 | 10 25 | 26 | 27 | 10 28 | 29 | 30 | 10 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Qt::AlignCenter 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::Horizontal 48 | 49 | 50 | 51 | 40 52 | 20 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | OK 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/SnapshotWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SnapshotWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 640 10 | 300 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 20 | 21 | 10 22 | 23 | 24 | 20 25 | 26 | 27 | 10 28 | 29 | 30 | 10 31 | 32 | 33 | 34 | 35 | Take snapshots 36 | 37 | 38 | 39 | 40 | 41 | 42 | Qt::Vertical 43 | 44 | 45 | QSizePolicy::Fixed 46 | 47 | 48 | 49 | 20 50 | 10 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | All s&ubvolumes 59 | 60 | 61 | 62 | 63 | 64 | 65 | One speci&fic subvolume 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Qt::Horizontal 75 | 76 | 77 | QSizePolicy::Fixed 78 | 79 | 80 | 81 | 40 82 | 20 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | Qt::Vertical 96 | 97 | 98 | 99 | 20 100 | 40 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Qt::Horizontal 111 | 112 | 113 | 114 | 40 115 | 20 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | OK 124 | 125 | 126 | 127 | 128 | 129 | 130 | Cancel 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/SubvolumeWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SubvolumeWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 640 10 | 300 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 10 20 | 21 | 22 | 10 23 | 24 | 25 | 26 | 27 | Qt::Vertical 28 | 29 | 30 | QSizePolicy::Fixed 31 | 32 | 33 | 34 | 20 35 | 10 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Add subvolume 44 | 45 | 46 | 47 | 48 | 49 | 50 | Qt::Vertical 51 | 52 | 53 | QSizePolicy::Fixed 54 | 55 | 56 | 57 | 20 58 | 10 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Subvolume to manage 71 | 72 | 73 | 74 | 75 | 76 | 77 | Select the subvolume you want to create the snapshot, f.i. / if you have a subvolume created for root 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | Selet the path where the snapshot will be stored. it is advisable to store the snapshot in a different subvolume 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Path where the snapshot will be stored 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Snapshot prefix 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | Don't remove any snapshot 127 | 128 | 129 | 130 | 131 | 132 | 133 | Qt::Horizontal 134 | 135 | 136 | 137 | 40 138 | 20 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | Snapshot to keep 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Qt::Vertical 159 | 160 | 161 | 162 | 20 163 | 40 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | Qt::Horizontal 174 | 175 | 176 | 177 | 40 178 | 20 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | OK 187 | 188 | 189 | 190 | 191 | 192 | 193 | Cancel 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/ui/UpdatesWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | UpdatesWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 767 10 | 442 11 | 12 | 13 | 14 | ButterManager 15 | 16 | 17 | 18 | 19 | 10 20 | 21 | 22 | 10 23 | 24 | 25 | 26 | 27 | 10 28 | 29 | 30 | 10 31 | 32 | 33 | 10 34 | 35 | 36 | 10 37 | 38 | 39 | 40 | 41 | 42 | 43 | Packages to update 44 | 45 | 46 | 47 | 48 | 49 | 50 | Qt::Horizontal 51 | 52 | 53 | 54 | 40 55 | 20 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Upgrade the system creating a previous snapshot and removing old snapshots according to configuration 64 | 65 | 66 | Upgrade with snapshots 67 | 68 | 69 | 70 | 71 | 72 | 73 | Upgrade the system without creating snapshots 74 | 75 | 76 | Upgrade without snapshots 77 | 78 | 79 | 80 | 81 | 82 | 83 | Cancel 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/util/__init__.py: -------------------------------------------------------------------------------- 1 | #Copyright 2018-2019 Eloy García Almadén 2 | # 3 | # This file is part of buttermanager. 4 | # 5 | # This program is free software: you can redistribute it and / or modify it 6 | # under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation, version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . -------------------------------------------------------------------------------- /buttermanager/buttermanager/util/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018-2019 Eloy García Almadén 4 | # 5 | # This file is part of buttermanager. 6 | # 7 | # This program is free software: you can redistribute it and / or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation, version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """This module gathers all the global attributes, methods and classes needed for application settings. 20 | 21 | """ 22 | from . import utils 23 | from .. import filesystem 24 | import os 25 | import yaml 26 | 27 | # Global module constants 28 | CONF_FILE = "buttermanager.yaml" 29 | VERSION = "2.5.2" 30 | 31 | # Global module attributes 32 | # Application version 33 | application_version = "" 34 | # Application name 35 | application_name = "" 36 | # Application work directory 37 | application_path = "" 38 | # Logs directory 39 | logs_path = "" 40 | # User's password 41 | user_password = "" 42 | # Linux distribution 43 | user_os = "" 44 | # Do user want to remove snapshots? 0=False 1=True 45 | remove_snapshots = 1 46 | # Do user want to upgrade snap packages? 0=False 1=True 47 | snap_packages = 1 48 | # Do user want to upgrade flatpak packages? 0=False 1=True 49 | flatpak_packages = 1 50 | # Do user want to upgrade packages from AUR? 0=False 1=True 51 | aur_repository = 1 52 | # Do user want to check for updates at startup? 0=False 1=True 53 | check_at_startup = 1 54 | # The path of the root snapshot that must be within /etc/fstab as / mount point 55 | path_to_consolidate_root_snapshot = "0" 56 | # Do user want to boot the system from GRUB using snapshots? 0=False 1=True 57 | grub_btrfs = 0 58 | # Do user want to save log automatically after upgrading system? 0=False 1=True 59 | save_log = 1 60 | # Subvolumes managed by the application 61 | # It will be a dictionary: 62 | # Key=origin path for the subvolume; Value=Subvolume object 63 | subvolumes = {} 64 | # Properties Manager 65 | properties_manager = None 66 | # Base fot size for all the UI elements (it is dynamically calculated during application start up) 67 | base_font_size = 10 68 | # Fot size increment defined by the user 69 | font_size_increment = 0 70 | # Location of the UI layouts directory 71 | ui_dir = "" 72 | # Location of the images directory 73 | images_dir = "" 74 | # Desktop environment 75 | desktop_environment = "" 76 | # Installation type 77 | installation_type = "" 78 | 79 | 80 | class PropertiesManager: 81 | """Manages the user properties for the application. 82 | 83 | If no user settings are loaded yet, then the yaml file ~/.buttermanager/buttermanager.yaml will be 84 | read and parsed in self.__user_settings dictionary. 85 | 86 | The keys of the dictionary will be the properties name in the yaml file. The values will be the values 87 | in the yaml file for every property. 88 | """ 89 | # Constructor 90 | def __init__(self): 91 | # Logger 92 | self.__logger = utils.Logger(self.__class__.__name__).get() 93 | # Setting global values related to the application 94 | self.__conf_file_path = '{application_path}/{conf_file}'.format(application_path=application_path, 95 | conf_file=CONF_FILE) 96 | self.__user_settings = [] 97 | # Reading configuration file (buttermanager.yaml file within ~/.buttermanager directory) 98 | if os.path.exists(self.__conf_file_path): 99 | conf_file = open(self.__conf_file_path) 100 | self.__user_settings = yaml.load(conf_file, Loader=yaml.FullLoader) 101 | conf_file.close() 102 | else: 103 | self.__logger.info("Warning: There is no configuration file...") 104 | 105 | def get_property(self, property): 106 | """Gets the value of a property. 107 | 108 | Arguments: 109 | property (string): Property to get its value. 110 | 111 | Returns: 112 | string: The value of the property. 0 if the property was not found. 113 | """ 114 | value = "" 115 | if len(self.__user_settings) > 0: 116 | value = self.__user_settings.get(property, 0) 117 | return value 118 | 119 | def set_property(self, property, value): 120 | """Sets the value of a property. 121 | 122 | Arguments: 123 | property (string): Property to set its value. 124 | value (string): Value to be set. 125 | """ 126 | self.__logger.info("Setting property {property} with value {value}".format(property=property, value=value)) 127 | # Setting property in memory 128 | self.__user_settings[property] = value 129 | 130 | # Setting property in buttermanager.yaml file 131 | self.__store_configuration() 132 | 133 | def remove_property(self, property): 134 | """Removes s property from properties file. 135 | 136 | Arguments: 137 | property (string): Property to be removed. 138 | """ 139 | self.__logger.info("Removing property {property}".format(property=property)) 140 | self.__user_settings.pop(property) 141 | 142 | # Storing buttermanager.yaml file 143 | self.__store_configuration() 144 | 145 | def set_subvolume(self, subvolume_selected, snapshot_where, snapshot_prefix, snapshots_to_keep): 146 | """Sets the value of a subvolume. 147 | 148 | If snapshot_where = None and snapshot_prefix = None, then the subvolume 149 | will be removed 150 | 151 | Arguments: 152 | subvolume_selected (string): Subvolume selected to be set with the new values. 153 | snapshot_where (string): Path where the snapshot is going to be stored. None if the subvolume is removed 154 | snapshot_prefix (string): Prefix used to store the snapshot of a specific subvolume. None if the subvolume 155 | is removed 156 | snapshots_to_keep (int): Number of the snapshots to keep in the filesystem for this subvolume 157 | """ 158 | self.__logger.info("Setting subvolume {subvolume} with new values: where {where}; prefix {prefix}; " 159 | "snapshots to keep {snapshots_to_keep}".format(subvolume=subvolume_selected, 160 | where=snapshot_where, 161 | prefix=snapshot_prefix, 162 | snapshots_to_keep=snapshots_to_keep)) 163 | if subvolume_selected in subvolumes: 164 | if not snapshot_where and not snapshot_prefix: 165 | # The subvolume has to be removed from memory 166 | subvolumes.pop(subvolume_selected, 'None') 167 | else: 168 | # Modifying subvolume in memory 169 | subvolumes[subvolume_selected].subvolume_dest = snapshot_where if snapshot_where[-1] == '/' else snapshot_where + '/' 170 | subvolumes[subvolume_selected].snapshot_name = snapshot_prefix 171 | subvolumes[subvolume_selected].snapshots_to_keep = snapshots_to_keep 172 | else: 173 | subvolumes[subvolume_selected] = filesystem.snapshot.Subvolume(subvolume_selected, 174 | snapshot_where, 175 | snapshot_prefix, 176 | snapshots_to_keep) 177 | subvolumes_orig = "" 178 | subvolumes_dest = "" 179 | subvolumes_prefix = "" 180 | subvolumes_snapshost_to_keep = "" 181 | index = 0 182 | 183 | for subvolume in subvolumes: 184 | subvolumes_orig += subvolumes[subvolume].subvolume_origin 185 | subvolumes_dest += subvolumes[subvolume].subvolume_dest 186 | subvolumes_prefix += subvolumes[subvolume].snapshot_name 187 | subvolumes_snapshost_to_keep += str(subvolumes[subvolume].snapshots_to_keep) 188 | if index + 1 < len(subvolumes): 189 | subvolumes_orig += "|" 190 | subvolumes_dest += "|" 191 | subvolumes_prefix += "|" 192 | subvolumes_snapshost_to_keep += "|" 193 | index += 1 194 | 195 | self.__user_settings['subvolumes_orig'] = subvolumes_orig 196 | self.__user_settings['subvolumes_dest'] = subvolumes_dest 197 | self.__user_settings['subvolumes_prefix'] = subvolumes_prefix 198 | self.__user_settings['subvolumes_snapshots_to_keep'] = subvolumes_snapshost_to_keep 199 | 200 | # Setting property in buttermanager.yaml file 201 | self.__store_configuration() 202 | 203 | def __store_configuration(self): 204 | """Stores configuration file. 205 | 206 | """ 207 | # Setting property in buttermanager.yaml file 208 | if os.path.exists(self.__conf_file_path): 209 | conf_file = open(self.__conf_file_path, 'w') 210 | yaml.dump(self.__user_settings, conf_file) 211 | conf_file.close() 212 | else: 213 | self.__logger.info("Warning: There is no configuration file...") 214 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/util/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018-2019 Eloy García Almadén 4 | # 5 | # This file is part of buttermanager. 6 | # 7 | # This program is free software: you can redistribute it and / or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation, version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """This module gathers all the utils and tools for buttermanager application. 20 | 21 | """ 22 | from . import settings 23 | from ..exception import exception 24 | from ..filesystem import snapshot 25 | from ..window import windows 26 | from PyQt5.QtWidgets import QFileDialog 27 | from tkinter import Tk 28 | from tkinter.filedialog import askdirectory 29 | import logging 30 | import logging.handlers 31 | import os 32 | import pathlib 33 | import shutil 34 | import subprocess 35 | import sys 36 | import urllib.request 37 | import urllib.error 38 | import yaml 39 | 40 | # Constants 41 | GB = "GiB" # Gigabytes 42 | MB = "MiB" # Megabytes 43 | KB = "KiB" # Kilobytes 44 | B = "B" # Bytes 45 | BYTE_SIZE = 1024 46 | ARCH_PM = "pacman" 47 | DEBIAN_PM = "apt" 48 | SUSE_PM = "zypper" 49 | FEDORA_PM = "dnf" 50 | SNAP_PM = "snap" 51 | OS_ARCH = "ARCH" 52 | OS_DEBIAN = "DEBIAN" 53 | OS_SUSE = "SUSE" 54 | OS_FEDORA = "FEDORA" 55 | VERSION_URL = "https://raw.githubusercontent.com/egara/buttermanager/master/version.txt" 56 | 57 | 58 | class ConfigManager: 59 | """Manages the configuration. 60 | 61 | """ 62 | # Constants 63 | APP_NAME = "buttermanager" 64 | LOGS_DIR = "logs" 65 | 66 | # Constructor 67 | def __init__(self): 68 | # Setting global values related to the application 69 | settings.application_name = self.APP_NAME 70 | application_directory = ".{name}".format(name=settings.application_name) 71 | settings.application_path = os.path.join(str(pathlib.Path.home()), application_directory) 72 | settings.logs_path = os.path.join(settings.application_path, self.LOGS_DIR) 73 | 74 | # Creating application's directory if it is needed 75 | if not os.path.exists(settings.application_path): 76 | # Application directory does not exist. Creating directory... 77 | os.makedirs(settings.application_path) 78 | 79 | # Creating buttermanager.yaml file with default values 80 | config_file_as_dictionary = ''' 81 | aur_repository: 0 82 | check_at_startup: 0 83 | snap_packages: 0 84 | flatpak_packages: 0 85 | save_log: 1 86 | grub_btrfs: 0 87 | path_to_consolidate_root_snapshot: 0 88 | subvolumes_dest: 89 | subvolumes_orig: 90 | subvolumes_prefix: 91 | subvolumes_snapshots_to_keep: 92 | font_size_increment: 0 93 | ''' 94 | config_file_dictionary = yaml.safe_load(config_file_as_dictionary) 95 | conf_file_path = '{application_path}/{conf_file}'.format(application_path=settings.application_path, 96 | conf_file=settings.CONF_FILE) 97 | conf_file = open(conf_file_path, 'w') 98 | yaml.dump(config_file_dictionary, conf_file) 99 | conf_file.close() 100 | 101 | # Sanitazing config file. All null values will be replaced by blank spaces 102 | with open(conf_file_path, 'r') as file: 103 | config_file_data = file.read() 104 | config_file_data = config_file_data.replace('null', '') 105 | 106 | with open(conf_file_path, 'w') as file: 107 | file.write(config_file_data) 108 | 109 | # Creating logs directory if it doesn't exist 110 | if not os.path.exists(settings.logs_path): 111 | os.makedirs(settings.logs_path) 112 | 113 | # Logger 114 | self.__logger = Logger(self.__class__.__name__).get() 115 | 116 | def configure(self): 117 | """Configures the application. 118 | 119 | """ 120 | # Version 121 | settings.application_version = settings.VERSION 122 | 123 | # Checking OS 124 | if exist_program(SUSE_PM): 125 | settings.user_os = OS_SUSE 126 | elif exist_program(DEBIAN_PM): 127 | settings.user_os = OS_DEBIAN 128 | elif exist_program(ARCH_PM): 129 | settings.user_os = OS_ARCH 130 | elif exist_program(FEDORA_PM): 131 | settings.user_os = OS_FEDORA 132 | self.__logger.info("Checking OS. {os} found".format(os=settings.user_os)) 133 | 134 | # Checking Desktop Environment 135 | settings.desktop_environment = get_desktop_environment() 136 | self.__logger.info("Checking Desktop Environment. {de} found".format(de=settings.desktop_environment)) 137 | 138 | # Checking Installation Type 139 | if not os.path.exists("/opt/buttermanager/buttermanager"): 140 | settings.installation_type = "native" 141 | else: 142 | settings.installation_type = "venv" 143 | self.__logger.info("Installation type: {installation}".format(installation=settings.installation_type)) 144 | 145 | # Creating a properties manager to manage all the application properties 146 | self.__logger.info("Creating PropertiesManager...") 147 | settings.properties_manager = settings.PropertiesManager() 148 | 149 | # Triggering migration process 150 | self.migrate_properties() 151 | 152 | # Retrieving configuration... 153 | self.__logger.info("Retrieving user's configuration from buttermanager.yaml file and loading it in memory...") 154 | 155 | # Do the user want to update snap packages during the upgrading process 156 | settings.snap_packages = int(settings.properties_manager.get_property('snap_packages')) 157 | 158 | # Do the user want to update flatpak packages during the upgrading process 159 | settings.flatpak_packages = int(settings.properties_manager.get_property('flatpak_packages')) 160 | 161 | # Do the user want to update AUR packages during the upgrading process 162 | settings.aur_repository = int(settings.properties_manager.get_property('aur_repository')) 163 | 164 | # Do the user want to check for updates at startup 165 | settings.check_at_startup = int(settings.properties_manager.get_property('check_at_startup')) 166 | 167 | # Do user want to boot the system from GRUB using snapshots 168 | settings.grub_btrfs = int(settings.properties_manager.get_property('grub_btrfs')) 169 | 170 | # The path of the root snapshot that must be within /etc/fstab as / mount point 171 | # It will be 0 if this property is not defined yet or it is empty 172 | settings.path_to_consolidate_root_snapshot = settings.properties_manager.\ 173 | get_property('path_to_consolidate_root_snapshot') 174 | 175 | # Do the user want to save logs automatically 176 | settings.save_log = int(settings.properties_manager.get_property('save_log')) 177 | 178 | # Font size increment defined by the user 179 | settings.font_size_increment = int(settings.properties_manager.get_property('font_size_increment')) 180 | 181 | # Subvolumes to manage 182 | subvolumes_list = get_subvolumes() 183 | subvolumes = {} 184 | for subvolume in subvolumes_list: 185 | subvolumes[subvolume.subvolume_origin] = subvolume 186 | 187 | settings.subvolumes = subvolumes 188 | 189 | def migrate_properties(self): 190 | """Migrates buttermanager.yaml properties file from one version to another if necessary. 191 | 192 | """ 193 | # Checking if it is necessary to do some migrations to newer versions 194 | 195 | # ########################################## 196 | # BEGIN Version 2.3 or older -> 2.4 or newer 197 | # ########################################## 198 | # Number of snapshots per subvolume have been introduced in version 2.4 199 | # Filling this property in case the user comes from version 2.3 200 | snapshots_to_keep = int(settings.properties_manager.get_property('snapshots_to_keep')) 201 | remove_snapshots = settings.properties_manager.get_property('remove_snapshots') 202 | if snapshots_to_keep != 0: 203 | # snapshots_to_keep property is still in buttermanager.yaml 204 | self.__logger.info("Migrating from version 2.3 or older to version 2.4 or newer. Please wait...") 205 | self.__logger.info("snapshots_to_keep property and remove_snapshots will be removed from buttermanager.yaml" 206 | " configuration file and every subvolume defined will have their own properties") 207 | subvolumes_orig_raw = settings.properties_manager.get_property('subvolumes_orig') 208 | subvolumes_snapshots_to_keep_raw = "" 209 | if subvolumes_orig_raw is not None and subvolumes_orig_raw != "": 210 | subvolumes_orig = subvolumes_orig_raw.split("|") 211 | for index, subvolume_orig in enumerate(subvolumes_orig): 212 | if remove_snapshots == 0: 213 | # From this moment, when snapshots_to_keep is -1, then the user havve decided not to 214 | # delete any snapshot 215 | snapshots_to_keep = -1 216 | subvolumes_snapshots_to_keep_raw += str(snapshots_to_keep) 217 | if index + 1 < len(subvolumes_orig): 218 | subvolumes_snapshots_to_keep_raw += "|" 219 | # Adding the new property 220 | settings.properties_manager.set_property('subvolumes_snapshots_to_keep', subvolumes_snapshots_to_keep_raw) 221 | 222 | # Removing old snapshots_to_keep and remove_snapshots properties 223 | settings.properties_manager.remove_property('snapshots_to_keep') 224 | settings.properties_manager.remove_property('remove_snapshots') 225 | 226 | # ######################################## 227 | # END Version 2.3 or older -> 2.4 or newer 228 | # ######################################## 229 | 230 | self.__logger.info("Migration process has finished successfully!") 231 | 232 | 233 | class Logger(object): 234 | """Creates the logs of the application. 235 | 236 | """ 237 | def __init__(self, class_name): 238 | name = os.path.join(settings.application_path, "buttermanager.log") 239 | logger = logging.getLogger(class_name) 240 | logger.setLevel(logging.DEBUG) 241 | 242 | # Add the log message handler to the logger 243 | handler = logging.handlers.RotatingFileHandler(name, maxBytes=1048576, backupCount=5) 244 | formatter = logging.Formatter('%(asctime)s %(levelname)s:%(name)s. %(message)s') 245 | handler.setFormatter(formatter) 246 | logger.addHandler(handler) 247 | self.__logger = logger 248 | 249 | def get(self): 250 | return self.__logger 251 | 252 | 253 | class VersionChecker: 254 | """Checks if there is a newest version of ButterManager available. 255 | 256 | """ 257 | def __init__(self, parent_window): 258 | # Logger 259 | self.__logger = Logger(self.__class__.__name__).get() 260 | self.__logger.info("Checking for a new version of ButterManager. Please wait...") 261 | self.__version_url = VERSION_URL 262 | self.__parent_window = parent_window 263 | 264 | def check_version(self): 265 | """Checks if there is a newest version of ButterManager available. 266 | 267 | """ 268 | try: 269 | # Retrieving the last version from GitHub 270 | response = urllib.request.urlopen(self.__version_url) 271 | last_version = response.read().decode(response.headers.get_content_charset()).strip() 272 | 273 | except urllib.error.HTTPError as exception: 274 | self.__logger.error("Error checking new versions of ButterManager. Reason: " + str(exception.reason)) 275 | except urllib.error.URLError as exception: 276 | self.__logger.error("Error checking new versions of ButterManager. Reason: " + str(exception.reason)) 277 | else: 278 | self.__logger.info("Last version is " + last_version + " and current version is " + 279 | settings.application_version) 280 | 281 | if last_version != settings.application_version: 282 | if settings.user_os == OS_ARCH: 283 | info_window = windows.GeneralInfoWindow(self.__parent_window, "New version " + 284 | last_version + " is available. Update ButterManager " 285 | "via AUR") 286 | else: 287 | info_window = windows.GeneralInfoWindow(self.__parent_window, "New version " + 288 | last_version + " is available. Check the repository " 289 | "\nof the project " 290 | "(https://github.com/egara/buttermanager)\n " 291 | "to get the latest code") 292 | 293 | info_window.show() 294 | 295 | 296 | # Module's methods 297 | def execute_command(command, console=False, root=False): 298 | """Executes a shell command. 299 | 300 | Arguments: 301 | command (str): Command to be executed. 302 | console (boolean): The command output needs to be redirected to the console. 303 | root (boolean): The command is only accesible by root user 304 | 305 | Returns: 306 | str: Command line output encoded in UTF-8. 307 | """ 308 | 309 | # Checking if the program executed by the command is installed in the system 310 | program = command.split() 311 | single_command = program[0] 312 | if "sudo" in program: 313 | sudo_position = program.index("sudo") 314 | single_command = program[sudo_position + 2] 315 | if exist_program(single_command, root=root): 316 | echo = subprocess.Popen(['echo', settings.user_password], stdout=subprocess.PIPE) 317 | # run method receives a list, so it is necessary to convert command string into a list using split 318 | result = subprocess.Popen(command.split(), stdin=echo.stdout, stdout=subprocess.PIPE) 319 | 320 | if not console: 321 | # The whole output will be returned 322 | # result is Bytes type, so it is needed to decode Unicode string using UTF-8 323 | commandline_output = result.stdout.read().decode('utf-8') 324 | else: 325 | # The output will be written in stdout in real time 326 | # It is good for operations that need to display the output 327 | # in the GUI terminal of the application in real time 328 | for line in iter(result.stdout.readline, b''): 329 | sys.stdout.write(line.decode('utf-8')) 330 | commandline_output = None 331 | 332 | return commandline_output 333 | else: 334 | # Logger 335 | logger = Logger(sys.modules['__main__'].__file__).get() 336 | logger.info(single_command + " program does not exist in the system") 337 | raise exception.NoCommandFound() 338 | 339 | 340 | def get_percentage(total, parcial): 341 | """Calculates the percentage between total amount and parcial amount. 342 | 343 | Arguments: 344 | total (str): Total amount. It should be specified the unit, f.i.: 30.00GiB 345 | parcial (str): Parcial amount. It should be specified the unit, f.i.: 3.00GiB 346 | Returns: 347 | int: Percentage between total and parcial, f.i.: 10 (3.00GiB is 10% of 30.00GiB). 348 | 349 | >>> get_percentage("30.00GiB", "3.00GiB") 350 | 10 351 | """ 352 | total_number_unit = get_number_unit(total) 353 | parcial_number_unit = get_number_unit(parcial) 354 | # All the operations will be done using Bytes unit as reference 355 | total_number = convert_to_bytes(total_number_unit) 356 | parcial_number = convert_to_bytes(parcial_number_unit) 357 | percentage = int((parcial_number * 100) / total_number) 358 | return percentage 359 | 360 | 361 | def get_number_unit(number_unit_string): 362 | """Gets the number and the unit present in a specific string. 363 | 364 | Arguments: 365 | number_unit_string (str): String consisting of amount and unit, f.i.: 30.00GiB 366 | 367 | Returns: 368 | dictionary (key=:obj:'str', value=:obj:'str' or obj:'int'): all the info. The keys of the dictionary will be: 369 | - total_size: Device size 370 | - total_allocated: Device allocated 371 | 372 | >>> get_number_unit("30.00GiB") 373 | ['number': 30.00, 'unit': 'GiB'] 374 | """ 375 | number_unit = {'number': 0.0, 'unit': 'GiB'} 376 | number_unit_string_list = number_unit_string.split('.') 377 | number = float("{integer}.{decimal}".format(integer=number_unit_string_list[0].strip(), 378 | decimal=number_unit_string_list[1][0:2])) 379 | number_unit['number'] = number 380 | number_unit['unit'] = number_unit_string_list[1][2:] 381 | return number_unit 382 | 383 | 384 | def convert_to_bytes(number_unit): 385 | """Converts a number into a bytes depending on its unit. 386 | 387 | Arguments: 388 | number_unit (dictionary): Number and unit to convert 389 | 390 | Returns: 391 | float: Number in bytes 392 | 393 | >>> number_unit = {'number': 30.00, 'unit': 'GiB'} 394 | >>> convert_to_bytes(number_unit) 395 | 32212254720 396 | """ 397 | factor = 1 398 | if number_unit['unit'] == GB: 399 | factor = factor * BYTE_SIZE * BYTE_SIZE * BYTE_SIZE 400 | elif number_unit['unit'] == MB: 401 | factor = factor * BYTE_SIZE * BYTE_SIZE 402 | elif number_unit['unit'] == KB: 403 | factor = factor * BYTE_SIZE 404 | elif number_unit['unit'] == B: 405 | factor = factor 406 | 407 | return number_unit['number'] * factor 408 | 409 | 410 | def exist_program(program, root=False): 411 | """Checks if a program is installed on the system. 412 | 413 | Some problems have been detected in distributions like SUSE and OpenSUSE using 414 | shutil.which function. There are some commands, like btrfs, that are only found 415 | if sudo which is used instead of simply which from current user. 416 | Because of that, root variable has been declared above. By default, root value 417 | will be False, i.e. for those commands which are discoverable simply by using 418 | which without sudo. 419 | 420 | Arguments: 421 | program (string): Program to check 422 | root (boolean): The program to be checked is only usable by root user 423 | 424 | Returns: 425 | bool: True if the program is installed, False otherwise 426 | 427 | >>> exist_program('ls') 428 | True 429 | """ 430 | if root: 431 | command = "sudo -S which " + program 432 | # Checking if the program executed by the command is installed in the system 433 | echo = subprocess.Popen(['echo', settings.user_password], stdout=subprocess.PIPE) 434 | # run method receives a list, so it is necessary to convert command string into a list using split 435 | result = subprocess.Popen(command.split(), stdin=echo.stdout, stdout=subprocess.PIPE) 436 | 437 | # The whole output will be returned 438 | # result is Bytes type, so it is needed to decode Unicode string using UTF-8 439 | commandline_output = result.stdout.read().decode('utf-8') 440 | exist = not commandline_output.startswith("which:") 441 | return exist 442 | else: 443 | path = shutil.which(program) 444 | return path is not None 445 | 446 | 447 | def get_subvolumes(): 448 | """Gets the subvolumes defined by the user in the properties file. 449 | 450 | Returns: 451 | list (:obj:`list` of :obj:`Subvolume`): subvolumes objects defined by the user. 452 | """ 453 | subvolumes = [] 454 | subvolumes_orig_raw = settings.properties_manager.get_property('subvolumes_orig') 455 | subvolumes_dest_raw = settings.properties_manager.get_property('subvolumes_dest') 456 | subvolumes_prefix_raw = settings.properties_manager.get_property('subvolumes_prefix') 457 | subvolumes_snapshots_to_keep_raw = settings.properties_manager.get_property('subvolumes_snapshots_to_keep') 458 | if subvolumes_orig_raw is not None and subvolumes_orig_raw != "": 459 | subvolumes_orig = subvolumes_orig_raw.split("|") 460 | subvolumes_dest = subvolumes_dest_raw.split("|") 461 | subvolumes_prefix = subvolumes_prefix_raw.split("|") 462 | subvolumes_snapshots_to_keep = subvolumes_snapshots_to_keep_raw.split("|") 463 | for index, subvolume_orig in enumerate(subvolumes_orig): 464 | subvolume = snapshot.Subvolume(subvolume_orig, subvolumes_dest[index], subvolumes_prefix[index], 465 | subvolumes_snapshots_to_keep[index]) 466 | subvolumes.append(subvolume) 467 | 468 | return subvolumes 469 | 470 | 471 | def scale_fonts(ui_elements): 472 | """Scales all the UI elements fonts in order to fit on the window. 473 | 474 | Arguments: 475 | ui_elements (list): UI elements to change the font 476 | """ 477 | font_size = settings.base_font_size + int(settings.properties_manager.get_property('font_size_increment')) 478 | # Changing the font for every UI element 479 | for label in ui_elements: 480 | font = label.font() 481 | font.setPointSize(font_size) 482 | label.setFont(font) 483 | 484 | 485 | def get_desktop_environment(): 486 | """Gets desktop environment. 487 | """ 488 | desktop_environment = 'generic' 489 | if os.environ.get('KDE_FULL_SESSION') == 'true': 490 | desktop_environment = 'kde' 491 | elif os.environ.get('GNOME_DESKTOP_SESSION_ID'): 492 | desktop_environment = 'gnome' 493 | else: 494 | try: 495 | info = subprocess.getoutput('xprop -root _DT_SAVE_MODE') 496 | if ' = "xfce4"' in info: 497 | desktop_environment = 'xfce' 498 | except (OSError, RuntimeError): 499 | pass 500 | return desktop_environment 501 | 502 | 503 | def open_file_browser_directory(parent_window): 504 | """Opens a file browser to select a directory. 505 | 506 | A bug has being detected in KDE Plasma native installatiom. When a native file browser is opened to 507 | select a directory, then the application crashes. This doesn't happen in GNOME for example. So a 508 | fallback has had to be implemented for this case, suing TKinter. 509 | 510 | Arguments: 511 | parent_window: Parent window 512 | 513 | Returns: 514 | str: Path of the directory selected 515 | """ 516 | 517 | # Creating a QFileDialog or Tkinter file browser to select the directory 518 | # Only directories will be allowed 519 | selected_path = "" 520 | if settings.desktop_environment == 'kde' and settings.installation_type == 'native': 521 | Tk().withdraw() 522 | filename = askdirectory() 523 | 524 | if filename: 525 | selected_path = filename 526 | else: 527 | file_dialog = QFileDialog(parent_window) 528 | file_dialog.setFileMode(QFileDialog.Directory) 529 | file_dialog.setOption(QFileDialog.ShowDirsOnly, True) 530 | file_dialog.setOption(QFileDialog.DontUseNativeDialog) 531 | 532 | if file_dialog.exec_(): 533 | selected_path = file_dialog.selectedFiles()[0] 534 | 535 | return selected_path 536 | -------------------------------------------------------------------------------- /buttermanager/buttermanager/window/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/buttermanager/buttermanager/window/__init__.py -------------------------------------------------------------------------------- /buttermanager/main.py: -------------------------------------------------------------------------------- 1 | from buttermanager.buttermanager import PasswordWindow 2 | from PyQt5.QtWidgets import QApplication 3 | import sys 4 | 5 | 6 | def main(): 7 | """Main wrapper for starting the program 8 | 9 | """ 10 | # Creating application instance 11 | application = QApplication(sys.argv) 12 | # Creating main window instance 13 | PasswordWindow(None) 14 | # Launching the application 15 | application.exec_() 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | This is the documentation related to [Buttermanager](https://github.com/egara/buttermanager) GUI tool for easily management of BTRFS snapshots and system upgrades. 3 | 4 | ## BTRFS filesystem initial layout 5 | For the purpose of this documentation, we are going to suppose that we have installed [Manjaro]() using a manual partitioning with this requirements: 6 | 7 | - Only one disk: **sda** 8 | - Only one partition in the disk: **sda1** 9 | - *sda1* has been formatted using **btrfs** 10 | - The mount point selected for this partition is **/** 11 | 12 | After the installation of the operating system, the subvolumes automatically created are two (**@** and **@home**) as you can see in the screenshot below: 13 | 14 | drawing 15 | 16 | So the layout is something like this 17 | 18 | ``` 19 | Main Volume (ID 5) 20 | | 21 | |--- @ (Subvolume ID 257) 22 | | 23 | |--- @home (Subvolume ID 258) 24 | ``` 25 | 26 | **Important Note: If you are installing Arch from scratch or want to reshape your default BTRFS layout, you can check out this tips [https://github.com/egara/arch-btrfs-installation](https://github.com/egara/arch-btrfs-installation).** 27 | 28 | ## Mounting main volume (ID 5) 29 | In order to manage in a proper way all the snapshots created and make things easier if you want to rollback your system to a previous snapshot, we are going to mount the main BTRFS volume in **/mnt/defvol** directory. For this, you first has to create this directory. 30 | 31 | drawing 32 | 33 | Include this new mounting point in **/etc/fstab** just to automount it when the system boots. Please, change the UID to the appropriate one or use labels if it is your case. 34 | 35 | drawing 36 | 37 | Once **fstab** is changed you can type 38 | 39 | sudo mount -a 40 | 41 | and go to **/mnt/defvol**. You should see something like this: 42 | 43 | drawing 44 | 45 | Create a new subvolume called **snapshots** at top level (as you can see above) using the command: 46 | 47 | sudo btrfs subvolume create snapshots 48 | 49 | The final layout will be: 50 | 51 | ``` 52 | Main Volume (ID 5) 53 | | 54 | |--- @ (Subvolume ID 257) 55 | | 56 | |--- @home (Subvolume ID 258) 57 | | 58 | |--- snapshots (Subvolume ID 271) 59 | ``` 60 | 61 | drawing 62 | 63 | ## Setting up Buttermanager 64 | Yes, finally we are going to configure **Buttermanager**!. The first time you open the application, you will be warned because no subvolumes to create snapshots has been defined yet. You could use the application, but if you upgrade your system, **Buttermanager** won't create any snapshot. 65 | 66 | drawing 67 | 68 | Go to **Settings** tab and click on **Add subvolume** button. 69 | 70 | drawing 71 | 72 | Now, using the layout defined in this example, we are going to configure two subvolumes to create snapshots of **root** and **home**. 73 | 74 | ### Subvolume 1 (root) 75 | If you want **Buttermanager** creates a snapshot of your **root** partition everytime it upgrades the system, then you should fill the **Add a subvolume** window like this: 76 | 77 | - *Subvolume to manage*: **/mnt/defvol/@** 78 | - *Path where the snapshot will be stored*: **/mnt/defvol/snapshots** 79 | - *Snapshot prefix*: **root** 80 | 81 | >Please, use always different prefixes for different subvolumes (prefixes shouldn't even include words contained in other prefixes, i.e. in this example, you should never include root word in other prefixes). 82 | 83 | drawing 84 | 85 | This way, everytime **Buttermanager** upgrades the system, it will automatically create a snapshot of the **root** mounted subvolume called **root-[date]-[number]** within **/mnt/defvol/snapshots/** directory. This snapshot will be **read only** by default. 86 | 87 | ### Subvolume 2 (/home) 88 | If you want **Buttermanager** creates a snapshot of your **home** partition everytime it upgrades the system, then you should fill the **Add a subvolume** window like this: 89 | 90 | - *Subvolume to manage*: **/mnt/defvol/@home** 91 | - *Path where the snapshot will be stored*: **/mnt/defvol/snapshots** 92 | - *Snapshot prefix*: **home** 93 | 94 | drawing 95 | 96 | This way, everytime **Buttermanager** upgrades the system, it will automatically create a snapshot of the **home** mounted subvolume called **home-[date]-[number]** within **/mnt/defvol/snapshots/** directory. This snapshot will be **read only** by default. 97 | 98 | ## Integrating Buttermanager with GRUB 99 | Since version **1.9**, Buttermanager can be integrated with GRUB using the awesome package **grub-btrfs**. If you have installed Buttermanager via your package manager, this depency is installed automatically, if don't, please install it manually. When the integration is enabled, all the root snapshots created since then, will be bootable directly from the GRUB menu. 100 | 101 | To enable this feature, go to **Settings** and check **Boot the system from GRUB using snapshots**. 102 | 103 | drawing 104 | 105 | When this option is checked, **all the snapshots created will have read and write permissions** and the **/etc/fstab** file within the snapshot created will be modified in order to let GRUB to boot from it. 106 | 107 | ### Consolidating the snapshot booted 108 | Buttermanager cannot properly work from a snapshot selected from the GRUB menu. Because of this, when you run Buttermanager again in this situation, you will see a message like this. 109 | 110 | drawing 111 | 112 | If you want to consolidate the current state of your filesystem (the snapshot in which you have booted) reverting all the changes done since then, then click button **Ok**. 113 | 114 | If you don't want to consolidate this snapshot and you want to boot your system using the default root, then restart your computer and don't select any snapshot. 115 | 116 | >After consolidating the current snapshot and rebooting the system, run Buttermanager again and click on button **Regenerate GRUB** in order to rebuild GRUB menu with all the snapshots present in the filesystem. 117 | 118 | drawing 119 | 120 | ### Common problems 121 | The integration of Buttermanager with GRUB can only be done if there is not any subvolume defined inside the root file system. As an example, take the case of the automatic installation using BTRFS in some distros. 122 | 123 | By default, some distros (or more precisely systemd) create this BTRFS layout when the system is installed for the first time: 124 | 125 | ``` 126 | Main Volume (ID 5) 127 | | 128 | |--- @ 129 | | | 130 | | |--- @/var/lib/portables 131 | | |--- @/var/lib/machines 132 | | 133 | |--- @home 134 | ``` 135 | 136 | You can see this layout in EndevourOS (MATE online installation) for example: 137 | 138 | drawing 139 | 140 | In this case, if you boot the system using an alternate snapshot, run Buttermanager and try to consolidate this snapshot as the default, you will receive this message: 141 | 142 | drawing 143 | 144 | If you want to fix this problem, first execute these two commands in a terminal (please, adjust this command to your needs. In this case, main volume with ID 5 is mounted on /defvol): 145 | 146 | drawing 147 | 148 | Those subvolumes (**@/var/lib/portables** and **@/var/lib/machines**) will be probably empty as you can see in the picture above. If this is the case, you can remove them without any problem. To do this, take a look at the commands you have to type in the picture below please, adjust this commands to your needs. In this case, main volume with ID 5 is mounted on /defvol): 149 | 150 | drawing 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /doc/screen-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-1.png -------------------------------------------------------------------------------- /doc/screen-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-10.png -------------------------------------------------------------------------------- /doc/screen-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-11.png -------------------------------------------------------------------------------- /doc/screen-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-12.png -------------------------------------------------------------------------------- /doc/screen-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-13.png -------------------------------------------------------------------------------- /doc/screen-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-14.png -------------------------------------------------------------------------------- /doc/screen-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-15.png -------------------------------------------------------------------------------- /doc/screen-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-16.png -------------------------------------------------------------------------------- /doc/screen-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-2.png -------------------------------------------------------------------------------- /doc/screen-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-3.png -------------------------------------------------------------------------------- /doc/screen-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-4.png -------------------------------------------------------------------------------- /doc/screen-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-5.png -------------------------------------------------------------------------------- /doc/screen-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-6.png -------------------------------------------------------------------------------- /doc/screen-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-7.png -------------------------------------------------------------------------------- /doc/screen-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-8.png -------------------------------------------------------------------------------- /doc/screen-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egara/buttermanager/5d83cce810a40bf94a57f072e921eb8215398c7f/doc/screen-9.png -------------------------------------------------------------------------------- /install/native_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for installing ButterManager 4 | # ----------------------------------- 5 | # 6 | # @author: Eloy García Almadén 7 | # @email: eloy.garcia.pca@gmail.com 8 | # ------------------------------------- 9 | 10 | 11 | # Displaying requirements 12 | echo "You are about to install ButterManager natively." 13 | echo "" 14 | echo "These packages MUST be installed before executing this script: 'python-setuptools' and 'tkinter'. The name of these packages can be different depending on the distro you are using. Example: On Arch -> 'python-setuptools' and 'tk'. On Fedora 'python3-setuptools' and 'python3-tkinter'. In addition, if you are on Ubuntu or derivative, 'libxcb-xinerama0' needs to be installed too." 15 | echo "" 16 | echo "Do you want to proceed with the installation? [y/n]" 17 | read installButterManager 18 | 19 | if [[ "$installButterManager" == "y" ]]; then 20 | # Installing ButterManager 21 | # Variables 22 | python_bin="python" 23 | 24 | # Getting python3 binary 25 | if hash python3 2>/dev/null; then 26 | python_bin="python3" 27 | fi 28 | 29 | # Removing old installations 30 | echo "Removing old installation..." 31 | sudo rm -rf /opt/buttermanager/ 32 | 33 | # Creating installation directory 34 | echo "Creating installation directory in /opt/buttermanager" 35 | sudo mkdir /opt/buttermanager/ 36 | sudo mkdir /opt/buttermanager/buttermanager 37 | sudo mkdir /opt/buttermanager/gui 38 | sudo chown ${USER:=$(/usr/bin/id -run)}:$USER -R /opt/buttermanager 39 | 40 | # Copying all the files needed 41 | echo "Copying all the files needed into /opt/buttermanager" 42 | cp -ar ../buttermanager/* /opt/buttermanager/buttermanager 43 | cp -ar ../setup.py /opt/buttermanager/ 44 | cp -ar ../README.md /opt/buttermanager 45 | cp -ar ../packaging/buttermanager.svg /opt/buttermanager/gui/ 46 | 47 | # Creating desktop launcher 48 | echo -e "Creating desktop launcher..." 49 | if [ ! -d "${HOME}/.local/share/applications/" ] 50 | then 51 | echo "Directory ${HOME}/.local/share/applications/ doesn't exist. Creating it to store ButterManager desktop launcher." 52 | mkdir -p ${HOME}/.local/share/applications/ 53 | fi 54 | cp ../packaging/buttermanager.desktop ${HOME}/.local/share/applications/ 55 | 56 | # Installing libraries and ButterManager natively 57 | echo "Installing libraries and ButterManager natively..." 58 | cd /opt/buttermanager/ 59 | sudo $python_bin setup.py install --record installed_files.txt 60 | echo "" 61 | echo "" 62 | echo '@@@@@@@@@@@@@@@@@@&&&&&&&&&&&&&&&&&&&@@@@@@@@@@@@@+ 63 | @@@@@@@@@@@@&&%%#########%%%%%#########%%&&@@@@@@@+ 64 | @@@@@@@&&&#((%&&#(/*****************/(%&&%((%&&@@@+ 65 | @@@@@@&%(#%#/*****************************/#%##%&@+ 66 | @@@@@&#(&(*****************/%%%%/************#%(%&+ 67 | @@@@&&##&/***************#%#****%%***********(&(%&+ 68 | @@@@@&&#(#%%%%%*******/%%/******#&******%%%%%#(%&&+ 69 | @@@@@@@&&&%%(#&*****(%(*******(%#*******&#(%%&&&@@+ 70 | @@@@@@@@@@&&(#&**/%%*,.****/%%/*********&##&&@@@@@+ 71 | @@@@@@@@@@&&((/(%#**.,.**(%%************&##&&@@@@@+ 72 | @@@@@@@@@@&&#%%(****.,,#%/**************&##&&@@@@@+ 73 | @@@@@@@@@@&&%/*******%%*****************&##&&@@@@@+ 74 | @@@@@@@&&%#(#%%(*/#%(*******************&##&&@@@@@+ 75 | @@@@@&%%((((#%&%%%**********************&##&&@@@@@+ 76 | @@&&%#(((#%&##&**********************%#*&##&&@@@@@+ 77 | &%%((((#&&&&(#&*******************//*%#*&##&&@@@@@+ 78 | %#((#%&@@@&&(#&*******************#%*%#*&##&&@@@@@+ 79 | @&%%%@@@@@&&(#&///////////////////#%/%#/&##&&@@@@@+ 80 | @@@@@@@@@@&&###############################&&@@@@@' 81 | echo "" 82 | echo "" 83 | echo "The installation has finished. Please, review the logs in order to see if everything was OK" 84 | echo "" 85 | echo "" 86 | echo "You should find a new icon and desktop launcher called ButterManager. You are good to go." 87 | else 88 | # Exit 89 | echo "Ok. Bye!" 90 | fi 91 | -------------------------------------------------------------------------------- /install/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for uninstalling ButterManager 4 | # ------------------------------------- 5 | # 6 | # @author: Eloy García Almadén 7 | # @email: eloy.garcia.pca@gmail.com 8 | # ------------------------------------- 9 | 10 | # Removing old installations 11 | echo "Removing old installation..." 12 | 13 | installed_files=/opt/buttermanager/installed_files.txt 14 | 15 | if test -f "$installed_files"; then 16 | echo "ButterManager was installed natively. Removing libraries..." 17 | cat $installed_files | xargs sudo rm -rf 18 | fi 19 | 20 | sudo rm -rf /opt/buttermanager/ 21 | rm ${HOME}/.local/share/applications/buttermanager.desktop 22 | 23 | echo "Uninstallation process has been successfully completed. You are good to go!" 24 | -------------------------------------------------------------------------------- /install/venv_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for installing ButterManager 4 | # ----------------------------------- 5 | # 6 | # @author: Eloy García Almadén 7 | # @email: eloy.garcia.pca@gmail.com 8 | # ------------------------------------- 9 | 10 | # Displaying requirements 11 | echo "You are about to install ButterManager in a virtual environment." 12 | echo "These packages MUST be installed before executing this script: 'python-setuptools', 'python-virtualenv' and 'tkinter'. The name of these packages can be different depending on the distro you are using. Example: On Arch -> 'python-setuptools', 'python-virtualenv' and 'tk'. On Fedora 'python3-setuptools', 'python3-virtualenv' and 'python3-tkinter'. In addition, if you are on Ubuntu or derivative, 'libxcb-xinerama0' needs to be installed too." 13 | echo "Do you want to proceed with the installation? [y/n]" 14 | read installButterManager 15 | 16 | if [[ "$installButterManager" == "y" ]]; then 17 | # Installing ButterManager 18 | # Variables 19 | python_bin="python" 20 | 21 | # Getting python3 binary 22 | if hash python3 2>/dev/null; then 23 | python_bin="python3" 24 | fi 25 | 26 | # Removing old installations 27 | echo "Removing old installation..." 28 | sudo rm -rf /opt/buttermanager/ 29 | 30 | # Creating installation directory 31 | echo "Creating installation directory in /opt/buttermanager" 32 | sudo mkdir /opt/buttermanager/ 33 | sudo mkdir /opt/buttermanager/buttermanager 34 | sudo mkdir /opt/buttermanager/gui 35 | sudo chown ${USER:=$(/usr/bin/id -run)}:$USER -R /opt/buttermanager 36 | 37 | # Copying all the files needed 38 | echo "Copying all the files needed into /opt/buttermanager" 39 | cp -ar ../buttermanager/* /opt/buttermanager/buttermanager 40 | cp -ar ../requirements.txt /opt/buttermanager/ 41 | cp -ar ../packaging/buttermanager.svg /opt/buttermanager/gui/ 42 | 43 | # Creating desktop launcher 44 | echo -e "Creating desktop launcher..." 45 | if [ ! -d "${HOME}/.local/share/applications/" ] 46 | then 47 | echo "Directory ${HOME}/.local/share/applications/ doesn't exist. Creating it to store ButterManager desktop launcher." 48 | mkdir -p ${HOME}/.local/share/applications/ 49 | fi 50 | cp ../packaging/buttermanager_venv.desktop ${HOME}/.local/share/applications/buttermanager.desktop 51 | 52 | # Creating virtual environment 53 | echo "Creating virtual environment..." 54 | cd /opt/buttermanager/ 55 | $python_bin -m venv env 56 | 57 | # Enabling virtual environment 58 | echo -e "Enabling virtual environment..." 59 | source env/bin/activate 60 | 61 | # Installing requirements 62 | echo -e "Installing all the required modules into the virtual environment. Please wait..." 63 | pip install --upgrade pip 64 | pip install -r requirements.txt 65 | echo "" 66 | echo "" 67 | echo '@@@@@@@@@@@@@@@@@@&&&&&&&&&&&&&&&&&&&@@@@@@@@@@@@@+ 68 | @@@@@@@@@@@@&&%%#########%%%%%#########%%&&@@@@@@@+ 69 | @@@@@@@&&&#((%&&#(/*****************/(%&&%((%&&@@@+ 70 | @@@@@@&%(#%#/*****************************/#%##%&@+ 71 | @@@@@&#(&(*****************/%%%%/************#%(%&+ 72 | @@@@&&##&/***************#%#****%%***********(&(%&+ 73 | @@@@@&&#(#%%%%%*******/%%/******#&******%%%%%#(%&&+ 74 | @@@@@@@&&&%%(#&*****(%(*******(%#*******&#(%%&&&@@+ 75 | @@@@@@@@@@&&(#&**/%%*,.****/%%/*********&##&&@@@@@+ 76 | @@@@@@@@@@&&((/(%#**.,.**(%%************&##&&@@@@@+ 77 | @@@@@@@@@@&&#%%(****.,,#%/**************&##&&@@@@@+ 78 | @@@@@@@@@@&&%/*******%%*****************&##&&@@@@@+ 79 | @@@@@@@&&%#(#%%(*/#%(*******************&##&&@@@@@+ 80 | @@@@@&%%((((#%&%%%**********************&##&&@@@@@+ 81 | @@&&%#(((#%&##&**********************%#*&##&&@@@@@+ 82 | &%%((((#&&&&(#&*******************//*%#*&##&&@@@@@+ 83 | %#((#%&@@@&&(#&*******************#%*%#*&##&&@@@@@+ 84 | @&%%%@@@@@&&(#&///////////////////#%/%#/&##&&@@@@@+ 85 | @@@@@@@@@@&&###############################&&@@@@@' 86 | echo "" 87 | echo "" 88 | echo "The installation has finished. Please, review the logs in order to see if everything was OK" 89 | echo "" 90 | echo "You should find a new icon and desktop launcher called ButterManager. You are good to go." 91 | else 92 | # Exit 93 | echo "Ok. Bye!" 94 | fi 95 | -------------------------------------------------------------------------------- /packaging/buttermanager.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=ButterManager 3 | GenericName=ButterManager 4 | Comment=ButterManager, a BTRFS tool for snapshoting, balancing and managing safe system upgrades. 5 | Keywords=BTRFS;btrfs;system;filesystem;snapshot;balance;tool;gui 6 | Exec=buttermanager 7 | Icon=/opt/buttermanager/gui/buttermanager.svg 8 | Terminal=false 9 | Type=Application 10 | Categories=Utility;System; 11 | -------------------------------------------------------------------------------- /packaging/buttermanager.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 44 | 48 | 52 | 54 | 58 | 62 | 63 | 65 | 66 | 68 | 69 | 71 | 72 | 74 | 75 | 77 | 78 | 80 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 92 | 93 | 95 | 96 | 98 | 99 | 101 | 102 | 104 | 105 | 107 | 108 | -------------------------------------------------------------------------------- /packaging/buttermanager_venv.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=ButterManager 3 | GenericName=ButterManager 4 | Comment=ButterManager, a BTRFS tool for snapshoting, balancing and managing safe system upgrades. 5 | Keywords=BTRFS;btrfs;system;filesystem;snapshot;balance;tool;gui 6 | Exec=/opt/buttermanager/env/bin/python3 /opt/buttermanager/buttermanager/main.py 7 | Path=/opt/buttermanager/buttermanager 8 | Icon=/opt/buttermanager/gui/buttermanager.svg 9 | Terminal=false 10 | Type=Application 11 | Categories=Utility;System; 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5>=5.10.1 2 | PyQt5-sip>=12.7.0 3 | PyYAML>=4.2b1 4 | sip>=4.19.8 5 | -------------------------------------------------------------------------------- /rpm/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains all the files needed to package ButterManager application for RPM based distributions. 2 | 3 | This packaging was validated for Fedora 33, and should work for newer releases of Fedora or similar distributions. 4 | -------------------------------------------------------------------------------- /rpm/buttermanager.spec: -------------------------------------------------------------------------------- 1 | Name: buttermanager 2 | Version: 2.5.2 3 | Release: 0%{?dist} 4 | Summary: Tool for managing Btrfs snapshots, balancing filesystems and more 5 | 6 | License: GPLv3 7 | URL: https://github.com/egara/buttermanager 8 | Source0: %{url}/archive/%{version}/%{name}-%{version}.tar.gz 9 | 10 | BuildArch: noarch 11 | BuildRequires: python3-devel 12 | BuildRequires: python3dist(setuptools) 13 | Requires: btrfs-progs 14 | Recommends: grub2-btrfs 15 | 16 | %description 17 | ButterManager is a BTRFS tool for managing snapshots, balancing filesystems 18 | and upgrading the system safely. 19 | 20 | %prep 21 | %autosetup -p1 22 | 23 | 24 | %build 25 | %py3_build 26 | 27 | 28 | %install 29 | %py3_install 30 | 31 | install -Dpm 644 packaging/%{name}.desktop %{buildroot}%{_datadir}/applications/%{name}.desktop 32 | install -Dpm 644 packaging/%{name}.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/%{name}.svg 33 | 34 | # Fix the desktop file 35 | sed -e "s/^Exec=.*/Exec=%{name}/" \ 36 | -e "/^Path=.*/d" \ 37 | -e "s/Icon=.*/Icon=%{name}/" \ 38 | -i %{buildroot}%{_datadir}/applications/%{name}.desktop 39 | 40 | 41 | %files 42 | %license LICENSE 43 | %doc README.md doc 44 | %{_bindir}/buttermanager 45 | %{python3_sitelib}/buttermanager* 46 | %{_datadir}/applications/%{name}.desktop 47 | %{_datadir}/icons/hicolor/scalable/%{name}.svg 48 | 49 | %changelog 50 | * Thu Aug 15 2024 Eloy García Almadén - 2.5.2-0 51 | - New release 2.5.2 52 | 53 | * Mon Feb 19 2024 Eloy García Almadén - 2.5.1-0 54 | - New release 2.5.1 55 | 56 | * Sun Sept 18 2022 Eloy García Almadén - 2.5.0-0 57 | - New release 2.5.0 58 | 59 | * Sat Apr 7 2022 Eloy García Almadén - 2.4.3-0 60 | - New release 2.4.3 61 | 62 | * Sat July 3 2021 Eloy García Almadén - 2.4.2-0 63 | - New release 2.4.2 64 | 65 | * Sat May 1 2021 Eloy García Almadén - 2.4.1-0 66 | - New release 2.4.1 67 | 68 | * Mon Mar 31 2021 Eloy García Almadén - 2.4-0 69 | - New release 2.4 70 | 71 | * Mon Feb 15 2021 Neal Gompa - 2.3-0 72 | - New release 2.3 73 | 74 | * Wed Dec 30 2020 Neal Gompa - 2.2-0 75 | - New release 2.2 76 | 77 | * Wed Jun 17 2020 Neal Gompa - 1.9-0 78 | - Initial package 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="buttermanager", 8 | version="2.5.2", 9 | author="Eloy García Almadén", 10 | author_email="eloy.garcia.pca@gmail.com", 11 | description="BTRFS tool for managing snapshots, balancing filesystems and upgrading the system safetly", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/egara/buttermanager", 15 | packages=['buttermanager', 'buttermanager.buttermanager', 'buttermanager.buttermanager.exception', 'buttermanager.buttermanager.filesystem', 'buttermanager.buttermanager.manager', 'buttermanager.buttermanager.util', 'buttermanager.buttermanager.window'], 16 | package_data= {'buttermanager.buttermanager': ['ui/*', 'images/*']}, 17 | install_requires=[ 18 | 'PyQt5>=5.10.1', 19 | 'PyYAML>=4.2b1', 20 | ], 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 24 | "Operating System :: POSIX :: Linux", 25 | "Development Status :: 4 - Beta", 26 | "Environment :: X11 Applications :: Qt", 27 | "Intended Audience :: End Users/Desktop", 28 | "Topic :: System :: Filesystems", 29 | "Topic :: Utilities" 30 | ], 31 | entry_points={ 32 | "console_scripts": [ 33 | "buttermanager = buttermanager.bm_main:main", 34 | ], 35 | }, 36 | 37 | ) 38 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 2.5.2 2 | --------------------------------------------------------------------------------