pax_global_header00006660000000000000000000000064142050035640014511gustar00rootroot0000000000000052 comment=3aa9c4692cf25a11055af74f28996047edb66f08 kup-backup-0.9.1/000077500000000000000000000000001420500356400135625ustar00rootroot00000000000000kup-backup-0.9.1/.gitignore000066400000000000000000000000201420500356400155420ustar00rootroot00000000000000*.user *.kdev4 kup-backup-0.9.1/CMakeLists.txt000066400000000000000000000023431420500356400163240ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later cmake_minimum_required(VERSION 3.0) cmake_policy(SET CMP0063 NEW) find_package(ECM REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) project(kup) find_package(LibGit2 REQUIRED) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings) include(FeatureSummary) include(ECMInstallIcons) include(ECMQtDeclareLoggingCategory) find_package(Qt5 REQUIRED COMPONENTS Widgets) find_package(KF5 REQUIRED COMPONENTS Solid KIO IdleTime I18n Notifications CoreAddons DBusAddons Config Init # needed for the kdeinit cmake macro JobWidgets Plasma WidgetsAddons ) add_subdirectory(daemon) add_subdirectory(dataengine) add_subdirectory(icons) add_subdirectory(filedigger) add_subdirectory(kcm) add_subdirectory(kioslave) add_subdirectory(purger) ecm_qt_install_logging_categories(EXPORT kup DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}") plasma_install_package(plasmoid org.kde.kupapplet) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.kup.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) kup-backup-0.9.1/LICENSES/000077500000000000000000000000001420500356400147675ustar00rootroot00000000000000kup-backup-0.9.1/LICENSES/GPL-2.0-only.txt000066400000000000000000000423271420500356400174360ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C)< yyyy> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. kup-backup-0.9.1/LICENSES/GPL-2.0-or-later.txt000066400000000000000000000423261420500356400202010ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. kup-backup-0.9.1/LICENSES/GPL-3.0-only.txt000066400000000000000000001032461420500356400174350ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . kup-backup-0.9.1/LICENSES/LicenseRef-KDE-Accepted-GPL.txt000066400000000000000000000012261420500356400222770ustar00rootroot00000000000000This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the license or (at your option) at any later version that is accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy as defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. kup-backup-0.9.1/MAINTAINER000066400000000000000000000000521420500356400151310ustar00rootroot00000000000000Simon Persson kup-backup-0.9.1/Messages.sh000066400000000000000000000003041420500356400156620ustar00rootroot00000000000000#! /usr/bin/env bash $EXTRACTRC `find . -name '*.rc' -o -name '*.ui' -o -name '*.kcfg' ` >> rc.cpp $XGETTEXT `find . -name '*.cpp' -o -name '*.h' -o -name '*.qml' ` -o $podir/kup.pot rm -f rc.cpp kup-backup-0.9.1/README.md000066400000000000000000000070061420500356400150440ustar00rootroot00000000000000# Kup Backup System # Kup is created for helping people to keep up-to-date backups of their personal files. Connecting a USB hard drive is the primary supported way to store files, but saving files to a server over a network connection is also possible for advanced users. When you plug in your external hard drive Kup will automatically start copying your latest changes, but of course it will only do so if you have been active on your computer for some number of hours since the last time you took a backup (and it can of course ask you first, before copying anything). In general Kup tries to not disturb you needlessly. There are two types of backup schemes supported, one which keeps the backup folder completely in sync with what you have on your computer, deleting from the backup any file that you have deleted on your computer etc. The other scheme also keeps older versions of your files in the backup folder. When using this, only the small parts of your files that has actually changed since last backup will be saved and therefore incremental backups are very cheap. This is especially useful if you are working on big files. At the same time it's as easy to access your files as if a complete backup was taken every time; every backup contains a complete version of your directories. Behind the scenes all the content that is actually the same is only stored once. To make this happen Kup runs the backup program "bup" in the background, look at https://github.com/bup/bup for more details. ## What the Kup backup system consists of ## - Configuration module, available in your system settings. Here you can configure backup plans, what to include, where to store the backup and how often. You can also see the status for the backup plans here. - A small program running in the background. It will monitor to see when your backup destination is available, schedule and run your backup plans. - Kioslave for accessing bup archives. This allows you to open files and folders directly from an archive, with any KDE application. - A file browsing application for bup archives, allowing you to locate the file you want to restore more easily than with the kioslave. It also helps you restore files or folders. ## Detailed list of features ## - backup types: - Synchronized folders with the use of "rsync". - Incremental backup archive with the use of "bup" - backup destinations: - local filesystem, monitored for availability. That means you can set a destination folder which only exist when perhaps a network shared drive is mounted and Kup will detect when it becomes available. - external storage, like usb hard drives. Also monitored for availability. - schedules: - manual only (triggered from system tray applet) - interval (suggests new backup after some time has passed since last backup) - usage based (suggests new backup after you have been active on your computer for some hours since last backup). ## Needed backup programs ## To actually create backups of your data you will need either "bup" or "rsync" installed. They provide the implementations for the two different types of backups that Kup supports. ## Compiling from source ## To compile you need: - CMake - extra-cmake-modules - The following libraries (including their development headers): - qt5-base - kcoreaddons - kdbusaddons - ki18n - kio - solid - kidletime - knotifications - kconfig - kinit - kjobwidgets Run from the source directory: ``` mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=release .. make sudo make install ``` kup-backup-0.9.1/daemon/000077500000000000000000000000001420500356400150255ustar00rootroot00000000000000kup-backup-0.9.1/daemon/CMakeLists.txt000066400000000000000000000022661420500356400175730ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later include_directories("../settings") set(kupdaemon_SRCS main.cpp kupdaemon.cpp planexecutor.cpp edexecutor.cpp fsexecutor.cpp backupjob.cpp bupjob.cpp bupverificationjob.cpp buprepairjob.cpp rsyncjob.cpp ../settings/backupplan.cpp ../settings/kupsettings.cpp ../settings/kuputils.cpp ) ecm_qt_declare_logging_category(kupdaemon_SRCS HEADER kupdaemon_debug.h IDENTIFIER KUPDAEMON CATEGORY_NAME kup.daemon DEFAULT_SEVERITY Warning EXPORT kup DESCRIPTION "Kup Daemon" ) kf5_add_kdeinit_executable(kup-daemon ${kupdaemon_SRCS}) target_link_libraries(kdeinit_kup-daemon Qt5::Core Qt5::DBus Qt5::Gui KF5::ConfigCore KF5::KIOCore KF5::KIOFileWidgets KF5::IdleTime KF5::I18n KF5::JobWidgets KF5::Solid KF5::Notifications KF5::CoreAddons KF5::DBusAddons ) ########### install files ############### install(TARGETS kup-daemon ${INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS kdeinit_kup-daemon ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES kup-daemon.desktop DESTINATION ${AUTOSTART_INSTALL_DIR}) install(FILES kupdaemon.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) kup-backup-0.9.1/daemon/backupjob.cpp000066400000000000000000000064501420500356400174760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "backupjob.h" #include "bupjob.h" #include "kupdaemon.h" #include "rsyncjob.h" #include #include #ifdef Q_OS_LINUX #include #endif #include #include #include BackupJob::BackupJob(BackupPlan &pBackupPlan, QString pDestinationPath, QString pLogFilePath, KupDaemon *pKupDaemon) :mBackupPlan(pBackupPlan), mDestinationPath(std::move(pDestinationPath)), mLogFilePath(std::move(pLogFilePath)), mKupDaemon(pKupDaemon) { mLogFile.setFileName(mLogFilePath); mLogFile.open(QIODevice::WriteOnly | QIODevice::Truncate); mLogStream.setDevice(&mLogFile); } void BackupJob::start() { mKupDaemon->registerJob(this); QStringList lRemovedPaths; for(const QString &lPath: mBackupPlan.mPathsIncluded) { if(!QFile::exists(lPath)) { lRemovedPaths << lPath; } } if(!lRemovedPaths.isEmpty()) { jobFinishedError(ErrorSourcesConfig, xi18ncp("@info notification", "One source folder no longer exists. Please open settings and confirm what to include in backup." "%2", "%1 source folders no longer exist. Please open settings and confirm what to include in backup." "%2", lRemovedPaths.length(), lRemovedPaths.join(QChar('\n')))); return; } QTimer::singleShot(0, this, &BackupJob::performJob); } void BackupJob::makeNice(int pPid) { #ifdef Q_OS_LINUX // See linux documentation Documentation/block/ioprio.txt for details of the syscall syscall(SYS_ioprio_set, 1, pPid, 3 << 13 | 7); #endif setpriority(PRIO_PROCESS, static_cast(pPid), 19); } QString BackupJob::quoteArgs(const QStringList &pCommand) { QString lResult; bool lFirst = true; foreach(const QString &lArg, pCommand) { if(lFirst) { lResult.append(lArg); lFirst = false; } else { lResult.append(QStringLiteral(" \"")); lResult.append(lArg); lResult.append(QStringLiteral("\"")); } } return lResult; } void BackupJob::jobFinishedSuccess() { // unregistring a job will normally show a UI notification that it the job was completed // setting the error code to indicate that the user canceled the job makes the UI not show // any notification. We want that since we want to trigger our own notification which has // more buttons and stuff. setError(KilledJobError); mKupDaemon->unregisterJob(this); // The error code is still used by our internal logic, for triggering our own notification. // So make sure to set it correctly. setError(NoError); emitResult(); } void BackupJob::jobFinishedError(BackupJob::ErrorCodes pErrorCode, const QString &pErrorText) { // if job has already set the error that it was killed by the user then ignore any fault // we get here as that fault is surely about the process exit code was not zero. // And we don't want to report about that (with our notification) in this case. bool lWasKilled = (error() == KilledJobError); setError(KilledJobError); mKupDaemon->unregisterJob(this); if(!lWasKilled) { setError(pErrorCode); setErrorText(pErrorText); } emitResult(); } kup-backup-0.9.1/daemon/backupjob.h000066400000000000000000000020171420500356400171360ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BACKUPJOB_H #define BACKUPJOB_H #include "backupplan.h" #include #include #include #include class KupDaemon; class BackupJob : public KJob { Q_OBJECT public: enum ErrorCodes { ErrorWithLog = UserDefinedError, ErrorWithoutLog, ErrorSuggestRepair, ErrorSourcesConfig }; void start() override; protected slots: virtual void performJob() = 0; protected: BackupJob(BackupPlan &pBackupPlan, QString pDestinationPath, QString pLogFilePath, KupDaemon *pKupDaemon); static void makeNice(int pPid); static QString quoteArgs(const QStringList &pCommand); void jobFinishedSuccess(); void jobFinishedError(ErrorCodes pErrorCode, const QString &pErrorText); BackupPlan &mBackupPlan; QString mDestinationPath; QString mLogFilePath; QFile mLogFile; QTextStream mLogStream; KupDaemon *mKupDaemon; }; #endif // BACKUPJOB_H kup-backup-0.9.1/daemon/bupjob.cpp000066400000000000000000000322171420500356400170170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "bupjob.h" #include "kupdaemon_debug.h" #include #include #include #include BupJob::BupJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon) :BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon) { mFsckProcess.setOutputChannelMode(KProcess::SeparateChannels); mIndexProcess.setOutputChannelMode(KProcess::SeparateChannels); mSaveProcess.setOutputChannelMode(KProcess::SeparateChannels); mPar2Process.setOutputChannelMode(KProcess::SeparateChannels); setCapabilities(KJob::Suspendable); mHarmlessErrorCount = 0; mAllErrorsHarmless = false; mLineBreaksRegExp = QRegularExpression(QStringLiteral("\n|\r")); mLineBreaksRegExp.optimize(); mNonsenseRegExp = QRegularExpression(QStringLiteral("^(?:Reading index|bloom|midx)")); mNonsenseRegExp.optimize(); mFileGoneRegExp = QRegularExpression(QStringLiteral("\\[Errno 2\\]")); mFileGoneRegExp.optimize(); mProgressRegExp = QRegularExpression(QStringLiteral("(\\d+)/(\\d+)k, (\\d+)/(\\d+) files\\) \\S* (?:(\\d+)k/s|)")); mProgressRegExp.optimize(); mErrorCountRegExp = QRegularExpression(QStringLiteral("^WARNING: (\\d+) errors encountered while saving.")); mErrorCountRegExp.optimize(); mFileInfoRegExp = QRegularExpression(QStringLiteral("^(?: |A|M) \\/")); mFileInfoRegExp.optimize(); } void BupJob::performJob() { KProcess lPar2Process; lPar2Process.setOutputChannelMode(KProcess::SeparateChannels); lPar2Process << QStringLiteral("bup") << QStringLiteral("fsck") << QStringLiteral("--par2-ok"); int lExitCode = lPar2Process.execute(); if(lExitCode < 0) { jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", "The bup program is " "needed but could not be found, maybe it is not installed?")); return; } if(mBackupPlan.mGenerateRecoveryInfo && lExitCode != 0) { jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", "The par2 program is " "needed but could not be found, maybe it is not installed?")); return; } mLogStream << QStringLiteral("Kup is starting bup backup job at ") << QLocale().toString(QDateTime::currentDateTime()) << endl << endl; KProcess lInitProcess; lInitProcess.setOutputChannelMode(KProcess::SeparateChannels); lInitProcess << QStringLiteral("bup"); lInitProcess << QStringLiteral("-d") << mDestinationPath; lInitProcess << QStringLiteral("init"); mLogStream << quoteArgs(lInitProcess.program()) << endl; if(lInitProcess.execute() != 0) { mLogStream << QString::fromUtf8(lInitProcess.readAllStandardError()) << endl; mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: " "failed to initialize backup destination.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup destination could not be initialised. " "See log file for more details.")); return; } if(mBackupPlan.mCheckBackups) { mFsckProcess << QStringLiteral("bup"); mFsckProcess << QStringLiteral("-d") << mDestinationPath; mFsckProcess << QStringLiteral("fsck") << QStringLiteral("--quick"); mFsckProcess << QStringLiteral("-j") << QString::number(qMin(4, QThread::idealThreadCount())); connect(&mFsckProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotCheckingDone(int,QProcess::ExitStatus))); connect(&mFsckProcess, SIGNAL(started()), SLOT(slotCheckingStarted())); mLogStream << quoteArgs(mFsckProcess.program()) << endl; mFsckProcess.start(); mInfoRateLimiter.start(); } else { startIndexing(); } } void BupJob::slotCheckingStarted() { makeNice(mFsckProcess.pid()); emit description(this, i18n("Checking backup integrity")); } void BupJob::slotCheckingDone(int pExitCode, QProcess::ExitStatus pExitStatus) { QString lErrors = QString::fromUtf8(mFsckProcess.readAllStandardError()); if(!lErrors.isEmpty()) { mLogStream << lErrors << endl; } mLogStream << "Exit code: " << pExitCode << endl; if(pExitStatus != QProcess::NormalExit || pExitCode != 0) { mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: " "failed integrity check. Your backups could be " "corrupted! See above for details.") << endl; if(mBackupPlan.mGenerateRecoveryInfo) { jobFinishedError(ErrorSuggestRepair, xi18nc("@info notification", "Failed backup integrity check. Your backups could be corrupted! " "See log file for more details. Do you want to try repairing the backup files?")); } else { jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed backup integrity check. Your backups could be corrupted! " "See log file for more details.")); } return; } startIndexing(); } void BupJob::startIndexing() { mIndexProcess << QStringLiteral("bup"); mIndexProcess << QStringLiteral("-d") << mDestinationPath; mIndexProcess << QStringLiteral("index") << QStringLiteral("-u"); foreach(QString lExclude, mBackupPlan.mPathsExcluded) { mIndexProcess << QStringLiteral("--exclude"); mIndexProcess << lExclude; } QString lExcludesPath = mBackupPlan.absoluteExcludesFilePath(); if(mBackupPlan.mExcludePatterns && QFileInfo::exists(lExcludesPath)) { mIndexProcess << QStringLiteral("--exclude-rx-from") << lExcludesPath; } mIndexProcess << mBackupPlan.mPathsIncluded; connect(&mIndexProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotIndexingDone(int,QProcess::ExitStatus))); connect(&mIndexProcess, SIGNAL(started()), SLOT(slotIndexingStarted())); mLogStream << quoteArgs(mIndexProcess.program()) << endl; mIndexProcess.start(); } void BupJob::slotIndexingStarted() { makeNice(mIndexProcess.pid()); emit description(this, i18n("Checking what to copy")); } void BupJob::slotIndexingDone(int pExitCode, QProcess::ExitStatus pExitStatus) { QString lErrors = QString::fromUtf8(mIndexProcess.readAllStandardError()); if(!lErrors.isEmpty()) { mLogStream << lErrors << endl; } mLogStream << "Exit code: " << pExitCode << endl; if(pExitStatus != QProcess::NormalExit || pExitCode != 0) { mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: failed to index everything.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to analyze files. " "See log file for more details.")); return; } mSaveProcess << QStringLiteral("bup"); mSaveProcess << QStringLiteral("-d") << mDestinationPath; mSaveProcess << QStringLiteral("save"); mSaveProcess << QStringLiteral("-n") << QStringLiteral("kup") << QStringLiteral("-vv"); mSaveProcess << mBackupPlan.mPathsIncluded; mLogStream << quoteArgs(mSaveProcess.program()) << endl; connect(&mSaveProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotSavingDone(int,QProcess::ExitStatus))); connect(&mSaveProcess, SIGNAL(started()), SLOT(slotSavingStarted())); connect(&mSaveProcess, &KProcess::readyReadStandardError, this, &BupJob::slotReadBupErrors); mSaveProcess.setEnv(QStringLiteral("BUP_FORCE_TTY"), QStringLiteral("2")); mSaveProcess.start(); } void BupJob::slotSavingStarted() { makeNice(mSaveProcess.pid()); emit description(this, i18n("Saving backup")); } void BupJob::slotSavingDone(int pExitCode, QProcess::ExitStatus pExitStatus) { slotReadBupErrors(); mLogStream << "Exit code: " << pExitCode << endl; if(pExitStatus != QProcess::NormalExit || pExitCode != 0) { if(mAllErrorsHarmless) { mLogStream << QStringLiteral("Only harmless errors detected by Kup.") << endl; } else { mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: " "failed to save everything.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to save backup. " "See log file for more details.")); return; } } if(mBackupPlan.mGenerateRecoveryInfo) { mPar2Process << QStringLiteral("bup"); mPar2Process << QStringLiteral("-d") << mDestinationPath; mPar2Process << QStringLiteral("fsck") << QStringLiteral("-g"); mPar2Process << QStringLiteral("-j") << QString::number(qMin(4, QThread::idealThreadCount())); connect(&mPar2Process, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotRecoveryInfoDone(int,QProcess::ExitStatus))); connect(&mPar2Process, SIGNAL(started()), SLOT(slotRecoveryInfoStarted())); mLogStream << quoteArgs(mPar2Process.program()) << endl; mPar2Process.start(); } else { mLogStream << QStringLiteral("Kup successfully completed the bup backup job at ") << QLocale().toString(QDateTime::currentDateTime()) << endl; jobFinishedSuccess(); } } void BupJob::slotRecoveryInfoStarted() { makeNice(mPar2Process.pid()); emit description(this, i18n("Generating recovery information")); } void BupJob::slotRecoveryInfoDone(int pExitCode, QProcess::ExitStatus pExitStatus) { QString lErrors = QString::fromUtf8(mPar2Process.readAllStandardError()); if(!lErrors.isEmpty()) { mLogStream << lErrors << endl; } mLogStream << "Exit code: " << pExitCode << endl; if(pExitStatus != QProcess::NormalExit || pExitCode != 0) { mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: " "failed to generate recovery info.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to generate recovery info for the backup. " "See log file for more details.")); } else { mLogStream << QStringLiteral("Kup successfully completed the bup backup job.") << endl; jobFinishedSuccess(); } } void BupJob::slotReadBupErrors() { qulonglong lCopiedKBytes = 0, lTotalKBytes = 0, lCopiedFiles = 0, lTotalFiles = 0; ulong lSpeedKBps = 0, lPercent = 0; QString lFileName; const auto lInput = QString::fromUtf8(mSaveProcess.readAllStandardError()); #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) const auto lLines = lInput.split(mLineBreaksRegExp, QString::SkipEmptyParts); #else const auto lLines = lInput.split(mLineBreaksRegExp, Qt::SkipEmptyParts); #endif for(const QString &lLine: lLines) { qCDebug(KUPDAEMON) << lLine; if(mNonsenseRegExp.match(lLine).hasMatch()) { continue; } if(mFileGoneRegExp.match(lLine).hasMatch()) { mHarmlessErrorCount++; mLogStream << lLine << endl; continue; } const auto lCountMatch = mErrorCountRegExp.match(lLine); if(lCountMatch.hasMatch()) { mAllErrorsHarmless = lCountMatch.captured(1).toInt() == mHarmlessErrorCount; mLogStream << lLine << endl; continue; } const auto lProgressMatch = mProgressRegExp.match(lLine); if(lProgressMatch.hasMatch()) { lCopiedKBytes = lProgressMatch.captured(1).toULongLong(); lTotalKBytes = lProgressMatch.captured(2).toULongLong(); lCopiedFiles = lProgressMatch.captured(3).toULongLong(); lTotalFiles = lProgressMatch.captured(4).toULongLong(); lSpeedKBps = lProgressMatch.captured(5).toULong(); if(lTotalKBytes != 0) { lPercent = qMax(100*lCopiedKBytes/lTotalKBytes, static_cast(1)); } continue; } if(mFileInfoRegExp.match(lLine).hasMatch()) { lFileName = lLine.mid(2); continue; } if(!lLine.startsWith(QStringLiteral("D /"))) { mLogStream << lLine << endl; } } if(mInfoRateLimiter.hasExpired(200)) { if(lTotalFiles != 0) { setPercent(lPercent); setTotalAmount(KJob::Bytes, lTotalKBytes*1024); setTotalAmount(KJob::Files, lTotalFiles); setProcessedAmount(KJob::Bytes, lCopiedKBytes*1024); setProcessedAmount(KJob::Files, lCopiedFiles); emitSpeed(lSpeedKBps * 1024); } if(!lFileName.isEmpty()) { emit description(this, i18n("Saving backup"), qMakePair(i18nc("Label for file currently being copied", "File"), lFileName)); } mInfoRateLimiter.start(); } } bool BupJob::doSuspend() { if(mFsckProcess.state() == KProcess::Running) { return 0 == ::kill(mFsckProcess.pid(), SIGSTOP); } if(mIndexProcess.state() == KProcess::Running) { return 0 == ::kill(mIndexProcess.pid(), SIGSTOP); } if(mSaveProcess.state() == KProcess::Running) { return 0 == ::kill(mSaveProcess.pid(), SIGSTOP); } if(mPar2Process.state() == KProcess::Running) { return 0 == ::kill(mPar2Process.pid(), SIGSTOP); } return false; } bool BupJob::doResume() { if(mFsckProcess.state() == KProcess::Running) { return 0 == ::kill(mFsckProcess.pid(), SIGCONT); } if(mIndexProcess.state() == KProcess::Running) { return 0 == ::kill(mIndexProcess.pid(), SIGCONT); } if(mSaveProcess.state() == KProcess::Running) { return 0 == ::kill(mSaveProcess.pid(), SIGCONT); } if(mPar2Process.state() == KProcess::Running) { return 0 == ::kill(mPar2Process.pid(), SIGCONT); } return false; } kup-backup-0.9.1/daemon/bupjob.h000066400000000000000000000027141420500356400164630ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BUPJOB_H #define BUPJOB_H #include "backupjob.h" #include #include #include class KupDaemon; class BupJob : public BackupJob { Q_OBJECT public: BupJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon); protected slots: void performJob() override; void slotCheckingStarted(); void slotCheckingDone(int pExitCode, QProcess::ExitStatus pExitStatus); void startIndexing(); void slotIndexingStarted(); void slotIndexingDone(int pExitCode, QProcess::ExitStatus pExitStatus); void slotSavingStarted(); void slotSavingDone(int pExitCode, QProcess::ExitStatus pExitStatus); void slotRecoveryInfoStarted(); void slotRecoveryInfoDone(int pExitCode, QProcess::ExitStatus pExitStatus); void slotReadBupErrors(); protected: bool doSuspend() override; bool doResume() override; KProcess mFsckProcess; KProcess mIndexProcess; KProcess mSaveProcess; KProcess mPar2Process; QElapsedTimer mInfoRateLimiter; int mHarmlessErrorCount; bool mAllErrorsHarmless; QRegularExpression mLineBreaksRegExp; QRegularExpression mNonsenseRegExp; QRegularExpression mFileGoneRegExp; QRegularExpression mProgressRegExp; QRegularExpression mErrorCountRegExp; QRegularExpression mFileInfoRegExp; }; #endif /*BUPJOB_H*/ kup-backup-0.9.1/daemon/buprepairjob.cpp000066400000000000000000000076071420500356400202270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "buprepairjob.h" #include #include BupRepairJob::BupRepairJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon) : BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon){ mFsckProcess.setOutputChannelMode(KProcess::SeparateChannels); } void BupRepairJob::performJob() { KProcess lPar2Process; lPar2Process.setOutputChannelMode(KProcess::SeparateChannels); lPar2Process << QStringLiteral("bup") << QStringLiteral("fsck") << QStringLiteral("--par2-ok"); int lExitCode = lPar2Process.execute(); if(lExitCode < 0) { jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", "The bup program is needed but could not be found, " "maybe it is not installed?")); return; } if(mBackupPlan.mGenerateRecoveryInfo && lExitCode != 0) { jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", "The par2 program is needed but could not be found, " "maybe it is not installed?")); return; } mLogStream << QStringLiteral("Kup is starting bup repair job at ") << QLocale().toString(QDateTime::currentDateTime()) << endl << endl; mFsckProcess << QStringLiteral("bup"); mFsckProcess << QStringLiteral("-d") << mDestinationPath; mFsckProcess << QStringLiteral("fsck") << QStringLiteral("-r"); mFsckProcess << QStringLiteral("-j") << QString::number(qMin(4, QThread::idealThreadCount())); connect(&mFsckProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotRepairDone(int,QProcess::ExitStatus))); connect(&mFsckProcess, SIGNAL(started()), SLOT(slotRepairStarted())); mLogStream << mFsckProcess.program().join(QStringLiteral(" ")) << endl; mFsckProcess.start(); } void BupRepairJob::slotRepairStarted() { makeNice(mFsckProcess.pid()); } void BupRepairJob::slotRepairDone(int pExitCode, QProcess::ExitStatus pExitStatus) { QString lErrors = QString::fromUtf8(mFsckProcess.readAllStandardError()); if(!lErrors.isEmpty()) { mLogStream << lErrors << endl; } mLogStream << "Exit code: " << pExitCode << endl; if(pExitStatus != QProcess::NormalExit) { mLogStream << QStringLiteral("Repair failed (the repair process crashed). Your backups could be " "corrupted! See above for details.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup repair failed. Your backups could be corrupted! " "See log file for more details.")); } else if(pExitCode == 100) { mLogStream << QStringLiteral("Repair succeeded. See above for details.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Success! Backup repair worked. See log file for more details.")); } else if(pExitCode == 0) { mLogStream << QStringLiteral("Repair was not necessary. Your backups are fine. See " "above for details.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup repair was not necessary. Your backups are not corrupted. " "See log file for more details.")); } else { mLogStream << QStringLiteral("Repair failed. Your backups could still be " "corrupted! See above for details.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup repair failed. Your backups could still be corrupted! " "See log file for more details.")); } } kup-backup-0.9.1/daemon/buprepairjob.h000066400000000000000000000012171420500356400176630ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BUPREPAIRJOB_H #define BUPREPAIRJOB_H #include "backupjob.h" #include class KupDaemon; class BupRepairJob : public BackupJob { Q_OBJECT public: BupRepairJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon); protected slots: void performJob() override; void slotRepairStarted(); void slotRepairDone(int pExitCode, QProcess::ExitStatus pExitStatus); protected: KProcess mFsckProcess; }; #endif // BUPREPAIRJOB_H kup-backup-0.9.1/daemon/bupverificationjob.cpp000066400000000000000000000100151420500356400214120ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "bupverificationjob.h" #include #include BupVerificationJob::BupVerificationJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon) : BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon){ mFsckProcess.setOutputChannelMode(KProcess::SeparateChannels); } void BupVerificationJob::performJob() { KProcess lVersionProcess; lVersionProcess.setOutputChannelMode(KProcess::SeparateChannels); lVersionProcess << QStringLiteral("bup") << QStringLiteral("version"); if(lVersionProcess.execute() < 0) { jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", "The bup program is needed but could not be found, " "maybe it is not installed?")); return; } mLogStream << QStringLiteral("Kup is starting bup verification job at ") << QLocale().toString(QDateTime::currentDateTime()) << endl << endl; mFsckProcess << QStringLiteral("bup"); mFsckProcess << QStringLiteral("-d") << mDestinationPath; mFsckProcess << QStringLiteral("fsck") << QStringLiteral("--quick"); mFsckProcess << QStringLiteral("-j") << QString::number(qMin(4, QThread::idealThreadCount())); connect(&mFsckProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotCheckingDone(int,QProcess::ExitStatus))); connect(&mFsckProcess, SIGNAL(started()), SLOT(slotCheckingStarted())); mLogStream << mFsckProcess.program().join(QStringLiteral(" ")) << endl; mFsckProcess.start(); } void BupVerificationJob::slotCheckingStarted() { makeNice(mFsckProcess.pid()); } void BupVerificationJob::slotCheckingDone(int pExitCode, QProcess::ExitStatus pExitStatus) { QString lErrors = QString::fromUtf8(mFsckProcess.readAllStandardError()); if(!lErrors.isEmpty()) { mLogStream << lErrors << endl; } mLogStream << "Exit code: " << pExitCode << endl; if(pExitStatus != QProcess::NormalExit) { mLogStream << QStringLiteral("Integrity check failed (the process crashed). Your backups could be " "corrupted! See above for details.") << endl; if(mBackupPlan.mGenerateRecoveryInfo) { jobFinishedError(ErrorSuggestRepair, xi18nc("@info notification", "Failed backup integrity check. Your backups could be corrupted! " "See log file for more details. Do you want to try repairing the backup files?")); } else { jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed backup integrity check. Your backups are corrupted! " "See log file for more details.")); } } else if(pExitCode == 0) { mLogStream << QStringLiteral("Backup integrity test was successful. " "Your backups are fine. See above for details.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup integrity test was successful. " "Your backups are fine.")); } else { mLogStream << QStringLiteral("Integrity check failed. Your backups are " "corrupted! See above for details.") << endl; if(mBackupPlan.mGenerateRecoveryInfo) { jobFinishedError(ErrorSuggestRepair, xi18nc("@info notification", "Failed backup integrity check. Your backups are corrupted! " "See log file for more details. Do you want to try repairing the backup files?")); } else { jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed backup integrity check. Your backups are corrupted! " "See log file for more details.")); } } } kup-backup-0.9.1/daemon/bupverificationjob.h000066400000000000000000000012611420500356400210620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BUPVERIFICATIONJOB_H #define BUPVERIFICATIONJOB_H #include "backupjob.h" #include class KupDaemon; class BupVerificationJob : public BackupJob { Q_OBJECT public: BupVerificationJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon); protected slots: void performJob() override; void slotCheckingStarted(); void slotCheckingDone(int pExitCode, QProcess::ExitStatus pExitStatus); protected: KProcess mFsckProcess; }; #endif // BUPVERIFICATIONJOB_H kup-backup-0.9.1/daemon/edexecutor.cpp000066400000000000000000000065011420500356400177020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "edexecutor.h" #include "backupplan.h" #include #include #include #include #include #include #include #include #include EDExecutor::EDExecutor(BackupPlan *pPlan, KupDaemon *pKupDaemon) :PlanExecutor(pPlan, pKupDaemon), mStorageAccess(nullptr), mWantsToRunBackup(false), mWantsToShowFiles(false), mWantsToPurge(false) { connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), SLOT(deviceAdded(QString))); connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), SLOT(deviceRemoved(QString))); } void EDExecutor::checkStatus() { QList lDeviceList = Solid::Device::listFromType(Solid::DeviceInterface::StorageVolume); foreach (const Solid::Device &lDevice, lDeviceList) { deviceAdded(lDevice.udi()); } updateAccessibility(); } void EDExecutor::deviceAdded(const QString &pUdi) { Solid::Device lDevice(pUdi); if(!lDevice.is()) { return; } auto *lVolume = lDevice.as(); QString lUUID = lVolume->uuid(); if(lUUID.isEmpty()) { //seems to happen for vfat partitions Solid::Device lDriveDevice; if(lDevice.is()) { lDriveDevice = lDevice; } else { lDriveDevice = lDevice.parent(); } lUUID += lDriveDevice.description(); lUUID += QStringLiteral("|"); lUUID += lVolume->label(); } if(mPlan->mExternalUUID == lUUID) { mCurrentUdi = pUdi; mStorageAccess = lDevice.as(); enterAvailableState(); } } void EDExecutor::deviceRemoved(const QString &pUdi) { if(mCurrentUdi == pUdi) { mWantsToRunBackup = false; mCurrentUdi.clear(); mStorageAccess = nullptr; enterNotAvailableState(); } } void EDExecutor::updateAccessibility() { if(mWantsToRunBackup) { startBackup(); // run startBackup again now that it has been mounted } else if(mWantsToShowFiles) { showBackupFiles(); } else if(mWantsToPurge) { showBackupPurger(); } } void EDExecutor::startBackup() { if(!ensureAccessible(mWantsToRunBackup)) { exitBackupRunningState(false); return; } PlanExecutor::startBackup(); } void EDExecutor::showBackupFiles() { if(!ensureAccessible(mWantsToShowFiles)) { return; } PlanExecutor::showBackupFiles(); } void EDExecutor::showBackupPurger() { if(!ensureAccessible(mWantsToPurge)) { return; } PlanExecutor::showBackupPurger(); } bool EDExecutor::ensureAccessible(bool &pReturnLater) { pReturnLater = false; // reset in case we are here for the second time if(!mStorageAccess) { return false; } if(mStorageAccess->isAccessible()) { if(!mStorageAccess->filePath().isEmpty()) { mDestinationPath = mStorageAccess->filePath(); mDestinationPath += QStringLiteral("/"); mDestinationPath += mPlan->mExternalDestinationPath; QFileInfo lDestinationInfo(mDestinationPath); if(lDestinationInfo.exists() && lDestinationInfo.isDir()) { return true; } } return false; } connect(mStorageAccess, SIGNAL(accessibilityChanged(bool,QString)), SLOT(updateAccessibility())); mStorageAccess->setup(); //try to mount it, fail silently for now. pReturnLater = true; return false; } kup-backup-0.9.1/daemon/edexecutor.h000066400000000000000000000017701420500356400173520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef EDEXECUTOR_H #define EDEXECUTOR_H #include "planexecutor.h" #include #include class BackupPlan; // Plan executor that stores the backup to an external disk. // Uses libsolid to monitor for when it becomes available. class EDExecutor: public PlanExecutor { Q_OBJECT public: EDExecutor(BackupPlan *pPlan, KupDaemon *pKupDaemon); public slots: void checkStatus() override; void showBackupFiles() override; void showBackupPurger() override; protected slots: void deviceAdded(const QString &pUdi); void deviceRemoved(const QString &pUdi); void updateAccessibility(); void startBackup() override; protected: bool ensureAccessible(bool &pReturnLater); Solid::StorageAccess *mStorageAccess; QString mCurrentUdi; bool mWantsToRunBackup; bool mWantsToShowFiles; bool mWantsToPurge; }; #endif // EDEXECUTOR_H kup-backup-0.9.1/daemon/fsexecutor.cpp000066400000000000000000000074111420500356400177230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "fsexecutor.h" #include "backupplan.h" #include #include #include #include #include #include #include #include namespace { // very light check if a directory exists that works on automounts where QDir::exists fails bool checkDirExists(const QDir &dir) { struct stat s; return stat(dir.absolutePath().toLocal8Bit().data(), &s) == 0 && S_ISDIR(s.st_mode); } } FSExecutor::FSExecutor(BackupPlan *pPlan, KupDaemon *pKupDaemon) :PlanExecutor(pPlan, pKupDaemon) { mDestinationPath = QDir::cleanPath(mPlan->mFilesystemDestinationPath.toLocalFile()); mDirWatch = new KDirWatch(this); connect(mDirWatch, SIGNAL(deleted(QString)), SLOT(checkStatus())); mMountWatcher.start(); } FSExecutor::~FSExecutor() { mMountWatcher.terminate(); mMountWatcher.wait(); } void FSExecutor::checkStatus() { static bool lComingBackLater = false; if(!mWatchedParentDir.isEmpty() && !lComingBackLater) { // came here because something happened to a parent folder, // come back in a few seconds, give a new mount some time before checking // status of destination folder QTimer::singleShot(5000, this, SLOT(checkStatus())); lComingBackLater = true; return; } lComingBackLater = false; QDir lDir(mDestinationPath); if(!lDir.exists()) { // Destination doesn't exist, find nearest existing parent folder and // watch that for dirty or deleted if(mDirWatch->contains(mDestinationPath)) { mDirWatch->removeDir(mDestinationPath); } QString lExisting = mDestinationPath; do { lExisting += QStringLiteral("/.."); lDir = QDir(QDir::cleanPath(lExisting)); } while(!checkDirExists(lDir)); lExisting = lDir.canonicalPath(); if(lExisting != mWatchedParentDir) { // new parent to watch if(!mWatchedParentDir.isEmpty()) { // were already watching a parent mDirWatch->removeDir(mWatchedParentDir); } else { // start watching a parent connect(mDirWatch, SIGNAL(dirty(QString)), SLOT(checkStatus())); connect(&mMountWatcher, SIGNAL(mountsChanged()), SLOT(checkMountPoints()), Qt::QueuedConnection); } mWatchedParentDir = lExisting; mDirWatch->addDir(mWatchedParentDir); } if(mState != NOT_AVAILABLE) { enterNotAvailableState(); } } else { // Destination exists... only watch for delete if(!mWatchedParentDir.isEmpty()) { disconnect(mDirWatch, SIGNAL(dirty(QString)), this, SLOT(checkStatus())); disconnect(&mMountWatcher, SIGNAL(mountsChanged()), this, SLOT(checkMountPoints())); mDirWatch->removeDir(mWatchedParentDir); mWatchedParentDir.clear(); } mDirWatch->addDir(mDestinationPath); QFileInfo lInfo(mDestinationPath); if(lInfo.isWritable() && mState == NOT_AVAILABLE) { enterAvailableState(); }else if(!lInfo.isWritable() && mState != NOT_AVAILABLE) { enterNotAvailableState(); } } } void FSExecutor::checkMountPoints() { QFile lMountsFile(QStringLiteral("/proc/mounts")); if(!lMountsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } // don't use atEnd() to detect when finished reading file, size of // this special file is 0 but still returns data when read. forever { QByteArray lLine = lMountsFile.readLine(); if(lLine.isEmpty()) { break; } QTextStream lTextStream(lLine); QString lDevice, lMountPoint; lTextStream >> lDevice >> lMountPoint; if(lMountPoint == mWatchedParentDir) { checkStatus(); } } } void MountWatcher::run() { int lMountsFd = open("/proc/mounts", O_RDONLY); fd_set lFdSet; forever { FD_ZERO(&lFdSet); FD_SET(lMountsFd, &lFdSet); if(select(lMountsFd+1, nullptr, nullptr, &lFdSet, nullptr) > 0) { emit mountsChanged(); } } } kup-backup-0.9.1/daemon/fsexecutor.h000066400000000000000000000022711420500356400173670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef FSEXECUTOR_H #define FSEXECUTOR_H #include "planexecutor.h" #include class BackupPlan; class KDirWatch; class QTimer; // KDirWatch (well, inotify) does not detect when something gets mounted on a watched directory. // work around this problem by monitoring the mounts of the system in a separate thread. class MountWatcher: public QThread { Q_OBJECT signals: void mountsChanged(); protected: void run() override; }; // Plan executor that stores the backup to a path in the local // filesystem, uses KDirWatch to monitor for when the folder // becomes available/unavailable. Can be used for external // drives or networked filesystems if you always mount it at // the same mountpoint. class FSExecutor: public PlanExecutor { Q_OBJECT public: FSExecutor(BackupPlan *pPlan, KupDaemon *pKupDaemon); ~FSExecutor() override; public slots: void checkStatus() override; protected slots: void checkMountPoints(); protected: QString mWatchedParentDir; KDirWatch *mDirWatch; MountWatcher mMountWatcher; }; #endif // FSEXECUTOR_H kup-backup-0.9.1/daemon/kup-daemon.desktop000077500000000000000000000031251420500356400204640ustar00rootroot00000000000000[Desktop Entry] Name=Kup Name[ca]=Kup Name[ca@valencia]=Kup Name[cs]=Proces služby Kup Name[de]=Kup Name[en_GB]=Kup Name[es]=Kup Name[et]=Kup Name[eu]=Kup Name[fi]=Kup Name[fr]=Kup Name[it]=Kup Name[ko]=Kup Name[nl]=Kup Name[pl]=Kup Name[pt]=Kup Name[pt_BR]=Kup Name[ru]=Kup Name[sk]=Kup Name[sl]=Kup Name[sv]=Kup Name[uk]=Kup Name[x-test]=xxKupxx Name[zh_CN]=K 备份 Name[zh_TW]=Kup GenericName=Backup Monitor GenericName[ca]=Supervisor per a còpia de seguretat GenericName[ca@valencia]=Supervisor per a còpia de seguretat GenericName[de]=Sicherungs-Überwachung GenericName[en_GB]=Backup Monitor GenericName[es]=Monitor de copias de seguridad GenericName[et]=Varundamise jälgimine GenericName[eu]=Babes-kopia begiralea GenericName[fi]=Varmuuskopiovalvonta GenericName[fr]=Moniteur de sauvegarde GenericName[it]=Monitor delle copie di sicurezza GenericName[ko]=백업 모니터 GenericName[nl]=Monitor van back-ups GenericName[pl]=Monitor kopii zapasowej GenericName[pt]=Monitor de Cópias de Segurança GenericName[pt_BR]=Monitor de backup GenericName[sk]=Monitor záloh GenericName[sl]=Nadzornik varnostnih kopij GenericName[sv]=Övervakning av säkerhetskopior GenericName[uk]=Монітор резервного копіювання GenericName[x-test]=xxBackup Monitorxx GenericName[zh_CN]=备份监视器 GenericName[zh_TW]=備份監控工具 Exec=kup-daemon Icon=kup Type=Application Terminal=false X-KDE-StartupNotify=false X-DBUS-StartupType=Unique X-KDE-UniqueApplet=true X-KDE-autostart-condition=kuprc:Kup settings:Backups enabled:true OnlyShowIn=KDE; Categories=Qt;KDE;Utility;X-KDE-Utilities-Desktop; kup-backup-0.9.1/daemon/kupdaemon.cpp000066400000000000000000000245231420500356400175220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupdaemon.h" #include "kupsettings.h" #include "backupplan.h" #include "edexecutor.h" #include "fsexecutor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include KupDaemon::KupDaemon() { mWaitingToReloadConfig = false; mConfig = KSharedConfig::openConfig(QStringLiteral("kuprc")); mSettings = new KupSettings(mConfig, this); mJobTracker = new KUiServerJobTracker(this); mLocalServer = new QLocalServer(this); } KupDaemon::~KupDaemon() { while(!mExecutors.isEmpty()) { delete mExecutors.takeFirst(); } KIdleTime::instance()->removeAllIdleTimeouts(); } bool KupDaemon::shouldStart() { return mSettings->mBackupsEnabled; } void KupDaemon::setupGuiStuff() { // timer to update logged time and also trigger warning if too long // time has now passed since last backup mUsageAccTimer = new QTimer(this); mUsageAccTimer->setInterval(KUP_USAGE_MONITOR_INTERVAL_S * 1000); mUsageAccTimer->start(); KIdleTime *lIdleTime = KIdleTime::instance(); lIdleTime->addIdleTimeout(KUP_IDLE_TIMEOUT_S * 1000); connect(lIdleTime, SIGNAL(timeoutReached(int)), mUsageAccTimer, SLOT(stop())); connect(lIdleTime, SIGNAL(timeoutReached(int)), lIdleTime, SLOT(catchNextResumeEvent())); connect(lIdleTime, SIGNAL(resumingFromIdle()), mUsageAccTimer, SLOT(start())); mStatusUpdateTimer = new QTimer(this); // delay status update to avoid sending a status to plasma applet // that will be changed again just a microsecond later anyway mStatusUpdateTimer->setInterval(500); mStatusUpdateTimer->setSingleShot(true); connect(mStatusUpdateTimer, &QTimer::timeout, this, [this]{ foreach(QLocalSocket *lSocket, mSockets) { sendStatus(lSocket); } if(mWaitingToReloadConfig) { // quite likely the config can be reloaded now, give it a try. QTimer::singleShot(0, this, SLOT(reloadConfig())); } }); QDBusConnection lDBus = QDBusConnection::sessionBus(); if(lDBus.isConnected()) { if(lDBus.registerService(KUP_DBUS_SERVICE_NAME)) { lDBus.registerObject(KUP_DBUS_OBJECT_PATH, this, QDBusConnection::ExportAllSlots); } } QString lSocketName = QStringLiteral("kup-daemon-"); lSocketName += QString::fromLocal8Bit(qgetenv("USER")); connect(mLocalServer, &QLocalServer::newConnection, this, [this]{ QLocalSocket *lSocket = mLocalServer->nextPendingConnection(); if(lSocket == nullptr) { return; } sendStatus(lSocket); mSockets.append(lSocket); connect(lSocket, &QLocalSocket::readyRead, this, [this,lSocket]{handleRequests(lSocket);}); connect(lSocket, &QLocalSocket::disconnected, this, [this,lSocket]{ mSockets.removeAll(lSocket); lSocket->deleteLater(); }); }); // remove old socket first in case it's still there, otherwise listen() fails. QLocalServer::removeServer(lSocketName); mLocalServer->listen(lSocketName); reloadConfig(); } void KupDaemon::reloadConfig() { foreach(PlanExecutor *lExecutor, mExecutors) { if(lExecutor->busy()) { mWaitingToReloadConfig = true; return; } } mWaitingToReloadConfig = false; mSettings->load(); while(!mExecutors.isEmpty()) { delete mExecutors.takeFirst(); } if(!mSettings->mBackupsEnabled) qApp->quit(); setupExecutors(); // Juuuust in case all those executors for some reason never // triggered an updated status... Doesn't hurt anyway. mStatusUpdateTimer->start(); } // This method is exposed over DBus so that filedigger can call it void KupDaemon::runIntegrityCheck(const QString& pPath) { foreach(PlanExecutor *lExecutor, mExecutors) { // if caller passes in an empty path, startsWith will return true and we will try to check // all backup plans. if(lExecutor->mDestinationPath.startsWith(pPath)) { lExecutor->startIntegrityCheck(); } } } void KupDaemon::registerJob(KJob *pJob) { mJobTracker->registerJob(pJob); } void KupDaemon::unregisterJob(KJob *pJob) { mJobTracker->unregisterJob(pJob); } void KupDaemon::slotShutdownRequest(QSessionManager &pManager) { // this will make session management not try (and fail because of KDBusService starting only // one instance) to start this daemon. We have autostart for the purpose of launching this // daemon instead. pManager.setRestartHint(QSessionManager::RestartNever); foreach(PlanExecutor *lExecutor, mExecutors) { if(lExecutor->busy() && pManager.allowsErrorInteraction()) { QMessageBox lMessageBox; QPushButton *lContinueButton = lMessageBox.addButton(i18n("Continue"), QMessageBox::RejectRole); lMessageBox.addButton(i18n("Stop"), QMessageBox::AcceptRole); lMessageBox.setText(i18nc("%1 is a text explaining the current activity", "Currently busy: %1", lExecutor->currentActivityTitle())); lMessageBox.setInformativeText(i18n("Do you really want to stop?")); lMessageBox.setIcon(QMessageBox::Warning); lMessageBox.setWindowIcon(QIcon::fromTheme(QStringLiteral("kup"))); lMessageBox.setWindowTitle(i18n("User Backups")); lMessageBox.exec(); if(lMessageBox.clickedButton() == lContinueButton) { pManager.cancel(); } return; //only ask for one active executor. } } } void KupDaemon::setupExecutors() { for(int i = 0; i < mSettings->mNumberOfPlans; ++i) { PlanExecutor *lExecutor; auto *lPlan = new BackupPlan(i+1, mConfig, this); if(lPlan->mPathsIncluded.isEmpty()) { delete lPlan; continue; } if(lPlan->mDestinationType == 0) { lExecutor = new FSExecutor(lPlan, this); } else if(lPlan->mDestinationType == 1) { lExecutor = new EDExecutor(lPlan, this); } else { delete lPlan; continue; } connect(lExecutor, &PlanExecutor::stateChanged, this, [this]{mStatusUpdateTimer->start();}); connect(lExecutor, &PlanExecutor::backupStatusChanged, this, [this]{mStatusUpdateTimer->start();}); connect(mUsageAccTimer, &QTimer::timeout, lExecutor, &PlanExecutor::updateAccumulatedUsageTime); lExecutor->checkStatus(); mExecutors.append(lExecutor); } } void KupDaemon::handleRequests(QLocalSocket *pSocket) { if(pSocket->bytesAvailable() <= 0) { return; } QJsonDocument lDoc = QJsonDocument::fromBinaryData(pSocket->readAll()); if(!lDoc.isObject()) { return; } QJsonObject lCommand = lDoc.object(); QString lOperation = lCommand["operation name"].toString(); if(lOperation == QStringLiteral("get status")) { sendStatus(pSocket); return; } if(lOperation == QStringLiteral("reload")) { reloadConfig(); return; } int lPlanNumber = lCommand["plan number"].toInt(-1); if(lPlanNumber < 0 || lPlanNumber >= mExecutors.count()) { return; } if(lOperation == QStringLiteral("save backup")) { mExecutors.at(lPlanNumber)->startBackupSaveJob(); } if(lOperation == QStringLiteral("remove backups")) { mExecutors.at(lPlanNumber)->showBackupPurger(); } if(lOperation == QStringLiteral("show log file")) { mExecutors.at(lPlanNumber)->showLog(); } if(lOperation == QStringLiteral("show backup files")) { mExecutors.at(lPlanNumber)->showBackupFiles(); } } void KupDaemon::sendStatus(QLocalSocket *pSocket) { bool lTrayIconActive = false; bool lAnyPlanBusy = false; // If all backup plans have status == NO_STATUS then tooltip title will be empty QString lToolTipTitle; QString lToolTipSubTitle = i18nc("status in tooltip", "Backup destination not available"); QString lToolTipIconName = QStringLiteral("kup"); if(mExecutors.isEmpty()) { lToolTipTitle = i18n("No backup plans configured"); lToolTipSubTitle.clear(); } foreach(PlanExecutor *lExec, mExecutors) { if(lExec->destinationAvailable()) { lToolTipSubTitle = i18nc("status in tooltip", "Backup destination available"); if(lExec->scheduleType() == BackupPlan::MANUAL) { lTrayIconActive = true; } } } foreach(PlanExecutor *lExec, mExecutors) { if(lExec->mPlan->backupStatus() == BackupPlan::GOOD) { lToolTipIconName = BackupPlan::iconName(BackupPlan::GOOD); lToolTipTitle = i18nc("status in tooltip", "Backup status OK"); } } foreach(PlanExecutor *lExec, mExecutors) { if(lExec->mPlan->backupStatus() == BackupPlan::MEDIUM) { lToolTipIconName = BackupPlan::iconName(BackupPlan::MEDIUM); lToolTipTitle = i18nc("status in tooltip", "New backup suggested"); } } foreach(PlanExecutor *lExec, mExecutors) { if(lExec->mPlan->backupStatus() == BackupPlan::BAD) { lToolTipIconName = BackupPlan::iconName(BackupPlan::BAD); lToolTipTitle = i18nc("status in tooltip", "New backup needed"); lTrayIconActive = true; } } foreach(PlanExecutor *lExecutor, mExecutors) { if(lExecutor->busy()) { lToolTipIconName = QStringLiteral("kup"); lToolTipTitle = lExecutor->currentActivityTitle(); lToolTipSubTitle = lExecutor->mPlan->mDescription; lAnyPlanBusy = true; } } if(lToolTipTitle.isEmpty() && !lToolTipSubTitle.isEmpty()) { lToolTipTitle = lToolTipSubTitle; lToolTipSubTitle.clear(); } QJsonObject lStatus; lStatus["event"] = QStringLiteral("status update"); lStatus["tray icon active"] = lTrayIconActive; lStatus["tooltip icon name"] = lToolTipIconName; lStatus["tooltip title"] = lToolTipTitle; lStatus["tooltip subtitle"] = lToolTipSubTitle; lStatus["any plan busy"] = lAnyPlanBusy; lStatus["no plan reason"] = mExecutors.isEmpty() ? i18n("No backup plans configured") : QString(); QJsonArray lPlans; foreach(PlanExecutor *lExecutor, mExecutors) { QJsonObject lPlan; lPlan[QStringLiteral("description")] = lExecutor->mPlan->mDescription; lPlan[QStringLiteral("destination available")] = lExecutor->destinationAvailable(); lPlan[QStringLiteral("status heading")] = lExecutor->currentActivityTitle(); lPlan[QStringLiteral("status details")] = lExecutor->mPlan->statusText(); lPlan[QStringLiteral("icon name")] = BackupPlan::iconName(lExecutor->mPlan->backupStatus()); lPlan[QStringLiteral("log file exists")] = QFileInfo::exists(lExecutor->mLogFilePath); lPlan[QStringLiteral("busy")] = lExecutor->busy(); lPlan[QStringLiteral("bup type")] = lExecutor->mPlan->mBackupType == BackupPlan::BupType; lPlans.append(lPlan); } lStatus["plans"] = lPlans; QJsonDocument lDoc(lStatus); pSocket->write(lDoc.toBinaryData()); } kup-backup-0.9.1/daemon/kupdaemon.h000066400000000000000000000023561420500356400171670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPDAEMON_H #define KUPDAEMON_H #include #define KUP_DBUS_SERVICE_NAME QStringLiteral("org.kde.kupdaemon") #define KUP_DBUS_OBJECT_PATH QStringLiteral("/DaemonControl") class KupSettings; class PlanExecutor; class KJob; class KUiServerJobTracker; class QLocalServer; class QLocalSocket; class QSessionManager; class QTimer; class KupDaemon : public QObject { Q_OBJECT public: KupDaemon(); ~KupDaemon() override; bool shouldStart(); void setupGuiStuff(); void slotShutdownRequest(QSessionManager &pManager); void registerJob(KJob *pJob); void unregisterJob(KJob *pJob); public slots: void reloadConfig(); void runIntegrityCheck(const QString& pPath); private: void setupExecutors(); void handleRequests(QLocalSocket *pSocket); void sendStatus(QLocalSocket *pSocket); KSharedConfigPtr mConfig; KupSettings *mSettings; QList mExecutors; QTimer *mUsageAccTimer{}; QTimer *mStatusUpdateTimer{}; bool mWaitingToReloadConfig; KUiServerJobTracker *mJobTracker; QLocalServer *mLocalServer; QList mSockets; }; #endif /*KUPDAEMON_H*/ kup-backup-0.9.1/daemon/kupdaemon.notifyrc000066400000000000000000000324621420500356400205760ustar00rootroot00000000000000[Global] IconName=kup Name=Kup Backup System Name[ca]=Sistema per a còpia de seguretat Kup Name[ca@valencia]=Sistema per a còpia de seguretat Kup Name[cs]=Zálohovací systém Kup Name[de]=Kup-Sicherungssystem Name[en_GB]=Kup Backup System Name[es]=Sistema de copias de seguridad Kup Name[fi]=Kup-varmuuskopiointijärjestelmä Name[fr]=Système de sauvegarde Kup Name[it]=Sistema di copie di sicurezza Kup Name[ko]=Kup 백업 시스템 Name[nl]=Kup systeem voor back-up Name[pl]=System kopii zapasowych Kup Name[pt]=Sistema de Cópias de Segurança Kup Name[pt_BR]=Sistema de backups Kup Name[sl]=Kup sistem varnostnega kopiranja Name[sv]=Kup säkerhetskopieringssystem Name[uk]=Система резервного копіювання Kup Name[x-test]=xxKup Backup Systemxx Name[zh_CN]=K 备份系统 Comment=Kup Backup System Comment[ca]=Sistema per a còpia de seguretat Kup Comment[ca@valencia]=Sistema per a còpia de seguretat Kup Comment[de]=Kup-Sicherungssystem Comment[en_GB]=Kup Backup System Comment[es]=Sistema de copias de seguridad Kup Comment[et]=Kupi süsteemi varundamine Comment[eu]=Kup babes-kopia sistema Comment[fi]=Kup-varmuuskopiointijärjestelmä Comment[fr]=Système de sauvegarde Kup Comment[it]=Sistema di copie di sicurezza Kup Comment[ko]=Kup 백업 시스템 Comment[nl]=Kup systeem voor back-up Comment[pl]=System kopii zapasowych Kup Comment[pt]=Sistema de Cópias de Segurança Kup Comment[pt_BR]=Sistema de backups Kup Comment[sl]=Kup sistem varnostnih kopij Comment[sv]=Kup säkerhetskopieringssystem Comment[uk]=Система резервного копіювання Kup Comment[x-test]=xxKup Backup Systemxx Comment[zh_CN]=K 备份系统 Comment[zh_TW]=Kup 備份系統 [Event/StartBackup] Name=Start Saving Backup Name[ca]=Comença a desar la còpia de seguretat Name[ca@valencia]=Comença a guardar la còpia de seguretat Name[cs]=Započít ukládání zálohy Name[de]=Speichern der Sicherung starten Name[en_GB]=Start Saving Backup Name[es]=Empezar a crear una copia de seguridad Name[fi]=Aloita varmuuskopiointi Name[fr]=Démarrage de l'enregistrement de la sauvegarde Name[it]=Avvia il salvataggio della copia di sicurezza Name[ko]=백업 저장 시작 Name[nl]=Begin met opslaan van back-up Name[pl]=Rozpocznij zapisywanie kopii zapasowej Name[pt]=Iniciar a Gravação da Cópia de Segurança Name[pt_BR]=Comece a salvar backups Name[sl]=Začni shranjevati varnostno kopijo Name[sv]=Starta säkerhetskopiering Name[uk]=Початок збереження резервної копії Name[x-test]=xxStart Saving Backupxx Name[zh_CN]=开始保存备份 Comment=A question if user wants to start saving a backup Comment[ca]=Una pregunta per a si l'usuari vol començar a desar una còpia de seguretat Comment[ca@valencia]=Una pregunta per a si l'usuari vol començar a guardar una còpia de seguretat Comment[de]=Frage, ob der Benutzer das Speichern einer Sicherung starten möchte Comment[en_GB]=A question if user wants to start saving a backup Comment[es]=Preguntar si el usuario quiere empezar a crear una copia de seguridad Comment[fi]=Kysyy, haluaako käyttäjä aloittaa varmuuskopioinnin Comment[fr]=Une question si un utilisateur souhaite démarrer un enregistrement d'une sauvegarde Comment[it]=Una domanda se l'utente vuole iniziare il salvataggio di una copia di sicurezza Comment[ko]=사용자가 백업 저장을 시작할지 질문 Comment[nl]=Een vraag of gebruiker wil beginnen met het opslaan van een backup Comment[pl]=Pytanie, czy użytkownik chce rozpocząć zapisywanie kopii zapasowej Comment[pt]=Uma pergunta se o utilizador deseja iniciar a gravação da cópia de segurança Comment[pt_BR]=Uma pergunta se o usuário deseja começar a salvar um backup Comment[sl]=Vprašanje, ali želi uporabnik začeti shranjevati varnostno kopijo Comment[sv]=En fråga om användaren vill starta en säkerhetskopiering Comment[uk]=Запитання щодо того, чи хоче користувач зберегти резервну копію Comment[x-test]=xxA question if user wants to start saving a backupxx Comment[zh_CN]=用户想要开始保存备份时询问 Action=Popup Urgency=Normal [Event/BackupSucceeded] Name=Backup Succeeded Name[ca]=La còpia de seguretat ha tingut èxit Name[ca@valencia]=La còpia de seguretat ha tingut èxit Name[cs]=Zálohování bylo úspěšné Name[de]=Sicherung erfolgreich Name[en_GB]=Backup Succeeded Name[es]=Copia de seguridad creada con éxito Name[fi]=Varmuuskopiointi onnistui Name[fr]=Sauvegarde réussie Name[it]=Copia di sicurezza riuscita Name[ko]=백업 성공 Name[nl]=Backup maken is gelukt Name[pl]=Pomyślnie utworzono kopię zapasową Name[pt]=Cópia de Segurança com Sucesso Name[pt_BR]=Backup feito com sucesso Name[sl]=Varnostno kopiranje je uspelo Name[sv]=Säkerhetskopiering lyckades Name[uk]=Резервну копію успішно створено Name[x-test]=xxBackup Succeededxx Name[zh_CN]=备份成功 Comment=Saving of backup successfully completed Comment[ca]=S'ha finalitzat correctament el desament de la còpia de seguretat Comment[ca@valencia]=S'ha finalitzat correctament el desament de la còpia de seguretat Comment[de]=Speichern der Sicherung erfolgreich abgeschlossen Comment[en_GB]=Saving of backup successfully completed Comment[es]=El guardado de la copia de seguridad acabó satisfactoriamente Comment[et]=Varukoopia salvestamine lõpetati edukalt Comment[eu]=Babes-kopia gordetzea ondo burutu da Comment[fi]=Varmuuskopion tallennus päättyi onnistuneesti Comment[fr]=Enregistrement de la sauvegarde terminée avec succès Comment[it]=Salvataggio della copia di sicurezza completato correttamente Comment[ko]=백업 저장이 성공적으로 완료됨 Comment[nl]=Opslaan van back-up met succes voltooid Comment[pl]=Pomyślnie ukończono zapisywanie kopii zapasowej Comment[pt]=A gravação da cópia de segurança terminou com sucesso Comment[pt_BR]=Salvamento do backup concluído com sucesso Comment[sl]=Shranjevanje varnostne kopije uspešno končano Comment[sv]=Säkerhetskopieringen har avslutats med lyckat resultat Comment[uk]=Збереження резервної копії успішно завершено Comment[x-test]=xxSaving of backup successfully completedxx Comment[zh_CN]=备份保存完成 Comment[zh_TW]=成功儲存備份 Action=Popup Urgency=Normal [Event/BackupFailed] Name=Backup Failed Name[ca]=Ha fallat la còpia de seguretat Name[ca@valencia]=Ha fallat la còpia de seguretat Name[cs]=Zálohování selhalo Name[de]=Sicherung fehlgeschlagen Name[en_GB]=Backup Failed Name[es]=La copia de seguridad falló Name[fi]=Varmuuskopiointi epäonnistui Name[fr]=Échec de la sauvegarde Name[it]=Copia di sicurezza non riuscita Name[ko]=백업 실패 Name[nl]=Back-up maken is mislukt Name[pl]=Nie udało się zapisać kopii zapasowej Name[pt]=Cópia de Segurança sem Sucesso Name[pt_BR]=Falha no backup Name[sl]=Varnostno kopiranje ni uspelo Name[sv]=Säkerhetskopiering misslyckades Name[uk]=Не вдалося виконати резервне копіювання Name[x-test]=xxBackup Failedxx Name[zh_CN]=备份失败 Comment=Saving of backup failed, offer user to see log file Comment[ca]=Ha fallat el desament de la còpia de seguretat, s'ofereix a l'usuari el fitxer de registre Comment[ca@valencia]=Ha fallat el desament de la còpia de seguretat, s'ofereix a l'usuari el fitxer de registre Comment[de]=Speicherung der Sicherung ist fehlgeschlagen, der Benutzer kann die Protokolldatei lesen Comment[en_GB]=Saving of backup failed, offer user to see log file Comment[es]=El guardado de la copia de seguridad falló, ofreciendo al usuario ver los registros Comment[et]=Varukoopia salvestamine nurjus, kasutajal palutakse uurida logifaili Comment[eu]=Babes-kopia gordetzea huts egin du, eskaini erabiltzaileari egunkari fitxategia ikustea Comment[fi]=Varmuuskopion tallennus epäonnistui: anna käyttäjälle mahdollisuus tarkastella lokitiedostoa Comment[fr]=Échec de l'enregistrement de la sauvegarde, l'utilisateur est invité à regarder le fichier de journal Comment[it]=Salvataggio del backup non riuscito, permetti all'utente di vedere il file di registro Comment[ko]=백업 저장이 실패함, 로그 파일을 볼 수 있음 Comment[nl]=Opslaan van back-up is mislukt, biedt de gebruiker om het logbestand te zien Comment[pl]=Nie udało się zapisać kopii zapasowej. Użytkownik może zobaczyć dziennik Comment[pt]=A gravação da cópia de segurança falhou; permitir ao utilizador ver o ficheiro de registo Comment[pt_BR]=Falha ao salvar o backup, oferecer ao usuário ver o arquivo de log Comment[sl]=Shranjevanje varnostne kopije ni uspelo, ponudba uporabniku za vpogled v dnevnik dogajanja Comment[sv]=Misslyckades spara säkerhetskopia, erbjud användaren att titta på loggfilen Comment[uk]=Не вдалося зберегти резервну копію, користувачеві запропоновано переглянути файл журналу Comment[x-test]=xxSaving of backup failed, offer user to see log filexx Comment[zh_CN]=备份保存失败,请查看日志 Action=Popup Urgency=Normal [Event/IntegrityCheckCompleted] Name=Integrity Check Completed Name[ca]=S'ha completat el control de la integritat Name[ca@valencia]=S'ha completat el control de la integritat Name[cs]=Kontrola neporušenosti dokončena Name[de]=Integritätsprüfung abgeschlossen Name[en_GB]=Integrity Check Completed Name[es]=Comprobación de integridad completada Name[fi]=Eheystarkistus on valmis Name[fr]=Vérification d'intégrité terminée Name[it]=Controllo di integrità completato Name[ko]=무결성 검사 완료됨 Name[nl]=Integriteitscontrole afgerond Name[pl]=Ukończono sprawdzanie spójności Name[pt]=Verificação de Integridade Completa Name[pt_BR]=Verificação de integridade concluída Name[sl]=Preverjanje celovitosti dokončano Name[sv]=Integritetskontroll klar Name[uk]=Перевірку цілісності завершено Name[x-test]=xxIntegrity Check Completedxx Name[zh_CN]=完整性检查完成 Comment=Finished checking integrity of backup archive Comment[ca]=S'ha acabat de verificar la integritat de l'arxiu de còpia de seguretat Comment[ca@valencia]=S'ha acabat de verificar la integritat de l'arxiu de còpia de seguretat Comment[de]=Integritätsprüfung des Sicherungsarchivs abgeschlossen Comment[en_GB]=Finished checking integrity of backup archive Comment[es]=Se terminó de comprobar la integridad de la copia de seguridad Comment[et]=Varukoopa terviklikkuse kontrollimine lõpetati Comment[eu]=Babes-kopia artxiboaren osotasun egiaztatzea amaitu da Comment[fi]=Varmuuskopioarkiston eheyden tarkistus on valmis Comment[fr]=Fin de vérification d'intégrité de l'archive de sauvegarde Comment[it]=Controllo di integrità dell'archivio della copia di sicurezza terminato Comment[ko]=백업 압축 파일의 무결성 검사가 완료됨 Comment[nl]=Controleren van integriteit van back-up-archief is beëindigd Comment[pl]=Ukończono sprawdzanie spójności archiwum kopii zapasowej Comment[pt]=Terminou a verificação de integridade do pacote da cópia de segurança Comment[pt_BR]=Concluída a verificação de integridade do arquivo de backup Comment[sl]=Končano preverjanje celovitosti varnostnega arhiva Comment[sv]=Avslutade kontroll av säkerhetskopieringsarkivets integritet Comment[uk]=Перевірку цілісності архіву резервної копії завершено Comment[x-test]=xxFinished checking integrity of backup archivexx Comment[zh_CN]=检查备份归档的完整性完成 Comment[zh_TW]=完成檢查備份封存檔的完整性 Action=Popup Urgency=Normal [Event/RepairCompleted] Name=Repair Completed Name[ca]=Reparació finalitzada Name[ca@valencia]=Reparació finalitzada Name[cs]=Oprava dokončena Name[de]=Reparatur abgeschlossen Name[en_GB]=Repair Completed Name[es]=Reparación completa Name[fi]=Korjaus on valmis Name[fr]=Réparation terminée Name[it]=Riparazione completata Name[ko]=복구 완료됨 Name[nl]=Reparatie afgerond Name[pl]=Ukończono naprawianie Name[pt]=Reparação Completa Name[pt_BR]=Reparo concluído Name[sl]=Popravljanje končano Name[sv]=Reparation klar Name[uk]=Виправлення завершено Name[x-test]=xxRepair Completedxx Name[zh_CN]=修复完成 Comment=Finished repairing backup archive Comment[ca]=S'ha acabat la reparació de l'arxiu de còpia de seguretat Comment[ca@valencia]=S'ha acabat la reparació de l'arxiu de còpia de seguretat Comment[de]=Reparatur des Sicherungsarchivs abgeschlossen Comment[en_GB]=Finished repairing backup archive Comment[es]=Se terminó de reparar la copia de seguridad Comment[et]=Varukoopia parandamine lõpetati Comment[eu]=Babes-kopiaren artxiboa konpontzen amaitu da Comment[fi]=Varmuuskopioarkiston korjaus on valmis Comment[fr]=Fin de réparation de l'archive de sauvegarde Comment[it]=Riparazione dell'archivio della copia di sicurezza terminata Comment[ko]=백업 압축 파일 복구가 완료됨 Comment[nl]=Repareren van back-up-archief is beëindigd Comment[pl]=Ukończono naprawianie archiwum kopii zapasowej Comment[pt]=Terminou a reparação do pacote da cópia de segurança Comment[pt_BR]=Concluído o reparo do arquivo de backup Comment[sl]=Končano popravljanje varnostnega arhiva Comment[sv]=Avslutade reparation av säkerhetskopieringsarkivet Comment[uk]=Завершено відновлення резервної копії з архіву Comment[x-test]=xxFinished repairing backup archivexx Comment[zh_CN]=修复备份归档完成 Comment[zh_TW]=完成修復備份封存檔 Action=Popup Urgency=Normal kup-backup-0.9.1/daemon/main.cpp000066400000000000000000000041361420500356400164610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupdaemon.h" #include "kupdaemon_debug.h" #include #include #include #include #include extern "C" int Q_DECL_EXPORT kdemain(int argc, char *argv[]) { QApplication lApp(argc, argv); QApplication::setQuitOnLastWindowClosed(false); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); KLocalizedString::setApplicationDomain("kup"); qCDebug(KUPDAEMON) << "Running Kup daemon..."; auto *lDaemon = new KupDaemon(); if(!lDaemon->shouldStart()) { qCCritical(KUPDAEMON) << xi18nc("@info:shell Error message at startup", "Kup is not enabled, enable it from the " "system settings module. You can do that by running " "kcmshell5 kup"); return 0; } KAboutData lAbout(QStringLiteral("kupdaemon"), xi18nc("@title", "Kup Daemon"), QStringLiteral("0.9.1"), i18n("Kup is a flexible backup solution using the backup storage system 'bup'. " "This allows it to quickly perform incremental backups, only saving the " "parts of files that has actually changed since last backup was saved."), KAboutLicense::GPL, i18n("Copyright (C) 2011-2020 Simon Persson")); lAbout.addAuthor(i18n("Simon Persson"), i18n("Maintainer"), "simon.persson@mykolab.com"); lAbout.setTranslator(xi18nc("NAME OF TRANSLATORS", "Your names"), xi18nc("EMAIL OF TRANSLATORS", "Your emails")); KAboutData::setApplicationData(lAbout); QCommandLineParser lParser; lAbout.setupCommandLine(&lParser); lParser.process(lApp); lAbout.processCommandLine(&lParser); // This call will exit() if an instance is already running KDBusService lService(KDBusService::Unique); lDaemon->setupGuiStuff(); KupDaemon::connect(&lApp, &QApplication::commitDataRequest, lDaemon, [lDaemon](QSessionManager &pManager) { lDaemon->slotShutdownRequest(pManager); }); return QApplication::exec(); } kup-backup-0.9.1/daemon/planexecutor.cpp000066400000000000000000000416311420500356400202470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "planexecutor.h" #include "bupjob.h" #include "bupverificationjob.h" #include "buprepairjob.h" #include "kupdaemon.h" #include "kupdaemon_debug.h" #include "rsyncjob.h" #include #include #include #include #include #include #include #include #include #include static const QString cPwrMgmtServiceName = QStringLiteral("org.freedesktop.PowerManagement"); static const QString cPwrMgmtPath = QStringLiteral("/org/freedesktop/PowerManagement"); static const QString cPwrMgmtInhibitInterface = QStringLiteral("org.freedesktop.PowerManagement.Inhibit"); static const QString cPwrMgmtInterface = QStringLiteral("org.freedesktop.PowerManagement"); PlanExecutor::PlanExecutor(BackupPlan *pPlan, KupDaemon *pKupDaemon) :QObject(pKupDaemon), mState(NOT_AVAILABLE), mPlan(pPlan), mQuestion(nullptr), mFailNotification(nullptr), mIntegrityNotification(nullptr), mRepairNotification(nullptr), mLastState(NOT_AVAILABLE), mKupDaemon(pKupDaemon), mSleepCookie(0) { QString lCachePath = QString::fromLocal8Bit(qgetenv("XDG_CACHE_HOME").constData()); if(lCachePath.isEmpty()) { lCachePath = QDir::homePath(); lCachePath.append(QStringLiteral("/.cache")); } lCachePath.append(QStringLiteral("/kup")); QDir lCacheDir(lCachePath); if(!lCacheDir.exists()) { if(!lCacheDir.mkpath(lCachePath)) { lCachePath = QStringLiteral("/tmp"); } } mLogFilePath = lCachePath; mLogFilePath.append(QStringLiteral("/kup_plan")); mLogFilePath.append(QString::number(mPlan->planNumber())); mLogFilePath.append(QStringLiteral(".log")); mSchedulingTimer = new QTimer(this); mSchedulingTimer->setSingleShot(true); connect(mSchedulingTimer, SIGNAL(timeout()), SLOT(enterAvailableState())); } PlanExecutor::~PlanExecutor() = default; QString PlanExecutor::currentActivityTitle() { switch(mState) { case BACKUP_RUNNING: return i18nc("status in tooltip", "Saving backup"); case INTEGRITY_TESTING: return i18nc("status in tooltip", "Checking backup integrity"); case REPAIRING: return i18nc("status in tooltip", "Repairing backups"); default:; } switch (mPlan->backupStatus()) { case BackupPlan::GOOD: return i18nc("status in tooltip", "Backup status OK"); case BackupPlan::MEDIUM: return i18nc("status in tooltip", "New backup suggested"); case BackupPlan::BAD: return i18nc("status in tooltip", "New backup needed"); default:; } return QString(); } // dispatcher code for entering one of the available states void PlanExecutor::enterAvailableState() { if(mState == NOT_AVAILABLE) { mState = WAITING_FOR_FIRST_BACKUP; //initial child state of "Available" state emit stateChanged(); } QDateTime lNow = QDateTime::currentDateTimeUtc(); switch(mPlan->mScheduleType) { case BackupPlan::MANUAL: break; case BackupPlan::INTERVAL: { QDateTime lNextTime = mPlan->nextScheduledTime(); if(!lNextTime.isValid() || lNextTime < lNow) { if(!mPlan->mLastCompleteBackup.isValid()) askUserOrStart(xi18nc("@info", "Do you want to save a first backup now?")); else { QString t = KFormat().formatSpelloutDuration(static_cast(mPlan->mLastCompleteBackup.secsTo(lNow)) * 1000); askUserOrStart(xi18nc("@info", "It has been %1 since last backup was saved.\n" "Save a new backup now?", t)); } } else { // schedule a wakeup for asking again when the time is right. mSchedulingTimer->start(static_cast(lNow.secsTo(lNextTime)*1000)); } break; } case BackupPlan::USAGE: if(!mPlan->mLastCompleteBackup.isValid()) { askUserOrStart(xi18nc("@info", "Do you want to save a first backup now?")); } else if(mPlan->mAccumulatedUsageTime > static_cast(mPlan->mUsageLimit) * 3600) { QString t = KFormat().formatSpelloutDuration(mPlan->mAccumulatedUsageTime * 1000); askUserOrStart(xi18nc("@info", "You have been active for %1 since last backup was saved.\n" "Save a new backup now?", t)); } break; } } void PlanExecutor::askUserOrStart(const QString& pUserQuestion) { // Only ask the first time after destination has become available. // Always ask if power saving is active. if( (mPlan->mAskBeforeTakingBackup && mState == WAITING_FOR_FIRST_BACKUP) || powerSaveActive()) { askUser(pUserQuestion); } else { startBackupSaveJob(); } } void PlanExecutor::enterNotAvailableState() { discardUserQuestion(); mSchedulingTimer->stop(); mState = NOT_AVAILABLE; emit stateChanged(); } void PlanExecutor::askUser(const QString &pQuestion) { discardUserQuestion(); mQuestion = new KNotification(QStringLiteral("StartBackup"), KNotification::Persistent); mQuestion->setTitle(mPlan->mDescription); mQuestion->setText(pQuestion); QStringList lAnswers; lAnswers << xi18nc("@action:button", "Yes") << xi18nc("@action:button", "No"); mQuestion->setActions(lAnswers); connect(mQuestion, SIGNAL(action1Activated()), SLOT(startBackupSaveJob())); connect(mQuestion, SIGNAL(action2Activated()), SLOT(discardUserQuestion())); connect(mQuestion, SIGNAL(closed()), SLOT(discardUserQuestion())); connect(mQuestion, SIGNAL(ignored()), SLOT(discardUserQuestion())); // enter this "do nothing" state, if user answers "no" or ignores, remain there mState = WAITING_FOR_MANUAL_BACKUP; emit stateChanged(); mQuestion->sendEvent(); } void PlanExecutor::discardUserQuestion() { if(mQuestion) { mQuestion->deleteLater(); mQuestion = nullptr; } } void PlanExecutor::notifyBackupFailed(KJob *pFailedJob) { discardFailNotification(); mFailNotification = new KNotification(QStringLiteral("BackupFailed"), KNotification::Persistent); mFailNotification->setTitle(xi18nc("@title:window", "Saving of Backup Failed")); mFailNotification->setText(pFailedJob->errorText()); QStringList lAnswers; if(pFailedJob->error() == BackupJob::ErrorWithLog) { lAnswers << xi18nc("@action:button", "Show log file"); connect(mFailNotification, SIGNAL(action1Activated()), SLOT(showLog())); } else if(pFailedJob->error() == BackupJob::ErrorSuggestRepair) { lAnswers << xi18nc("@action:button", "Yes"); lAnswers << xi18nc("@action:button", "No"); connect(mFailNotification, SIGNAL(action1Activated()), SLOT(startRepairJob())); } else if(pFailedJob->error() == BackupJob::ErrorSourcesConfig) { lAnswers << xi18nc("@action:button", "Open settings"); connect(mFailNotification, &KNotification::action1Activated, this, [this] { QProcess::startDetached(QStringLiteral("kcmshell5"), {QStringLiteral("--args"), QStringLiteral("show_sources %1").arg(mPlan->planNumber()), QStringLiteral("kcm_kup")}); }); } mFailNotification->setActions(lAnswers); connect(mFailNotification, SIGNAL(action2Activated()), SLOT(discardFailNotification())); connect(mFailNotification, SIGNAL(closed()), SLOT(discardFailNotification())); connect(mFailNotification, SIGNAL(ignored()), SLOT(discardFailNotification())); mFailNotification->sendEvent(); } void PlanExecutor::discardFailNotification() { if(mFailNotification) { mFailNotification->deleteLater(); mFailNotification = nullptr; } } void PlanExecutor::notifyBackupSucceeded() { auto *lNotification = new KNotification(QStringLiteral("BackupSucceeded")); lNotification->setTitle(xi18nc("@title:window", "Backup Saved")); lNotification->setText(xi18nc("@info notification", "Saving backup completed successfully.")); lNotification->sendEvent(); } void PlanExecutor::showLog() { KRun::runUrl(QUrl::fromLocalFile(mLogFilePath), QStringLiteral("text/x-log"), nullptr, KRun::RunFlags()); } void PlanExecutor::startIntegrityCheck() { if(mPlan->mBackupType != BackupPlan::BupType || busy() || !destinationAvailable()) { return; } KJob *lJob = new BupVerificationJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); connect(lJob, SIGNAL(result(KJob*)), SLOT(integrityCheckFinished(KJob*))); lJob->start(); mLastState = mState; mState = INTEGRITY_TESTING; emit stateChanged(); startSleepInhibit(); } void PlanExecutor::startRepairJob() { if(mPlan->mBackupType != BackupPlan::BupType || busy() || !destinationAvailable()) { return; } KJob *lJob = new BupRepairJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); connect(lJob, SIGNAL(result(KJob*)), SLOT(repairFinished(KJob*))); lJob->start(); mLastState = mState; mState = REPAIRING; emit stateChanged(); startSleepInhibit(); } void PlanExecutor::startBackupSaveJob() { if(busy() || !destinationAvailable()) { return; } discardUserQuestion(); mState = BACKUP_RUNNING; emit stateChanged(); startSleepInhibit(); startBackup(); } void PlanExecutor::startBackup() { QDir lDir(mDestinationPath); if(!lDir.exists()) { lDir.mkdir(mDestinationPath); } QFileInfo lInfo(mDestinationPath); if(!lInfo.isWritable()) { KNotification::event(KNotification::Error, xi18nc("@title:window", "Problem"), xi18nc("notification", "You don't have write permission to backup destination.")); exitBackupRunningState(false); return; } BackupJob *lJob = createBackupJob(); if(lJob == nullptr) { KNotification::event(KNotification::Error, xi18nc("@title:window", "Problem"), xi18nc("notification", "Invalid type of backup in configuration.")); exitBackupRunningState(false); return; } connect(lJob, &KJob::result, this, &PlanExecutor::finishBackup); lJob->start(); } void PlanExecutor::finishBackup(KJob *pJob) { if(pJob->error()) { if(pJob->error() != KJob::KilledJobError) { notifyBackupFailed(pJob); } exitBackupRunningState(false); } else { notifyBackupSucceeded(); mPlan->mLastCompleteBackup = QDateTime::currentDateTimeUtc(); auto lSpaceInfo = KDiskFreeSpaceInfo::freeSpaceInfo(mDestinationPath); if(lSpaceInfo.isValid()) mPlan->mLastAvailableSpace = static_cast(lSpaceInfo.available()); else mPlan->mLastAvailableSpace = -1.0; //unknown size auto lSizeJob = KIO::directorySize(QUrl::fromLocalFile(mDestinationPath)); connect(lSizeJob, &KJob::result, this, &PlanExecutor::finishSizeCheck); lSizeJob->start(); } } void PlanExecutor::finishSizeCheck(KJob* pJob) { if(pJob->error()) { KNotification::event(KNotification::Error, xi18nc("@title:window", "Problem"), pJob->errorText()); mPlan->mLastBackupSize = -1.0; //unknown size } else { auto lSizeJob = qobject_cast(pJob); mPlan->mLastBackupSize = static_cast(lSizeJob->totalSize()); } mPlan->save(); exitBackupRunningState(pJob->error() == 0); } void PlanExecutor::integrityCheckFinished(KJob *pJob) { endSleepInhibit(); discardIntegrityNotification(); mIntegrityNotification = new KNotification(QStringLiteral("IntegrityCheckCompleted"), KNotification::Persistent); mIntegrityNotification->setTitle(xi18nc("@title:window", "Integrity Check Completed")); mIntegrityNotification->setText(pJob->errorText()); QStringList lAnswers; if(pJob->error() == BackupJob::ErrorWithLog) { lAnswers << xi18nc("@action:button", "Show log file"); connect(mIntegrityNotification, SIGNAL(action1Activated()), SLOT(showLog())); } else if(pJob->error() == BackupJob::ErrorSuggestRepair) { lAnswers << xi18nc("@action:button", "Yes"); lAnswers << xi18nc("@action:button", "No"); connect(mIntegrityNotification, SIGNAL(action1Activated()), SLOT(startRepairJob())); } mIntegrityNotification->setActions(lAnswers); connect(mIntegrityNotification, SIGNAL(action2Activated()), SLOT(discardIntegrityNotification())); connect(mIntegrityNotification, SIGNAL(closed()), SLOT(discardIntegrityNotification())); connect(mIntegrityNotification, SIGNAL(ignored()), SLOT(discardIntegrityNotification())); mIntegrityNotification->sendEvent(); if(mState == INTEGRITY_TESTING) { //only restore if nothing has changed during the run mState = mLastState; } emit stateChanged(); } void PlanExecutor::discardIntegrityNotification() { if(mIntegrityNotification) { mIntegrityNotification->deleteLater(); mIntegrityNotification = nullptr; } } void PlanExecutor::repairFinished(KJob *pJob) { endSleepInhibit(); discardRepairNotification(); mRepairNotification = new KNotification(QStringLiteral("RepairCompleted"), KNotification::Persistent); mRepairNotification->setTitle(xi18nc("@title:window", "Repair Completed")); mRepairNotification->setText(pJob->errorText()); QStringList lAnswers; lAnswers << xi18nc("@action:button", "Show log file"); mRepairNotification->setActions(lAnswers); connect(mRepairNotification, SIGNAL(action1Activated()), SLOT(showLog())); connect(mRepairNotification, SIGNAL(closed()), SLOT(discardRepairNotification())); connect(mRepairNotification, SIGNAL(ignored()), SLOT(discardRepairNotification())); mRepairNotification->sendEvent(); if(mState == REPAIRING) { //only restore if nothing has changed during the run mState = mLastState; } emit stateChanged(); } void PlanExecutor::discardRepairNotification() { if(mRepairNotification) { mRepairNotification->deleteLater(); mRepairNotification = nullptr; } } void PlanExecutor::startSleepInhibit() { if(mSleepCookie != 0) { return; } QDBusMessage lMsg = QDBusMessage::createMethodCall(cPwrMgmtServiceName, cPwrMgmtPath, cPwrMgmtInhibitInterface, QStringLiteral("Inhibit")); lMsg << i18n("Kup Backup System"); lMsg << currentActivityTitle(); QDBusReply lReply = QDBusConnection::sessionBus().call(lMsg); mSleepCookie = lReply.value(); } void PlanExecutor::endSleepInhibit() { if(mSleepCookie == 0) { return; } QDBusMessage lMsg = QDBusMessage::createMethodCall(cPwrMgmtServiceName, cPwrMgmtPath, cPwrMgmtInhibitInterface, QStringLiteral("UnInhibit")); lMsg << mSleepCookie; QDBusConnection::sessionBus().asyncCall(lMsg); mSleepCookie = 0; } void PlanExecutor::exitBackupRunningState(bool pWasSuccessful) { endSleepInhibit(); if(pWasSuccessful) { if(mPlan->mScheduleType == BackupPlan::USAGE) { //reset usage time after successful backup mPlan->mAccumulatedUsageTime =0; mPlan->save(); } mState = WAITING_FOR_BACKUP_AGAIN; emit stateChanged(); //don't know if status actually changed, potentially did... so trigger a re-read of status emit backupStatusChanged(); // re-enter the main "available" state dispatcher enterAvailableState(); } else { mState = WAITING_FOR_MANUAL_BACKUP; emit stateChanged(); } } void PlanExecutor::updateAccumulatedUsageTime() { if(mState == BACKUP_RUNNING) { //usage time during backup doesn't count... return; } if(mPlan->mScheduleType == BackupPlan::USAGE) { mPlan->mAccumulatedUsageTime += KUP_USAGE_MONITOR_INTERVAL_S; mPlan->save(); } // trigger refresh of backup status, potentially changed since some time has passed... // this is the reason why this slot is called repeatedly even when // not in BackupPlan::USAGE mode emit backupStatusChanged(); //if we're waiting to run backup again, check if it is time now. if(mPlan->mScheduleType == BackupPlan::USAGE && (mState == WAITING_FOR_FIRST_BACKUP || mState == WAITING_FOR_BACKUP_AGAIN)) { enterAvailableState(); } } void PlanExecutor::showBackupFiles() { if(mState == NOT_AVAILABLE) return; if(mPlan->mBackupType == BackupPlan::BupType) { QStringList lArgs; lArgs << QStringLiteral("--title") << mPlan->mDescription; lArgs << mDestinationPath; KProcess::startDetached(QStringLiteral("kup-filedigger"), lArgs); } else if(mPlan->mBackupType == BackupPlan::RsyncType) { KRun::runUrl(QUrl::fromLocalFile(mDestinationPath), QStringLiteral("inode/directory"), nullptr, KRun::RunFlags()); } } void PlanExecutor::showBackupPurger() { if(mPlan->mBackupType != BackupPlan::BupType || busy() || !destinationAvailable()) { return; } QStringList lArgs; lArgs << QStringLiteral("--title") << mPlan->mDescription; lArgs << mDestinationPath; KProcess::startDetached(QStringLiteral("kup-purger"), lArgs); } BackupJob *PlanExecutor::createBackupJob() { if(mPlan->mBackupType == BackupPlan::BupType) { return new BupJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); } if(mPlan->mBackupType == BackupPlan::RsyncType) { return new RsyncJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); } qCWarning(KUPDAEMON) << "Invalid backup type in configuration!"; return nullptr; } bool PlanExecutor::powerSaveActive() { QDBusMessage lMsg = QDBusMessage::createMethodCall(cPwrMgmtServiceName, cPwrMgmtPath, cPwrMgmtInterface, QStringLiteral("GetPowerSaveStatus")); QDBusReply lReply = QDBusConnection::sessionBus().call(lMsg); return lReply.value(); } kup-backup-0.9.1/daemon/planexecutor.h000066400000000000000000000051621420500356400177130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef PLANEXECUTOR_H #define PLANEXECUTOR_H #include "backupplan.h" #include "backupjob.h" #include class KupDaemon; class KRun; class KNotification; class KProcess; class QTimer; // Accumulate usage time every KUP_USAGE_MONITOR_INTERVAL_S while user is active. // Consider user inactive after KUP_IDLE_TIMEOUT_S s of no keyboard or mouse activity. #define KUP_USAGE_MONITOR_INTERVAL_S 2*60 #define KUP_IDLE_TIMEOUT_S 30 class PlanExecutor : public QObject { Q_OBJECT public: PlanExecutor(BackupPlan *pPlan, KupDaemon *pKupDaemon); ~PlanExecutor() override; BackupPlan::ScheduleType scheduleType() { return static_cast(mPlan->mScheduleType); } bool busy() { return mState == BACKUP_RUNNING || mState == INTEGRITY_TESTING || mState == REPAIRING; } bool destinationAvailable() { return mState != NOT_AVAILABLE; } QString currentActivityTitle(); enum ExecutorState {NOT_AVAILABLE, WAITING_FOR_FIRST_BACKUP, WAITING_FOR_BACKUP_AGAIN, BACKUP_RUNNING, WAITING_FOR_MANUAL_BACKUP, INTEGRITY_TESTING, REPAIRING}; ExecutorState mState; QString mDestinationPath; QString mLogFilePath; BackupPlan *mPlan; public slots: virtual void checkStatus() = 0; virtual void showBackupFiles(); virtual void showBackupPurger(); void updateAccumulatedUsageTime(); void startIntegrityCheck(); void startRepairJob(); void startBackupSaveJob(); void showLog(); signals: void stateChanged(); void backupStatusChanged(); protected slots: virtual void startBackup(); void finishBackup(KJob *pJob); void finishSizeCheck(KJob *pJob); void exitBackupRunningState(bool pWasSuccessful); void enterAvailableState(); void askUserOrStart(const QString& pUserQuestion); void enterNotAvailableState(); void askUser(const QString &pQuestion); void discardUserQuestion(); void notifyBackupFailed(KJob *pFailedJob); void discardFailNotification(); static void notifyBackupSucceeded(); void integrityCheckFinished(KJob *pJob); void discardIntegrityNotification(); void repairFinished(KJob *pJob); void discardRepairNotification(); void startSleepInhibit(); void endSleepInhibit(); protected: BackupJob *createBackupJob(); static bool powerSaveActive(); KNotification *mQuestion; QTimer *mSchedulingTimer; KNotification *mFailNotification; KNotification *mIntegrityNotification; KNotification *mRepairNotification; ExecutorState mLastState; KupDaemon *mKupDaemon; uint mSleepCookie; }; #endif // PLANEXECUTOR_H kup-backup-0.9.1/daemon/rsyncjob.cpp000066400000000000000000000201641420500356400173650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "rsyncjob.h" #include "kuputils.h" #include #include #include #include #include RsyncJob::RsyncJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon) :BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon) { mRsyncProcess.setOutputChannelMode(KProcess::SeparateChannels); setCapabilities(KJob::Suspendable | KJob::Killable); } void RsyncJob::performJob() { KProcess lVersionProcess; lVersionProcess.setOutputChannelMode(KProcess::SeparateChannels); lVersionProcess << QStringLiteral("rsync") << QStringLiteral("--version"); if(lVersionProcess.execute() < 0) { jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", "The rsync program is needed but " "could not be found, maybe it is not installed?")); return; } // Remove this and the performMigration method when it is likely that all users of pre 0.8 kup have now started using post 0.8. if(mBackupPlan.mBackupVersion < 1 && mBackupPlan.mLastCompleteBackup.isValid() && mBackupPlan.mPathsIncluded.length() == 1) { mLogStream << QStringLiteral("Migrating saved files to new location, after update to version 0.8 of Kup.") << endl; if(!performMigration()) { mLogStream << QStringLiteral("Migration failed. Continuing backup save regardless, may result in files stored twice.") << endl; } } mBackupPlan.mBackupVersion = 1; mBackupPlan.save(); mLogStream << QStringLiteral("Kup is starting rsync backup job at ") << QLocale().toString(QDateTime::currentDateTime()) << endl; emit description(this, i18n("Checking what to copy")); mRsyncProcess << QStringLiteral("rsync") << QStringLiteral("-avX") << QStringLiteral("--delete-excluded") << QStringLiteral("--delete-before") << QStringLiteral("--info=progress2"); QStringList lIncludeNames; foreach(const QString &lInclude, mBackupPlan.mPathsIncluded) { lIncludeNames << lastPartOfPath(lInclude); } if(lIncludeNames.removeDuplicates() > 0) { // There would be a naming conflict in the destination folder, instead use full paths. mRsyncProcess << QStringLiteral("-R"); foreach(const QString &lExclude, mBackupPlan.mPathsExcluded) { mRsyncProcess << QStringLiteral("--exclude") << lExclude; } } else { // when NOT using -R, need to then strip parent paths from excludes, everything above the // include. Leave the leading slash! foreach(QString lExclude, mBackupPlan.mPathsExcluded) { for(int i = 0; i < mBackupPlan.mPathsIncluded.length(); ++i) { const QString &lInclude = mBackupPlan.mPathsIncluded.at(i); QString lIncludeWithSlash = lInclude; ensureTrailingSlash(lIncludeWithSlash); if(lExclude.startsWith(lIncludeWithSlash)) { lExclude.remove(0, lInclude.length() - lIncludeNames.at(i).length() - 1); break; } } mRsyncProcess << QStringLiteral("--exclude") << lExclude; } } QString lExcludesPath = mBackupPlan.absoluteExcludesFilePath(); if(mBackupPlan.mExcludePatterns && QFileInfo::exists(lExcludesPath)) { mRsyncProcess << QStringLiteral("--exclude-from") << lExcludesPath; } mRsyncProcess << mBackupPlan.mPathsIncluded; mRsyncProcess << mDestinationPath; connect(&mRsyncProcess, SIGNAL(started()), SLOT(slotRsyncStarted())); connect(&mRsyncProcess, &KProcess::readyReadStandardOutput, this, &RsyncJob::slotReadRsyncOutput); connect(&mRsyncProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotRsyncFinished(int,QProcess::ExitStatus))); mLogStream << quoteArgs(mRsyncProcess.program()) << endl; mRsyncProcess.start(); mInfoRateLimiter.start(); } void RsyncJob::slotRsyncStarted() { makeNice(mRsyncProcess.pid()); } void RsyncJob::slotRsyncFinished(int pExitCode, QProcess::ExitStatus pExitStatus) { QString lErrors = QString::fromUtf8(mRsyncProcess.readAllStandardError()); if(!lErrors.isEmpty()) { mLogStream << lErrors << endl; } mLogStream << "Exit code: " << pExitCode << endl; // exit code 24 means source files disappeared during copying. No reason to worry about that. if(pExitStatus != QProcess::NormalExit || (pExitCode != 0 && pExitCode != 24)) { mLogStream << QStringLiteral("Kup did not successfully complete the rsync backup job.") << endl; jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to save backup. " "See log file for more details.")); } else { mLogStream << QStringLiteral("Kup successfully completed the rsync backup job at ") << QLocale().toString(QDateTime::currentDateTime()) << endl; jobFinishedSuccess(); } } void RsyncJob::slotReadRsyncOutput() { bool lValidInfo = false; bool lValidFileName = false; QString lFileName; ulong lPercent{}; qulonglong lTransfered{}; double lSpeed{}; QChar lUnit; QRegularExpression lProgressInfoExp(QStringLiteral("^\\s+([\\d,\\.]+)\\s+(\\d+)%\\s+(\\d*[,\\.]\\d+)(\\S)")); // very ugly and rough indication that this is a file path... what else to do.. QRegularExpression lNotFileNameExp(QStringLiteral("^(building file list|done$|deleting \\S+|.+/$|$)")); QString lLine; QTextStream lStream(mRsyncProcess.readAllStandardOutput()); while(lStream.readLineInto(&lLine, 500)) { QRegularExpressionMatch lMatch = lProgressInfoExp.match(lLine); if(lMatch.hasMatch()) { lValidInfo = true; lTransfered = lMatch.captured(1).remove(',').remove('.').toULongLong(); lPercent = qMax(lMatch.captured(2).toULong(), 1UL); lSpeed = QLocale().toDouble(lMatch.captured(3)); lUnit = lMatch.captured(4).at(0); } else { lMatch = lNotFileNameExp.match(lLine); if(!lMatch.hasMatch()) { lValidFileName = true; lFileName = lLine; } } } if(mInfoRateLimiter.hasExpired(200)) { if(lValidInfo) { setPercent(lPercent); if(lUnit == 'k') { lSpeed *= 1e3; } else if(lUnit == 'M') { lSpeed *= 1e6; } else if(lUnit == 'G') { lSpeed *= 1e9; } emitSpeed(static_cast(lSpeed)); if(lPercent > 5) { // the rounding to integer percent gives big error with small percentages setProcessedAmount(KJob::Bytes, lTransfered); setTotalAmount(KJob::Bytes, lTransfered*100/lPercent); } } if(lValidFileName) { emit description(this, i18n("Saving backup"), qMakePair(i18nc("Label for file currently being copied", "File"), lFileName)); } mInfoRateLimiter.start(); } } bool RsyncJob::doKill() { setError(KilledJobError); if(0 == ::kill(mRsyncProcess.pid(), SIGINT)) { return mRsyncProcess.waitForFinished(); } return false; } bool RsyncJob::doSuspend() { return 0 == ::kill(mRsyncProcess.pid(), SIGSTOP); } bool RsyncJob::doResume() { return 0 == ::kill(mRsyncProcess.pid(), SIGCONT); } // This migration moves files from being stored directly in destination folder, to // being stored in a subfolder of the destination. The subfolder is named same as the // source folder. This migration will only be done if there is exactly one source folder. bool RsyncJob::performMigration() { QString lSourceDirName = lastPartOfPath(mBackupPlan.mPathsIncluded.first()); //only one included QDir lDestDir = QDir(mDestinationPath); mLogStream << QStringLiteral("Creating directory named ") << lSourceDirName << " inside of " << mDestinationPath << endl; if(!lDestDir.mkdir(lSourceDirName)) { mLogStream << QStringLiteral("Failed to create directory, aborting migration.") << endl; return false; } foreach(const QString &lContent, lDestDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if(lContent != lSourceDirName) { QString lDest = lSourceDirName + QLatin1Char('/') + lContent; mLogStream << QStringLiteral("Renaming ") << lContent << " to " << lDest << endl; if(!lDestDir.rename(lContent, lDest)) { mLogStream << QStringLiteral("Failed to rename, aborting migration.") << endl; return false; } } } mLogStream << QStringLiteral("File migration completed.") << endl; return true; } kup-backup-0.9.1/daemon/rsyncjob.h000066400000000000000000000015221420500356400170270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef RSYNCJOB_H #define RSYNCJOB_H #include "backupjob.h" #include #include class KupDaemon; class RsyncJob : public BackupJob { Q_OBJECT public: RsyncJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon); protected slots: void performJob() override; protected slots: void slotRsyncStarted(); void slotRsyncFinished(int pExitCode, QProcess::ExitStatus pExitStatus); void slotReadRsyncOutput(); protected: bool doKill() override; bool doSuspend() override; bool doResume() override; bool performMigration(); KProcess mRsyncProcess; QElapsedTimer mInfoRateLimiter; }; #endif // RSYNCJOB_H kup-backup-0.9.1/dataengine/000077500000000000000000000000001420500356400156615ustar00rootroot00000000000000kup-backup-0.9.1/dataengine/CMakeLists.txt000066400000000000000000000015771420500356400204330ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later #this is a library so it needs to enforce it's translation domain, not use the application's domain. add_definitions(-DTRANSLATION_DOMAIN="kup") include_directories("../daemon") # Plasma Data Engine set(plasma_engine_kup_SRCS kupengine.cpp kupservice.cpp kupjob.cpp ) add_library(plasma_engine_kup MODULE ${plasma_engine_kup_SRCS}) target_link_libraries(plasma_engine_kup Qt5::Network KF5::Plasma ) kcoreaddons_desktop_to_json(plasma_engine_kup plasma-dataengine-kup.desktop) install(TARGETS plasma_engine_kup DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) install(FILES plasma-dataengine-kup.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES kupservice.operations kupdaemonservice.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) kup-backup-0.9.1/dataengine/kupdaemonservice.operations000066400000000000000000000002341420500356400233310ustar00rootroot00000000000000 kup-backup-0.9.1/dataengine/kupengine.cpp000066400000000000000000000070231420500356400203540ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupengine.h" #include "kupservice.h" #include "kupdaemon.h" #include #include #include KupEngine::KupEngine(QObject *pParent, const QVariantList &pArgs) : Plasma::DataEngine(pParent, pArgs) { mSocketName = QStringLiteral("kup-daemon-"); mSocketName += QString::fromLocal8Bit(qgetenv("USER")); mSocket = new QLocalSocket(this); connect(mSocket, &QLocalSocket::readyRead, this, &KupEngine::processData); connect(mSocket, &QLocalSocket::stateChanged, this, &KupEngine::checkConnection); // wait 5 seconds before trying to connect first time QTimer::singleShot(5000, mSocket, [&]{mSocket->connectToServer(mSocketName);}); setData(QStringLiteral("common"), QStringLiteral("plan count"), 0); } Plasma::Service *KupEngine::serviceForSource(const QString &pSource) { if (pSource == "daemon") { return new KupDaemonService(mSocket, this); } bool lIntOk; int lPlanNumber = pSource.toInt(&lIntOk); if(lIntOk) { return new KupService(lPlanNumber, mSocket, this); } return nullptr; } void KupEngine::processData() { if(mSocket->bytesAvailable() <= 0) { return; } QJsonDocument lDoc = QJsonDocument::fromBinaryData(mSocket->readAll()); if(!lDoc.isObject()) { return; } QJsonObject lEvent = lDoc.object(); if(lEvent["event"] == QStringLiteral("status update")) { QJsonArray lPlans = lEvent["plans"].toArray(); setData(QStringLiteral("common"), QStringLiteral("plan count"), lPlans.count()); setCommonData(lEvent, QStringLiteral("tray icon active")); setCommonData(lEvent, QStringLiteral("tooltip icon name")); setCommonData(lEvent, QStringLiteral("tooltip title")); setCommonData(lEvent, QStringLiteral("tooltip subtitle")); setCommonData(lEvent, QStringLiteral("any plan busy")); setCommonData(lEvent, QStringLiteral("no plan reason")); for(int i = 0; i < lPlans.count(); ++i) { QJsonObject lPlan = lPlans[i].toObject(); setPlanData(i, lPlan, QStringLiteral("description")); setPlanData(i, lPlan, QStringLiteral("destination available")); setPlanData(i, lPlan, QStringLiteral("status heading")); setPlanData(i, lPlan, QStringLiteral("status details")); setPlanData(i, lPlan, QStringLiteral("icon name")); setPlanData(i, lPlan, QStringLiteral("log file exists")); setPlanData(i, lPlan, QStringLiteral("busy")); setPlanData(i, lPlan, QStringLiteral("bup type")); } } } void KupEngine::checkConnection(QLocalSocket::LocalSocketState pState) { if(pState != QLocalSocket::ConnectedState && pState != QLocalSocket::ConnectingState) { QTimer::singleShot(10000, mSocket, [&]{mSocket->connectToServer(mSocketName);}); } if(pState == QLocalSocket::UnconnectedState) { // Don't bother to translate this error message, guessing the error string from qt // is not translated also. QString lErrorText = QStringLiteral("Error, no connection to kup-daemon: "); lErrorText += mSocket->errorString(); setData(QStringLiteral("common"), QStringLiteral("no plan reason"), lErrorText); } } void KupEngine::setPlanData(int i, const QJsonObject &pPlan, const QString &pKey) { setData(QString(QStringLiteral("plan %1")).arg(i), pKey, pPlan[pKey].toVariant()); } void KupEngine::setCommonData(const QJsonObject &pCommonStatus, const QString &pKey) { setData(QStringLiteral("common"), pKey, pCommonStatus[pKey].toVariant()); } K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(backups, KupEngine, "plasma-dataengine-kup.json") #include "kupengine.moc" kup-backup-0.9.1/dataengine/kupengine.h000066400000000000000000000014201420500356400200140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPENGINE_H #define KUPENGINE_H #include #include class KupEngine : public Plasma::DataEngine { Q_OBJECT public: KupEngine(QObject *pParent, const QVariantList &pArgs); Plasma::Service *serviceForSource (const QString &pSource) override; public slots: // void refresh(); void processData(); void checkConnection(QLocalSocket::LocalSocketState pState); private: void setPlanData(int i, const QJsonObject &pPlan, const QString &pKey); void setCommonData(const QJsonObject &pCommonStatus, const QString &pKey); QLocalSocket *mSocket; QString mSocketName; }; #endif // KUPENGINE_H kup-backup-0.9.1/dataengine/kupjob.cpp000066400000000000000000000014611420500356400176610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupjob.h" #include #include #include KupJob::KupJob(int pPlanNumber, QLocalSocket *pSocket, const QString &pOperation, QMap &pParameters, QObject *pParent) : ServiceJob(pParent->objectName(), pOperation, pParameters, pParent), mSocket(pSocket), mPlanNumber(pPlanNumber) { } void KupJob::start() { if(mSocket->state() != QLocalSocket::ConnectedState) { return; } QJsonObject lCommand; lCommand["plan number"] = mPlanNumber; lCommand["operation name"] = operationName(); QJsonDocument lDoc(lCommand); mSocket->write(lDoc.toBinaryData()); setResult(false); } kup-backup-0.9.1/dataengine/kupjob.h000066400000000000000000000010311420500356400173170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPJOB_H #define KUPJOB_H #include class QLocalSocket; class KupJob : public Plasma::ServiceJob { Q_OBJECT public: KupJob(int pPlanNumber, QLocalSocket *pSocket, const QString &pOperation, QMap &pParameters, QObject *pParent = nullptr); protected: void start() override; QLocalSocket *mSocket; int mPlanNumber; }; #endif kup-backup-0.9.1/dataengine/kupservice.cpp000066400000000000000000000016611420500356400205510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupservice.h" #include "kupjob.h" #include KupService::KupService(int pPlanNumber, QLocalSocket *pSocket, QObject *pParent) : Plasma::Service(pParent), mSocket(pSocket), mPlanNumber(pPlanNumber) { setName(QStringLiteral("kupservice")); } ServiceJob *KupService::createJob(const QString &pOperation, QMap &pParameters) { return new KupJob(mPlanNumber, mSocket, pOperation, pParameters, this); } KupDaemonService::KupDaemonService(QLocalSocket *pSocket, QObject *pParent) : Plasma::Service(pParent), mSocket(pSocket) { setName(QStringLiteral("kupdaemonservice")); } ServiceJob *KupDaemonService::createJob(const QString &pOperation, QMap &pParameters) { return new KupJob(-1, mSocket, pOperation, pParameters, this); } kup-backup-0.9.1/dataengine/kupservice.h000066400000000000000000000016061420500356400202150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPSERVICE_H #define KUPSERVICE_H #include #include using namespace Plasma; class QLocalSocket; class KupService : public Plasma::Service { Q_OBJECT public: KupService(int pPlanNumber, QLocalSocket *pSocket, QObject *pParent = nullptr); ServiceJob *createJob(const QString &pOperation, QMap &pParameters) override; protected: QLocalSocket *mSocket; int mPlanNumber; }; class KupDaemonService : public Plasma::Service { Q_OBJECT public: KupDaemonService(QLocalSocket *pSocket, QObject *pParent = nullptr); ServiceJob *createJob(const QString &pOperation, QMap &pParameters) override; protected: QLocalSocket *mSocket; }; #endif // KUPSERVICE_H kup-backup-0.9.1/dataengine/kupservice.operations000066400000000000000000000004251420500356400221470ustar00rootroot00000000000000 kup-backup-0.9.1/dataengine/plasma-dataengine-kup.desktop000066400000000000000000000036651420500356400234350ustar00rootroot00000000000000[Desktop Entry] Type=Service Icon=kup X-KDE-ServiceTypes=Plasma/DataEngine X-KDE-Library=plasma_engine_kup X-KDE-PluginInfo-Author=Simon Persson X-KDE-PluginInfo-Email=simon.persson@mykolab.com X-KDE-PluginInfo-Name=backups X-KDE-PluginInfo-Version=0.1 X-KDE-PluginInfo-Website=https://github.com/spersson/kup X-KDE-PluginInfo-Category= X-KDE-PluginInfo-License=GPL X-KDE-PluginInfo-EnabledByDefault=true Name=Backups Name[ca]=Còpies de seguretat Name[ca@valencia]=Còpies de seguretat Name[cs]=Zálohy Name[de]=Sicherungen Name[en_GB]=Backups Name[es]=Copias de seguridad Name[et]=Varukoopiad Name[eu]=Babes-kopiak Name[fi]=Varmuuskopiot Name[fr]=Sauvegardes Name[it]=Copie di sicurezza Name[ko]=백업 Name[nl]=Back-ups Name[pl]=Kopie zapasowe Name[pt]=Cópias de Segurança Name[pt_BR]=Backups Name[sk]=Zálohy Name[sl]=Varnostne kopije Name[sv]=Säkerhetskopior Name[uk]=Резервні копії Name[x-test]=xxBackupsxx Name[zh_CN]=备份 Name[zh_TW]=備份 Comment=Status of backup plans Comment[ca]=Estat dels plans per a còpia de seguretat Comment[ca@valencia]=Estat dels plans per a còpia de seguretat Comment[de]=Status der Sicherungspläne Comment[en_GB]=Status of backup plans Comment[es]=Estado de la planificación de copias de seguridad Comment[et]=Varukoopiakavade olek Comment[eu]=Babes-kopia egitasmoen egoera Comment[fi]=Varmuuskopiointisuunnitelmien tila Comment[fr]=État des plans de sauvegarde Comment[it]=Stato dei piani di copia di sicurezza Comment[ko]=백업 계획 상태 Comment[nl]=Status van back-up-plannen Comment[pl]=Stan planów kopii zapasowej Comment[pt]=Estado dos planos de cópias de segurança Comment[pt_BR]=Status dos planos de backup Comment[sl]=Stanje plana izdelav varnostnih kopij Comment[sv]=Säkerhetskopieringsplanernas status Comment[uk]=Стан планів резервного копіювання Comment[x-test]=xxStatus of backup plansxx Comment[zh_CN]=备份计划状态 Comment[zh_TW]=備份計畫狀態 kup-backup-0.9.1/filedigger/000077500000000000000000000000001420500356400156635ustar00rootroot00000000000000kup-backup-0.9.1/filedigger/CMakeLists.txt000066400000000000000000000021131420500356400204200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later include_directories("../daemon") include_directories("../kcm") include_directories("../kioslave") include_directories("../settings") set(filedigger_SRCS filedigger.cpp main.cpp mergedvfs.cpp mergedvfsmodel.cpp restoredialog.cpp restorejob.cpp versionlistdelegate.cpp versionlistmodel.cpp ../kioslave/vfshelpers.cpp ../kcm/dirselector.cpp ../settings/kuputils.cpp ) ecm_qt_declare_logging_category(filedigger_SRCS HEADER kupfiledigger_debug.h IDENTIFIER KUPFILEDIGGER CATEGORY_NAME kup.filedigger DEFAULT_SEVERITY Warning EXPORT kup DESCRIPTION "Kup Filedigger" ) add_definitions(-fexceptions) ki18n_wrap_ui(filedigger_SRCS restoredialog.ui) add_executable(kup-filedigger ${filedigger_SRCS}) target_link_libraries(kup-filedigger Qt5::Core Qt5::Gui KF5::KIOCore KF5::KIOFileWidgets KF5::I18n KF5::JobWidgets KF5::WidgetsAddons LibGit2::LibGit2 ) ########### install files ############### install(TARGETS kup-filedigger ${INSTALL_TARGETS_DEFAULT_ARGS}) kup-backup-0.9.1/filedigger/filedigger.cpp000066400000000000000000000140441420500356400204730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "filedigger.h" #include "mergedvfsmodel.h" #include "restoredialog.h" #include "versionlistmodel.h" #include "versionlistdelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include FileDigger::FileDigger(QString pRepoPath, QString pBranchName, QWidget *pParent) : KMainWindow(pParent), mRepoPath(std::move(pRepoPath)), mBranchName(std::move(pBranchName)), mDirOperator(nullptr) { setWindowIcon(QIcon::fromTheme(QStringLiteral("kup"))); KToolBar *lAppToolBar = toolBar(); lAppToolBar->addAction(KStandardAction::quit(this, SLOT(close()), this)); QTimer::singleShot(0, this, [this]{repoPathAvailable();}); } QSize FileDigger::sizeHint() const { return {800, 600}; } void FileDigger::updateVersionModel(const QModelIndex &pCurrent, const QModelIndex &pPrevious) { Q_UNUSED(pPrevious) mVersionModel->setNode(MergedVfsModel::node(pCurrent)); mVersionView->selectionModel()->setCurrentIndex(mVersionModel->index(0,0), QItemSelectionModel::Select); } void FileDigger::open(const QModelIndex &pIndex) { KRun::runUrl(pIndex.data(VersionBupUrlRole).toUrl(), pIndex.data(VersionMimeTypeRole).toString(), this, KRun::RunFlags()); } void FileDigger::restore(const QModelIndex &pIndex) { auto lDialog = new RestoreDialog(pIndex.data(VersionSourceInfoRole).value(), this); lDialog->setAttribute(Qt::WA_DeleteOnClose); lDialog->show(); } void FileDigger::repoPathAvailable() { if(mRepoPath.isEmpty()) { createSelectionView(); } else { QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); MergedRepository *lRepository = createRepo(); if(lRepository != nullptr) { createRepoView(lRepository); } QGuiApplication::restoreOverrideCursor(); } } void FileDigger::checkFileWidgetPath() { KFileItemList lList = mDirOperator->selectedItems(); if(lList.isEmpty()) { mRepoPath = mDirOperator->url().toLocalFile(); } else { mRepoPath = lList.first().url().toLocalFile(); } mBranchName = QStringLiteral("kup"); repoPathAvailable(); } void FileDigger::enterUrl(const QUrl &pUrl) { mDirOperator->setUrl(pUrl, true); } MergedRepository *FileDigger::createRepo() { auto lRepository = new MergedRepository(nullptr, mRepoPath, mBranchName); if(!lRepository->open()) { KMessageBox::sorry(nullptr, xi18nc("@info messagebox, %1 is a folder path", "The backup archive %1 could not be opened. " "Check if the backups really are located there.", mRepoPath)); return nullptr; } if(!lRepository->readBranch()) { if(!lRepository->permissionsOk()) { KMessageBox::sorry(nullptr, xi18nc("@info messagebox", "You do not have permission needed to read this backup archive.")); } else { MergedRepository::askForIntegrityCheck(); } return nullptr; } return lRepository; } void FileDigger::createRepoView(MergedRepository *pRepository) { auto lSplitter = new QSplitter(); mMergedVfsModel = new MergedVfsModel(pRepository, this); mMergedVfsView = new QTreeView(); mMergedVfsView->setHeaderHidden(true); mMergedVfsView->setSelectionMode(QAbstractItemView::SingleSelection); mMergedVfsView->setModel(mMergedVfsModel); lSplitter->addWidget(mMergedVfsView); connect(mMergedVfsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(updateVersionModel(QModelIndex,QModelIndex))); mVersionView = new QListView(); mVersionView->setSelectionMode(QAbstractItemView::SingleSelection); mVersionModel = new VersionListModel(this); mVersionView->setModel(mVersionModel); auto lVersionDelegate = new VersionListDelegate(mVersionView,this); mVersionView->setItemDelegate(lVersionDelegate); lSplitter->addWidget(mVersionView); connect(lVersionDelegate, SIGNAL(openRequested(QModelIndex)), SLOT(open(QModelIndex))); connect(lVersionDelegate, SIGNAL(restoreRequested(QModelIndex)), SLOT(restore(QModelIndex))); mMergedVfsView->setFocus(); //expand all levels from the top until the node has more than one child QModelIndex lIndex; forever { mMergedVfsView->expand(lIndex); if(mMergedVfsModel->rowCount(lIndex) == 1) { lIndex = mMergedVfsModel->index(0, 0, lIndex); } else { break; } } mMergedVfsView->selectionModel()->setCurrentIndex(mMergedVfsModel->index(0, 0, lIndex), QItemSelectionModel::Select); setCentralWidget(lSplitter); } void FileDigger::createSelectionView() { if(mDirOperator != nullptr) { return; } auto lLabel = new QLabel(i18n("Select location of backup archive to open.")); auto lPlaces = new KFilePlacesView; lPlaces->setModel(new KFilePlacesModel); mDirOperator = new KDirOperator(); mDirOperator->setView(KFile::Tree); mDirOperator->setMode(KFile::Directory); mDirOperator->setEnableDirHighlighting(true); mDirOperator->setShowHiddenFiles(true); connect(lPlaces, &KFilePlacesView::urlChanged, this, &FileDigger::enterUrl); auto lOkButton = new QPushButton(this); KGuiItem::assign(lOkButton, KStandardGuiItem::ok()); connect(lOkButton, &QPushButton::pressed, this, &FileDigger::checkFileWidgetPath); auto lSelectionView = new QWidget; auto lVLayout1 = new QVBoxLayout; auto lSplitter = new QSplitter; lVLayout1->addWidget(lLabel); lSplitter->addWidget(lPlaces); lSplitter->addWidget(mDirOperator); lVLayout1->addWidget(lSplitter, 1); lVLayout1->addWidget(lOkButton); lSelectionView->setLayout(lVLayout1); setCentralWidget(lSelectionView); } kup-backup-0.9.1/filedigger/filedigger.h000066400000000000000000000022451420500356400201400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef FILEDIGGER_H #define FILEDIGGER_H #include #include class KDirOperator; class MergedVfsModel; class MergedRepository; class VersionListModel; class QListView; class QModelIndex; class QTreeView; class FileDigger : public KMainWindow { Q_OBJECT public: explicit FileDigger(QString pRepoPath, QString pBranchName, QWidget *pParent = nullptr); QSize sizeHint() const override; protected slots: void updateVersionModel(const QModelIndex &pCurrent, const QModelIndex &pPrevious); void open(const QModelIndex &pIndex); void restore(const QModelIndex &pIndex); void repoPathAvailable(); void checkFileWidgetPath(); void enterUrl(const QUrl &pUrl); protected: MergedRepository *createRepo(); void createRepoView(MergedRepository *pRepository); void createSelectionView(); MergedVfsModel *mMergedVfsModel{}; QTreeView *mMergedVfsView{}; VersionListModel *mVersionModel{}; QListView *mVersionView{}; QString mRepoPath; QString mBranchName; KDirOperator *mDirOperator; }; #endif // FILEDIGGER_H kup-backup-0.9.1/filedigger/main.cpp000066400000000000000000000041651420500356400173210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "filedigger.h" #include "mergedvfs.h" #include #include #include #include #include #include #include #include #include int main(int pArgCount, char **pArgArray) { QApplication lApp(pArgCount, pArgArray); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); KLocalizedString::setApplicationDomain("kup"); KAboutData lAbout(QStringLiteral("kupfiledigger"), xi18nc("@title", "File Digger"), QStringLiteral("0.9.1"), i18n("Browser for bup archives."), KAboutLicense::GPL, i18n("Copyright (C) 2013-2020 Simon Persson")); lAbout.addAuthor(i18n("Simon Persson"), i18n("Maintainer"), "simon.persson@mykolab.com"); lAbout.setTranslator(xi18nc("NAME OF TRANSLATORS", "Your names"), xi18nc("EMAIL OF TRANSLATORS", "Your emails")); KAboutData::setApplicationData(lAbout); //this calls qApp.setApplicationName, setVersion, etc. QCommandLineParser lParser; lParser.addOption(QCommandLineOption(QStringList() << QStringLiteral("b") << QStringLiteral("branch"), i18n("Name of the branch to be opened."), QStringLiteral("branch name"), QStringLiteral("kup"))); lParser.addPositionalArgument(QStringLiteral(""), i18n("Path to the bup repository to be opened.")); lAbout.setupCommandLine(&lParser); lParser.process(lApp); lAbout.processCommandLine(&lParser); QString lRepoPath; QStringList lPosArgs = lParser.positionalArguments(); if(!lPosArgs.isEmpty()) { auto lDir = QDir(lPosArgs.takeFirst()); lRepoPath = lDir.absolutePath(); } // This needs to be called first thing, before any other calls to libgit2. git_libgit2_init(); auto lFileDigger = new FileDigger(lRepoPath, lParser.value(QStringLiteral("branch"))); lFileDigger->show(); int lRetVal = QApplication::exec(); git_libgit2_shutdown(); return lRetVal; } kup-backup-0.9.1/filedigger/mergedvfs.cpp000066400000000000000000000231111420500356400203470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "mergedvfs.h" #include "kupdaemon.h" #include "vfshelpers.h" #include "kupfiledigger_debug.h" #include #include #include #include #include #include #include using NameMap = QMap; using NameMapIterator = QMapIterator; git_repository *MergedNode::mRepository = nullptr; bool mergedNodeLessThan(const MergedNode *a, const MergedNode *b) { if(a->isDirectory() != b->isDirectory()) { return a->isDirectory(); } return a->objectName() < b->objectName(); } bool versionGreaterThan(const VersionData *a, const VersionData *b) { return a->mModifiedDate > b->mModifiedDate; } MergedNode::MergedNode(QObject *pParent, const QString &pName, uint pMode) :QObject(pParent) { mSubNodes = nullptr; setObjectName(pName); mMode = pMode; } void MergedNode::getBupUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath, QString *pBranchName, qint64 *pCommitTime, QString *pPathInRepo) const { QList lStack; const MergedNode *lNode = this; while(lNode != nullptr) { lStack.append(lNode); lNode = qobject_cast(lNode->parent()); } const auto lRepo = qobject_cast(lStack.takeLast()); if(pComplete) { pComplete->setUrl("bup://" + lRepo->objectName() + lRepo->mBranchName + '/' + vfsTimeToString(static_cast(mVersionList.at(pVersionIndex)->mCommitTime))); } if(pRepoPath) { *pRepoPath = lRepo->objectName(); } if(pBranchName) { *pBranchName = lRepo->mBranchName; } if(pCommitTime) { *pCommitTime = mVersionList.at(pVersionIndex)->mCommitTime; } if(pPathInRepo) { pPathInRepo->clear(); } while(!lStack.isEmpty()) { QString lPathComponent = lStack.takeLast()->objectName(); if(pComplete) { pComplete->setPath(pComplete->path() + '/' + lPathComponent); } if(pPathInRepo) { pPathInRepo->append(QLatin1Char('/')); pPathInRepo->append(lPathComponent); } } } MergedNodeList &MergedNode::subNodes() { if(mSubNodes == nullptr) { mSubNodes = new MergedNodeList(); if(S_ISDIR(mMode)) { QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); generateSubNodes(); QGuiApplication::restoreOverrideCursor(); } } return *mSubNodes; } void MergedNode::askForIntegrityCheck() { int lAnswer = KMessageBox::questionYesNo(nullptr, xi18nc("@info messagebox", "Could not read this backup archive. Perhaps some files " "have become corrupted. Do you want to run an integrity " "check to test this?")); if(lAnswer == KMessageBox::Yes) { QDBusInterface lInterface(KUP_DBUS_SERVICE_NAME, KUP_DBUS_OBJECT_PATH); if(lInterface.isValid()) { lInterface.call(QStringLiteral("runIntegrityCheck"), QDir::cleanPath(QString::fromLocal8Bit(git_repository_path(mRepository)))); } } } void MergedNode::generateSubNodes() { NameMap lSubNodeMap; foreach(VersionData *lCurrentVersion, mVersionList) { git_tree *lTree; if(0 != git_tree_lookup(&lTree, mRepository, &lCurrentVersion->mOid)) { askForIntegrityCheck(); continue; // try to be fault tolerant by not aborting... } git_blob *lMetadataBlob = nullptr; VintStream *lMetadataStream = nullptr; const git_tree_entry *lTreeEntry = git_tree_entry_byname(lTree, ".bupm"); if(lTreeEntry != nullptr && 0 == git_blob_lookup(&lMetadataBlob, mRepository, git_tree_entry_id(lTreeEntry))) { lMetadataStream = new VintStream(git_blob_rawcontent(lMetadataBlob), static_cast(git_blob_rawsize(lMetadataBlob)), this); Metadata lMetadata; readMetadata(*lMetadataStream, lMetadata); // the first entry is metadata for the directory itself, discard it. } ulong lEntryCount = git_tree_entrycount(lTree); for(uint i = 0; i < lEntryCount; ++i) { uint lMode; const git_oid *lOid; QString lName; bool lChunked; const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lTree, i); getEntryAttributes(lTreeEntry, lMode, lChunked, lOid, lName); if(lName == QStringLiteral(".bupm")) { continue; } MergedNode *lSubNode = lSubNodeMap.value(lName, nullptr); if(lSubNode == nullptr) { lSubNode = new MergedNode(this, lName, lMode); lSubNodeMap.insert(lName, lSubNode); mSubNodes->append(lSubNode); } else if((S_IFMT & lMode) != (S_IFMT & lSubNode->mMode)) { if(S_ISDIR(lMode)) { lName.append(xi18nc("added after folder name in some cases", " (folder)")); } else if(S_ISLNK(lMode)) { lName.append(xi18nc("added after file name in some cases", " (symlink)")); } else { lName.append(xi18nc("added after file name in some cases", " (file)")); } lSubNode = lSubNodeMap.value(lName, nullptr); if(lSubNode == nullptr) { lSubNode = new MergedNode(this, lName, lMode); lSubNodeMap.insert(lName, lSubNode); mSubNodes->append(lSubNode); } } bool lAlreadySeen = false; foreach(VersionData *lVersion, lSubNode->mVersionList) { if(lVersion->mOid == *lOid) { lAlreadySeen = true; break; } } if(S_ISDIR(lMode)) { if(!lAlreadySeen) { lSubNode->mVersionList.append(new VersionData(lOid, lCurrentVersion->mCommitTime, lCurrentVersion->mModifiedDate, 0)); } } else { qint64 lModifiedDate = lCurrentVersion->mModifiedDate; qint64 lSize = -1; Metadata lMetadata; if(lMetadataStream != nullptr && 0 == readMetadata(*lMetadataStream, lMetadata)) { lModifiedDate = lMetadata.mMtime; lSize = lMetadata.mSize; } if(!lAlreadySeen) { VersionData *lVersionData; if(lSize >= 0) { lVersionData = new VersionData(lOid, lCurrentVersion->mCommitTime, lModifiedDate, static_cast(lSize)); } else { lVersionData = new VersionData(lChunked, lOid, lCurrentVersion->mCommitTime, lModifiedDate); } lSubNode->mVersionList.append(lVersionData); } } } if(lMetadataStream != nullptr) { delete lMetadataStream; git_blob_free(lMetadataBlob); } git_tree_free(lTree); } std::sort(mSubNodes->begin(), mSubNodes->end(), mergedNodeLessThan); foreach(MergedNode *lNode, *mSubNodes) { std::sort(lNode->mVersionList.begin(), lNode->mVersionList.end(), versionGreaterThan); } } MergedRepository::MergedRepository(QObject *pParent, const QString &pRepositoryPath, QString pBranchName) : MergedNode(pParent, pRepositoryPath, DEFAULT_MODE_DIRECTORY), mBranchName(std::move(pBranchName)) { if(!objectName().endsWith(QLatin1Char('/'))) { setObjectName(objectName() + QLatin1Char('/')); } } MergedRepository::~MergedRepository() { if(mRepository != nullptr) { git_repository_free(mRepository); } } bool MergedRepository::open() { if(0 != git_repository_open(&mRepository, objectName().toLocal8Bit())) { qCWarning(KUPFILEDIGGER) << "could not open repository " << objectName(); mRepository = nullptr; return false; } return true; } bool MergedRepository::readBranch() { if(mRepository == nullptr) { return false; } git_revwalk *lRevisionWalker; if(0 != git_revwalk_new(&lRevisionWalker, mRepository)) { qCWarning(KUPFILEDIGGER) << "could not create a revision walker in repository " << objectName(); return false; } QString lCompleteBranchName = QStringLiteral("refs/heads/"); lCompleteBranchName.append(mBranchName); if(0 != git_revwalk_push_ref(lRevisionWalker, lCompleteBranchName.toLocal8Bit())) { qCWarning(KUPFILEDIGGER) << "Unable to read branch " << mBranchName << " in repository " << objectName(); git_revwalk_free(lRevisionWalker); return false; } bool lEmptyList = true; git_oid lOid; while(0 == git_revwalk_next(&lOid, lRevisionWalker)) { git_commit *lCommit; if(0 != git_commit_lookup(&lCommit, mRepository, &lOid)) { continue; } git_time_t lTime = git_commit_time(lCommit); mVersionList.append(new VersionData(git_commit_tree_id(lCommit), lTime, lTime, 0)); lEmptyList = false; git_commit_free(lCommit); } git_revwalk_free(lRevisionWalker); return !lEmptyList; } bool MergedRepository::permissionsOk() { if(mRepository == nullptr) { return false; } QDir lRepoDir(objectName()); if(!lRepoDir.exists()) { return false; } QList lDirectories; lDirectories << lRepoDir; while(!lDirectories.isEmpty()) { QDir lDir = lDirectories.takeFirst(); foreach(QFileInfo lFileInfo, lDir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if(!lFileInfo.isReadable()) { return false; } if(lFileInfo.isDir()) { lDirectories << QDir(lFileInfo.absoluteFilePath()); } } } return true; } uint qHash(git_oid pOid) { return qHash(QByteArray::fromRawData(reinterpret_cast(pOid.id), GIT_OID_RAWSZ)); } bool operator ==(const git_oid &pOidA, const git_oid &pOidB) { QByteArray a = QByteArray::fromRawData(reinterpret_cast(pOidA.id), GIT_OID_RAWSZ); QByteArray b = QByteArray::fromRawData(reinterpret_cast(pOidB.id), GIT_OID_RAWSZ); return a == b; } quint64 VersionData::size() { if(mSizeIsValid) { return mSize; } if(mChunkedFile) { mSize = calculateChunkFileSize(&mOid, MergedNode::mRepository); } else { git_blob *lBlob; if(0 == git_blob_lookup(&lBlob, MergedNode::mRepository, &mOid)) { mSize = static_cast(git_blob_rawsize(lBlob)); git_blob_free(lBlob); } else { mSize = 0; } } mSizeIsValid = true; return mSize; } kup-backup-0.9.1/filedigger/mergedvfs.h000066400000000000000000000044411420500356400200210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef MERGEDVFS_H #define MERGEDVFS_H #include uint qHash(git_oid pOid); bool operator ==(const git_oid &pOidA, const git_oid &pOidB); #include #include #include #include struct VersionData { VersionData(bool pChunkedFile, const git_oid *pOid, qint64 pCommitTime, qint64 pModifiedDate) :mChunkedFile(pChunkedFile), mOid(*pOid), mCommitTime(pCommitTime), mModifiedDate(pModifiedDate) { mSizeIsValid = false; } VersionData(const git_oid *pOid, qint64 pCommitTime, qint64 pModifiedDate, quint64 pSize) :mOid(*pOid), mCommitTime(pCommitTime), mModifiedDate(pModifiedDate), mSize(pSize) { mSizeIsValid = true; } quint64 size(); bool mSizeIsValid; bool mChunkedFile; git_oid mOid; qint64 mCommitTime; qint64 mModifiedDate; protected: quint64 mSize; }; class MergedNode; typedef QList MergedNodeList; typedef QListIterator MergedNodeListIterator; typedef QList VersionList; typedef QListIterator VersionListIterator; class MergedNode: public QObject { Q_OBJECT friend struct VersionData; public: MergedNode(QObject *pParent, const QString &pName, uint pMode); ~MergedNode() override { delete mSubNodes; while (!mVersionList.isEmpty()) delete mVersionList.takeFirst(); } bool isDirectory() const { return S_ISDIR(mMode); } void getBupUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath = nullptr, QString *pBranchName = nullptr, qint64 *pCommitTime = nullptr, QString *pPathInRepo = nullptr) const; virtual MergedNodeList &subNodes(); const VersionList *versionList() const { return &mVersionList; } uint mode() const { return mMode; } static void askForIntegrityCheck(); protected: virtual void generateSubNodes(); static git_repository *mRepository; uint mMode; VersionList mVersionList; MergedNodeList *mSubNodes; }; class MergedRepository: public MergedNode { Q_OBJECT public: MergedRepository(QObject *pParent, const QString &pRepositoryPath, QString pBranchName); ~MergedRepository() override; bool open(); bool readBranch(); bool permissionsOk(); QString mBranchName; }; #endif // MERGEDVFS_H kup-backup-0.9.1/filedigger/mergedvfsmodel.cpp000066400000000000000000000054231420500356400213760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "mergedvfsmodel.h" #include "mergedvfs.h" #include "vfshelpers.h" #include #include #include #include #include MergedVfsModel::MergedVfsModel(MergedRepository *pRoot, QObject *pParent) : QAbstractItemModel(pParent), mRoot(pRoot) { } MergedVfsModel::~MergedVfsModel() { delete mRoot; } int MergedVfsModel::columnCount(const QModelIndex &pParent) const { Q_UNUSED(pParent) return 1; } QVariant MergedVfsModel::data(const QModelIndex &pIndex, int pRole) const { if(!pIndex.isValid()) { return QVariant(); } auto *lNode = static_cast(pIndex.internalPointer()); switch (pRole) { case Qt::DisplayRole: return lNode->objectName(); case Qt::DecorationRole: { QString lIconName = KIO::iconNameForUrl(QUrl::fromLocalFile(lNode->objectName())); if(lNode->isDirectory()) { QMimeDatabase db; lIconName = db.mimeTypeForName(QStringLiteral("inode/directory")).iconName(); } return QIcon::fromTheme(lIconName); } default: return QVariant(); } } QModelIndex MergedVfsModel::index(int pRow, int pColumn, const QModelIndex &pParent) const { if(pColumn != 0 || pRow < 0) { return {}; // invalid } if(!pParent.isValid()) { if(pRow >= mRoot->subNodes().count()) { return {}; // invalid } return createIndex(pRow, 0, mRoot->subNodes().at(pRow)); } auto lParentNode = static_cast(pParent.internalPointer()); if(pRow >= lParentNode->subNodes().count()) { return {}; // invalid } return createIndex(pRow, 0, lParentNode->subNodes().at(pRow)); } QModelIndex MergedVfsModel::parent(const QModelIndex &pChild) const { if(!pChild.isValid()) { return {}; } auto lChild = static_cast(pChild.internalPointer()); auto lParent = qobject_cast(lChild->parent()); if(lParent == nullptr || lParent == mRoot) { return {}; //invalid } auto lGrandParent = qobject_cast(lParent->parent()); if(lGrandParent == nullptr) { return {}; //invalid } return createIndex(lGrandParent->subNodes().indexOf(lParent), 0, lParent); } int MergedVfsModel::rowCount(const QModelIndex &pParent) const { if(!pParent.isValid()) { return mRoot->subNodes().count(); } auto lParent = static_cast(pParent.internalPointer()); if(lParent == nullptr) { return 0; } return lParent->subNodes().count(); } const VersionList *MergedVfsModel::versionList(const QModelIndex &pIndex) { auto lNode = static_cast(pIndex.internalPointer()); return lNode->versionList(); } const MergedNode *MergedVfsModel::node(const QModelIndex &pIndex) { return static_cast(pIndex.internalPointer()); } kup-backup-0.9.1/filedigger/mergedvfsmodel.h000066400000000000000000000017071420500356400210440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef MERGEDVFSMODEL_H #define MERGEDVFSMODEL_H #include #include "mergedvfs.h" class MergedVfsModel : public QAbstractItemModel { Q_OBJECT public: explicit MergedVfsModel(MergedRepository *pRoot, QObject *pParent = nullptr); ~MergedVfsModel() override; int columnCount(const QModelIndex &pParent) const override; QVariant data(const QModelIndex &pIndex, int pRole) const override; QModelIndex index(int pRow, int pColumn, const QModelIndex &pParent) const override; QModelIndex parent(const QModelIndex &pChild) const override; int rowCount(const QModelIndex &pParent) const override; static const VersionList *versionList(const QModelIndex &pIndex); static const MergedNode *node(const QModelIndex &pIndex); protected: MergedRepository *mRoot; }; #endif // MERGEDVFSMODEL_H kup-backup-0.9.1/filedigger/restoredialog.cpp000066400000000000000000000436471420500356400212500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "restoredialog.h" #include "ui_restoredialog.h" #include "restorejob.h" #include "dirselector.h" #include "kuputils.h" #include "kupfiledigger_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const QString cKupTempRestoreFolder = QStringLiteral("_kup_temporary_restore_folder_"); RestoreDialog::RestoreDialog(BupSourceInfo pPathInfo, QWidget *parent) : QDialog(parent), mUI(new Ui::RestoreDialog), mSourceInfo(std::move(pPathInfo)) { mSourceFileName = mSourceInfo.mPathInRepo.section(QDir::separator(), -1); qCDebug(KUPFILEDIGGER) << "Starting restore dialog for repo: " << mSourceInfo.mRepoPath << ", restoring: " << mSourceInfo.mPathInRepo; mUI->setupUi(this); mFileWidget = nullptr; mDirSelector = nullptr; mJobTracker = nullptr; mUI->mRestoreOriginalButton->setMinimumHeight(mUI->mRestoreOriginalButton->sizeHint().height() * 2); mUI->mRestoreCustomButton->setMinimumHeight(mUI->mRestoreCustomButton->sizeHint().height() * 2); connect(mUI->mRestoreOriginalButton, SIGNAL(clicked()), SLOT(setOriginalDestination())); connect(mUI->mRestoreCustomButton, SIGNAL(clicked()), SLOT(setCustomDestination())); mMessageWidget = new KMessageWidget(this); mMessageWidget->setWordWrap(true); mUI->mTopLevelVLayout->insertWidget(0, mMessageWidget); mMessageWidget->hide(); connect(mUI->mDestBackButton, SIGNAL(clicked()), mMessageWidget, SLOT(hide())); connect(mUI->mDestNextButton, SIGNAL(clicked()), SLOT(checkDestinationSelection())); connect(mUI->mDestBackButton, &QPushButton::clicked, this, [this]{mUI->mStackedWidget->setCurrentIndex(0);}); connect(mUI->mOverwriteBackButton, &QPushButton::clicked, this, [this]{mUI->mStackedWidget->setCurrentIndex(0);}); connect(mUI->mConfirmButton, SIGNAL(clicked()), SLOT(fileOverwriteConfirmed())); connect(mUI->mOpenDestinationButton, SIGNAL(clicked()), SLOT(openDestinationFolder())); } RestoreDialog::~RestoreDialog() { delete mUI; } void RestoreDialog::changeEvent(QEvent *pEvent) { QDialog::changeEvent(pEvent); switch (pEvent->type()) { case QEvent::LanguageChange: mUI->retranslateUi(this); break; default: break; } } void RestoreDialog::setOriginalDestination() { if(mSourceInfo.mIsDirectory) { // the path in repo could have had slashes appended below, we are back here because user clicked "back" ensureNoTrailingSlash(mSourceInfo.mPathInRepo); //select parent of folder to be restored mDestination.setFile(mSourceInfo.mPathInRepo.section(QDir::separator(), 0, -2)); } else { mDestination.setFile(mSourceInfo.mPathInRepo); } startPrechecks(); } void RestoreDialog::setCustomDestination() { if(mSourceInfo.mIsDirectory && mDirSelector == nullptr) { mDirSelector = new DirSelector(this); mDirSelector->setRootUrl(QUrl::fromLocalFile(QStringLiteral("/"))); QString lDirPath = mSourceInfo.mPathInRepo.section(QDir::separator(), 0, -2); mDirSelector->expandToUrl(QUrl::fromLocalFile(lDirPath)); mUI->mDestinationVLayout->insertWidget(0, mDirSelector); auto lNewFolderButton = new QPushButton(QIcon::fromTheme(QStringLiteral("folder-new")), xi18nc("@action:button","New Folder...")); connect(lNewFolderButton, SIGNAL(clicked()), SLOT(createNewFolder())); mUI->mDestinationHLayout->insertWidget(0, lNewFolderButton); } else if(!mSourceInfo.mIsDirectory && mFileWidget == nullptr) { QFileInfo lFileInfo(mSourceInfo.mPathInRepo); do { lFileInfo.setFile(lFileInfo.absolutePath()); // check the file's directory first, not the file. } while(!lFileInfo.exists()); QUrl lStartSelection = QUrl::fromLocalFile(lFileInfo.absoluteFilePath() + '/' + mSourceFileName); mFileWidget = new KFileWidget(lStartSelection, this); mFileWidget->setOperationMode(KFileWidget::Saving); mFileWidget->setMode(KFile::File | KFile::LocalOnly); mUI->mDestinationVLayout->insertWidget(0, mFileWidget); } mUI->mDestNextButton->setFocus(); mUI->mStackedWidget->setCurrentIndex(1); } void RestoreDialog::checkDestinationSelection() { if(mSourceInfo.mIsDirectory) { QUrl lUrl = mDirSelector->url(); if(!lUrl.isEmpty()) { mDestination.setFile(lUrl.path()); startPrechecks(); } else { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "No destination was selected, please select one.")); mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->animatedShow(); } } else { connect(mFileWidget, SIGNAL(accepted()), SLOT(checkDestinationSelection2())); mFileWidget->slotOk(); // will emit accepted() if selection is valid, continue below then } } void RestoreDialog::checkDestinationSelection2() { mFileWidget->accept(); // This call is needed for selectedFile() to return something. QString lFilePath = mFileWidget->selectedFile(); if(!lFilePath.isEmpty()) { mDestination.setFile(lFilePath); startPrechecks(); } else { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "No destination was selected, please select one.")); mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->animatedShow(); } } void RestoreDialog::startPrechecks() { mUI->mFileConflictList->clear(); mSourceSize = 0; mFileSizes.clear(); qCDebug(KUPFILEDIGGER) << "Destination has been selected: " << mDestination.absoluteFilePath(); if(mSourceInfo.mIsDirectory) { mDirectoriesCount = 1; // the folder being restored, rest will be added during listing. mRestorationPath = mDestination.absoluteFilePath(); mFolderToCreate = QFileInfo(mDestination.absoluteFilePath() + QDir::separator() + mSourceFileName); mSavedWorkingDirectory.clear(); if(mFolderToCreate.exists()) { if(mFolderToCreate.isDir()) { // destination dir exists, first restore to a subfolder, then move files up. mRestorationPath = mFolderToCreate.absoluteFilePath(); QDir lDir(mFolderToCreate.absoluteFilePath()); lDir.setFilter(QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot); if(lDir.count() > 0) { // destination dir exists and is non-empty. mRestorationPath.append(QDir::separator()); mRestorationPath.append(cKupTempRestoreFolder); } // make bup not restore the source folder itself but instead it's contents mSourceInfo.mPathInRepo.append(QDir::separator()); // folder already exists, need to check for files about to be overwritten. // will create QFileInfos with relative paths during listing and compare with listed source entries. mSavedWorkingDirectory = QDir::currentPath(); QDir::setCurrent(mFolderToCreate.absoluteFilePath()); } else { mUI->mFileConflictList->addItem(mFolderToCreate.absoluteFilePath()); mRestorationPath.append(QDir::separator()); mRestorationPath.append(cKupTempRestoreFolder); } } qCDebug(KUPFILEDIGGER) << "Starting source file listing job on: " << mSourceInfo.mBupKioPath; KIO::ListJob *lListJob = KIO::listRecursive(mSourceInfo.mBupKioPath, KIO::HideProgressInfo); auto lJobTracker = new KWidgetJobTracker(this); lJobTracker->registerJob(lListJob); QWidget *lProgressWidget = lJobTracker->widget(lListJob); mUI->mSourceScanLayout->insertWidget(2, lProgressWidget); lProgressWidget->show(); connect(lListJob, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)), SLOT(collectSourceListing(KIO::Job*,KIO::UDSEntryList))); connect(lListJob, SIGNAL(result(KJob*)), SLOT(sourceListingCompleted(KJob*))); lListJob->start(); mUI->mStackedWidget->setCurrentIndex(4); } else { mDirectoriesCount = 0; mSourceSize = mSourceInfo.mSize; mFileSizes.insert(mSourceFileName, mSourceInfo.mSize); mRestorationPath = mDestination.absolutePath(); if(mDestination.exists() || mDestination.fileName() != mSourceFileName) { mRestorationPath.append(QDir::separator()); mRestorationPath.append(cKupTempRestoreFolder); if(mDestination.exists()) { mUI->mFileConflictList->addItem(mDestination.absoluteFilePath()); } } completePrechecks(); } } void RestoreDialog::collectSourceListing(KIO::Job *pJob, const KIO::UDSEntryList &pEntryList) { Q_UNUSED(pJob) KIO::UDSEntryList::ConstIterator it = pEntryList.begin(); const KIO::UDSEntryList::ConstIterator end = pEntryList.end(); for(; it != end; ++it) { QString lEntryName = it->stringValue(KIO::UDSEntry::UDS_NAME); if(it->isDir()) { if(lEntryName != QStringLiteral(".") && lEntryName != QStringLiteral("..")) { mDirectoriesCount++; } } else { if(!it->isLink()) { auto lEntrySize = static_cast(it->numberValue(KIO::UDSEntry::UDS_SIZE)); mSourceSize += lEntrySize; mFileSizes.insert(mSourceFileName + QDir::separator() + lEntryName, lEntrySize); } if(!mSavedWorkingDirectory.isEmpty()) { if(QFileInfo::exists(lEntryName)) { mUI->mFileConflictList->addItem(lEntryName); } } } } } void RestoreDialog::sourceListingCompleted(KJob *pJob) { qCDebug(KUPFILEDIGGER) << "Source listing job completed. Exit status: " << pJob->error(); if(!mSavedWorkingDirectory.isEmpty()) { QDir::setCurrent(mSavedWorkingDirectory); } if(pJob->error() != 0) { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "There was a problem while getting a list of all files to restore: %1", pJob->errorString())); mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->animatedShow(); mUI->mStackedWidget->setCurrentIndex(0); } else { completePrechecks(); } } void RestoreDialog::completePrechecks() { qCDebug(KUPFILEDIGGER) << "Starting free disk space check on: " << mDestination.absolutePath(); KDiskFreeSpaceInfo lSpaceInfo = KDiskFreeSpaceInfo::freeSpaceInfo(mDestination.absolutePath()); if(lSpaceInfo.isValid() && lSpaceInfo.available() < mSourceSize) { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "The destination does not have enough space available. " "Please choose a different destination or free some space.")); mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->animatedShow(); mUI->mStackedWidget->setCurrentIndex(0); } else if(mUI->mFileConflictList->count() > 0) { qCDebug(KUPFILEDIGGER) << "Detected file conflicts."; if(mSourceInfo.mIsDirectory) { QString lDateString = QLocale().toString(QDateTime::fromSecsSinceEpoch(static_cast(mSourceInfo.mCommitTime)).toLocalTime()); lDateString.replace(QLatin1Char('/'), QLatin1Char('-')); // make sure no slashes in suggested folder name mUI->mNewFolderNameEdit->setText(mSourceFileName + xi18nc("added to the suggested filename when restoring, %1 is the time when backup was saved", " - saved at %1", lDateString)); mUI->mConflictTitleLabel->setText(xi18nc("@info", "Folder already exists, please choose a solution")); } else { mUI->mOverwriteRadioButton->setChecked(true); mUI->mOverwriteRadioButton->hide(); mUI->mNewNameRadioButton->hide(); mUI->mNewFolderNameEdit->hide(); mUI->mConflictTitleLabel->setText(xi18nc("@info", "File already exists")); } mUI->mStackedWidget->setCurrentIndex(2); } else { startRestoring(); } } void RestoreDialog::fileOverwriteConfirmed() { if(mSourceInfo.mIsDirectory && mUI->mNewNameRadioButton->isChecked()) { QFileInfo lNewFolderInfo(mDestination.absoluteFilePath() + QDir::separator() + mUI->mNewFolderNameEdit->text()); if(lNewFolderInfo.exists()) { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "The new name entered already exists, please enter a different one.")); mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->animatedShow(); return; } mFolderToCreate = QFileInfo(mDestination.absoluteFilePath() + QDir::separator() + mUI->mNewFolderNameEdit->text()); mRestorationPath = mFolderToCreate.absoluteFilePath(); if(!mSourceInfo.mPathInRepo.endsWith(QDir::separator())) { mSourceInfo.mPathInRepo.append(QDir::separator()); } } startRestoring(); } void RestoreDialog::startRestoring() { QString lSourcePath(QDir::separator()); lSourcePath.append(mSourceInfo.mBranchName); lSourcePath.append(QDir::separator()); QDateTime lCommitTime = QDateTime::fromSecsSinceEpoch(static_cast(mSourceInfo.mCommitTime)); lSourcePath.append(lCommitTime.toString(QStringLiteral("yyyy-MM-dd-hhmmss"))); lSourcePath.append(mSourceInfo.mPathInRepo); qCDebug(KUPFILEDIGGER) << "Starting restore. Source path: " << lSourcePath << ", restore path: " << mRestorationPath; auto lRestoreJob = new RestoreJob(mSourceInfo.mRepoPath, lSourcePath, mRestorationPath, mDirectoriesCount, mSourceSize, mFileSizes); if(mJobTracker == nullptr) { mJobTracker = new KWidgetJobTracker(this); } mJobTracker->registerJob(lRestoreJob); QWidget *lProgressWidget = mJobTracker->widget(lRestoreJob); mUI->mRestoreProgressLayout->insertWidget(2, lProgressWidget); lProgressWidget->show(); connect(lRestoreJob, SIGNAL(result(KJob*)), SLOT(restoringCompleted(KJob*))); lRestoreJob->start(); mUI->mCloseButton->hide(); mUI->mStackedWidget->setCurrentIndex(3); } void RestoreDialog::restoringCompleted(KJob *pJob) { qCDebug(KUPFILEDIGGER) << "Restore job completed. Exit status: " << pJob->error(); if(pJob->error() != 0) { mUI->mRestorationOutput->setPlainText(pJob->errorText()); mUI->mRestorationStackWidget->setCurrentIndex(1); mUI->mCloseButton->show(); } else { if(!mSourceInfo.mIsDirectory && mSourceFileName != mDestination.fileName()) { QUrl lSourceUrl = QUrl::fromLocalFile(mRestorationPath + '/' + mSourceFileName); QUrl lDestinationUrl = QUrl::fromLocalFile(mRestorationPath + '/' + mDestination.fileName()); KIO::CopyJob *lFileMoveJob = KIO::move(lSourceUrl, lDestinationUrl, KIO::HideProgressInfo); connect(lFileMoveJob, SIGNAL(result(KJob*)), SLOT(fileMoveCompleted(KJob*))); qCDebug(KUPFILEDIGGER) << "Starting file move job from: " << lSourceUrl << ", to: " << lDestinationUrl; lFileMoveJob->start(); } else { moveFolder(); } } } void RestoreDialog::fileMoveCompleted(KJob *pJob) { qCDebug(KUPFILEDIGGER) << "File move job completed. Exit status: " << pJob->error(); if(pJob->error() != 0) { mUI->mRestorationOutput->setPlainText(pJob->errorText()); mUI->mRestorationStackWidget->setCurrentIndex(1); } else { moveFolder(); } } void RestoreDialog::createNewFolder() { bool lUserAccepted; QUrl lUrl = mDirSelector->url(); QString lNameSuggestion = xi18nc("default folder name when creating a new folder", "New Folder"); if(QFileInfo::exists(lUrl.adjusted(QUrl::StripTrailingSlash).path() + '/' + lNameSuggestion)) { lNameSuggestion = KIO::suggestName(lUrl, lNameSuggestion); } QString lSelectedName = QInputDialog::getText(this, xi18nc("@title:window", "New Folder" ), xi18nc("@label:textbox", "Create new folder in:\n%1", lUrl.path()), QLineEdit::Normal, lNameSuggestion, &lUserAccepted); if (!lUserAccepted) return; QUrl lPartialUrl(lUrl); const QStringList lDirectories = lSelectedName.split(QDir::separator(), QString::SkipEmptyParts); foreach(QString lSubDirectory, lDirectories) { QDir lDir(lPartialUrl.path()); if(lDir.exists(lSubDirectory)) { lPartialUrl = lPartialUrl.adjusted(QUrl::StripTrailingSlash); lPartialUrl.setPath(lPartialUrl.path() + '/' + (lSubDirectory)); KMessageBox::sorry(this, i18n("A folder named %1 already exists.", lPartialUrl.path())); return; } if(!lDir.mkdir(lSubDirectory)) { lPartialUrl = lPartialUrl.adjusted(QUrl::StripTrailingSlash); lPartialUrl.setPath(lPartialUrl.path() + '/' + (lSubDirectory)); KMessageBox::sorry(this, i18n("You do not have permission to create %1.", lPartialUrl.path())); return; } lPartialUrl = lPartialUrl.adjusted(QUrl::StripTrailingSlash); lPartialUrl.setPath(lPartialUrl.path() + '/' + (lSubDirectory)); } mDirSelector->expandToUrl(lPartialUrl); } void RestoreDialog::openDestinationFolder() { KRun::runUrl(QUrl::fromLocalFile(mSourceInfo.mIsDirectory ? mFolderToCreate.absoluteFilePath() : mDestination.absolutePath()), QStringLiteral("inode/directory"), nullptr, KRun::RunFlags()); } void RestoreDialog::moveFolder() { if(!mRestorationPath.endsWith(cKupTempRestoreFolder)) { mUI->mRestorationStackWidget->setCurrentIndex(2); mUI->mCloseButton->show(); qCDebug(KUPFILEDIGGER) << "Overall restore operation completed."; return; } QUrl lSourceUrl = QUrl::fromLocalFile(mRestorationPath); QUrl lDestinationUrl = QUrl::fromLocalFile(mRestorationPath.section(QDir::separator(), 0, -2)); KIO::CopyJob *lFolderMoveJob = KIO::moveAs(lSourceUrl, lDestinationUrl, KIO::Overwrite | KIO::HideProgressInfo); connect(lFolderMoveJob, SIGNAL(result(KJob*)), SLOT(folderMoveCompleted(KJob*))); mJobTracker->registerJob(lFolderMoveJob); QWidget *lProgressWidget = mJobTracker->widget(lFolderMoveJob); mUI->mRestoreProgressLayout->insertWidget(1, lProgressWidget); lProgressWidget->show(); qCDebug(KUPFILEDIGGER) << "Starting folder move job from: " << lSourceUrl << ", to: " << lDestinationUrl; lFolderMoveJob->start(); } void RestoreDialog::folderMoveCompleted(KJob *pJob) { qCDebug(KUPFILEDIGGER) << "Folder move job completed. Exit status: " << pJob->error(); mUI->mCloseButton->show(); if(pJob->error() != 0) { mUI->mRestorationOutput->setPlainText(pJob->errorText()); mUI->mRestorationStackWidget->setCurrentIndex(1); } else { qCDebug(KUPFILEDIGGER) << "Overall restore operation completed."; mUI->mRestorationStackWidget->setCurrentIndex(2); } } kup-backup-0.9.1/filedigger/restoredialog.h000066400000000000000000000035171420500356400207050ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef RESTOREDIALOG_H #define RESTOREDIALOG_H #include "versionlistmodel.h" #include #include #include namespace Ui { class RestoreDialog; } class DirSelector; class KFileWidget; class KMessageWidget; class KWidgetJobTracker; class QTreeWidget; class RestoreDialog : public QDialog { Q_OBJECT public: explicit RestoreDialog(BupSourceInfo pPathInfo, QWidget *parent = nullptr); ~RestoreDialog() override; protected: void changeEvent(QEvent *pEvent) override; protected slots: void setOriginalDestination(); void setCustomDestination(); void checkDestinationSelection(); void checkDestinationSelection2(); void startPrechecks(); void collectSourceListing(KIO::Job *pJob, const KIO::UDSEntryList &pEntryList); void sourceListingCompleted(KJob *pJob); void completePrechecks(); void fileOverwriteConfirmed(); void startRestoring(); void restoringCompleted(KJob *pJob); void fileMoveCompleted(KJob *pJob); void folderMoveCompleted(KJob *pJob); void createNewFolder(); void openDestinationFolder(); private: void checkForExistingFiles(const KIO::UDSEntryList &pEntryList); void moveFolder(); Ui::RestoreDialog *mUI; KFileWidget *mFileWidget; DirSelector *mDirSelector; QFileInfo mDestination; QFileInfo mFolderToCreate; QString mRestorationPath; // not necessarily same as destination BupSourceInfo mSourceInfo; quint64 mDestinationSize{}; //size of files about to be overwritten quint64 mSourceSize{}; //size of files about to be read KMessageWidget *mMessageWidget; QString mSavedWorkingDirectory; QString mSourceFileName; QHash mFileSizes; int mDirectoriesCount{}; KWidgetJobTracker *mJobTracker; }; #endif // RESTOREDIALOG_H kup-backup-0.9.1/filedigger/restoredialog.ui000066400000000000000000000445401420500356400210740ustar00rootroot00000000000000 RestoreDialog 0 0 600 400 Restore Guide 0 Qt::Horizontal 40 20 Qt::Vertical 20 40 Restore to original location Qt::Vertical QSizePolicy::Fixed 20 40 Choose where to restore Qt::Vertical 20 40 Qt::Horizontal 40 20 Qt::Horizontal 40 20 Back .. Next .. 75 true Restore the folder under a new name true Qt::Horizontal QSizePolicy::Fixed 20 20 Merge folders Qt::Horizontal QSizePolicy::Fixed 20 20 false The following files would be overwritten, please confirm that you wish to continue. true false Qt::Horizontal 40 20 Back .. Confirm .. 0 Qt::Vertical 20 40 Restoring files Qt::AlignCenter Qt::Vertical 20 40 An error occurred while restoring: true Qt::Vertical 20 149 Restoration completed successfully! Qt::AlignCenter Qt::Vertical QSizePolicy::Fixed 20 20 Qt::Horizontal 192 20 Open Destination .. Qt::Horizontal 192 20 Qt::Vertical 20 149 Qt::Horizontal 40 20 Close .. true Qt::Vertical 20 176 Checking file sizes Qt::AlignCenter Qt::Vertical 20 176 mRestoreOriginalButton mRestoreCustomButton mDestNextButton mOverwriteBackButton mDestBackButton mFileConflictList mConfirmButton mRestorationOutput mCloseButton mCloseButton clicked() RestoreDialog accept() 590 390 236 199 mOverwriteRadioButton toggled(bool) mFileConflictList setEnabled(bool) 299 99 312 241 mOverwriteRadioButton toggled(bool) mConfirmOverwriteLabel setEnabled(bool) 299 99 312 122 mNewNameRadioButton toggled(bool) mNewFolderNameEdit setEnabled(bool) 299 45 312 72 kup-backup-0.9.1/filedigger/restorejob.cpp000066400000000000000000000073051420500356400205520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "restorejob.h" #include #include #include #include #include #ifdef Q_OS_LINUX #include #endif RestoreJob::RestoreJob(QString pRepositoryPath, QString pSourcePath, QString pRestorationPath, int pTotalDirCount, quint64 pTotalFileSize, const QHash &pFileSizes) : mRepositoryPath(std::move(pRepositoryPath)), mSourcePath(std::move(pSourcePath)), mRestorationPath(std::move(pRestorationPath)), mTotalDirCount(pTotalDirCount), mTotalFileSize(pTotalFileSize), mFileSizes(pFileSizes) { setCapabilities(Killable); mRestoreProcess.setOutputChannelMode(KProcess::SeparateChannels); int lOffset = mSourcePath.endsWith(QDir::separator()) ? -2: -1; mSourceFileName = mSourcePath.section(QDir::separator(), lOffset, lOffset); } void RestoreJob::start() { setTotalAmount(Bytes, mTotalFileSize); setProcessedAmount(Bytes, 0); setTotalAmount(Files, static_cast(mFileSizes.count())); setProcessedAmount(Files, 0); setTotalAmount(Directories, static_cast(mTotalDirCount)); setProcessedAmount(Directories, 0); setPercent(0); mRestoreProcess << QStringLiteral("bup"); mRestoreProcess << QStringLiteral("-d") << mRepositoryPath; mRestoreProcess << QStringLiteral("restore") << QStringLiteral("-vv"); mRestoreProcess << QStringLiteral("-C") << mRestorationPath; mRestoreProcess << mSourcePath; connect(&mRestoreProcess, SIGNAL(started()), SLOT(slotRestoringStarted())); connect(&mRestoreProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotRestoringDone(int,QProcess::ExitStatus))); mRestoreProcess.start(); mTimerId = startTimer(100); } void RestoreJob::slotRestoringStarted() { makeNice(mRestoreProcess.pid()); } void RestoreJob::timerEvent(QTimerEvent *pTimerEvent) { Q_UNUSED(pTimerEvent) quint64 lProcessedDirectories = processedAmount(Directories); quint64 lProcessedFiles = processedAmount(Files); quint64 lProcessedBytes = processedAmount(Bytes); bool lDirWasUpdated = false; bool lFileWasUpdated = false; QString lLastFileName; while(mRestoreProcess.canReadLine()) { QString lFileName = QString::fromLocal8Bit(mRestoreProcess.readLine()).trimmed(); if(lFileName.count() == 0) { break; } if(lFileName.endsWith(QLatin1Char('/'))) { // it's a directory lProcessedDirectories++; lDirWasUpdated = true; } else { if(mSourcePath.endsWith(QDir::separator())) { lFileName.prepend(QDir::separator()); lFileName.prepend(mSourceFileName); } lProcessedBytes += mFileSizes.value(lFileName); lProcessedFiles++; lLastFileName = lFileName; lFileWasUpdated = true; } } if(lDirWasUpdated) { setProcessedAmount(Directories, lProcessedDirectories); } if(lFileWasUpdated) { emit description(this, xi18nc("progress report, current operation", "Restoring"), qMakePair(xi18nc("progress report, label", "File"), lLastFileName)); setProcessedAmount(Files, lProcessedFiles); setProcessedAmount(Bytes, lProcessedBytes); // this will also call emitPercent() } } void RestoreJob::slotRestoringDone(int pExitCode, QProcess::ExitStatus pExitStatus) { killTimer(mTimerId); if(pExitStatus != QProcess::NormalExit || pExitCode != 0) { setError(1); setErrorText(QString::fromUtf8(mRestoreProcess.readAllStandardError())); } emitResult(); } void RestoreJob::makeNice(int pPid) { #ifdef Q_OS_LINUX // See linux documentation Documentation/block/ioprio.txt for details of the syscall syscall(SYS_ioprio_set, 1, pPid, 3 << 13 | 7); #endif setpriority(PRIO_PROCESS, static_cast(pPid), 19); } kup-backup-0.9.1/filedigger/restorejob.h000066400000000000000000000020211420500356400202050ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef RESTOREJOB_H #define RESTOREJOB_H #include "versionlistmodel.h" #include #include class RestoreJob : public KJob { Q_OBJECT public: explicit RestoreJob(QString pRepositoryPath, QString pSourcePath, QString pRestorationPath, int pTotalDirCount, quint64 pTotalFileSize, const QHash &pFileSizes); void start() override; protected slots: void slotRestoringStarted(); void slotRestoringDone(int pExitCode, QProcess::ExitStatus pExitStatus); protected: void timerEvent(QTimerEvent *pTimerEvent) override; static void makeNice(int pPid); void moveFolder(); KProcess mRestoreProcess; QString mRepositoryPath; QString mSourcePath; QString mRestorationPath; QString mSourceFileName; int mTotalDirCount; quint64 mTotalFileSize; const QHash &mFileSizes; int mTimerId{}; }; #endif // RESTOREJOB_H kup-backup-0.9.1/filedigger/versionlistdelegate.cpp000066400000000000000000000244751420500356400224570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "versionlistdelegate.h" #include "versionlistmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include static const int cMargin = 4; Button::Button(QString pText, QWidget *pParent) { // intentionally don't make this QObject owned by pParent mStyleOption.initFrom(pParent); mStyleOption.features = QStyleOptionButton::None; mStyleOption.state = QStyle::State_Enabled; mStyleOption.text = std::move(pText); const QSize lContentsSize = mStyleOption.fontMetrics.size(Qt::TextSingleLine, mStyleOption.text); mStyleOption.rect = QRect(QPoint(0, 0), QApplication::style()->sizeFromContents(QStyle::CT_PushButton, &mStyleOption, lContentsSize)); mPushed = false; mParent = pParent; } void Button::setPosition(const QPoint &pTopRight) { mStyleOption.rect.moveTopRight(pTopRight); } void Button::paint(QPainter *pPainter, float pOpacity) { pPainter->setOpacity(static_cast(pOpacity)); QApplication::style()->drawControl(QStyle::CE_PushButton, &mStyleOption, pPainter); } bool Button::event(QEvent *pEvent) { auto lMouseEvent = dynamic_cast(pEvent); bool lActivated = false; switch(pEvent->type()) { case QEvent::MouseMove: if(mStyleOption.rect.contains(lMouseEvent->pos())) { if(!(mStyleOption.state & QStyle::State_MouseOver)) { mStyleOption.state |= QStyle::State_MouseOver; if(mPushed) { mStyleOption.state |= QStyle::State_Sunken; mStyleOption.state &= ~QStyle::State_Raised; } mParent->update(mStyleOption.rect); } } else { if(mStyleOption.state & QStyle::State_MouseOver) { mStyleOption.state &= ~QStyle::State_MouseOver; if(mPushed) { mStyleOption.state &= ~QStyle::State_Sunken; mStyleOption.state |= QStyle::State_Raised; } mParent->update(mStyleOption.rect); } } break; case QEvent::MouseButtonPress: if(lMouseEvent->button() == Qt::LeftButton && !mPushed && (mStyleOption.state & QStyle::State_MouseOver)) { mPushed = true; mStyleOption.state |= QStyle::State_Sunken; mStyleOption.state &= ~QStyle::State_Raised; mParent->update(mStyleOption.rect); } break; case QEvent::MouseButtonRelease: if(lMouseEvent->button() == Qt::LeftButton) { if(mPushed && (mStyleOption.state & QStyle::State_MouseOver)) { lActivated = true; } mPushed = false; mStyleOption.state &= ~QStyle::State_Sunken; mStyleOption.state |= QStyle::State_Raised; mParent->update(mStyleOption.rect); } break; case QEvent::KeyPress: { auto lKeyEvent = dynamic_cast(pEvent); if((mStyleOption.state & QStyle::State_HasFocus)) { if(lKeyEvent->key() == Qt::Key_Left || lKeyEvent->key() == Qt::Key_Right) { mStyleOption.state &= ~QStyle::State_HasFocus; emit focusChangeRequested(lKeyEvent->key() == Qt::Key_Right); mParent->update(mStyleOption.rect); } else if(lKeyEvent->key() == Qt::Key_Space || lKeyEvent->key() == Qt::Key_Return || lKeyEvent->key() == Qt::Key_Enter) { lActivated = true; } } break; } default: break; } return lActivated; } VersionItemAnimation::VersionItemAnimation(QWidget *pParent) : QParallelAnimationGroup(pParent) { mParent = pParent; mExtraHeight = 0; mOpacity = 0; mOpenButton = new Button(xi18nc("@action:button", "Open"), pParent); connect(mOpenButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); mRestoreButton = new Button(xi18nc("@action:button", "Restore"), pParent); connect(mRestoreButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); auto lHeightAnimation = new QPropertyAnimation(this, "extraHeight", this); lHeightAnimation->setStartValue(0.0); lHeightAnimation->setEndValue(1.0); lHeightAnimation->setDuration(300); lHeightAnimation->setEasingCurve(QEasingCurve::InOutBack); addAnimation(lHeightAnimation); auto lWidgetOpacityAnimation = new QPropertyAnimation(this, "opacity", this); lWidgetOpacityAnimation->setStartValue(0.0); lWidgetOpacityAnimation->setEndValue(1.0); lWidgetOpacityAnimation->setDuration(300); addAnimation(lWidgetOpacityAnimation); } void VersionItemAnimation::setExtraHeight(qreal pExtraHeight) { mExtraHeight = pExtraHeight; emit sizeChanged(mIndex); } void VersionItemAnimation::changeFocus(bool pForward) { Q_UNUSED(pForward) if(sender() == mOpenButton) { mRestoreButton->mStyleOption.state |= QStyle::State_HasFocus; mParent->update(mRestoreButton->mStyleOption.rect); } else if(sender() == mRestoreButton) { mOpenButton->mStyleOption.state |= QStyle::State_HasFocus; mParent->update(mOpenButton->mStyleOption.rect); } } void VersionItemAnimation::setFocus(bool pFocused) { if(!pFocused) { mOpenButton->mStyleOption.state &= ~QStyle::State_HasFocus; mRestoreButton->mStyleOption.state &= ~QStyle::State_HasFocus; } else { mOpenButton->mStyleOption.state |= QStyle::State_HasFocus; mRestoreButton->mStyleOption.state &= ~QStyle::State_HasFocus; } mParent->update(mOpenButton->mStyleOption.rect); mParent->update(mRestoreButton->mStyleOption.rect); } VersionListDelegate::VersionListDelegate(QAbstractItemView *pItemView, QObject *pParent) : QAbstractItemDelegate(pParent) { mView = pItemView; mModel = pItemView->model(); connect(pItemView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(updateCurrent(QModelIndex,QModelIndex))); connect(pItemView->model(), SIGNAL(modelReset()), SLOT(reset())); pItemView->viewport()->installEventFilter(this); //mouse events pItemView->installEventFilter(this); //keyboard events pItemView->viewport()->setMouseTracking(true); } VersionListDelegate::~VersionListDelegate()=default; void VersionListDelegate::paint(QPainter *pPainter, const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const { QStyle * lStyle = QApplication::style(); lStyle->drawPrimitive(QStyle::PE_PanelItemViewItem, &pOption, pPainter); pPainter->save(); pPainter->setPen(pOption.palette.color(pOption.state & QStyle::State_Selected ? QPalette::HighlightedText: QPalette::Text)); QRect lMarginRect = pOption.rect.adjusted(cMargin, cMargin, -cMargin, -cMargin); QRect lSizeDisplayBounds; if(!pIndex.data(VersionIsDirectoryRole).toBool()) { QString lSizeText = KFormat().formatByteSize(static_cast(pIndex.data(VersionSizeRole).toULongLong())); pPainter->drawText(lMarginRect, Qt::AlignRight | Qt::AlignTop, lSizeText, &lSizeDisplayBounds); } QString lDateText = pOption.fontMetrics.elidedText(pIndex.data().toString(), Qt::ElideRight, lMarginRect.width() - lSizeDisplayBounds.width()); pPainter->drawText(lMarginRect, Qt::AlignLeft | Qt::AlignTop, lDateText); pPainter->restore(); VersionItemAnimation *lAnimation = mActiveAnimations.value(pIndex); if(lAnimation != nullptr) { pPainter->save(); pPainter->setClipRect(pOption.rect); lAnimation->mRestoreButton->setPosition(pOption.rect.topRight() + QPoint(-cMargin, pOption.fontMetrics.height() + 2*cMargin)); lAnimation->mRestoreButton->paint(pPainter, lAnimation->opacity()); lAnimation->mOpenButton->setPosition(lAnimation->mRestoreButton->mStyleOption.rect.topLeft() + QPoint(-cMargin , 0)); lAnimation->mOpenButton->paint(pPainter, lAnimation->opacity()); pPainter->restore(); } } QSize VersionListDelegate::sizeHint(const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const { int lExtraHeight = 0; int lExtraWidth = 0; VersionItemAnimation *lAnimation = mActiveAnimations.value(pIndex); if(lAnimation != nullptr) { int lButtonHeight = lAnimation->mOpenButton->mStyleOption.rect.height(); lExtraHeight = qCeil(lAnimation->extraHeight() * (lButtonHeight + cMargin)); lExtraWidth = lAnimation->mOpenButton->mStyleOption.rect.width() + lAnimation->mRestoreButton->mStyleOption.rect.width(); } return {lExtraWidth, cMargin*2 + pOption.fontMetrics.height() + lExtraHeight}; } bool VersionListDelegate::eventFilter(QObject *pObject, QEvent *pEvent) { foreach (VersionItemAnimation *lAnimation, mActiveAnimations) { if(lAnimation->mOpenButton->event(pEvent)) { emit openRequested(lAnimation->mIndex); } if(lAnimation->mRestoreButton->event(pEvent)) { emit restoreRequested(lAnimation->mIndex); } } return QAbstractItemDelegate::eventFilter(pObject, pEvent); } void VersionListDelegate::updateCurrent(const QModelIndex &pCurrent, const QModelIndex &pPrevious) { if(pPrevious.isValid()) { VersionItemAnimation *lPrevAnim = mActiveAnimations.value(pPrevious); if(lPrevAnim != nullptr) { lPrevAnim->setDirection(QAbstractAnimation::Backward); lPrevAnim->start(); lPrevAnim->setFocus(false); } } if(pCurrent.isValid()) { VersionItemAnimation *lCurAnim = mActiveAnimations.value(pCurrent); if(lCurAnim == nullptr) { if(!mInactiveAnimations.isEmpty()) { lCurAnim = mInactiveAnimations.takeFirst(); } else { lCurAnim = new VersionItemAnimation(mView->viewport()); connect(lCurAnim, SIGNAL(sizeChanged(QModelIndex)), SIGNAL(sizeHintChanged(QModelIndex))); connect(lCurAnim, SIGNAL(finished()), SLOT(reclaimAnimation())); } lCurAnim->mIndex = pCurrent; mActiveAnimations.insert(pCurrent, lCurAnim); } lCurAnim->setDirection(QAbstractAnimation::Forward); lCurAnim->start(); lCurAnim->setFocus(true); } } void VersionListDelegate::reset() { mInactiveAnimations.append(mActiveAnimations.values()); mActiveAnimations.clear(); } void VersionListDelegate::reclaimAnimation() { auto lAnimation = qobject_cast(sender()); if(lAnimation->direction() == QAbstractAnimation::Backward) { mInactiveAnimations.append(lAnimation); foreach(VersionItemAnimation *lActiveAnimation, mActiveAnimations) { if(lActiveAnimation == lAnimation) { mActiveAnimations.remove(lAnimation->mIndex); break; } } } } kup-backup-0.9.1/filedigger/versionlistdelegate.h000066400000000000000000000045261420500356400221170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef VERSIONLISTDELEGATE_H #define VERSIONLISTDELEGATE_H #include #include class Button : public QObject { Q_OBJECT public: Button(QString pText, QWidget *pParent); bool mPushed; QStyleOptionButton mStyleOption; QWidget *mParent; void setPosition(const QPoint &pTopRight); void paint(QPainter *pPainter, float pOpacity); bool event(QEvent *pEvent) override; signals: void focusChangeRequested(bool pForward); }; class VersionItemAnimation : public QParallelAnimationGroup { Q_OBJECT Q_PROPERTY(float extraHeight READ extraHeight WRITE setExtraHeight) Q_PROPERTY(float opacity READ opacity WRITE setOpacity) public: explicit VersionItemAnimation(QWidget *pParent); ~VersionItemAnimation() { delete mOpenButton; delete mRestoreButton; } qreal extraHeight() {return mExtraHeight;} float opacity() {return mOpacity;} signals: void sizeChanged(const QModelIndex &pIndex); public slots: void setExtraHeight(qreal pExtraHeight); void setOpacity(float pOpacity) {mOpacity = pOpacity;} void changeFocus(bool pForward); void setFocus(bool pFocused); public: QPersistentModelIndex mIndex; qreal mExtraHeight; float mOpacity; Button *mOpenButton; Button *mRestoreButton; QWidget *mParent; }; class VersionListDelegate : public QAbstractItemDelegate { Q_OBJECT public: explicit VersionListDelegate(QAbstractItemView *pItemView, QObject *pParent = nullptr); ~VersionListDelegate() override; void paint(QPainter *pPainter, const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const override; QSize sizeHint(const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const override; bool eventFilter(QObject *pObject, QEvent *pEvent) override; signals: void openRequested(const QModelIndex &pIndex); void restoreRequested(const QModelIndex &pIndex); public slots: void updateCurrent(const QModelIndex &pCurrent, const QModelIndex &pPrevious); void reset(); void reclaimAnimation(); protected: void initialize(); QAbstractItemView *mView; QAbstractItemModel *mModel; QHash mActiveAnimations; QList mInactiveAnimations; }; #endif // VERSIONLISTDELEGATE_H kup-backup-0.9.1/filedigger/versionlistmodel.cpp000066400000000000000000000037571420500356400220050ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "versionlistmodel.h" #include "vfshelpers.h" #include #include #include #include #include #include VersionListModel::VersionListModel(QObject *parent) : QAbstractListModel(parent) { mVersionList = nullptr; } void VersionListModel::setNode(const MergedNode *pNode) { beginResetModel(); mNode = pNode; mVersionList = mNode->versionList(); endResetModel(); } int VersionListModel::rowCount(const QModelIndex &pParent) const { Q_UNUSED(pParent) if(mVersionList != nullptr) { return mVersionList->count(); } return 0; } QVariant VersionListModel::data(const QModelIndex &pIndex, int pRole) const { if(!pIndex.isValid() || mVersionList == nullptr) { return QVariant(); } QMimeDatabase db; KFormat lFormat; VersionData *lData = mVersionList->at(pIndex.row()); switch (pRole) { case Qt::DisplayRole: return lFormat.formatRelativeDateTime(QDateTime::fromSecsSinceEpoch(static_cast(lData->mModifiedDate)), QLocale::ShortFormat); case VersionBupUrlRole: { QUrl lUrl; mNode->getBupUrl(pIndex.row(), &lUrl); return lUrl; } case VersionMimeTypeRole: if(mNode->isDirectory()) { return QString(QStringLiteral("inode/directory")); } return db.mimeTypeForFile(mNode->objectName(), QMimeDatabase::MatchExtension).name(); case VersionSizeRole: return lData->size(); case VersionSourceInfoRole: { BupSourceInfo lSourceInfo; mNode->getBupUrl(pIndex.row(), &lSourceInfo.mBupKioPath, &lSourceInfo.mRepoPath, &lSourceInfo.mBranchName, &lSourceInfo.mCommitTime, &lSourceInfo.mPathInRepo); lSourceInfo.mIsDirectory = mNode->isDirectory(); lSourceInfo.mSize = lData->size(); return QVariant::fromValue(lSourceInfo); } case VersionIsDirectoryRole: return mNode->isDirectory(); default: return QVariant(); } } kup-backup-0.9.1/filedigger/versionlistmodel.h000066400000000000000000000020511420500356400214340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef VERSIONLISTMODEL_H #define VERSIONLISTMODEL_H #include #include "mergedvfs.h" struct BupSourceInfo { QUrl mBupKioPath; QString mRepoPath; QString mBranchName; QString mPathInRepo; qint64 mCommitTime; quint64 mSize; bool mIsDirectory; }; Q_DECLARE_METATYPE(BupSourceInfo) class VersionListModel : public QAbstractListModel { Q_OBJECT public: explicit VersionListModel(QObject *parent = nullptr); void setNode(const MergedNode *pNode); int rowCount(const QModelIndex &pParent) const override; QVariant data(const QModelIndex &pIndex, int pRole) const override; protected: const VersionList *mVersionList; const MergedNode *mNode{}; }; enum VersionDataRole { VersionBupUrlRole = Qt::UserRole + 1, // QUrl VersionMimeTypeRole, // QString VersionSizeRole, // quint64 VersionSourceInfoRole, // PathInfo VersionIsDirectoryRole // bool }; #endif // VERSIONLISTMODEL_H kup-backup-0.9.1/icons/000077500000000000000000000000001420500356400146755ustar00rootroot00000000000000kup-backup-0.9.1/icons/CMakeLists.txt000066400000000000000000000003341420500356400174350ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later ecm_install_icons(ICONS sc-apps-kup.svg DESTINATION ${ICON_INSTALL_DIR} THEME hicolor ) kup-backup-0.9.1/icons/sc-apps-kup.svg000066400000000000000000000037541420500356400175720ustar00rootroot00000000000000kup-backup-0.9.1/kcm/000077500000000000000000000000001420500356400143345ustar00rootroot00000000000000kup-backup-0.9.1/kcm/CMakeLists.txt000066400000000000000000000017161420500356400171010ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later include_directories("../settings") include_directories("../daemon") set(kcm_kup_SRCS backupplanwidget.cpp dirselector.cpp driveselection.cpp driveselectiondelegate.cpp folderselectionmodel.cpp kupkcm.cpp planstatuswidget.cpp kbuttongroup.cpp ../settings/backupplan.cpp ../settings/kupsettings.cpp ../settings/kuputils.cpp ) add_library(kcm_kup MODULE ${kcm_kup_SRCS}) #this is a library so it needs to enforce it's translation domain, not use the application's domain. add_definitions(-DTRANSLATION_DOMAIN="kup") target_link_libraries(kcm_kup Qt5::Core Qt5::DBus Qt5::Gui KF5::CoreAddons KF5::ConfigCore KF5::KIOCore KF5::KIOFileWidgets KF5::Solid KF5::I18n KF5::WidgetsAddons ) ########### install files ############### install(TARGETS kcm_kup DESTINATION ${PLUGIN_INSTALL_DIR}) install(FILES kcm_kup.desktop DESTINATION ${SERVICES_INSTALL_DIR}) kup-backup-0.9.1/kcm/backupplanwidget.cpp000066400000000000000000001223221420500356400203660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "backupplanwidget.h" #include "backupplan.h" #include "folderselectionmodel.h" #include "dirselector.h" #include "driveselection.h" #include "kbuttongroup.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class ScanFolderEvent : public QEvent { public: ScanFolderEvent(QString pPath): QEvent(eventType), mPath(std::move(pPath)) {} QString mPath; static const QEvent::Type eventType = static_cast(QEvent::User + 1); }; FileScanner::FileScanner() { // create a timer that will call a slot to send the pending updates to UI, one second // after the last update comes in, just to minimize risk of showing incomplete // information to the user. mUnreadablesTimer = new QTimer(this); mUnreadablesTimer->setSingleShot(true); mUnreadablesTimer->setInterval(1000); connect(mUnreadablesTimer, &QTimer::timeout, this, &FileScanner::sendPendingUnreadables); mSymlinkTimer = new QTimer(this); mSymlinkTimer->setSingleShot(true); mSymlinkTimer->setInterval(1000); connect(mSymlinkTimer, &QTimer::timeout, this, &FileScanner::sendPendingSymlinks); } bool FileScanner::event(QEvent *pEvent) { if(pEvent->type() == ScanFolderEvent::eventType) { auto lEvent = dynamic_cast(pEvent); if(isPathIncluded(lEvent->mPath)) { scanFolder(lEvent->mPath); } return true; } return QObject::event(pEvent); } void FileScanner::includePath(const QString &pPath) { if(!mExcludedFolders.remove(pPath)) { mIncludedFolders += pPath; } checkPathForProblems(QFileInfo(pPath)); QMutableHashIterator i(mSymlinksNotOk); while(i.hasNext()) { i.next(); if(isPathIncluded(i.value())) { mSymlinksOk.insert(i.key(), i.value()); i.remove(); mSymlinkTimer->start(); } } } void FileScanner::excludePath(const QString &pPath) { if(!mIncludedFolders.remove(pPath)) { mExcludedFolders += pPath; } QString lPath = pPath + QStringLiteral("/"); QSet::iterator it = mUnreadableFiles.begin(); while(it != mUnreadableFiles.end()) { if(it->startsWith(lPath)) { mUnreadablesTimer->start(); it = mUnreadableFiles.erase(it); } else { ++it; } } it = mUnreadableFolders.begin(); while(it != mUnreadableFolders.end()) { if(it->startsWith(lPath) || *it == pPath) { mUnreadablesTimer->start(); it = mUnreadableFolders.erase(it); } else { ++it; } } QMutableHashIterator i(mSymlinksNotOk); while(i.hasNext()) { if(!isPathIncluded(i.next().key())) { i.remove(); mSymlinkTimer->start(); } } i = mSymlinksOk; while(i.hasNext()) { i.next(); if(!isPathIncluded(i.key())) { i.remove(); } else if(isSymlinkProblematic(i.value())) { mSymlinksNotOk.insert(i.key(), i.value()); mSymlinkTimer->start(); i.remove(); } } } void FileScanner::sendPendingUnreadables() { emit unreadablesChanged(QPair, QSet>(mUnreadableFolders, mUnreadableFiles)); } void FileScanner::sendPendingSymlinks() { emit symlinkProblemsChanged(mSymlinksNotOk); } bool FileScanner::isPathIncluded(const QString &pPath) { int lLongestInclude = 0; foreach(const QString &lPath, mIncludedFolders) { bool lMatches = pPath == lPath || pPath.startsWith(lPath + QStringLiteral("/")); if(lMatches && lPath.length() > lLongestInclude) { lLongestInclude = lPath.length(); } } int lLongestExclude = 0; foreach(const QString &lPath, mExcludedFolders) { bool lMatches = pPath == lPath || pPath.startsWith(lPath + QStringLiteral("/")); if(lMatches && lPath.length() > lLongestExclude) { lLongestExclude = lPath.length(); } } return lLongestInclude > lLongestExclude; } void FileScanner::checkPathForProblems(const QFileInfo &pFileInfo) { if(pFileInfo.isSymLink()) { if(isSymlinkProblematic(pFileInfo.symLinkTarget())) { mSymlinksNotOk.insert(pFileInfo.absoluteFilePath(), pFileInfo.symLinkTarget()); mSymlinkTimer->start(); } else { mSymlinksOk.insert(pFileInfo.absoluteFilePath(), pFileInfo.symLinkTarget()); } } else if(pFileInfo.isDir()) { QCoreApplication::postEvent(this, new ScanFolderEvent(pFileInfo.absoluteFilePath()), Qt::LowEventPriority); } else { if(!pFileInfo.isReadable()) { mUnreadableFiles += pFileInfo.absoluteFilePath(); mUnreadablesTimer->start(); } } } bool FileScanner::isSymlinkProblematic(const QString &pTarget) { QFileInfo lTargetInfo(pTarget); return lTargetInfo.exists() && !isPathIncluded(pTarget) && pTarget.startsWith(QStringLiteral("/home/")); } void FileScanner::scanFolder(const QString &pPath) { QDir lDir(pPath); if(!lDir.isReadable()) { mUnreadableFolders += pPath; mUnreadablesTimer->start(); } else { QFileInfoList lInfoList = lDir.entryInfoList(QDir::Files | QDir::Dirs | QDir::Hidden| QDir::NoDotAndDotDot); foreach(const QFileInfo &lFileInfo, lInfoList) { checkPathForProblems(lFileInfo); } } } ConfigIncludeDummy::ConfigIncludeDummy(FolderSelectionModel *pModel, FolderSelectionWidget *pParent) : QWidget(pParent), mModel(pModel), mTreeView(pParent) { connect(mModel, &FolderSelectionModel::includedPathAdded, this, &ConfigIncludeDummy::includeListChanged); connect(mModel, &FolderSelectionModel::includedPathRemoved, this, &ConfigIncludeDummy::includeListChanged); } QStringList ConfigIncludeDummy::includeList() { QStringList lList = mModel->includedPaths().values(); lList.sort(); return lList; } void ConfigIncludeDummy::setIncludeList(QStringList pIncludeList) { for(int i = 0; i < pIncludeList.count(); ++i) { if(!QFile::exists(pIncludeList.at(i))) { pIncludeList.removeAt(i--); } } mModel->setIncludedPaths(QSet::fromList(pIncludeList)); mTreeView->expandToShowSelections(); } ConfigExcludeDummy::ConfigExcludeDummy(FolderSelectionModel *pModel, FolderSelectionWidget *pParent) : QWidget(pParent), mModel(pModel), mTreeView(pParent) { connect(mModel, &FolderSelectionModel::excludedPathAdded, this, &ConfigExcludeDummy::excludeListChanged); connect(mModel, &FolderSelectionModel::excludedPathRemoved, this, &ConfigExcludeDummy::excludeListChanged); } QStringList ConfigExcludeDummy::excludeList() { QStringList lList = mModel->excludedPaths().values(); lList.sort(); return lList; } void ConfigExcludeDummy::setExcludeList(QStringList pExcludeList) { for(int i = 0; i < pExcludeList.count(); ++i) { if(!QFile::exists(pExcludeList.at(i))) { pExcludeList.removeAt(i--); } } mModel->setExcludedPaths(QSet::fromList(pExcludeList)); mTreeView->expandToShowSelections(); } FolderSelectionWidget::FolderSelectionWidget(FolderSelectionModel *pModel, QWidget *pParent) : QWidget(pParent), mModel(pModel) { mMessageWidget = new KMessageWidget(this); mMessageWidget->setCloseButtonVisible(false); mMessageWidget->setWordWrap(true); mMessageWidget->hide(); mTreeView = new QTreeView(this); auto lVLayout = new QVBoxLayout; lVLayout->addWidget(mMessageWidget); lVLayout->addWidget(mTreeView, 1); setLayout(lVLayout); connect(mMessageWidget, &KMessageWidget::hideAnimationFinished, this, &FolderSelectionWidget::updateMessage); mExcludeAction = new QAction(xi18nc("@action:button", "Exclude Folder"), this); connect(mExcludeAction, &QAction::triggered, this, &FolderSelectionWidget::executeExcludeAction); mIncludeAction = new QAction(xi18nc("@action:button", "Include Folder"), this); connect(mIncludeAction, &QAction::triggered, this, &FolderSelectionWidget::executeIncludeAction); mModel->setRootPath(QStringLiteral("/")); mModel->setParent(this); mTreeView->setAnimated(true); mTreeView->setModel(mModel); mTreeView->setHeaderHidden(true); // always expand the home folder, prevents problem with empty include&exclude lists. QModelIndex lIndex = mModel->index(QDir::homePath()); while(lIndex.isValid()) { mTreeView->expand(lIndex); lIndex = lIndex.parent(); } auto lIncludeDummy = new ConfigIncludeDummy(mModel, this); lIncludeDummy->setObjectName(QStringLiteral("kcfg_Paths included")); auto lExcludeDummy = new ConfigExcludeDummy(mModel, this); lExcludeDummy->setObjectName(QStringLiteral("kcfg_Paths excluded")); qRegisterMetaType,QSet>>("QPair,QSet>"); qRegisterMetaType>("QHash"); mWorkerThread = new QThread(this); auto lFileScanner = new FileScanner; lFileScanner->moveToThread(mWorkerThread); connect(mWorkerThread, &QThread::finished, lFileScanner, &QObject::deleteLater); connect(mModel, &FolderSelectionModel::includedPathAdded, lFileScanner, &FileScanner::includePath); connect(mModel, &FolderSelectionModel::excludedPathRemoved, lFileScanner, &FileScanner::includePath); connect(mModel, &FolderSelectionModel::excludedPathAdded, lFileScanner, &FileScanner::excludePath); connect(mModel, &FolderSelectionModel::includedPathRemoved, lFileScanner, &FileScanner::excludePath); connect(lFileScanner, &FileScanner::unreadablesChanged, this, &FolderSelectionWidget::setUnreadables); connect(lFileScanner, &FileScanner::symlinkProblemsChanged, this, &FolderSelectionWidget::setSymlinks); mWorkerThread->start(); } FolderSelectionWidget::~FolderSelectionWidget() { mWorkerThread->quit(); mWorkerThread->wait(); } void FolderSelectionWidget::setHiddenFoldersVisible(bool pVisible) { mModel->setHiddenFoldersVisible(pVisible); // give the filesystem model some time to refresh after changing filtering // before expanding folders again. if(pVisible) { QTimer::singleShot(2000, this, SLOT(expandToShowSelections())); } } void FolderSelectionWidget::expandToShowSelections() { foreach(const QString& lFolder, mModel->includedPaths() + mModel->excludedPaths()) { QFileInfo lFolderInfo(lFolder); bool lShouldBeShown = true; while(lFolderInfo.absoluteFilePath() != QStringLiteral("/")) { if(lFolderInfo.isHidden() && !mModel->hiddenFoldersVisible()) { lShouldBeShown = false; // skip if this folder should not be shown. break; } lFolderInfo = lFolderInfo.absolutePath(); // move up one level } if(lShouldBeShown) { QModelIndex lIndex = mModel->index(lFolder).parent(); while(lIndex.isValid()) { mTreeView->expand(lIndex); lIndex = lIndex.parent(); } } } } void FolderSelectionWidget::setUnreadables(const QPair, QSet > &pUnreadables) { mUnreadableFolders = pUnreadables.first.values(); mUnreadableFiles = pUnreadables.second.values(); updateMessage(); } void FolderSelectionWidget::setSymlinks(QHash pSymlinks) { mSymlinkProblems = std::move(pSymlinks); updateMessage(); } void FolderSelectionWidget::updateMessage() { if(mMessageWidget->isVisible() || mMessageWidget->isHideAnimationRunning()) { mMessageWidget->animatedHide(); return; } mMessageWidget->removeAction(mExcludeAction); mMessageWidget->removeAction(mIncludeAction); if(!mUnreadableFolders.isEmpty()) { mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->setText(xi18nc("@info message bar appearing on top", "You don't have permission to read this folder: %1" "It cannot be included in the source selection. " "If it does not contain anything important to you, one possible " "solution is to exclude the folder from the backup plan.", mUnreadableFolders.first())); mExcludeActionPath = mUnreadableFolders.first(); mMessageWidget->addAction(mExcludeAction); mMessageWidget->animatedShow(); } else if(!mUnreadableFiles.isEmpty()) { mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->setText(xi18nc("@info message bar appearing on top", "You don't have permission to read this file: %1" "It cannot be included in the source selection. " "If the file is not important to you, one possible solution is " "to exclude the whole folder where the file is stored from the backup plan.", mUnreadableFiles.first())); QFileInfo lFileInfo = mUnreadableFiles.first(); mExcludeActionPath = lFileInfo.absolutePath(); mMessageWidget->addAction(mExcludeAction); mMessageWidget->animatedShow(); } else if(!mSymlinkProblems.isEmpty()) { mMessageWidget->setMessageType(KMessageWidget::Warning); QHashIterator i(mSymlinkProblems); i.next(); QFileInfo lFileInfo =i.value(); if(lFileInfo.isDir()) { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "The symbolic link %1 is currently included but it points " "to a folder which is not: %2.That is probably not " "what you want. One solution is to simply include the target folder in the " "backup plan.", i.key(), i.value())); mIncludeActionPath = i.value(); } else { mMessageWidget->setText(xi18nc("@info message bar appearing on top", "The symbolic link %1 is currently included but it points " "to a file which is not: %2.That is probably not " "what you want. One solution is to simply include the folder where the file " "is stored in the backup plan.", i.key(), i.value())); mIncludeActionPath = lFileInfo.absolutePath(); } mMessageWidget->addAction(mIncludeAction); mMessageWidget->animatedShow(); } } void FolderSelectionWidget::executeExcludeAction() { mModel->excludePath(mExcludeActionPath); } void FolderSelectionWidget::executeIncludeAction() { mModel->includePath(mIncludeActionPath); } DirDialog::DirDialog(const QUrl &pRootDir, const QString &pStartSubDir, QWidget *pParent) : QDialog(pParent) { setWindowTitle(xi18nc("@title:window","Select Folder")); mDirSelector = new DirSelector(this); mDirSelector->setRootUrl(pRootDir); QUrl lSubUrl = QUrl::fromLocalFile(pRootDir.adjusted(QUrl::StripTrailingSlash).path() + '/' + pStartSubDir); mDirSelector->expandToUrl(lSubUrl); auto lButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); connect(lButtonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(lButtonBox, SIGNAL(rejected()), this, SLOT(reject())); auto lNewFolderButton = new QPushButton(xi18nc("@action:button", "New Folder...")); connect(lNewFolderButton, SIGNAL(clicked()), mDirSelector, SLOT(createNewFolder())); lButtonBox->addButton(lNewFolderButton, QDialogButtonBox::ActionRole); QPushButton *lOkButton = lButtonBox->button(QDialogButtonBox::Ok); lOkButton->setDefault(true); lOkButton->setShortcut(Qt::Key_Return); auto lMainLayout = new QVBoxLayout; lMainLayout->addWidget(mDirSelector); lMainLayout->addWidget(lButtonBox); setLayout(lMainLayout); mDirSelector->setFocus(); } QUrl DirDialog::url() const { return mDirSelector->url(); } BackupPlanWidget::BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupVersion, const QString &pRsyncVersion, bool pPar2Available) : mBackupPlan(pBackupPlan) { mDescriptionEdit = new KLineEdit; mDescriptionEdit->setObjectName(QStringLiteral("kcfg_Description")); mDescriptionEdit->setClearButtonEnabled(true); auto lDescriptionLabel = new QLabel(xi18nc("@label", "Description:")); lDescriptionLabel->setBuddy(mDescriptionEdit); mConfigureButton = new QPushButton(QIcon::fromTheme(QStringLiteral("go-previous-view")), xi18nc("@action:button", "Back to overview")); connect(mConfigureButton, SIGNAL(clicked()), this, SIGNAL(requestOverviewReturn())); mConfigPages = new KPageWidget; mConfigPages->addPage(createTypePage(pBupVersion, pRsyncVersion)); mSourcePage = createSourcePage(); mConfigPages->addPage(mSourcePage); mConfigPages->addPage(createDestinationPage()); mConfigPages->addPage(createSchedulePage()); mConfigPages->addPage(createAdvancedPage(pPar2Available)); auto lHLayout1 = new QHBoxLayout; lHLayout1->addWidget(mConfigureButton); lHLayout1->addStretch(); lHLayout1->addWidget(lDescriptionLabel); lHLayout1->addWidget(mDescriptionEdit); auto lVLayout1 = new QVBoxLayout; lVLayout1->addLayout(lHLayout1); lVLayout1->addWidget(mConfigPages); lVLayout1->setSpacing(0); setLayout(lVLayout1); } void BackupPlanWidget::saveExtraData() { mDriveSelection->saveExtraData(); } void BackupPlanWidget::showSourcePage() { mConfigPages->setCurrentPage(mSourcePage); } KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, const QString &pRsyncVersion) { mVersionedRadio = new QRadioButton; QString lVersionedInfo = xi18nc("@info", "This type of backup is an archive. It contains both " "the latest version of your files and earlier backed up versions. " "Using this type of backup allows you to recover older versions of your " "files, or files which were deleted on your computer at a later time. " "The storage space needed is minimized by looking for common parts of " "your files between versions and only storing those parts once. " "Nevertheless, the backup archive will keep growing in size as time goes by." "Also important to know is that the files in the archive can not be accessed " "directly with a general file manager, a special program is needed."); auto lVersionedInfoLabel = new QLabel(lVersionedInfo); lVersionedInfoLabel->setWordWrap(true); auto lVersionedWidget = new QWidget; lVersionedWidget->setVisible(false); QObject::connect(mVersionedRadio, SIGNAL(toggled(bool)), lVersionedWidget, SLOT(setVisible(bool))); if(pBupVersion.isEmpty()) { mVersionedRadio->setText(xi18nc("@option:radio", "Versioned Backup (not available " "because bup is not installed)")); mVersionedRadio->setEnabled(false); lVersionedWidget->setEnabled(false); } else { mVersionedRadio->setText(xi18nc("@option:radio", "Versioned Backup (recommended)")); } mSyncedRadio = new QRadioButton; QString lSyncedInfo = xi18nc("@info", "This type of backup is a folder which is synchronized with your " "selected source folders. Saving a backup simply means making the " "backup destination contain an exact copy of your source folders as " "they are now and nothing else. If a file has been deleted in a " "source folder it will get deleted from the backup folder." "This type of backup can protect you against data loss due to a broken " "hard drive but it does not help you to recover from your own mistakes."); auto lSyncedInfoLabel = new QLabel(lSyncedInfo); lSyncedInfoLabel->setWordWrap(true); auto lSyncedWidget = new QWidget; lSyncedWidget->setVisible(false); QObject::connect(mSyncedRadio, SIGNAL(toggled(bool)), lSyncedWidget, SLOT(setVisible(bool))); if(pRsyncVersion.isEmpty()) { mSyncedRadio->setText(xi18nc("@option:radio", "Synchronized Backup (not available " "because rsync is not installed)")); mSyncedRadio->setEnabled(false); lSyncedWidget->setEnabled(false); } else { mSyncedRadio->setText(xi18nc("@option:radio", "Synchronized Backup")); } auto lButtonGroup = new KButtonGroup; lButtonGroup->setObjectName(QStringLiteral("kcfg_Backup type")); lButtonGroup->setFlat(true); int lIndentation = lButtonGroup->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth) + lButtonGroup->style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing); auto lVersionedVLayout = new QGridLayout; lVersionedVLayout->setColumnMinimumWidth(0, lIndentation); lVersionedVLayout->setContentsMargins(0, 0, 0, 0); lVersionedVLayout->addWidget(lVersionedInfoLabel, 0, 1); lVersionedWidget->setLayout(lVersionedVLayout); auto lSyncedVLayout = new QGridLayout; lSyncedVLayout->setColumnMinimumWidth(0, lIndentation); lSyncedVLayout->setContentsMargins(0, 0, 0, 0); lSyncedVLayout->addWidget(lSyncedInfoLabel, 0, 1); lSyncedWidget->setLayout(lSyncedVLayout); auto lVLayout = new QVBoxLayout; lVLayout->addWidget(mVersionedRadio); lVLayout->addWidget(lVersionedWidget); lVLayout->addWidget(mSyncedRadio); lVLayout->addWidget(lSyncedWidget); lVLayout->addStretch(); lButtonGroup->setLayout(lVLayout); auto lPage = new KPageWidgetItem(lButtonGroup); lPage->setName(xi18nc("@title", "Backup Type")); lPage->setHeader(xi18nc("@label", "Select what type of backup you want")); lPage->setIcon(QIcon::fromTheme(QStringLiteral("folder-sync"))); return lPage; } KPageWidgetItem *BackupPlanWidget::createSourcePage() { mSourceSelectionWidget = new FolderSelectionWidget(new FolderSelectionModel(mBackupPlan->mShowHiddenFolders), this); auto lPage = new KPageWidgetItem(mSourceSelectionWidget); lPage->setName(xi18nc("@title", "Sources")); lPage->setHeader(xi18nc("@label", "Select which folders to include in backup")); lPage->setIcon(QIcon::fromTheme(QStringLiteral("cloud-upload"))); return lPage; } KPageWidgetItem *BackupPlanWidget::createDestinationPage() { auto lButtonGroup = new KButtonGroup(this); lButtonGroup->setObjectName(QStringLiteral("kcfg_Destination type")); lButtonGroup->setFlat(true); int lIndentation = lButtonGroup->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth) + lButtonGroup->style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing); auto lVLayout = new QVBoxLayout; auto lFileSystemRadio = new QRadioButton(xi18nc("@option:radio", "Filesystem Path")); auto lDriveRadio = new QRadioButton(xi18nc("@option:radio", "External Storage")); auto lFileSystemWidget = new QWidget; lFileSystemWidget->setVisible(false); QObject::connect(lFileSystemRadio, SIGNAL(toggled(bool)), lFileSystemWidget, SLOT(setVisible(bool))); auto lFileSystemInfoLabel = new QLabel(xi18nc("@info", "You can use this option for backing up to a secondary " "internal harddrive, an external eSATA drive or networked " "storage. The requirement is just that you always mount " "it at the same path in the filesystem. The path " "specified here does not need to exist at all times, its " "existence will be monitored.")); lFileSystemInfoLabel->setWordWrap(true); auto lFileSystemLabel = new QLabel(xi18nc("@label:textbox", "Destination Path for Backup:")); auto lFileSystemUrlEdit = new KUrlRequester; lFileSystemUrlEdit->setMode(KFile::Directory | KFile::LocalOnly); lFileSystemUrlEdit->setObjectName(QStringLiteral("kcfg_Filesystem destination path")); auto lFileSystemVLayout = new QGridLayout; lFileSystemVLayout->setColumnMinimumWidth(0, lIndentation); lFileSystemVLayout->setContentsMargins(0, 0, 0, 0); lFileSystemVLayout->addWidget(lFileSystemInfoLabel, 0, 1); auto lFileSystemHLayout = new QHBoxLayout; lFileSystemHLayout->addWidget(lFileSystemLabel); lFileSystemHLayout->addWidget(lFileSystemUrlEdit, 1); lFileSystemVLayout->addLayout(lFileSystemHLayout, 1, 1); lFileSystemWidget->setLayout(lFileSystemVLayout); auto lDriveWidget = new QWidget; lDriveWidget->setVisible(false); QObject::connect(lDriveRadio, SIGNAL(toggled(bool)), lDriveWidget, SLOT(setVisible(bool))); auto lDriveInfoLabel = new QLabel(xi18nc("@info", "Use this option if you want to backup your " "files on an external storage that can be plugged in " "to this computer, such as a USB hard drive or memory " "stick.")); lDriveInfoLabel->setWordWrap(true); mDriveSelection = new DriveSelection(mBackupPlan); mDriveSelection->setObjectName(QStringLiteral("kcfg_External drive UUID")); mDriveDestEdit = new KLineEdit; mDriveDestEdit->setObjectName(QStringLiteral("kcfg_External drive destination path")); mDriveDestEdit->setToolTip(xi18nc("@info:tooltip", "The specified folder will be created if it does not exist.")); mDriveDestEdit->setClearButtonEnabled(true); auto lDriveDestLabel = new QLabel(xi18nc("@label:textbox", "Folder on Destination Drive:")); lDriveDestLabel->setToolTip(xi18nc("@info:tooltip", "The specified folder will be created if it does not exist.")); lDriveDestLabel->setBuddy(mDriveDestEdit); auto lDriveDestButton = new QPushButton; lDriveDestButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int lButtonSize = lDriveDestButton->sizeHint().expandedTo(mDriveDestEdit->sizeHint()).height(); lDriveDestButton->setFixedSize(lButtonSize, lButtonSize); lDriveDestButton->setToolTip(xi18nc("@info:tooltip", "Open dialog to select a folder")); lDriveDestButton->setEnabled(false); connect(mDriveSelection, SIGNAL(selectedDriveIsAccessibleChanged(bool)), lDriveDestButton, SLOT(setEnabled(bool))); connect(lDriveDestButton, SIGNAL(clicked()), SLOT(openDriveDestDialog())); auto lDriveDestWidget = new QWidget; lDriveDestWidget->setVisible(false); connect(mDriveSelection, SIGNAL(driveIsSelectedChanged(bool)), lDriveDestWidget, SLOT(setVisible(bool))); connect(mSyncedRadio, SIGNAL(toggled(bool)), mDriveSelection, SLOT(updateSyncWarning(bool))); auto lDriveVLayout = new QGridLayout; lDriveVLayout->setColumnMinimumWidth(0, lIndentation); lDriveVLayout->setContentsMargins(0, 0, 0, 0); lDriveVLayout->addWidget(lDriveInfoLabel, 0, 1); lDriveVLayout->addWidget(mDriveSelection, 1, 1); auto lDriveHLayout = new QHBoxLayout; lDriveHLayout->addWidget(lDriveDestLabel); lDriveHLayout->addWidget(mDriveDestEdit, 1); lDriveHLayout->addWidget(lDriveDestButton); lDriveDestWidget->setLayout(lDriveHLayout); lDriveVLayout->addWidget(lDriveDestWidget, 2, 1); lDriveWidget->setLayout(lDriveVLayout); lVLayout->addWidget(lFileSystemRadio); lVLayout->addWidget(lFileSystemWidget); lVLayout->addWidget(lDriveRadio); lVLayout->addWidget(lDriveWidget, 1); lVLayout->addStretch(); lButtonGroup->setLayout(lVLayout); auto lPage = new KPageWidgetItem(lButtonGroup); lPage->setName(xi18nc("@title", "Destination")); lPage->setHeader(xi18nc("@label", "Select the backup destination")); lPage->setIcon(QIcon::fromTheme(QStringLiteral("cloud-download"))); return lPage; } KPageWidgetItem *BackupPlanWidget::createSchedulePage() { auto lTopWidget = new QWidget(this); auto lTopLayout = new QVBoxLayout; auto lButtonGroup = new KButtonGroup; lButtonGroup->setObjectName(QStringLiteral("kcfg_Schedule type")); lButtonGroup->setFlat(true); int lIndentation = lButtonGroup->style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth) + lButtonGroup->style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing); auto lVLayout = new QVBoxLayout; lVLayout->setContentsMargins(0, 0, 0, 0); auto lManualRadio = new QRadioButton(xi18nc("@option:radio", "Manual Activation")); auto lIntervalRadio = new QRadioButton(xi18nc("@option:radio", "Interval")); auto lUsageRadio = new QRadioButton(xi18nc("@option:radio", "Active Usage Time")); auto lManualLabel = new QLabel(xi18nc("@info", "Backups are only saved when manually requested. " "This can be done by using the popup menu from " "the backup system tray icon.")); lManualLabel->setVisible(false); lManualLabel->setWordWrap(true); connect(lManualRadio, SIGNAL(toggled(bool)), lManualLabel, SLOT(setVisible(bool))); auto lManualLayout = new QGridLayout; lManualLayout->setColumnMinimumWidth(0, lIndentation); lManualLayout->setContentsMargins(0, 0, 0, 0); lManualLayout->addWidget(lManualLabel, 0, 1); auto lIntervalWidget = new QWidget; lIntervalWidget->setVisible(false); connect(lIntervalRadio, SIGNAL(toggled(bool)), lIntervalWidget, SLOT(setVisible(bool))); auto lIntervalLabel = new QLabel(xi18nc("@info", "New backup will be triggered when backup " "destination becomes available and more than " "the configured interval has passed since the " "last backup was saved.")); lIntervalLabel->setWordWrap(true); auto lIntervalVertLayout = new QGridLayout; lIntervalVertLayout->setColumnMinimumWidth(0, lIndentation); lIntervalVertLayout->setContentsMargins(0, 0, 0, 0); lIntervalVertLayout->addWidget(lIntervalLabel, 0, 1); auto lIntervalLayout = new QHBoxLayout; lIntervalLayout->setContentsMargins(0, 0, 0, 0); auto lIntervalSpinBox = new QSpinBox; lIntervalSpinBox->setObjectName(QStringLiteral("kcfg_Schedule interval")); lIntervalSpinBox->setMinimum(1); lIntervalLayout->addWidget(lIntervalSpinBox); auto lIntervalUnit = new KComboBox; lIntervalUnit->setObjectName(QStringLiteral("kcfg_Schedule interval unit")); lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Minutes")); lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Hours")); lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Days")); lIntervalUnit->addItem(xi18nc("@item:inlistbox", "Weeks")); lIntervalLayout->addWidget(lIntervalUnit); lIntervalLayout->addStretch(); lIntervalVertLayout->addLayout(lIntervalLayout, 1, 1); lIntervalWidget->setLayout(lIntervalVertLayout); auto lUsageWidget = new QWidget; lUsageWidget->setVisible(false); connect(lUsageRadio, SIGNAL(toggled(bool)), lUsageWidget, SLOT(setVisible(bool))); auto lUsageLabel = new QLabel(xi18nc("@info", "New backup will be triggered when backup destination " "becomes available and you have been using your " "computer actively for more than the configured " "time limit since the last backup was saved.")); lUsageLabel->setWordWrap(true); auto lUsageVertLayout = new QGridLayout; lUsageVertLayout->setColumnMinimumWidth(0, lIndentation); lUsageVertLayout->setContentsMargins(0, 0, 0, 0); lUsageVertLayout->addWidget(lUsageLabel, 0, 1); auto lUsageLayout = new QHBoxLayout; lUsageLayout->setContentsMargins(0, 0, 0, 0); auto lUsageSpinBox = new QSpinBox; lUsageSpinBox->setObjectName(QStringLiteral("kcfg_Usage limit")); lUsageSpinBox->setMinimum(1); lUsageLayout->addWidget(lUsageSpinBox); lUsageLayout->addWidget(new QLabel(xi18nc("@item:inlistbox", "Hours"))); lUsageLayout->addStretch(); lUsageVertLayout->addLayout(lUsageLayout, 1, 1); lUsageWidget->setLayout(lUsageVertLayout); auto lAskFirstCheckBox = new QCheckBox(xi18nc("@option:check", "Ask for confirmation before saving backup")); lAskFirstCheckBox->setObjectName(QStringLiteral("kcfg_Ask first")); connect(lManualRadio, SIGNAL(toggled(bool)), lAskFirstCheckBox, SLOT(setHidden(bool))); lVLayout->addWidget(lManualRadio); lVLayout->addLayout(lManualLayout); lVLayout->addWidget(lIntervalRadio); lVLayout->addWidget(lIntervalWidget); lVLayout->addWidget(lUsageRadio); lVLayout->addWidget(lUsageWidget); lButtonGroup->setLayout(lVLayout); lTopLayout->addWidget(lButtonGroup); lTopLayout->addSpacing(lAskFirstCheckBox->fontMetrics().height()); lTopLayout->addWidget(lAskFirstCheckBox); lTopLayout->addStretch(); lTopWidget->setLayout(lTopLayout); auto lPage = new KPageWidgetItem(lTopWidget); lPage->setName(xi18nc("@title", "Schedule")); lPage->setHeader(xi18nc("@label", "Specify the backup schedule")); lPage->setIcon(QIcon::fromTheme(QStringLiteral("view-calendar"))); return lPage; } KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available) { auto lAdvancedWidget = new QWidget(this); auto lAdvancedLayout = new QVBoxLayout; int lIndentation = lAdvancedWidget->style()->pixelMetric(QStyle::PM_IndicatorWidth) + lAdvancedWidget->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing); auto lShowHiddenWidget = new QWidget; auto lShowHiddenCheckBox = new QCheckBox(xi18nc("@option:check", "Show hidden folders in source selection")); lShowHiddenCheckBox->setObjectName(QStringLiteral("kcfg_Show hidden folders")); connect(lShowHiddenCheckBox, SIGNAL(toggled(bool)), mSourceSelectionWidget, SLOT(setHiddenFoldersVisible(bool))); auto lShowHiddenLabel = new QLabel(xi18nc("@info", "This makes it possible to explicitly include or " "exclude hidden folders in the backup source " "selection. Hidden folders have a name that starts " "with a dot. They are typically located in your home " "folder and are used to store settings and temporary " "files for your applications.")); lShowHiddenLabel->setWordWrap(true); auto lShowHiddenLayout = new QGridLayout; lShowHiddenLayout->setContentsMargins(0, 0, 0, 0); lShowHiddenLayout->setSpacing(0); lShowHiddenLayout->setColumnMinimumWidth(0, lIndentation); lShowHiddenLayout->addWidget(lShowHiddenCheckBox, 0, 0, 1, 2); lShowHiddenLayout->addWidget(lShowHiddenLabel, 1, 1); lShowHiddenWidget->setLayout(lShowHiddenLayout); auto lRecoveryWidget = new QWidget; lRecoveryWidget->setVisible(false); auto lRecoveryCheckBox = new QCheckBox; lRecoveryCheckBox->setObjectName(QStringLiteral("kcfg_Generate recovery info")); auto lRecoveryLabel = new QLabel(xi18nc("@info", "This will make your backups use around 10% more storage " "space and saving backups will take slightly longer time. In " "return it will be possible to recover from a partially corrupted " "backup.")); lRecoveryLabel->setWordWrap(true); if(pPar2Available) { lRecoveryCheckBox->setText(xi18nc("@option:check", "Generate recovery information")); } else { lRecoveryCheckBox->setText(xi18nc("@option:check", "Generate recovery information (not available " "because par2 is not installed)")); lRecoveryCheckBox->setEnabled(false); lRecoveryLabel->setEnabled(false); } auto lRecoveryLayout = new QGridLayout; lRecoveryLayout->setContentsMargins(0, 0, 0, 0); lRecoveryLayout->setSpacing(0); lRecoveryLayout->setColumnMinimumWidth(0, lIndentation); lRecoveryLayout->addWidget(lRecoveryCheckBox,0, 0, 1, 2); lRecoveryLayout->addWidget(lRecoveryLabel, 1, 1); lRecoveryWidget->setLayout(lRecoveryLayout); connect(mVersionedRadio, SIGNAL(toggled(bool)), lRecoveryWidget, SLOT(setVisible(bool))); auto lVerificationWidget = new QWidget; lVerificationWidget->setVisible(false); auto lVerificationCheckBox = new QCheckBox(xi18nc("@option:check", "Verify integrity of backups")); lVerificationCheckBox->setObjectName(QStringLiteral("kcfg_Check backups")); auto lVerificationLabel = new QLabel(xi18nc("@info", "Checks the whole backup archive for corruption " "every time you save new data. Saving backups will take a " "little bit longer time but it allows you to catch corruption " "problems sooner than at the time you need to use a backup, " "at that time it could be too late.")); lVerificationLabel->setWordWrap(true); auto lVerificationLayout = new QGridLayout; lVerificationLayout->setContentsMargins(0, 0, 0, 0); lVerificationLayout->setSpacing(0); lVerificationLayout->setColumnMinimumWidth(0, lIndentation); lVerificationLayout->addWidget(lVerificationCheckBox,0, 0, 1, 2); lVerificationLayout->addWidget(lVerificationLabel, 1, 1); lVerificationWidget->setLayout(lVerificationLayout); connect(mVersionedRadio, SIGNAL(toggled(bool)), lVerificationWidget, SLOT(setVisible(bool))); auto lExcludesWidget = new QWidget; auto lExcludesCheckBox = new QCheckBox(xi18nc("@option:check", "Exclude files and folders based on patterns")); lExcludesCheckBox->setObjectName(QStringLiteral("kcfg_Exclude patterns")); auto lExcludesLabel = new QLabel(); lExcludesLabel->setWordWrap(true); lExcludesLabel->setTextFormat(Qt::RichText); lExcludesLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); connect(lExcludesLabel, &QLabel::linkActivated, this, [this](QString pLink){ // call runUrl ourselves instead of QLabel and QDesktopService doing it, just // so that we can give a .html file ending to the temp file. Required for QWebEngine // to understand that the file has html content. Firefox works either way. KRun::runUrl(QUrl(pLink), QStringLiteral("text/html"), this, KRun::RunFlags(), QStringLiteral("manpage.html")); }); auto lLabelUpdater = [lExcludesLabel](bool pVersioned){ QString lHelpUrl = pVersioned ? QStringLiteral("man:///bup-index") : QStringLiteral("man:///rsync"); lExcludesLabel->setText(xi18nc("@info", "Patterns need to be listed in a text file with one pattern per line. " "Files and folders with names that match any of the patterns will be " "excluded from the backup. The pattern format is documented here.", lHelpUrl)); }; lLabelUpdater(false); connect(mVersionedRadio, &QCheckBox::toggled, this, lLabelUpdater); auto lExcludesEdit = new KLineEdit; lExcludesEdit->setObjectName(QStringLiteral("kcfg_Exclude patterns file path")); lExcludesEdit->setEnabled(false); lExcludesEdit->setClearButtonEnabled(true); lExcludesEdit->setCompletionObject(new KUrlCompletion); lExcludesEdit->setAutoDeleteCompletionObject(true); auto lExcludesButton = new QPushButton; lExcludesButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int lButtonSize = lExcludesButton->sizeHint().expandedTo(lExcludesEdit->sizeHint()).height(); lExcludesButton->setFixedSize(lButtonSize, lButtonSize); lExcludesButton->setEnabled(false); lExcludesButton->setToolTip(xi18nc("@info:tooltip", "Open dialog to select a file")); connect(lExcludesButton, &QPushButton::clicked, this, [this,lExcludesEdit]{ QString lNewFilePath = QFileDialog::getOpenFileName(this, i18n("Select pattern file"), lExcludesEdit->text()); if(!lNewFilePath.isEmpty()) { lExcludesEdit->setText(lNewFilePath); } }); connect(lExcludesCheckBox, &QCheckBox::toggled, lExcludesEdit, &KLineEdit::setEnabled); connect(lExcludesCheckBox, &QCheckBox::toggled, lExcludesButton, &QPushButton::setEnabled); auto lExcludesLayout = new QGridLayout; lExcludesLayout->setContentsMargins(0, 0, 0, 0); lExcludesLayout->setSpacing(0); lExcludesLayout->setColumnMinimumWidth(0, lIndentation); lExcludesLayout->addWidget(lExcludesCheckBox, 0, 0, 1, 3); lExcludesLayout->addWidget(lExcludesLabel, 1, 1, 1, 2); lExcludesLayout->addWidget(lExcludesEdit, 2, 1); lExcludesLayout->addWidget(lExcludesButton, 2, 2); lExcludesWidget->setLayout(lExcludesLayout); lAdvancedLayout->addWidget(lShowHiddenWidget); lAdvancedLayout->addWidget(lVerificationWidget); lAdvancedLayout->addWidget(lRecoveryWidget); lAdvancedLayout->addWidget(lExcludesWidget); lAdvancedLayout->addStretch(); lAdvancedLayout->setSpacing(lIndentation); lAdvancedWidget->setLayout(lAdvancedLayout); auto lPage = new KPageWidgetItem(lAdvancedWidget); lPage->setName(xi18nc("@title", "Advanced")); lPage->setHeader(xi18nc("@label", "Extra options for advanced users")); lPage->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); return lPage; } void BackupPlanWidget::openDriveDestDialog() { QString lMountPoint = mDriveSelection->mountPathOfSelectedDrive(); QString lSelectedPath; QPointer lDirDialog = new DirDialog(QUrl::fromLocalFile(lMountPoint), mDriveDestEdit->text(), this); if(lDirDialog->exec() == QDialog::Accepted) { lSelectedPath = lDirDialog->url().path(); lSelectedPath.remove(0, lMountPoint.length()); while(lSelectedPath.startsWith(QLatin1Char('/'))) { lSelectedPath.remove(0, 1); } mDriveDestEdit->setText(lSelectedPath); } delete lDirDialog; } kup-backup-0.9.1/kcm/backupplanwidget.h000066400000000000000000000103761420500356400200400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BACKUPPLANWIDGET_H #define BACKUPPLANWIDGET_H #include #include #include class BackupPlan; class DirSelector; class DriveSelection; class FolderSelectionModel; class KLineEdit; class KMessageWidget; class KPageWidget; class KPageWidgetItem; class QAction; class QFileInfo; class QPushButton; class QRadioButton; class QThread; class QTimer; class QTreeView; class FileScanner : public QObject { Q_OBJECT public: FileScanner(); bool event(QEvent *pEvent) override; public slots: void includePath(const QString &pPath); void excludePath(const QString &pPath); signals: void unreadablesChanged(QPair, QSet>); void symlinkProblemsChanged(QHash); protected slots: void sendPendingUnreadables(); void sendPendingSymlinks(); protected: bool isPathIncluded(const QString &pPath); void checkPathForProblems(const QFileInfo &pFileInfo); bool isSymlinkProblematic(const QString &pTarget); void scanFolder(const QString &pPath); QSet mIncludedFolders; QSet mExcludedFolders; QSet mUnreadableFolders; QSet mUnreadableFiles; QTimer *mUnreadablesTimer; QHash mSymlinksNotOk; QHash mSymlinksOk; QTimer *mSymlinkTimer; }; class FolderSelectionWidget : public QWidget { Q_OBJECT public: explicit FolderSelectionWidget(FolderSelectionModel *pModel, QWidget *pParent = nullptr); virtual ~FolderSelectionWidget(); public slots: void setHiddenFoldersVisible(bool pVisible); void expandToShowSelections(); void setUnreadables(const QPair, QSet> &pUnreadables); void setSymlinks(QHash pSymlinks); void updateMessage(); void executeExcludeAction(); void executeIncludeAction(); protected: QTreeView *mTreeView; FolderSelectionModel *mModel; KMessageWidget *mMessageWidget; QThread *mWorkerThread; QStringList mUnreadableFolders; QStringList mUnreadableFiles; QString mExcludeActionPath; QAction *mExcludeAction; QHash mSymlinkProblems; QString mIncludeActionPath; QAction *mIncludeAction; }; class ConfigIncludeDummy : public QWidget { Q_OBJECT signals: void includeListChanged(); public: Q_PROPERTY(QStringList includeList READ includeList WRITE setIncludeList NOTIFY includeListChanged USER true) ConfigIncludeDummy(FolderSelectionModel *pModel, FolderSelectionWidget *pParent); QStringList includeList(); void setIncludeList(QStringList pIncludeList); FolderSelectionModel *mModel; FolderSelectionWidget *mTreeView; }; class ConfigExcludeDummy : public QWidget { Q_OBJECT signals: void excludeListChanged(); public: Q_PROPERTY(QStringList excludeList READ excludeList WRITE setExcludeList NOTIFY excludeListChanged USER true) ConfigExcludeDummy(FolderSelectionModel *pModel, FolderSelectionWidget *pParent); QStringList excludeList(); void setExcludeList(QStringList pExcludeList); FolderSelectionModel *mModel; FolderSelectionWidget *mTreeView; }; class DirDialog: public QDialog { Q_OBJECT public: explicit DirDialog(const QUrl &pRootDir, const QString &pStartSubDir, QWidget *pParent = nullptr); QUrl url() const; private: DirSelector *mDirSelector; }; class BackupPlanWidget : public QWidget { Q_OBJECT public: BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupVersion, const QString &pRsyncVersion, bool pPar2Available); void saveExtraData(); void showSourcePage(); KLineEdit *mDescriptionEdit; protected: KPageWidgetItem *createTypePage(const QString &pBupVersion, const QString &pRsyncVersion); KPageWidgetItem *createSourcePage(); KPageWidgetItem *createDestinationPage(); KPageWidgetItem *createSchedulePage(); KPageWidgetItem *createAdvancedPage(bool pPar2Available); QPushButton *mConfigureButton; KPageWidget *mConfigPages; BackupPlan *mBackupPlan; DriveSelection *mDriveSelection{}; KLineEdit *mDriveDestEdit{}; QRadioButton *mVersionedRadio{}; QRadioButton *mSyncedRadio{}; FolderSelectionWidget *mSourceSelectionWidget{}; KPageWidgetItem *mSourcePage; protected slots: void openDriveDestDialog(); signals: void requestOverviewReturn(); }; #endif // BACKUPPLANWIDGET_H kup-backup-0.9.1/kcm/dirselector.cpp000066400000000000000000000054651420500356400173710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "dirselector.h" #include #include #include #include #include #include DirSelector::DirSelector(QWidget *pParent) : QTreeView(pParent) { mDirModel = new KDirModel(this); mDirModel->dirLister()->setDirOnlyMode(true); setModel(mDirModel); for (int i = 1; i < mDirModel->columnCount(); ++i) { hideColumn(i); } setHeaderHidden(true); connect(mDirModel, SIGNAL(expand(QModelIndex)), SLOT(expand(QModelIndex))); connect(mDirModel, SIGNAL(expand(QModelIndex)), SLOT(selectEntry(QModelIndex))); } QUrl DirSelector::url() const { const KFileItem lFileItem = mDirModel->itemForIndex(currentIndex()); return !lFileItem.isNull() ? lFileItem.url() : QUrl(); } void DirSelector::createNewFolder() { bool lUserAccepted; QString lNameSuggestion = xi18nc("default folder name when creating a new folder", "New Folder"); if(QFileInfo::exists(url().adjusted(QUrl::StripTrailingSlash).path() + '/' + lNameSuggestion)) { lNameSuggestion = KIO::suggestName(url(), lNameSuggestion); } QString lSelectedName = QInputDialog::getText(this, xi18nc("@title:window", "New Folder" ), xi18nc("@label:textbox", "Create new folder in:\n%1", url().path()), QLineEdit::Normal, lNameSuggestion, &lUserAccepted); if (!lUserAccepted) return; QUrl lPartialUrl(url()); const QStringList lDirectories = lSelectedName.split(QLatin1Char('/'), QString::SkipEmptyParts); foreach(QString lSubDirectory, lDirectories) { QDir lDir(lPartialUrl.path()); if(lDir.exists(lSubDirectory)) { lPartialUrl = lPartialUrl.adjusted(QUrl::StripTrailingSlash); lPartialUrl.setPath(lPartialUrl.path() + '/' + (lSubDirectory)); KMessageBox::sorry(this, i18n("A folder named %1 already exists.", lPartialUrl.path())); return; } if(!lDir.mkdir(lSubDirectory)) { lPartialUrl = lPartialUrl.adjusted(QUrl::StripTrailingSlash); lPartialUrl.setPath(lPartialUrl.path() + '/' + (lSubDirectory)); KMessageBox::sorry(this, i18n("You do not have permission to create %1.", lPartialUrl.path())); return; } lPartialUrl = lPartialUrl.adjusted(QUrl::StripTrailingSlash); lPartialUrl.setPath(lPartialUrl.path() + '/' + (lSubDirectory)); } mDirModel->expandToUrl(lPartialUrl); } void DirSelector::selectEntry(QModelIndex pIndex) { selectionModel()->clearSelection(); selectionModel()->setCurrentIndex(pIndex, QItemSelectionModel::SelectCurrent); scrollTo(pIndex); } void DirSelector::expandToUrl(const QUrl &pUrl) { mDirModel->expandToUrl(pUrl); } void DirSelector::setRootUrl(const QUrl &pUrl) { mDirModel->dirLister()->openUrl(pUrl); } kup-backup-0.9.1/kcm/dirselector.h000066400000000000000000000011101420500356400170150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef DIRSELECTOR_H #define DIRSELECTOR_H #include class KDirModel; class DirSelector : public QTreeView { Q_OBJECT public: explicit DirSelector(QWidget *pParent = nullptr); QUrl url() const; signals: public slots: void createNewFolder(); void selectEntry(QModelIndex pIndex); void expandToUrl(const QUrl &pUrl); void setRootUrl(const QUrl &pUrl); private: KDirModel *mDirModel; }; #endif // DIRSELECTOR_H kup-backup-0.9.1/kcm/driveselection.cpp000066400000000000000000000275161420500356400200720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "driveselection.h" #include "driveselectiondelegate.h" #include "backupplan.h" #include #include #include #include #include #include #include #include #include #include #include #include #include bool deviceLessThan(const Solid::Device &a, const Solid::Device &b) { return a.udi() < b.udi(); } DriveSelection::DriveSelection(BackupPlan *pBackupPlan, QWidget *parent) : QListView(parent), mBackupPlan(pBackupPlan), mSelectedAndAccessible(false), mSyncedBackupType(false) { mDrivesModel = new QStandardItemModel(this); setModel(mDrivesModel); setItemDelegate(new DriveSelectionDelegate(this)); setSelectionMode(QAbstractItemView::SingleSelection); setWordWrap(true); if(!mBackupPlan->mExternalUUID.isEmpty()) { auto *lItem = new QStandardItem(); lItem->setEditable(false); lItem->setData(QString(), DriveSelection::UDI); lItem->setData(mBackupPlan->mExternalUUID, DriveSelection::UUID); lItem->setData(0, DriveSelection::UsedSpace); lItem->setData(mBackupPlan->mExternalPartitionNumber, DriveSelection::PartitionNumber); lItem->setData(mBackupPlan->mExternalPartitionsOnDrive, DriveSelection::PartitionsOnDrive); lItem->setData(mBackupPlan->mExternalDeviceDescription, DriveSelection::DeviceDescription); lItem->setData(mBackupPlan->mExternalVolumeCapacity, DriveSelection::TotalSpace); lItem->setData(mBackupPlan->mExternalVolumeLabel, DriveSelection::Label); mDrivesModel->appendRow(lItem); } QList lDeviceList = Solid::Device::listFromType(Solid::DeviceInterface::StorageDrive); foreach (const Solid::Device &lDevice, lDeviceList) { deviceAdded(lDevice.udi()); } connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), SLOT(deviceAdded(QString))); connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), SLOT(deviceRemoved(QString))); connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(updateSelection(QItemSelection,QItemSelection))); } QString DriveSelection::mountPathOfSelectedDrive() const { if(mSelectedAndAccessible) { QStandardItem *lItem; findItem(DriveSelection::UUID, mSelectedUuid, &lItem); if(lItem != nullptr) { Solid::Device lDevice(lItem->data(DriveSelection::UDI).toString()); auto *lAccess = lDevice.as(); if(lAccess) { return lAccess->filePath(); } } } return QString(); } void DriveSelection::deviceAdded(const QString &pUdi) { Solid::Device lDevice(pUdi); if(!lDevice.is()) { return; } auto *lDrive = lDevice.as(); if(!lDrive->isHotpluggable() && !lDrive->isRemovable()) { return; } if(mDrivesToAdd.contains(pUdi)) { return; } mDrivesToAdd.append(pUdi); QTimer::singleShot(2000, this, SLOT(delayedDeviceAdded())); } void DriveSelection::delayedDeviceAdded() { if(mDrivesToAdd.isEmpty()) { return; } Solid::Device lParentDevice(mDrivesToAdd.takeFirst()); QList lDeviceList = Solid::Device::listFromType(Solid::DeviceInterface::StorageVolume, lParentDevice.udi()); // check for when there is no partitioning scheme, then the drive is also the storage volume if(lParentDevice.is()) { lDeviceList.append(lParentDevice); } // filter out some volumes that should not be visible QList lVolumeDeviceList; foreach(Solid::Device lVolumeDevice, lDeviceList) { auto *lVolume = lVolumeDevice.as(); if(lVolume && !lVolume->isIgnored() && ( lVolume->usage() == Solid::StorageVolume::FileSystem || lVolume->usage() == Solid::StorageVolume::Encrypted)) { lVolumeDeviceList.append(lVolumeDevice); } } // simplest attempt at getting the same partition numbering every time a device is plugged in std::sort(lVolumeDeviceList.begin(), lVolumeDeviceList.end(), deviceLessThan); int lPartitionNumber = 1; foreach(Solid::Device lVolumeDevice, lVolumeDeviceList) { auto *lVolume = lVolumeDevice.as(); QString lUuid = lVolume->uuid(); if(lUuid.isEmpty()) { //seems to happen for vfat partitions lUuid += lParentDevice.description(); lUuid += QStringLiteral("|"); lUuid += lVolume->label(); } QStandardItem *lItem; bool lNeedsToBeAdded = false; findItem(DriveSelection::UUID, lUuid, &lItem); if(lItem == nullptr) { lItem = new QStandardItem(); lItem->setEditable(false); lItem->setData(lUuid, DriveSelection::UUID); lItem->setData(0, DriveSelection::TotalSpace); lItem->setData(0, DriveSelection::UsedSpace); lNeedsToBeAdded = true; } lItem->setData(lParentDevice.description(), DriveSelection::DeviceDescription); lItem->setData(lVolume->label(), DriveSelection::Label); lItem->setData(lVolumeDevice.udi(), DriveSelection::UDI); lItem->setData(lPartitionNumber, DriveSelection::PartitionNumber); lItem->setData(lVolumeDeviceList.count(), DriveSelection::PartitionsOnDrive); lItem->setData(lVolume->fsType(), DriveSelection::FileSystem); lItem->setData(mSyncedBackupType && (lVolume->fsType() == QStringLiteral("vfat") || lVolume->fsType() == QStringLiteral("ntfs")), DriveSelection::PermissionLossWarning); lItem->setData(mSyncedBackupType && lVolume->fsType() == QStringLiteral("vfat"), DriveSelection::SymlinkLossWarning); auto *lAccess = lVolumeDevice.as(); connect(lAccess, SIGNAL(accessibilityChanged(bool,QString)), SLOT(accessabilityChanged(bool,QString))); if(lAccess->isAccessible()) { KDiskFreeSpaceInfo lInfo = KDiskFreeSpaceInfo::freeSpaceInfo(lAccess->filePath()); if(lInfo.isValid()) { lItem->setData(lInfo.size(), DriveSelection::TotalSpace); lItem->setData(lInfo.used(), DriveSelection::UsedSpace); } if(lUuid == mSelectedUuid) { // Selected volume was just added, could not have been accessible before. mSelectedAndAccessible = true; emit selectedDriveIsAccessibleChanged(true); } } else if(lVolume->usage() != Solid::StorageVolume::Encrypted) { // Don't bother the user with password prompt just for sake of storage volume space info lAccess->setup(); } if(lNeedsToBeAdded) { mDrivesModel->appendRow(lItem); if(mDrivesModel->rowCount() == 1) { selectionModel()->select(mDrivesModel->index(0, 0), QItemSelectionModel::ClearAndSelect); } } lPartitionNumber++; } } void DriveSelection::deviceRemoved(const QString &pUdi) { QStandardItem *lItem; int lRow = findItem(DriveSelection::UDI, pUdi, &lItem); if(lRow >= 0) { QString lUuid = lItem->data(DriveSelection::UUID).toString(); if(lUuid == mBackupPlan->mExternalUUID) { // let the selected and saved item stay in the list // just clear the UDI so that it will be shown as disconnected. lItem->setData(QString(), DriveSelection::UDI); } else { mDrivesModel->removeRow(lRow); } if(lUuid == mSelectedUuid && mSelectedAndAccessible) { mSelectedAndAccessible = false; emit selectedDriveIsAccessibleChanged(false); } } } void DriveSelection::accessabilityChanged(bool pAccessible, const QString &pUdi) { QStandardItem *lItem; findItem(DriveSelection::UDI, pUdi, &lItem); if(lItem != nullptr) { if(pAccessible) { Solid::Device lDevice(pUdi); auto *lAccess = lDevice.as(); if(lAccess) { KDiskFreeSpaceInfo lInfo = KDiskFreeSpaceInfo::freeSpaceInfo(lAccess->filePath()); if(lInfo.isValid()) { lItem->setData(lInfo.size(), DriveSelection::TotalSpace); lItem->setData(lInfo.used(), DriveSelection::UsedSpace); } } } bool lSelectedAndAccessible = (lItem->data(DriveSelection::UUID).toString() == mSelectedUuid && pAccessible); if(lSelectedAndAccessible != mSelectedAndAccessible) { mSelectedAndAccessible = lSelectedAndAccessible; emit selectedDriveIsAccessibleChanged(lSelectedAndAccessible); } } } void DriveSelection::updateSelection(const QItemSelection &pSelected, const QItemSelection &pDeselected) { Q_UNUSED(pDeselected) if(!pSelected.indexes().isEmpty()) { QModelIndex lIndex = pSelected.indexes().first(); if(mSelectedUuid.isEmpty()) { emit driveIsSelectedChanged(true); } mSelectedUuid = lIndex.data(DriveSelection::UUID).toString(); emit selectedDriveChanged(mSelectedUuid); // check if the newly selected volume is accessible, compare to previous selection bool lIsAccessible = false; QString lUdiOfSelected = lIndex.data(DriveSelection::UDI).toString(); if(!lUdiOfSelected.isEmpty()) { Solid::Device lDevice(lUdiOfSelected); auto *lAccess = lDevice.as(); if(lAccess != nullptr) { lIsAccessible = lAccess->isAccessible(); } } if(mSelectedAndAccessible != lIsAccessible) { mSelectedAndAccessible = lIsAccessible; emit selectedDriveIsAccessibleChanged(mSelectedAndAccessible); } } else { mSelectedUuid.clear(); emit selectedDriveChanged(mSelectedUuid); emit driveIsSelectedChanged(false); mSelectedAndAccessible = false; emit selectedDriveIsAccessibleChanged(false); } } void DriveSelection::paintEvent(QPaintEvent *pPaintEvent) { QListView::paintEvent(pPaintEvent); if(mDrivesModel->rowCount() == 0) { QPainter lPainter(viewport()); style()->drawItemText(&lPainter, rect(), Qt::AlignCenter, palette(), false, xi18nc("@label Only shown if no drives are detected", "Plug in the external " "storage you wish to use, then select it in this list."), QPalette::Text); } } void DriveSelection::setSelectedDrive(const QString &pUuid) { if(pUuid == mSelectedUuid) { return; } if(pUuid.isEmpty()) { clearSelection(); } else { QStandardItem *lItem; findItem(DriveSelection::UUID, pUuid, &lItem); if(lItem != nullptr) { setCurrentIndex(mDrivesModel->indexFromItem(lItem)); } } } void DriveSelection::saveExtraData() { QStandardItem *lItem; findItem(DriveSelection::UUID, mSelectedUuid, &lItem); if(lItem != nullptr) { mBackupPlan->mExternalDeviceDescription = lItem->data(DriveSelection::DeviceDescription).toString(); mBackupPlan->mExternalPartitionNumber = lItem->data(DriveSelection::PartitionNumber).toInt(); mBackupPlan->mExternalPartitionsOnDrive = lItem->data(DriveSelection::PartitionsOnDrive).toInt(); mBackupPlan->mExternalVolumeCapacity = lItem->data(DriveSelection::TotalSpace).toULongLong(); mBackupPlan->mExternalVolumeLabel = lItem->data(DriveSelection::Label).toString(); } } void DriveSelection::updateSyncWarning(bool pSyncBackupSelected) { mSyncedBackupType = pSyncBackupSelected; for(int i = 0; i < mDrivesModel->rowCount(); ++i) { QString lFsType = mDrivesModel->item(i)->data(DriveSelection::FileSystem).toString(); mDrivesModel->item(i)->setData(mSyncedBackupType && (lFsType == QStringLiteral("vfat") || lFsType == QStringLiteral("ntfs")), DriveSelection::PermissionLossWarning); mDrivesModel->item(i)->setData(mSyncedBackupType && lFsType == QStringLiteral("vfat"), DriveSelection::SymlinkLossWarning); } } int DriveSelection::findItem(const DriveSelection::DataType pField, const QString &pSearchString, QStandardItem **pReturnedItem) const { for(int lRow = 0; lRow < mDrivesModel->rowCount(); ++lRow) { QStandardItem *lItem = mDrivesModel->item(lRow); if(lItem->data(pField).toString() == pSearchString) { if(pReturnedItem != nullptr) { *pReturnedItem = lItem; } return lRow; } } if(pReturnedItem != nullptr) { *pReturnedItem = nullptr; } return -1; } kup-backup-0.9.1/kcm/driveselection.h000066400000000000000000000042271420500356400175310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef DRIVESELECTION_H #define DRIVESELECTION_H #include #include class QStandardItem; class QStandardItemModel; class BackupPlan; class DriveSelection : public QListView { Q_OBJECT Q_PROPERTY(QString selectedDrive READ selectedDrive WRITE setSelectedDrive NOTIFY selectedDriveChanged USER true) Q_PROPERTY(bool driveIsSelected READ driveIsSelected NOTIFY driveIsSelectedChanged) Q_PROPERTY(bool selectedDriveIsAccessible READ selectedDriveIsAccessible NOTIFY selectedDriveIsAccessibleChanged) public: enum DataType { UUID = Qt::UserRole + 1, UDI, TotalSpace, UsedSpace, Label, DeviceDescription, PartitionNumber, PartitionsOnDrive, FileSystem, PermissionLossWarning, SymlinkLossWarning }; public: explicit DriveSelection(BackupPlan *pBackupPlan, QWidget *parent=nullptr); QString selectedDrive() const {return mSelectedUuid;} bool driveIsSelected() const {return !mSelectedUuid.isEmpty();} bool selectedDriveIsAccessible() const {return mSelectedAndAccessible;} QString mountPathOfSelectedDrive() const; public slots: void setSelectedDrive(const QString &pUuid); void saveExtraData(); void updateSyncWarning(bool pSyncBackupSelected); signals: void selectedDriveChanged(const QString &pSelectedDrive); void driveIsSelectedChanged(bool pDriveIsSelected); void selectedDriveIsAccessibleChanged(bool pDriveIsSelectedAndAccessible); protected slots: void deviceAdded(const QString &pUdi); void delayedDeviceAdded(); void deviceRemoved(const QString &pUdi); void accessabilityChanged(bool pAccessible, const QString &pUdi); void updateSelection(const QItemSelection &pSelected, const QItemSelection &pDeselected); protected: void paintEvent(QPaintEvent *pPaintEvent) override; int findItem(const DataType pField, const QString &pSearchString, QStandardItem **pReturnedItem = nullptr) const; QStandardItemModel *mDrivesModel; QString mSelectedUuid; BackupPlan *mBackupPlan; QStringList mDrivesToAdd; bool mSelectedAndAccessible; bool mSyncedBackupType; }; #endif kup-backup-0.9.1/kcm/driveselectiondelegate.cpp000066400000000000000000000156211420500356400215570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "driveselectiondelegate.h" #include "backupplan.h" #include "driveselection.h" #include #include #include #include #include #include #include #include static const int cMargin = 6; DriveSelectionDelegate::DriveSelectionDelegate(QListView *pParent) : QStyledItemDelegate(pParent), mListView(pParent) { mCapacityBar = new KCapacityBar(KCapacityBar::DrawTextInline); } void DriveSelectionDelegate::paint(QPainter* pPainter, const QStyleOptionViewItem& pOption, const QModelIndex& pIndex) const { pPainter->save(); pPainter->setRenderHint(QPainter::Antialiasing); QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &pOption, pPainter); auto lTotalSize = pIndex.data(DriveSelection::TotalSpace).toULongLong(); auto lUsedSize = pIndex.data(DriveSelection::UsedSpace).toULongLong(); bool lIsDisconnected = pIndex.data(DriveSelection::UDI).toString().isEmpty(); if(lTotalSize == 0 || lIsDisconnected) { mCapacityBar->setValue(0); } else { mCapacityBar->setValue(static_cast((lUsedSize * 100) / lTotalSize)); } mCapacityBar->drawCapacityBar(pPainter, pOption.rect.adjusted(cMargin, cMargin+QApplication::fontMetrics().height()+cMargin, -cMargin, 4*cMargin + QApplication::fontMetrics().height() - pOption.rect.height())); if (pOption.state & QStyle::State_Selected) pPainter->setPen(pOption.palette.color(QPalette::HighlightedText)); else pPainter->setPen(pOption.palette.color(QPalette::Text)); KFormat lFormat; QString lDisplayLabel, lPartitionLabel, lDisconnectedLabel; int lTextEnd = pOption.rect.right() - cMargin; if(lIsDisconnected) { lDisconnectedLabel = xi18nc("@item:inlistbox this text is added if selected drive is disconnected", " (disconnected)"); } else { lDisconnectedLabel = QString(); if(lTotalSize > 0) { QString lFreeSpace = xi18nc("@label %1 is amount of free storage space of hard drive","%1 free", lFormat.formatByteSize(lTotalSize - lUsedSize)); int lTextWidth = QApplication::fontMetrics().horizontalAdvance(lFreeSpace); lTextEnd -= lTextWidth + cMargin; QPoint lOffset = QPoint(-cMargin - lTextWidth, cMargin + QApplication::fontMetrics().height()); pPainter->drawText(pOption.rect.topRight() + lOffset, lFreeSpace); } } QString lDeviceDescription = pIndex.data(DriveSelection::DeviceDescription).toString(); QString lLabel = pIndex.data(DriveSelection::Label).toString(); int lPartitionNumber = pIndex.data(DriveSelection::PartitionNumber).toInt(); if(lLabel.isEmpty() || lLabel == lDeviceDescription) { if(pIndex.data(DriveSelection::PartitionsOnDrive).toInt() > 1) { lPartitionLabel = xi18nc("@item:inlistbox used for unnamed filesystems, more than one filesystem on device. %1 is partition number, %2 is device description, %3 is either empty or the \" (disconnected)\" text", "Partition %1 on %2%3", lPartitionNumber, lDeviceDescription, lDisconnectedLabel); } else { lPartitionLabel = xi18nc("@item:inlistbox used when there is only one unnamed filesystem on device. %1 is device description, %2 is either empty or the \" (disconnected)\" text", "%1%2", lDeviceDescription, lDisconnectedLabel); } } else { lPartitionLabel = xi18nc("@item:inlistbox %1 is filesystem label, %2 is the device description, %3 is either empty or the \" (disconnected)\" text", "%1 on %2%3", lLabel, lDeviceDescription, lDisconnectedLabel); } if(lTotalSize == 0) { lDisplayLabel = lPartitionLabel; } else { lDisplayLabel = xi18nc("@item:inlistbox %1 is drive(partition) label, %2 is storage capacity", "%1: %2 total capacity", lPartitionLabel, lFormat.formatByteSize(lTotalSize)); } lDisplayLabel = QApplication::fontMetrics().elidedText(lDisplayLabel, Qt::ElideMiddle, lTextEnd - pOption.rect.left() - cMargin); pPainter->drawText(pOption.rect.topLeft() + QPoint(cMargin, cMargin+QApplication::fontMetrics().height()), lDisplayLabel); int lIconSize = 48; QRect lWarningRect = warningRect(pOption.rect.adjusted(lIconSize + cMargin, 0, 0, 0), pIndex); if(!lWarningRect.isEmpty()) { QIcon lIcon = QIcon::fromTheme(QStringLiteral("dialog-warning")); lIcon.paint(pPainter, lWarningRect.left() - cMargin - lIconSize, lWarningRect.top(), lIconSize, lIconSize); pPainter->drawText(lWarningRect, Qt::AlignVCenter | Qt::TextWordWrap, warningText(pIndex)); } pPainter->restore(); } QSize DriveSelectionDelegate::sizeHint(const QStyleOptionViewItem& pOption, const QModelIndex& pIndex) const { Q_UNUSED(pOption) QSize lSize; lSize.setWidth(cMargin*2 + QApplication::fontMetrics().horizontalAdvance(pIndex.data().toString())); lSize.setHeight(cMargin*5 + QApplication::fontMetrics().height()); int lIconSize = 48; QRect lWarningRect = warningRect(mListView->rect().adjusted(lIconSize + cMargin, 0, 0, 0), pIndex); if(!lWarningRect.isEmpty()) { lSize.setHeight(lSize.height() + 2*cMargin + lWarningRect.height()); } return lSize; } QRect DriveSelectionDelegate::warningRect(const QRect &pRect, const QModelIndex &pIndex) const { QRect lTextLocation = pRect.adjusted(cMargin, 5*cMargin + QApplication::fontMetrics().height(), -cMargin, -cMargin); QString lWarningText = warningText(pIndex); if(lWarningText.isEmpty()) { return {}; } QRect lTextBoundary = QApplication::fontMetrics().boundingRect(lTextLocation, Qt::TextWordWrap, lWarningText); int lIconSize = 48; if(lTextBoundary.height() < lIconSize) { lTextBoundary.setHeight(lIconSize); } return lTextBoundary; } QString DriveSelectionDelegate::warningText(const QModelIndex &pIndex) { bool lPermissionWarning = pIndex.data(DriveSelection::PermissionLossWarning).toBool(); bool lSymlinkWarning = pIndex.data(DriveSelection::SymlinkLossWarning).toBool(); if(lPermissionWarning && lSymlinkWarning) { return xi18nc("@item:inlistbox", "Warning: Symbolic links and file permissions can not be saved " "to this file system. File permissions only matters if there is more than one " "user of this computer or if you are backing up executable program files."); } if(lPermissionWarning) { return xi18nc("@item:inlistbox", "Warning: File permissions can not be saved to this file " "system. File permissions only matters if there is more than one " "user of this computer or if you are backing up executable program files."); } return {}; } kup-backup-0.9.1/kcm/driveselectiondelegate.h000066400000000000000000000015161420500356400212220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef DRIVESELECTIONDELEGATE_H #define DRIVESELECTIONDELEGATE_H #include class QListView; class KCapacityBar; class DriveSelectionDelegate : public QStyledItemDelegate { public: explicit DriveSelectionDelegate(QListView *pParent); void paint(QPainter* pPainter, const QStyleOptionViewItem& pOption, const QModelIndex& pIndex) const override; QSize sizeHint(const QStyleOptionViewItem& pOption, const QModelIndex& pIndex) const override; private: QRect warningRect(const QRect &pRect, const QModelIndex &pIndex) const; static QString warningText(const QModelIndex &pIndex); KCapacityBar *mCapacityBar; QListView *mListView; }; #endif // DRIVESELECTIONDELEGATE_H kup-backup-0.9.1/kcm/folderselectionmodel.cpp000066400000000000000000000203131420500356400212410ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2003 Scott Wheeler // SPDX-FileCopyrightText: 2004 Max Howell // SPDX-FileCopyrightText: 2004 Mark Kretschmann // SPDX-FileCopyrightText: 2008 Seb Ruiz // SPDX-FileCopyrightText: 2008 Sebastian Trueg // SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-2.0-only #include "folderselectionmodel.h" #include "kuputils.h" #include #include #include #include #include #include #include namespace { bool setContainsSubdir(const QSet &pSet, const QString &pParentDir) { // we need the trailing slash to be able to use the startsWith() function to check for parent dirs. QString lPathWithSlash = pParentDir; ensureTrailingSlash(lPathWithSlash); foreach(QString lTestedPath, pSet) { if(lTestedPath.startsWith(lPathWithSlash)) { return true; } } return false; } } FolderSelectionModel::FolderSelectionModel(bool pHiddenFoldersVisible, QObject *pParent) : QFileSystemModel(pParent) { setHiddenFoldersVisible(pHiddenFoldersVisible); } Qt::ItemFlags FolderSelectionModel::flags(const QModelIndex &pIndex) const { Qt::ItemFlags lFlags = QFileSystemModel::flags(pIndex); lFlags |= Qt::ItemIsUserCheckable; return lFlags; } QVariant FolderSelectionModel::data(const QModelIndex& pIndex, int pRole) const { if(!pIndex.isValid() || pIndex.column() != 0) { return QFileSystemModel::data(pIndex, pRole); } const QString lPath = filePath(pIndex); const InclusionState lState = inclusionState(lPath); switch(pRole) { case Qt::CheckStateRole: { switch(lState) { case StateIncluded: case StateIncludeInherited: if(setContainsSubdir(mExcludedPaths, lPath)) { return Qt::PartiallyChecked; } return Qt::Checked; default: return Qt::Unchecked; } } case IncludeStateRole: return inclusionState(pIndex); case Qt::ForegroundRole: { switch(lState) { case StateIncluded: case StateIncludeInherited: return QVariant::fromValue(QPalette().brush(QPalette::Active, QPalette::Text)); default: if(setContainsSubdir(mIncludedPaths, lPath)) { return QVariant::fromValue(QPalette().brush( QPalette::Active, QPalette::Text)); } return QVariant::fromValue(QPalette().brush( QPalette::Disabled, QPalette::Text)); } } case Qt::ToolTipRole: { switch(lState) { case StateIncluded: case StateIncludeInherited: if(setContainsSubdir(mExcludedPaths, lPath)) { return xi18nc("@info:tooltip %1 is the path of the folder in a listview", "%1will be included in the backup, except " "for unchecked subfolders", filePath(pIndex)); } return xi18nc("@info:tooltip %1 is the path of the folder in a listview", "%1will be included in the backup", filePath(pIndex)); default: if(setContainsSubdir(mIncludedPaths, lPath)) { return xi18nc("@info:tooltip %1 is the path of the folder in a listview", "%1 will not be included " "in the backup but contains folders that will", filePath(pIndex)); } return xi18nc("@info:tooltip %1 is the path of the folder in a listview", "%1 will not be included " "in the backup", filePath(pIndex)); } } case Qt::DecorationRole: if(lPath == QDir::homePath()) { return QIcon::fromTheme(QStringLiteral("user-home")); } break; } return QFileSystemModel::data(pIndex, pRole); } bool FolderSelectionModel::setData(const QModelIndex& pIndex, const QVariant& pValue, int pRole) { if(!pIndex.isValid() || pIndex.column() != 0 || pRole != Qt::CheckStateRole) { return QFileSystemModel::setData(pIndex, pValue, pRole); } // here we ignore the check value, we treat it as a toggle // This is due to our using the Qt checking system in a virtual way const QString lPath = filePath(pIndex); const InclusionState lState = inclusionState(lPath); switch(lState) { case StateIncluded: case StateIncludeInherited: excludePath(lPath); break; default: includePath(lPath); } QModelIndex lRecurseIndex = pIndex; while(lRecurseIndex.isValid()) { emit dataChanged(lRecurseIndex, lRecurseIndex); lRecurseIndex = lRecurseIndex.parent(); } return true; } void FolderSelectionModel::includePath(const QString &pPath) { const InclusionState lState = inclusionState(pPath); if(lState == StateIncluded) { return; } removeSubDirs(pPath); if(lState == StateNone || lState == StateExcludeInherited) { mIncludedPaths.insert(pPath); emit includedPathAdded(pPath); } emit dataChanged(index(pPath), findLastLeaf(index(pPath))); } void FolderSelectionModel::excludePath(const QString& pPath) { const InclusionState lState = inclusionState(pPath); if(lState == StateExcluded) { return; } removeSubDirs(pPath); if(lState == StateIncludeInherited) { mExcludedPaths.insert(pPath); emit excludedPathAdded(pPath); } emit dataChanged(index(pPath), findLastLeaf(index(pPath))); } void FolderSelectionModel::setIncludedPaths(const QSet &pIncludedPaths) { QSet lRemoved = mIncludedPaths - pIncludedPaths; QSet lAdded = pIncludedPaths - mIncludedPaths; if(lRemoved.count() + lAdded.count() == 0) return; beginResetModel(); mIncludedPaths = pIncludedPaths; foreach(const QString &lRemovedPath, lRemoved) { emit includedPathRemoved(lRemovedPath); } foreach(const QString &lAddedPath, lAdded) { emit includedPathAdded(lAddedPath); } endResetModel(); } void FolderSelectionModel::setExcludedPaths(const QSet &pExcludedPaths) { QSet lRemoved = mExcludedPaths - pExcludedPaths; QSet lAdded = pExcludedPaths - mExcludedPaths; if(lRemoved.count() + lAdded.count() == 0)return; beginResetModel(); mExcludedPaths = pExcludedPaths; foreach(const QString &lRemovedPath, lRemoved) { emit excludedPathRemoved(lRemovedPath); } foreach(const QString &lAddedPath, lAdded) { emit excludedPathAdded(lAddedPath); } endResetModel(); } QSet FolderSelectionModel::includedPaths() const { return mIncludedPaths; } QSet FolderSelectionModel::excludedPaths() const { return mExcludedPaths; } FolderSelectionModel::InclusionState FolderSelectionModel::inclusionState(const QModelIndex& pIndex) const { return inclusionState(filePath(pIndex)); } FolderSelectionModel::InclusionState FolderSelectionModel::inclusionState(const QString& pPath) const { if(mIncludedPaths.contains(pPath)) { return StateIncluded; } if(mExcludedPaths.contains(pPath)) { return StateExcluded; } QString lParent = pPath.section(QDir::separator(), 0, -2, QString::SectionSkipEmpty|QString::SectionIncludeLeadingSep); if(lParent.isEmpty()) { return StateNone; } InclusionState state = inclusionState(lParent); if(state == StateNone) { return StateNone; } if(state == StateIncluded || state == StateIncludeInherited) { return StateIncludeInherited; } return StateExcludeInherited; } bool FolderSelectionModel::hiddenFoldersVisible() const { return filter() & QDir::Hidden; } void FolderSelectionModel::setHiddenFoldersVisible(bool pVisible){ if(pVisible) { setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); } else { setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); } } QModelIndex FolderSelectionModel::findLastLeaf(const QModelIndex &pIndex) { QModelIndex lIndex = pIndex; forever { int lRowCount = rowCount(lIndex); if(lRowCount > 0) { lIndex = index(lRowCount-1, 0, lIndex); } else { return lIndex; } } } void FolderSelectionModel::removeSubDirs(const QString& pPath) { QSet::iterator it = mExcludedPaths.begin(); QString lPath = pPath + QStringLiteral("/"); while(it != mExcludedPaths.end()) { if(*it == pPath || it->startsWith(lPath)) { QString lPathCopy = *it; it = mExcludedPaths.erase(it); emit excludedPathRemoved(lPathCopy); } else { ++it; } } it = mIncludedPaths.begin(); while(it != mIncludedPaths.end()) { if(*it == pPath || it->startsWith(lPath)) { QString lPathCopy = *it; it = mIncludedPaths.erase(it); emit includedPathRemoved(lPathCopy); } else { ++it; } } } kup-backup-0.9.1/kcm/folderselectionmodel.h000066400000000000000000000042771420500356400207210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2003 Scott Wheeler // SPDX-FileCopyrightText: 2004 Max Howell // SPDX-FileCopyrightText: 2004 Mark Kretschmann // SPDX-FileCopyrightText: 2008 Seb Ruiz // SPDX-FileCopyrightText: 2008 Sebastian Trueg // SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-2.0-only #ifndef FOLDER_SELECTION_MODEL_H #define FOLDER_SELECTION_MODEL_H #include #include class FolderSelectionModel : public QFileSystemModel { Q_OBJECT public: explicit FolderSelectionModel(bool pHiddenFoldersVisible = false, QObject *pParent = nullptr); enum InclusionState { StateNone, StateIncluded, StateExcluded, StateIncludeInherited, StateExcludeInherited }; enum CustomRoles { IncludeStateRole = 7777 }; Qt::ItemFlags flags(const QModelIndex &pIndex) const override; QVariant data(const QModelIndex& pIndex, int pRole = Qt::DisplayRole) const override; bool setData(const QModelIndex& pIndex, const QVariant& pValue, int pRole = Qt::EditRole) override; void setIncludedPaths(const QSet &pIncludedPaths); void setExcludedPaths(const QSet &pExcludedPaths); QSet includedPaths() const; QSet excludedPaths() const; /** * Include the specified path. All subdirs will be reset. */ void includePath(const QString &pPath); /** * Exclude the specified path. All subdirs will be reset. */ void excludePath(const QString &pPath); int columnCount(const QModelIndex&) const override { return 1; } InclusionState inclusionState(const QModelIndex &pIndex) const; InclusionState inclusionState(const QString &pPath) const; bool hiddenFoldersVisible() const; public slots: void setHiddenFoldersVisible(bool pVisible); signals: void includedPathAdded(const QString &pPath); void excludedPathAdded(const QString &pPath); void includedPathRemoved(const QString &pPath); void excludedPathRemoved(const QString &pPath); private: QModelIndex findLastLeaf(const QModelIndex& index); void removeSubDirs(const QString& path); QSet mIncludedPaths; QSet mExcludedPaths; }; #endif kup-backup-0.9.1/kcm/kbuttongroup.cpp000066400000000000000000000073441420500356400176130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2006 Pino Toscano // SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kbuttongroup.h" #include #include #include #include class KButtonGroup::Private { public: Private(KButtonGroup *q) : q(q), currentId(-1), nextId(0), wantToBeId(-1) { connect(&clickedMapper, SIGNAL(mapped(int)), q, SLOT(slotClicked(int))); connect(&pressedMapper, SIGNAL(mapped(int)), q, SIGNAL(pressed(int))); connect(&releasedMapper, SIGNAL(mapped(int)), q, SIGNAL(released(int))); } void slotClicked(int id); KButtonGroup *q; QSignalMapper clickedMapper; QSignalMapper pressedMapper; QSignalMapper releasedMapper; QHash btnMap; int currentId; int nextId; int wantToBeId; }; KButtonGroup::KButtonGroup(QWidget *parent) : QGroupBox(parent), d(new Private(this)) { } KButtonGroup::~KButtonGroup() { delete d; } void KButtonGroup::setSelected(int id) { if (!testAttribute(Qt::WA_WState_Polished)) { d->wantToBeId = id; ensurePolished(); return; } QHash::Iterator it = d->btnMap.begin(); QHash::Iterator itEnd = d->btnMap.end(); QAbstractButton *button = nullptr; for (; it != itEnd; ++it) { if ((it.value() == id) && (button = qobject_cast(it.key()))) { button->setChecked(true); d->currentId = id; emit changed(id); d->wantToBeId = -1; return; } } // button not found, it might still show up though, eg. because of premature polishing above d->wantToBeId = id; } int KButtonGroup::selected() const { return d->currentId; } void KButtonGroup::childEvent(QChildEvent *event) { if (event->polished()) { auto button = qobject_cast(event->child()); if (!d->btnMap.contains(event->child()) && button) { connect(button, SIGNAL(clicked()), &d->clickedMapper, SLOT(map())); d->clickedMapper.setMapping(button, d->nextId); connect(button, SIGNAL(pressed()), &d->pressedMapper, SLOT(map())); d->pressedMapper.setMapping(button, d->nextId); connect(button, SIGNAL(released()), &d->releasedMapper, SLOT(map())); d->releasedMapper.setMapping(button, d->nextId); d->btnMap[button] = d->nextId; if (d->nextId == d->wantToBeId) { d->currentId = d->wantToBeId; d->wantToBeId = -1; button->setChecked(true); emit changed(d->currentId); } ++d->nextId; } } else if (event->removed()) { QObject *obj = event->child(); QHash::ConstIterator it = d->btnMap.constFind(obj); if (it != d->btnMap.constEnd()) { d->clickedMapper.removeMappings(obj); d->pressedMapper.removeMappings(obj); d->releasedMapper.removeMappings(obj); if (it.value() == d->currentId) { d->currentId = -1; } d->btnMap.remove(obj); } } // be transparent QGroupBox::childEvent(event); } int KButtonGroup::id(QAbstractButton *button) const { QHash::ConstIterator it = d->btnMap.constFind(button); if (it != d->btnMap.constEnd()) { return it.value(); } return -1; } void KButtonGroup::Private::slotClicked(int id) { currentId = id; emit q->clicked(id); emit q->changed(id); } #include "moc_kbuttongroup.cpp" kup-backup-0.9.1/kcm/kbuttongroup.h000066400000000000000000000043121420500356400172500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2006 Pino Toscano // SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KBUTTONGROUP_H #define KBUTTONGROUP_H #include class QAbstractButton; /** * @deprecated since 5.0, use QGroupBox and QButtonGroup instead * * @short Group box with index of the selected button * KButtonGroup is a simple group box that can keep track of the current selected * button of the ones added to it. * * Use normally as you would with a QGroupBox. * * \image html kbuttongroup.png "KDE Button Group containing 3 KPushButtons" * * @author Pino Toscano */ class KButtonGroup : public QGroupBox { Q_OBJECT Q_PROPERTY(int current READ selected WRITE setSelected NOTIFY changed USER true) public: /** * Construct a new empty KGroupBox. */ explicit KButtonGroup(QWidget *parent = nullptr); /** * Destroys the widget. */ ~KButtonGroup() override; /** * Return the index of the selected QAbstractButton, among the QAbstractButton's * added to the widget. * @return the index of the selected button */ int selected() const; /** * @return the index of @p button. * @since 4.3 */ int id(QAbstractButton *button) const; public Q_SLOTS: /** * Select the \p id -th button */ void setSelected(int id); Q_SIGNALS: /** * The button with index \p id was clicked */ void clicked(int id); /** * The button with index \p id was pressed */ void pressed(int id); /** * The button with index \p id was released */ void released(int id); /** * Emitted when anything (a click on a button, or calling setSelected()) * change the id of the current selected. \p id is the index of the new * selected button. */ void changed(int id); protected: /** * Reimplemented from QGroupBox. */ void childEvent(QChildEvent *event) override; private: Q_PRIVATE_SLOT(d, void slotClicked(int id)) class Private; friend class Private; Private *const d; }; #endif kup-backup-0.9.1/kcm/kcm_kup.desktop000066400000000000000000000062351420500356400173660ustar00rootroot00000000000000[Desktop Entry] Exec=kcmshell5 kcm_kup Icon=kup Type=Service X-KDE-ServiceTypes=KCModule X-KDE-Library=kcm_kup X-KDE-ParentApp=kcontrol X-KDE-System-Settings-Parent-Category=personalization Name=Backups Name[ca]=Còpies de seguretat Name[ca@valencia]=Còpies de seguretat Name[cs]=Zálohy Name[de]=Sicherungen Name[en_GB]=Backups Name[es]=Copias de seguridad Name[et]=Varukoopiad Name[eu]=Babes-kopiak Name[fi]=Varmuuskopiot Name[fr]=Sauvegardes Name[it]=Copie di sicurezza Name[ko]=백업 Name[nl]=Back-ups Name[pl]=Kopie zapasowe Name[pt]=Cópias de Segurança Name[pt_BR]=Backups Name[sk]=Zálohy Name[sl]=Varnostne kopije Name[sv]=Säkerhetskopior Name[uk]=Резервні копії Name[x-test]=xxBackupsxx Name[zh_CN]=备份 Name[zh_TW]=備份 Comment=Configure backup plans Comment[ca]=Configura els plans per a còpia de seguretat Comment[ca@valencia]=Configura els plans per a còpia de seguretat Comment[de]=Sicherungspläne einrichten Comment[en_GB]=Configure backup plans Comment[es]=Configurar planificación de copias de seguridad Comment[et]=Varukoopiakavade seadistamine Comment[eu]=Konfiguratu babes-kopia egitasmoak Comment[fi]=Varmuuskopiointisuunnitelmien asetukset Comment[fr]=Configurer les plans de sauvegarde Comment[it]=Configura i piani di copia di sicurezza Comment[ko]=백업 계획 설정 Comment[nl]=Back-up-plannen configureren Comment[pl]=Ustawienia planów kopii zapasowej Comment[pt]=Configurar os planos de cópias de segurança Comment[pt_BR]=Configurar planos de backup Comment[sl]=Nastavitve planov izdelav varnostnih kopij Comment[sv]=Anpassa säkerhetskopieringsplaner Comment[uk]=Налаштовування планів резервного копіювання Comment[x-test]=xxConfigure backup plansxx Comment[zh_CN]=配置备份计划 Comment[zh_TW]=設定備份計畫 X-KDE-Keywords=backup,recovery,safety X-KDE-Keywords[ca]=còpia de seguretat,recuperació,seguretat X-KDE-Keywords[ca@valencia]=còpia de seguretat,recuperació,seguretat X-KDE-Keywords[de]=backup,recovery,safety, sicherung,wiederherstellung,sicherheit X-KDE-Keywords[en_GB]=backup,recovery,safety X-KDE-Keywords[es]=copiado, recuperación, seguridad X-KDE-Keywords[et]=varukoopia,varundamine,taastamine,turvalisus X-KDE-Keywords[eu]=babes-kopia,berreskuratzea,segurtasuna X-KDE-Keywords[fi]=varmuuskopio,varmuuskopiointi,palautus,palauttaminen,turvallisuus X-KDE-Keywords[fr]=Sauvegarde, récupération, sécurité X-KDE-Keywords[it]=copia di sicurezza,ripristino,sicurezza X-KDE-Keywords[ko]=backup,recovery,safety,백업,복구,안전 X-KDE-Keywords[nl]=back-up,herstel,veiligheid X-KDE-Keywords[pl]=kopia zapasowa,przywracanie,bezpieczeństwo X-KDE-Keywords[pt]=cópia,recuperação,segurança X-KDE-Keywords[pt_BR]=backup,recuperação,segurança,cópia X-KDE-Keywords[sl]=backup,recovery,safety,varnostne kopije,obnova podatkov,varnost X-KDE-Keywords[sv]=säkerhetskopia,återställning,säkerhet X-KDE-Keywords[uk]=backup,recovery,safety,резерв,копіювання,відновлення,безпека X-KDE-Keywords[x-test]=xxbackupxx,xxrecoveryxx,xxsafetyxx X-KDE-Keywords[zh_CN]=backup,recovery,safety,备份,恢复,安全 X-KDE-Keywords[zh_TW]=backup,recovery,safety,備份,復原,安全性 kup-backup-0.9.1/kcm/kupkcm.cpp000066400000000000000000000272141420500356400163400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupkcm.h" #include "backupplan.h" #include "backupplanwidget.h" #include "kupdaemon.h" #include "kupsettings.h" #include "planstatuswidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY(KupKcmFactory, registerPlugin();) KupKcm::KupKcm(QWidget *pParent, const QVariantList &pArgs) : KCModule(pParent, pArgs), mSourcePageToShow(0) { KAboutData lAbout(QStringLiteral("kcm_kup"), i18n("Kup Configuration Module"), QStringLiteral("0.9.1"), i18n("Configuration of backup plans for the Kup backup system"), KAboutLicense::GPL, i18n("Copyright (C) 2011-2020 Simon Persson")); lAbout.addAuthor(i18n("Simon Persson"), i18n("Maintainer"), "simon.persson@mykolab.com"); lAbout.setTranslator(xi18nc("NAME OF TRANSLATORS", "Your names"), xi18nc("EMAIL OF TRANSLATORS", "Your emails")); setAboutData(new KAboutData(lAbout)); setObjectName(QStringLiteral("kcm_kup")); //needed for the kconfigdialogmanager magic setButtons((Apply | buttons()) & ~Default); KProcess lBupProcess; lBupProcess << QStringLiteral("bup") << QStringLiteral("version"); lBupProcess.setOutputChannelMode(KProcess::MergedChannels); int lExitCode = lBupProcess.execute(); if(lExitCode >= 0) { mBupVersion = QString::fromUtf8(lBupProcess.readAllStandardOutput()); KProcess lPar2Process; lPar2Process << QStringLiteral("bup") << QStringLiteral("fsck") << QStringLiteral("--par2-ok"); mPar2Available = lPar2Process.execute() == 0; } else { mPar2Available = false; } KProcess lRsyncProcess; lRsyncProcess << QStringLiteral("rsync") << QStringLiteral("--version"); lRsyncProcess.setOutputChannelMode(KProcess::MergedChannels); lExitCode = lRsyncProcess.execute(); if(lExitCode >= 0) { QString lOutput = QString::fromLocal8Bit(lRsyncProcess.readLine()); mRsyncVersion = lOutput.split(QLatin1Char(' '), QString::SkipEmptyParts).at(2); } if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty()) { auto lSorryIcon = new QLabel; lSorryIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(64, 64)); QString lInstallMessage = i18n("

Backup programs are missing

" "

Before you can activate any backup plan you need to " "install either of

" "
  • bup, for versioned backups
  • " "
  • rsync, for synchronized backups
"); auto lSorryText = new QLabel(lInstallMessage); lSorryText->setWordWrap(true); auto lHLayout = new QHBoxLayout; lHLayout->addWidget(lSorryIcon); lHLayout->addWidget(lSorryText, 1); setLayout(lHLayout); } else { Kdelibs4ConfigMigrator lMigrator(QStringLiteral("kup")); lMigrator.setConfigFiles(QStringList() << QStringLiteral("kuprc")); lMigrator.migrate(); mConfig = KSharedConfig::openConfig(QStringLiteral("kuprc")); mSettings = new KupSettings(mConfig, this); for(int i = 0; i < mSettings->mNumberOfPlans; ++i) { mPlans.append(new BackupPlan(i+1, mConfig, this)); mConfigManagers.append(nullptr); mPlanWidgets.append(nullptr); mStatusWidgets.append(nullptr); } createSettingsFrontPage(); addConfig(mSettings, mFrontPage); mStackedLayout = new QStackedLayout; mStackedLayout->addWidget(mFrontPage); setLayout(mStackedLayout); QListIterator lIter(pArgs); while(lIter.hasNext()) { QVariant lVariant = lIter.next(); if(lVariant.type() == QVariant::String) { QString lArgument = lVariant.toString(); if(lArgument == QStringLiteral("show_sources") && lIter.hasNext()) { mSourcePageToShow = lIter.next().toString().toInt(); } } } } } QSize KupKcm::sizeHint() const { return {800, 600}; } void KupKcm::load() { if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty()) { return; } // status will be set correctly after construction, set to checked here to // match the enabled status of other widgets mEnableCheckBox->setChecked(true); for(int i = 0; i < mSettings->mNumberOfPlans; ++i) { if(!mConfigManagers.at(i)) createPlanWidgets(i); mConfigManagers.at(i)->updateWidgets(); } for(int i = mSettings->mNumberOfPlans; i < mPlans.count();) { completelyRemovePlan(i); } KCModule::load(); // this call is needed because it could have been set true before, now load() is called // because user pressed reset button. need to manually reset the "changed" state to false // in this case. unmanagedWidgetChangeState(false); if(mSourcePageToShow > 0) { mStackedLayout->setCurrentIndex(mSourcePageToShow); mPlanWidgets[mSourcePageToShow - 1]->showSourcePage(); mSourcePageToShow = 0; //only trigger on first load after startup. } } void KupKcm::save() { KConfigDialogManager *lManager; BackupPlan *lPlan; int lPlansRemoved = 0; for(int i=0; i < mPlans.count(); ++i) { lPlan = mPlans.at(i); lManager = mConfigManagers.at(i); if(lManager != nullptr) { if(lPlansRemoved != 0) { lPlan->removePlanFromConfig(); lPlan->setPlanNumber(i + 1); // config manager does not detect a changed group name of the config items. // To work around, read default settings - config manager will then notice // changed values and save current widget status into the config using the // new group name. If all settings for the plan already was default then // nothing was saved anyway, either under old or new group name. lPlan->setDefaults(); } mPlanWidgets.at(i)->saveExtraData(); lManager->updateSettings(); mStatusWidgets.at(i)->updateIcon(); if(lPlan->mDestinationType == 1 && lPlan->mExternalUUID.isEmpty()) { KMessageBox::information(this, xi18nc("@title:window", "Warning"), xi18nc("@info %1 is the name of the backup plan", "%1 does not have a destination!" "No backups will be saved by this plan.", lPlan->mDescription), QString(), KMessageBox::Dangerous); } } else { lPlan->removePlanFromConfig(); delete mPlans.takeAt(i); mConfigManagers.removeAt(i); mStatusWidgets.removeAt(i); mPlanWidgets.removeAt(i); ++lPlansRemoved; --i; } } mSettings->mNumberOfPlans = mPlans.count(); mSettings->save(); KCModule::save(); QDBusInterface lInterface(KUP_DBUS_SERVICE_NAME, KUP_DBUS_OBJECT_PATH); if(lInterface.isValid()) { lInterface.call(QStringLiteral("reloadConfig")); } else { KProcess::startDetached(QStringLiteral("kup-daemon")); } } void KupKcm::updateChangedStatus() { bool lHasUnmanagedChanged = false; foreach(KConfigDialogManager *lConfigManager, mConfigManagers) { if(!lConfigManager || lConfigManager->hasChanged()) { lHasUnmanagedChanged = true; break; } } if(mPlanWidgets.count() != mSettings->mNumberOfPlans) lHasUnmanagedChanged = true; unmanagedWidgetChangeState(lHasUnmanagedChanged); } void KupKcm::showFrontPage() { mStackedLayout->setCurrentIndex(0); } void KupKcm::createSettingsFrontPage() { mFrontPage = new QWidget; auto lHLayout = new QHBoxLayout; auto lVLayout = new QVBoxLayout; auto lScrollArea = new QScrollArea; auto lCentralWidget = new QWidget(lScrollArea); mVerticalLayout = new QVBoxLayout; lScrollArea->setWidget(lCentralWidget); lScrollArea->setWidgetResizable(true); lScrollArea->setFrameStyle(QFrame::NoFrame); auto lAddPlanButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), xi18nc("@action:button", "Add New Plan")); connect(lAddPlanButton, &QPushButton::clicked, this, [this]{ mPlans.append(new BackupPlan(mPlans.count() + 1, mConfig, this)); if(mBupVersion.isEmpty()) mPlans.last()->mBackupType = 1; mConfigManagers.append(nullptr); mPlanWidgets.append(nullptr); mStatusWidgets.append(nullptr); createPlanWidgets(mPlans.count() - 1); updateChangedStatus(); emit mStatusWidgets.at(mPlans.count() - 1)->configureMe(); }); mEnableCheckBox = new QCheckBox(xi18nc("@option:check", "Backups Enabled")); mEnableCheckBox->setObjectName(QStringLiteral("kcfg_Backups enabled")); connect(mEnableCheckBox, &QCheckBox::toggled, lAddPlanButton, &QPushButton::setEnabled); lHLayout->addWidget(mEnableCheckBox); lHLayout->addStretch(); lHLayout->addWidget(lAddPlanButton); lVLayout->addLayout(lHLayout); lVLayout->addWidget(lScrollArea); mFrontPage->setLayout(lVLayout); auto lFilediggerButton = new QPushButton(xi18nc("@action:button", "Open and restore from existing backups")); connect(lFilediggerButton, &QPushButton::clicked, []{KProcess::startDetached(QStringLiteral("kup-filedigger"));}); mVerticalLayout->addWidget(lFilediggerButton); mVerticalLayout->addStretch(1); lCentralWidget->setLayout(mVerticalLayout); } void KupKcm::createPlanWidgets(int pIndex) { auto lPlanWidget = new BackupPlanWidget(mPlans.at(pIndex), mBupVersion, mRsyncVersion, mPar2Available); connect(lPlanWidget, SIGNAL(requestOverviewReturn()), this, SLOT(showFrontPage())); auto lConfigManager = new KConfigDialogManager(lPlanWidget, mPlans.at(pIndex)); lConfigManager->setObjectName(objectName()); connect(lConfigManager, SIGNAL(widgetModified()), this, SLOT(updateChangedStatus())); auto lStatusWidget = new PlanStatusWidget(mPlans.at(pIndex)); connect(lStatusWidget, &PlanStatusWidget::removeMe, this, [this,pIndex]{ if(pIndex < mSettings->mNumberOfPlans) partiallyRemovePlan(pIndex); else completelyRemovePlan(pIndex); updateChangedStatus(); }); connect(lStatusWidget, &PlanStatusWidget::configureMe, this, [this,pIndex]{ mStackedLayout->setCurrentIndex(pIndex + 1); }); connect(lStatusWidget, &PlanStatusWidget::duplicateMe, this, [this,pIndex]{ auto lNewPlan = new BackupPlan(mPlans.count() + 1, mConfig, this); lNewPlan->copyFrom(*mPlans.at(pIndex)); mPlans.append(lNewPlan); mConfigManagers.append(nullptr); mPlanWidgets.append(nullptr); mStatusWidgets.append(nullptr); createPlanWidgets(mPlans.count() - 1); // crazy trick to make the config system realize that stuff has changed // and will need to be saved. lNewPlan->setDefaults(); updateChangedStatus(); }); connect(mEnableCheckBox, &QCheckBox::toggled, lStatusWidget, &PlanStatusWidget::setEnabled); connect(lPlanWidget->mDescriptionEdit, &KLineEdit::textChanged, lStatusWidget->mDescriptionLabel, &QLabel::setText); mConfigManagers[pIndex] = lConfigManager; mPlanWidgets[pIndex] = lPlanWidget; mStackedLayout->insertWidget(pIndex + 1, lPlanWidget); mStatusWidgets[pIndex] = lStatusWidget; mVerticalLayout->insertWidget(pIndex, lStatusWidget); } void KupKcm::completelyRemovePlan(int pIndex) { mVerticalLayout->removeWidget(mStatusWidgets.at(pIndex)); mStackedLayout->removeWidget(mPlanWidgets.at(pIndex)); delete mConfigManagers.takeAt(pIndex); delete mStatusWidgets.takeAt(pIndex); delete mPlanWidgets.takeAt(pIndex); delete mPlans.takeAt(pIndex); } void KupKcm::partiallyRemovePlan(int pIndex) { mVerticalLayout->removeWidget(mStatusWidgets.at(pIndex)); mStackedLayout->removeWidget(mPlanWidgets.at(pIndex)); mConfigManagers.at(pIndex)->deleteLater(); mConfigManagers[pIndex] = nullptr; mStatusWidgets.at(pIndex)->deleteLater(); mStatusWidgets[pIndex] = nullptr; mPlanWidgets.at(pIndex)->deleteLater(); mPlanWidgets[pIndex] = nullptr; } #include "kupkcm.moc" kup-backup-0.9.1/kcm/kupkcm.h000066400000000000000000000024621420500356400160030ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPKCM_H #define KUPKCM_H #include #include class BackupPlan; class BackupPlanWidget; class KupSettings; class KupSettingsWidget; class PlanStatusWidget; class KAssistantDialog; class KPageWidgetItem; class QPushButton; class QCheckBox; class QStackedLayout; class QVBoxLayout; class KupKcm : public KCModule { Q_OBJECT public: KupKcm(QWidget *pParent, const QVariantList &pArgs); QSize sizeHint() const override; public slots: void load() override; void save() override; void updateChangedStatus(); void showFrontPage(); private: void createSettingsFrontPage(); void createPlanWidgets(int pIndex); void completelyRemovePlan(int pIndex); void partiallyRemovePlan(int pIndex); KSharedConfigPtr mConfig; KupSettings *mSettings; QWidget *mFrontPage{}; QList mPlans; QList mPlanWidgets; QList mStatusWidgets; QList mConfigManagers; QStackedLayout *mStackedLayout; QVBoxLayout *mVerticalLayout{}; QCheckBox *mEnableCheckBox{}; QString mBupVersion; QString mRsyncVersion; bool mPar2Available; int mSourcePageToShow; }; #endif // KUPKCM_H kup-backup-0.9.1/kcm/planstatuswidget.cpp000066400000000000000000000044331420500356400204460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "planstatuswidget.h" #include "kupsettings.h" #include "backupplan.h" #include #include #include #include PlanStatusWidget::PlanStatusWidget(BackupPlan *pPlan, QWidget *pParent) : QGroupBox(pParent), mPlan(pPlan) { auto lVLayout1 = new QVBoxLayout; auto lVLayout2 = new QVBoxLayout; auto lHLayout1 = new QHBoxLayout; auto lHLayout2 = new QHBoxLayout; mDescriptionLabel = new QLabel(mPlan->mDescription); QFont lDescriptionFont = mDescriptionLabel->font(); lDescriptionFont.setPointSizeF(lDescriptionFont.pointSizeF() + 2.0); lDescriptionFont.setBold(true); mDescriptionLabel->setFont(lDescriptionFont); mStatusIconLabel = new QLabel(); //TODO: add dbus interface to be notified from daemon when this is updated. mStatusTextLabel = new QLabel(mPlan->statusText()); auto lConfigureButton = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), xi18nc("@action:button", "Configure")); connect(lConfigureButton, SIGNAL(clicked()), this, SIGNAL(configureMe())); auto lRemoveButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), xi18nc("@action:button", "Remove")); connect(lRemoveButton, SIGNAL(clicked()), this, SIGNAL(removeMe())); auto lCopyButton = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-duplicate")), xi18nc("@action:button", "Duplicate")); connect(lCopyButton, &QPushButton::clicked, this, &PlanStatusWidget::duplicateMe); lVLayout2->addWidget(mDescriptionLabel); lVLayout2->addWidget(mStatusTextLabel); lHLayout1->addLayout(lVLayout2); lHLayout1->addStretch(); lHLayout1->addWidget(mStatusIconLabel); lVLayout1->addLayout(lHLayout1); lHLayout2->addStretch(); lHLayout2->addWidget(lCopyButton); lHLayout2->addWidget(lConfigureButton); lHLayout2->addWidget(lRemoveButton); lVLayout1->addLayout(lHLayout2); setLayout(lVLayout1); updateIcon(); } void PlanStatusWidget::updateIcon() { mStatusIconLabel->setPixmap(QIcon::fromTheme(BackupPlan::iconName(mPlan->backupStatus())).pixmap(64,64)); } kup-backup-0.9.1/kcm/planstatuswidget.h000066400000000000000000000013201420500356400201030ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef PLANSTATUSWIDGET_H #define PLANSTATUSWIDGET_H #include class BackupPlan; class KupSettings; class QPushButton; class QLabel; class PlanStatusWidget: public QGroupBox { Q_OBJECT public: explicit PlanStatusWidget(BackupPlan *pPlan, QWidget *pParent = nullptr); BackupPlan *plan() const {return mPlan;} BackupPlan *mPlan; QLabel *mDescriptionLabel; QLabel *mStatusIconLabel; QLabel *mStatusTextLabel; public slots: void updateIcon(); signals: void removeMe(); void configureMe(); void duplicateMe(); }; #endif // PLANSTATUSWIDGET_H kup-backup-0.9.1/kioslave/000077500000000000000000000000001420500356400153775ustar00rootroot00000000000000kup-backup-0.9.1/kioslave/CMakeLists.txt000066400000000000000000000012351420500356400201400ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2020 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later set(bupslave_SRCS bupslave.cpp bupvfs.cpp vfshelpers.cpp ) ecm_qt_declare_logging_category(bupslave_SRCS HEADER kupkio_debug.h IDENTIFIER KUPKIO CATEGORY_NAME kup.kio DEFAULT_SEVERITY Warning EXPORT kup DESCRIPTION "Kup KIO slave for bup" ) add_library(kio_bup MODULE ${bupslave_SRCS}) target_link_libraries(kio_bup Qt5::Core KF5::KIOCore KF5::I18n LibGit2::LibGit2 ) install(TARGETS kio_bup DESTINATION ${PLUGIN_INSTALL_DIR}) install(FILES bup.protocol DESTINATION ${SERVICES_INSTALL_DIR}) add_definitions(-fexceptions) kup-backup-0.9.1/kioslave/bup.protocol000066400000000000000000000033551420500356400177560ustar00rootroot00000000000000[Protocol] exec=kio_bup protocol=bup input=filesystem listing=Name,Type,Size,Date,AccessDate,Access,Owner,Group,Link output=filesystem reading=true source=true writing=false makedir=false deleting=false moving=false opening=true linking=false Icon=folder-important Description=A kioslave for bup backup repositories Description[ca]=Un esclau «kio» per als repositoris de còpia de seguretat de «bup» Description[ca@valencia]=Un esclau «kio» per als repositoris de còpia de seguretat de «bup» Description[de]=Ein Ein-/Ausgabemodul für Bup-Sicherungsarchive Description[en_GB]=A kioslave for bup backup repositories Description[es]=Un «kioslave» para los repositorios de copia de seguridad de bup Description[et]=bup varukoopiahoidlate KIO-moodul Description[eu]=Bup backup gordetegietarako «kioslave» bat Description[fi]=KIO-asiakas bup-varmuuskopiolähteille Description[fr]=Un module d'entrées / sorties pour vos dépôts de sauvegarde de Kup Description[it]=Un kioslave per i depositi delle copie di sicurezza di bup Description[ko]=bup 백업 저장소용 KIO 슬레이브 Description[nl]=Een kioslave voor bup back-up-opslagruimten Description[pl]=kioslave dla repozytoriów kopii zapasowych bup Description[pt]=Um 'kioslave' para os repositórios de cópias de segurança do 'bup' Description[pt_BR]=Um kioslave para os repositórios de backups do bup Description[sl]=Kioslave za skladišča varnostnih kopij programa bup Description[sv]=En I/O-slav för bup säkerhetskopieringsarkiv Description[uk]=Допоміжний засіб введення-виведення для сховищ резервних копій bup Description[x-test]=xxA kioslave for bup backup repositoriesxx Description[zh_CN]=bup 备份仓库的 KIO 从属。 Class=:local kup-backup-0.9.1/kioslave/bupslave.cpp000066400000000000000000000252201420500356400177250ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "bupvfs.h" #include #include #include #include using namespace KIO; #include #include #include #include class BupSlave : public SlaveBase { public: BupSlave(const QByteArray &pPoolSocket, const QByteArray &pAppSocket); ~BupSlave() override; void close() override; void get(const QUrl &pUrl) override; void listDir(const QUrl &pUrl) override ; void open(const QUrl &pUrl, QIODevice::OpenMode pMode) override; void read(filesize_t pSize) override; void seek(filesize_t pOffset) override; void stat(const QUrl &pUrl) override; void mimetype(const QUrl &pUrl) override; private: bool checkCorrectRepository(const QUrl &pUrl, QStringList &pPathInRepository); QString getUserName(uid_t pUid); QString getGroupName(gid_t pGid); void createUDSEntry(Node *pNode, KIO::UDSEntry & pUDSEntry, int pDetails); QHash mUsercache; QHash mGroupcache; Repository *mRepository; File *mOpenFile; }; BupSlave::BupSlave(const QByteArray &pPoolSocket, const QByteArray &pAppSocket) : SlaveBase("bup", pPoolSocket, pAppSocket) { mRepository = nullptr; mOpenFile = nullptr; git_libgit2_init(); } BupSlave::~BupSlave() { delete mRepository; git_libgit2_shutdown(); } void BupSlave::close() { mOpenFile = nullptr; emit finished(); } void BupSlave::get(const QUrl &pUrl) { QStringList lPathInRepo; if(!checkCorrectRepository(pUrl, lPathInRepo)) { emit error(KIO::ERR_SLAVE_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString())); return; } // Assume that a symlink should be followed. // Kio will never call get() on a symlink if it actually wants to copy a // symlink, it would just create a symlink on the destination kioslave using the // target it already got from calling stat() on this one. Node *lNode = mRepository->resolve(lPathInRepo, true); if(lNode == nullptr) { emit error(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/"))); return; } File *lFile = qobject_cast(lNode); if(lFile == nullptr) { emit error(KIO::ERR_IS_DIRECTORY, lPathInRepo.join(QStringLiteral("/"))); return; } emit mimeType(lFile->mMimeType); // Emit total size AFTER mimetype emit totalSize(lFile->size()); //make sure file is at the beginning lFile->seek(0); KIO::filesize_t lProcessedSize = 0; const QString lResumeOffset = metaData(QStringLiteral("resume")); if(!lResumeOffset.isEmpty()) { bool ok; quint64 lOffset = lResumeOffset.toULongLong(&ok); if (ok && lOffset < lFile->size()) { if(0 == lFile->seek(lOffset)) { emit canResume(); lProcessedSize = lOffset; } } } QByteArray lResultArray; int lRetVal; while(0 == (lRetVal = lFile->read(lResultArray))) { emit data(lResultArray); lProcessedSize += static_cast(lResultArray.length()); emit processedSize(lProcessedSize); } if(lRetVal == KIO::ERR_NO_CONTENT) { emit data(QByteArray()); emit processedSize(lProcessedSize); emit finished(); } else { emit error(lRetVal, lPathInRepo.join(QStringLiteral("/"))); } } void BupSlave::listDir(const QUrl &pUrl) { QStringList lPathInRepo; if(!checkCorrectRepository(pUrl, lPathInRepo)) { emit error(KIO::ERR_SLAVE_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString())); return; } Node *lNode = mRepository->resolve(lPathInRepo, true); if(lNode == nullptr) { emit error(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/"))); return; } auto lDir = qobject_cast(lNode); if(lDir == nullptr) { emit error(KIO::ERR_IS_FILE, lPathInRepo.join(QStringLiteral("/"))); return; } // give the directory a chance to reload if necessary. lDir->reload(); const QString sDetails = metaData(QStringLiteral("details")); const int lDetails = sDetails.isEmpty() ? 2 : sDetails.toInt(); NodeMapIterator i(lDir->subNodes()); UDSEntry lEntry; while(i.hasNext()) { createUDSEntry(i.next().value(), lEntry, lDetails); emit listEntry(lEntry); } emit finished(); } void BupSlave::open(const QUrl &pUrl, QIODevice::OpenMode pMode) { if(pMode & QIODevice::WriteOnly) { emit error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, pUrl.toDisplayString()); return; } QStringList lPathInRepo; if(!checkCorrectRepository(pUrl, lPathInRepo)) { emit error(KIO::ERR_SLAVE_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString())); return; } Node *lNode = mRepository->resolve(lPathInRepo, true); if(lNode == nullptr) { emit error(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/"))); return; } File *lFile = qobject_cast(lNode); if(lFile == nullptr) { emit error(KIO::ERR_IS_DIRECTORY, lPathInRepo.join(QStringLiteral("/"))); return; } if(0 != lFile->seek(0)) { emit error(KIO::ERR_CANNOT_OPEN_FOR_READING, pUrl.toDisplayString()); return; } mOpenFile = lFile; emit mimeType(lFile->mMimeType); emit totalSize(lFile->size()); emit position(0); emit opened(); } void BupSlave::read(filesize_t pSize) { if(mOpenFile == nullptr) { emit error(KIO::ERR_COULD_NOT_READ, QString()); return; } QByteArray lResultArray; int lRetVal = 0; while(pSize > 0 && 0 == (lRetVal = mOpenFile->read(lResultArray, static_cast(pSize)))) { pSize -= static_cast(lResultArray.size()); emit data(lResultArray); } if(lRetVal == 0) { emit data(QByteArray()); emit finished(); } else { emit error(lRetVal, mOpenFile->completePath()); } } void BupSlave::seek(filesize_t pOffset) { if(mOpenFile == nullptr) { emit error(KIO::ERR_COULD_NOT_SEEK, QString()); return; } if(0 != mOpenFile->seek(pOffset)) { emit error(KIO::ERR_COULD_NOT_SEEK, mOpenFile->completePath()); return; } emit position(pOffset); } void BupSlave::stat(const QUrl &pUrl) { QStringList lPathInRepo; if(!checkCorrectRepository(pUrl, lPathInRepo)) { emit error(KIO::ERR_SLAVE_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString())); return; } Node *lNode = mRepository->resolve(lPathInRepo); if(lNode == nullptr) { emit error(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/"))); return; } const QString sDetails = metaData(QStringLiteral("details")); const int lDetails = sDetails.isEmpty() ? 2 : sDetails.toInt(); UDSEntry lUDSEntry; createUDSEntry(lNode, lUDSEntry, lDetails); emit statEntry(lUDSEntry); emit finished(); } void BupSlave::mimetype(const QUrl &pUrl) { QStringList lPathInRepo; if(!checkCorrectRepository(pUrl, lPathInRepo)) { emit error(KIO::ERR_SLAVE_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString())); return; } Node *lNode = mRepository->resolve(lPathInRepo); if(lNode == nullptr) { emit error(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/"))); return; } emit mimeType(lNode->mMimeType); emit finished(); } bool BupSlave::checkCorrectRepository(const QUrl &pUrl, QStringList &pPathInRepository) { // make this slave accept most URLs.. even incorrect ones. (no slash (wrong), // one slash (correct), two slashes (wrong), three slashes (correct)) QString lPath; if(!pUrl.host().isEmpty()) { lPath = QStringLiteral("/") + pUrl.host() + pUrl.adjusted(QUrl::StripTrailingSlash).path() + '/'; } else { lPath = pUrl.adjusted(QUrl::StripTrailingSlash).path() + '/'; if(!lPath.startsWith(QLatin1Char('/'))) { lPath.prepend(QLatin1Char('/')); } } if(mRepository && mRepository->isValid()) { if(lPath.startsWith(mRepository->objectName())) { lPath.remove(0, mRepository->objectName().length()); pPathInRepository = lPath.split(QLatin1Char('/'), QString::SkipEmptyParts); return true; } delete mRepository; mRepository = nullptr; } pPathInRepository = lPath.split(QLatin1Char('/'), QString::SkipEmptyParts); QString lRepoPath = QStringLiteral("/"); while(!pPathInRepository.isEmpty()) { // make sure the repo path will end with a slash lRepoPath += pPathInRepository.takeFirst(); lRepoPath += QStringLiteral("/"); if((QFile::exists(lRepoPath + QStringLiteral("objects")) && QFile::exists(lRepoPath + QStringLiteral("refs"))) || (QFile::exists(lRepoPath + QStringLiteral(".git/objects")) && QFile::exists(lRepoPath + QStringLiteral(".git/refs")))) { mRepository = new Repository(nullptr, lRepoPath); return mRepository->isValid(); } } return false; } QString BupSlave::getUserName(uid_t pUid) { if(!mUsercache.contains(pUid)) { struct passwd *lUserInfo = getpwuid(pUid); if(lUserInfo) { mUsercache.insert(pUid, QString::fromLocal8Bit(lUserInfo->pw_name)); } else { return QString::number(pUid); } } return mUsercache.value(pUid); } QString BupSlave::getGroupName(gid_t pGid) { if(!mGroupcache.contains(pGid)) { struct group *lGroupInfo = getgrgid(pGid); if(lGroupInfo) { mGroupcache.insert(pGid, QString::fromLocal8Bit(lGroupInfo->gr_name)); } else { return QString::number( pGid ); } } return mGroupcache.value(pGid); } void BupSlave::createUDSEntry(Node *pNode, UDSEntry &pUDSEntry, int pDetails) { pUDSEntry.clear(); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_NAME, pNode->objectName()); if(!pNode->mSymlinkTarget.isEmpty()) { pUDSEntry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, pNode->mSymlinkTarget); if(pDetails > 1) { Node *lNode = qobject_cast(pNode->parent())->resolve(pNode->mSymlinkTarget, true); if(lNode != nullptr) { // follow symlink only if details > 1 and it leads to something pNode = lNode; } } } pUDSEntry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, pNode->mMode & S_IFMT); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_ACCESS, pNode->mMode & 07777); if(pDetails > 0) { quint64 lSize = 0; File *lFile = qobject_cast(pNode); if(lFile != nullptr) { lSize = lFile->size(); } pUDSEntry.fastInsert(KIO::UDSEntry::UDS_SIZE, static_cast(lSize)); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, pNode->mMimeType); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, pNode->mAtime); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, pNode->mMtime); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_USER, getUserName(static_cast(pNode->mUid))); pUDSEntry.fastInsert(KIO::UDSEntry::UDS_GROUP, getGroupName(static_cast(pNode->mGid))); } } extern "C" int Q_DECL_EXPORT kdemain(int pArgc, char **pArgv) { QCoreApplication lApp(pArgc, pArgv); QCoreApplication::setApplicationName(QStringLiteral("kio_bup")); KLocalizedString::setApplicationDomain("kup"); if(pArgc != 4) { fprintf(stderr, "Usage: kio_bup protocol domain-socket1 domain-socket2\n"); exit(-1); } BupSlave lSlave(pArgv[2], pArgv[3]); lSlave.dispatchLoop(); return 0; } kup-backup-0.9.1/kioslave/bupvfs.cpp000066400000000000000000000336671420500356400174270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "bupvfs.h" #include "kupkio_debug.h" #include #include #include #include git_revwalk *Node::mRevisionWalker = nullptr; git_repository *Node::mRepository = nullptr; Node::Node(QObject *pParent, const QString &pName, qint64 pMode) :QObject(pParent), Metadata(pMode) { setObjectName(pName); } int Node::readMetadata(VintStream &pMetadataStream) { return ::readMetadata(pMetadataStream, *this); } Node *Node::resolve(const QString &pPath, bool pFollowLinks) { Node *lParentNode = this; QString lTarget = pPath; if(lTarget.startsWith(QLatin1Char('/'))) { lTarget.remove(0, 1); lParentNode = parentCommit(); } return lParentNode->resolve(lTarget.split(QLatin1Char('/'), QString::SkipEmptyParts), pFollowLinks); } Node *Node::resolve(const QStringList &pPathList, bool pFollowLinks) { Node *lNode = this; foreach(QString lPathComponent, pPathList) { if(lPathComponent == QStringLiteral(".")) { continue; } if(lPathComponent == QStringLiteral("..")) { lNode = qobject_cast(lNode->parent()); } else { auto lDir = qobject_cast(lNode); if(lDir == nullptr) { return nullptr; } lNode = lDir->subNodes().value(lPathComponent, nullptr); } if(lNode == nullptr) { return nullptr; } } if(pFollowLinks && !lNode->mSymlinkTarget.isEmpty()) { return qobject_cast(lNode->parent())->resolve(lNode->mSymlinkTarget, true); } return lNode; } QString Node::completePath() { QString lCompletePath; Node *lNode = this; while(lNode != nullptr) { Node *lNewNode = qobject_cast(lNode->parent()); if(lNewNode == nullptr) { //this must be the repository, already starts and ends with slash. QString lObjectName = lNode->objectName(); lObjectName.chop(1); lCompletePath.prepend(lObjectName); } else { lCompletePath.prepend(lNode->objectName()); lCompletePath.prepend(QStringLiteral("/")); } lNode = lNewNode; } return lCompletePath; } Node *Node::parentCommit() { Node *lNode = this; while(lNode != nullptr && qobject_cast(lNode->parent()) == nullptr) { lNode = qobject_cast(lNode->parent()); } return lNode; } //Node *Node::parentRepository() { // Node *lNode = this; // while(lNode->parent() != nullptr && qobject_cast(lNode) == nullptr) { // lNode = qobject_cast(lNode->parent()); // } // return lNode; //} Directory::Directory(QObject *pParent, const QString &pName, qint64 pMode) :Node(pParent, pName, pMode) { mSubNodes = nullptr; mMimeType = QStringLiteral("inode/directory"); } NodeMap Directory::subNodes() { if(mSubNodes == nullptr) { mSubNodes = new NodeMap(); generateSubNodes(); } return *mSubNodes; } int File::readMetadata(VintStream &pMetadataStream) { int lRetVal = Node::readMetadata(pMetadataStream); QByteArray lContent, lNextData; seek(0); while(lContent.size() < 1000 && 0 == read(lNextData)) { lContent.append(lNextData); } seek(0); QMimeDatabase db; if(!lContent.isEmpty()) { mMimeType = db.mimeTypeForFileNameAndData(objectName(), lContent).name(); } else { mMimeType = db.mimeTypeForFile(objectName()).name(); } return lRetVal; } BlobFile::BlobFile(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) : File(pParent, pName, pMode), mOid(*pOid), mBlob(nullptr) {} BlobFile::~BlobFile() { git_blob_free(mBlob); } int BlobFile::read(QByteArray &pChunk, qint64 pReadSize) { if(mOffset >= size()) { return KIO::ERR_NO_CONTENT; } git_blob *lBlob = cachedBlob(); if(lBlob == nullptr) { return KIO::ERR_COULD_NOT_READ; } quint64 lAvailableSize = size() - mOffset; quint64 lReadSize = lAvailableSize; if(pReadSize > 0 && static_cast(pReadSize) < lAvailableSize) { lReadSize = static_cast(pReadSize); } pChunk = QByteArray::fromRawData(static_cast(git_blob_rawcontent(lBlob)) + mOffset, static_cast(lReadSize)); mOffset += lReadSize; return 0; } git_blob *BlobFile::cachedBlob() { if(mBlob == nullptr) { git_blob_lookup(&mBlob, mRepository, &mOid); } return mBlob; } quint64 BlobFile::calculateSize() { if(mSize >= 0) { return static_cast(mSize); } git_blob *lBlob = cachedBlob(); if(lBlob == nullptr) { return 0; } return static_cast(git_blob_rawsize(lBlob)); } ChunkFile::ChunkFile(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) : File(pParent, pName, pMode), mOid(*pOid), mCurrentBlob(nullptr), mValidSeekPosition(false) { seek(0); } ChunkFile::~ChunkFile() { if(mCurrentBlob != nullptr) { git_blob_free(mCurrentBlob); } } int ChunkFile::seek(quint64 pOffset) { if(pOffset >= size()) { return KIO::ERR_COULD_NOT_SEEK; } if(mOffset == pOffset && mValidSeekPosition) { return 0; // nothing to do, success } mOffset = pOffset; mValidSeekPosition = false; while(!mPositionStack.isEmpty()) { delete mPositionStack.takeLast(); } if(mCurrentBlob != nullptr) { git_blob_free(mCurrentBlob); mCurrentBlob = nullptr; } git_tree *lTree; if(0 != git_tree_lookup(&lTree, mRepository, &mOid)) { return KIO::ERR_COULD_NOT_SEEK; } auto lCurrentPos = new TreePosition(lTree); mPositionStack.append(lCurrentPos); quint64 lLocalOffset = mOffset; while(true) { ulong lLower = 0; const git_tree_entry *lLowerEntry = git_tree_entry_byindex(lCurrentPos->mTree, lLower); ulong lLowerOffset = 0; ulong lUpper = git_tree_entrycount(lCurrentPos->mTree); while(lUpper - lLower > 1) { ulong lToCheck = lLower + (lUpper - lLower)/2; const git_tree_entry *lCheckEntry = git_tree_entry_byindex(lCurrentPos->mTree, lToCheck); quint64 lCheckOffset; if(!offsetFromName(lCheckEntry, lCheckOffset)) { return KIO::ERR_COULD_NOT_SEEK; } if(lCheckOffset > lLocalOffset) { lUpper = lToCheck; } else { lLower = lToCheck; lLowerEntry = lCheckEntry; lLowerOffset = lCheckOffset; } } lCurrentPos->mIndex = lLower; // the remainder of the offset will be a local offset into the blob or into the subtree. lLocalOffset -= lLowerOffset; if(S_ISDIR(git_tree_entry_filemode(lLowerEntry))) { git_tree *lTree; if(0 != git_tree_lookup(&lTree, mRepository, git_tree_entry_id(lLowerEntry))) { return KIO::ERR_COULD_NOT_SEEK; } lCurrentPos = new TreePosition(lTree); mPositionStack.append(lCurrentPos); } else { lCurrentPos->mSkipSize = lLocalOffset; break; } } mValidSeekPosition = true; return 0; // success. } int ChunkFile::read(QByteArray &pChunk, qint64 pReadSize) { if(mOffset >= size()) { return KIO::ERR_NO_CONTENT; } if(!mValidSeekPosition) { return KIO::ERR_COULD_NOT_READ; } TreePosition *lCurrentPos = mPositionStack.last(); if(mCurrentBlob != nullptr && lCurrentPos->mSkipSize == 0) { // skipsize has been reset, this means current blob has been exhausted. Free it // now as we're about to fetch a new one. git_blob_free(mCurrentBlob); mCurrentBlob = nullptr; } if(mCurrentBlob == nullptr) { const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lCurrentPos->mTree, lCurrentPos->mIndex); if(0 != git_blob_lookup(&mCurrentBlob, mRepository, git_tree_entry_id(lTreeEntry))) { return KIO::ERR_COULD_NOT_READ; } } auto lTotalSize = static_cast(git_blob_rawsize(mCurrentBlob)); if(lTotalSize < lCurrentPos->mSkipSize) { // this must mean a corrupt bup tree somehow return KIO::ERR_COULD_NOT_READ; } quint64 lAvailableSize = lTotalSize - lCurrentPos->mSkipSize; quint64 lReadSize = lAvailableSize; if(pReadSize > 0 && static_cast(pReadSize) < lAvailableSize) { lReadSize = static_cast(pReadSize); } pChunk = QByteArray::fromRawData(static_cast(git_blob_rawcontent(mCurrentBlob)) + lCurrentPos->mSkipSize, static_cast(lReadSize)); mOffset += lReadSize; lCurrentPos->mSkipSize += lReadSize; // check if it's time to find next blob. if(lCurrentPos->mSkipSize == lTotalSize) { lCurrentPos->mSkipSize = 0; lCurrentPos->mIndex++; while(true) { if(lCurrentPos->mIndex < git_tree_entrycount(lCurrentPos->mTree)) { const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lCurrentPos->mTree, lCurrentPos->mIndex); if(S_ISDIR(git_tree_entry_filemode(lTreeEntry))) { git_tree *lTree; if(0 != git_tree_lookup(&lTree, mRepository, git_tree_entry_id(lTreeEntry))) { return KIO::ERR_COULD_NOT_READ; } lCurrentPos = new TreePosition(lTree); // will have index and skipsize initialized to zero. mPositionStack.append(lCurrentPos); } else { // it's a blob break; } } else { delete mPositionStack.takeLast(); if(mPositionStack.isEmpty()) { Q_ASSERT(mOffset == size()); break; } lCurrentPos = mPositionStack.last(); lCurrentPos->mIndex++; } } } return 0; // success. } quint64 ChunkFile::calculateSize() { if(mSize >= 0) { return static_cast(mSize); } return calculateChunkFileSize(&mOid, mRepository); } ChunkFile::TreePosition::TreePosition(git_tree *pTree) { mTree = pTree; mIndex = 0; mSkipSize = 0; } ChunkFile::TreePosition::~TreePosition() { git_tree_free(mTree); } ArchivedDirectory::ArchivedDirectory(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) : Directory(pParent, pName, pMode) { mOid = *pOid; mMetadataStream = nullptr; mTree = nullptr; if(0 != git_tree_lookup(&mTree, mRepository, &mOid)) { return; } const git_tree_entry *lTreeEntry = git_tree_entry_byname(mTree, ".bupm"); if(lTreeEntry != nullptr && 0 == git_blob_lookup(&mMetadataBlob, mRepository, git_tree_entry_id(lTreeEntry))) { mMetadataStream = new VintStream(git_blob_rawcontent(mMetadataBlob), static_cast(git_blob_rawsize(mMetadataBlob)), this); readMetadata(*mMetadataStream); // the first entry is metadata for the directory itself } } void ArchivedDirectory::generateSubNodes() { if(mTree == nullptr) { return; } ulong lEntryCount = git_tree_entrycount(mTree); for(uint i = 0; i < lEntryCount; ++i) { uint lMode; const git_oid *lOid; QString lName; bool lChunked; const git_tree_entry *lTreeEntry = git_tree_entry_byindex(mTree, i); getEntryAttributes(lTreeEntry, lMode, lChunked, lOid, lName); if(lName == QStringLiteral(".bupm")) { continue; } Node *lSubNode = nullptr; if(S_ISDIR(lMode)) { lSubNode = new ArchivedDirectory(this, lOid, lName, lMode); } else if(S_ISLNK(lMode)) { lSubNode = new Symlink(this, lOid, lName, lMode); } else if(lChunked) { lSubNode = new ChunkFile(this, lOid, lName, lMode); } else { lSubNode = new BlobFile(this, lOid, lName, lMode); } mSubNodes->insert(lName, lSubNode); if(!S_ISDIR(lMode) && mMetadataStream != nullptr) { lSubNode->readMetadata(*mMetadataStream); } } if(mMetadataStream != nullptr) { delete mMetadataStream; mMetadataStream = nullptr; git_blob_free(mMetadataBlob); mMetadataBlob = nullptr; } git_tree_free(mTree); mTree = nullptr; } Branch::Branch(Node *pParent, const char *pName) : Directory(pParent, QString::fromLocal8Bit(pName).remove(0, 11), DEFAULT_MODE_DIRECTORY) { mRefName = QByteArray(pName); QByteArray lPath = parent()->objectName().toLocal8Bit(); lPath.append(mRefName); struct stat lStat; if(0 == stat(lPath, &lStat)) { mAtime = lStat.st_atime; mMtime = lStat.st_mtime; } } void Branch::reload() { if(mSubNodes == nullptr) { mSubNodes = new NodeMap(); } // potentially changed content in a branch, generateSubNodes is written so // that it can be called repeatedly. generateSubNodes(); } void Branch::generateSubNodes() { if(0 != git_revwalk_push_ref(mRevisionWalker, mRefName)) { return; } git_oid lOid; while(0 == git_revwalk_next(&lOid, mRevisionWalker)) { git_commit *lCommit; if(0 != git_commit_lookup(&lCommit, mRepository, &lOid)) { continue; } QString lCommitTimeLocal = vfsTimeToString(git_commit_time(lCommit)); if(!mSubNodes->contains(lCommitTimeLocal)) { Directory * lDirectory = new ArchivedDirectory(this, git_commit_tree_id(lCommit), lCommitTimeLocal, DEFAULT_MODE_DIRECTORY); lDirectory->mMtime = git_commit_time(lCommit); mSubNodes->insert(lCommitTimeLocal, lDirectory); } git_commit_free(lCommit); } } Repository::Repository(QObject *pParent, const QString &pRepositoryPath) : Directory(pParent, pRepositoryPath, DEFAULT_MODE_DIRECTORY) { if(!objectName().endsWith(QLatin1Char('/'))) { setObjectName(objectName() + QLatin1Char('/')); } if(0 != git_repository_open(&mRepository, pRepositoryPath.toLocal8Bit())) { qCWarning(KUPKIO) << "could not open repository " << pRepositoryPath; mRepository = nullptr; return; } git_strarray lBranchNames; git_reference_list(&lBranchNames, mRepository); for(uint i = 0; i < lBranchNames.count; ++i) { QString lRefName = QString::fromLocal8Bit(lBranchNames.strings[i]); if(lRefName.startsWith(QStringLiteral("refs/heads/"))) { QString lPath = objectName(); lPath.append(lRefName); struct stat lStat; stat(lPath.toLocal8Bit(), &lStat); if(lStat.st_atime > mAtime) { mAtime = lStat.st_atime; } if(lStat.st_mtime > mMtime) { mMtime = lStat.st_mtime; } } } git_strarray_free(&lBranchNames); if(0 != git_revwalk_new(&mRevisionWalker, mRepository)) { qCWarning(KUPKIO) << "could not create a revision walker in repository " << pRepositoryPath; mRevisionWalker = nullptr; return; } } Repository::~Repository() { if(mRepository != nullptr) { git_repository_free(mRepository); } if(mRevisionWalker != nullptr) { git_revwalk_free(mRevisionWalker); } } void Repository::generateSubNodes() { git_strarray lBranchNames; git_reference_list(&lBranchNames, mRepository); for(uint i = 0; i < lBranchNames.count; ++i) { auto lRefName = QString::fromLocal8Bit(lBranchNames.strings[i]); if(lRefName.startsWith(QStringLiteral("refs/heads/"))) { auto lBranch = new Branch(this, lBranchNames.strings[i]); mSubNodes->insert(lBranch->objectName(), lBranch); } } git_strarray_free(&lBranchNames); } kup-backup-0.9.1/kioslave/bupvfs.h000066400000000000000000000074131420500356400170620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BUPVFS_H #define BUPVFS_H #include #include #include #include #include "vfshelpers.h" class Node: public QObject, public Metadata { Q_OBJECT public: Node(QObject *pParent, const QString &pName, qint64 pMode); ~Node() override {} virtual int readMetadata(VintStream &pMetadataStream); Node *resolve(const QString &pPath, bool pFollowLinks = false); Node *resolve(const QStringList &pPathList, bool pFollowLinks = false); QString completePath(); Node *parentCommit(); // Node *parentRepository(); QString mMimeType; protected: static git_revwalk *mRevisionWalker; static git_repository *mRepository; }; typedef QHash NodeMap; typedef QHashIterator NodeMapIterator; class Directory: public Node { Q_OBJECT public: Directory(QObject *pParent, const QString &pName, qint64 pMode); ~Directory() override { delete mSubNodes; } virtual NodeMap subNodes(); virtual void reload() {} protected: virtual void generateSubNodes() {} NodeMap *mSubNodes; }; class File: public Node { Q_OBJECT public: File(QObject *pParent, const QString &pName, qint64 pMode) :Node(pParent, pName, pMode) { mOffset = 0; mCachedSize = 0; } virtual quint64 size() { if(mCachedSize == 0) { mCachedSize = calculateSize(); } return mCachedSize; } virtual int seek(quint64 pOffset) { if(pOffset >= size()) { return KIO::ERR_COULD_NOT_SEEK; } mOffset = pOffset; return 0; // success } virtual int read(QByteArray &pChunk, qint64 pReadSize = -1) = 0; int readMetadata(VintStream &pMetadataStream) override; protected: virtual quint64 calculateSize() = 0; quint64 mOffset; quint64 mCachedSize; }; class BlobFile: public File { Q_OBJECT public: BlobFile(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode); ~BlobFile() override; int read(QByteArray &pChunk, qint64 pReadSize = -1) override; protected: git_blob *cachedBlob(); quint64 calculateSize() override; git_oid mOid{}; git_blob *mBlob; }; class Symlink: public BlobFile { Q_OBJECT public: Symlink(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) : BlobFile(pParent, pOid, pName, pMode) { QByteArray lArray; if(0 == read(lArray)) { mSymlinkTarget = QString::fromUtf8(lArray.data(), lArray.size()); seek(0); } } }; class ChunkFile: public File { Q_OBJECT public: ChunkFile(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode); ~ChunkFile() override; int seek(quint64 pOffset) override; int read(QByteArray &pChunk, qint64 pReadSize = -1) override; protected: quint64 calculateSize() override; git_oid mOid; git_blob *mCurrentBlob; struct TreePosition { TreePosition(git_tree *pTree); ~TreePosition(); git_tree *mTree; ulong mIndex; quint64 mSkipSize; }; QList mPositionStack; bool mValidSeekPosition; }; class ArchivedDirectory: public Directory { Q_OBJECT public: ArchivedDirectory(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode); protected: void generateSubNodes() override; git_oid mOid{}; git_blob *mMetadataBlob{}; git_tree *mTree; VintStream *mMetadataStream; }; class Branch: public Directory { Q_OBJECT public: Branch(Node *pParent, const char *pName); void reload() override; protected: void generateSubNodes() override; QByteArray mRefName; }; class Repository: public Directory { Q_OBJECT public: Repository(QObject *pParent, const QString &pRepositoryPath); ~Repository() override; bool isValid() { return mRepository != nullptr && mRevisionWalker != nullptr; } protected: void generateSubNodes() override; }; #endif // BUPVFS_H kup-backup-0.9.1/kioslave/vfshelpers.cpp000066400000000000000000000151531420500356400202710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "vfshelpers.h" #include #include #include #include #include static const int cRecordEnd = 0; //static const int cRecordPath = 1; static const int cRecordCommonV1 = 2; // times, user, group, type, perms, etc. (legacy version 1) static const int cRecordSymlinkTarget = 3; //static const int cRecordPosix1eAcl = 4; // getfacl(1), setfacl(1), etc. //static const int cRecordNfsV4Acl = 5; // intended to supplant posix1e acls? //static const int cRecordLinuxAttr = 6; // lsattr(1) chattr(1) //static const int cRecordLinuxXattr = 7; // getfattr(1) setfattr(1) //static const int cRecordHardlinkTarget = 8; static const int cRecordCommonV2 = 9; // times, user, group, type, perms, etc. static const int cRecordCommonV3 = 10; // times, user, group, type, perms, etc. VintStream::VintStream(const void *pData, int pSize, QObject *pParent) : QObject(pParent) { mByteArray = QByteArray::fromRawData(static_cast(pData), pSize); mBuffer = new QBuffer(&mByteArray, this); mBuffer->open(QIODevice::ReadOnly); } VintStream &VintStream::operator>>(qint64 &pInt) { char c; if(!mBuffer->getChar(&c)) { throw 1; } int lOffset = 6; bool lNegative = (c & 0x40); pInt = c & 0x3F; while(c & 0x80) { if(!mBuffer->getChar(&c)) { throw 1; } pInt |= (c & 0x7F) << lOffset; lOffset += 7; } if(lNegative) { pInt = -pInt; } return *this; } VintStream &VintStream::operator >>(quint64 &pUint) { char c; int lOffset = 0; pUint = 0; do { if(!mBuffer->getChar(&c)) { throw 1; } pUint |= static_cast((c & 0x7F) << lOffset); lOffset += 7; } while(c & 0x80); return *this; } VintStream &VintStream::operator >>(QString &pString) { QByteArray lBytes; *this >> lBytes; pString = QString::fromUtf8(lBytes); return *this; } VintStream &VintStream::operator >>(QByteArray &pByteArray) { quint64 lByteCount; *this >> lByteCount; pByteArray.resize(static_cast(lByteCount)); if(mBuffer->read(pByteArray.data(), pByteArray.length()) < pByteArray.length()) { throw 1; } return *this; } qint64 Metadata::mDefaultUid; qint64 Metadata::mDefaultGid; bool Metadata::mDefaultsResolved = false; Metadata::Metadata(qint64 pMode) { mMode = pMode; mAtime = 0; mMtime = 0; if(!mDefaultsResolved) { mDefaultUid = getuid(); mDefaultGid = getgid(); mDefaultsResolved = true; } mUid = mDefaultUid; mGid = mDefaultGid; mSize = -1; } int readMetadata(VintStream &pMetadataStream, Metadata &pMetadata) { try { quint64 lTag; do { pMetadataStream >> lTag; switch(lTag) { case cRecordCommonV1: { qint64 lNotUsedInt; quint64 lNotUsedUint, lTempUint; QString lNotUsedString; pMetadataStream >> lNotUsedUint >> lTempUint; pMetadata.mMode = static_cast(lTempUint); pMetadataStream >> lTempUint >> lNotUsedString; // user name pMetadata.mUid = static_cast(lTempUint); pMetadataStream >> lTempUint >> lNotUsedString; // group name pMetadata.mGid = static_cast(lTempUint); pMetadataStream >> lNotUsedUint; // device number pMetadataStream >> pMetadata.mAtime >> lNotUsedUint; //nanoseconds pMetadataStream >> pMetadata.mMtime >> lNotUsedUint; // nanoseconds pMetadataStream >> lNotUsedInt >> lNotUsedUint; // status change time break; } case cRecordCommonV2: { qint64 lNotUsedInt; quint64 lNotUsedUint; QString lNotUsedString; pMetadataStream >> lNotUsedUint >> pMetadata.mMode; pMetadataStream >> pMetadata.mUid >> lNotUsedString; // user name pMetadataStream >> pMetadata.mGid >> lNotUsedString; // group name pMetadataStream >> lNotUsedInt; // device number pMetadataStream >> pMetadata.mAtime >> lNotUsedUint; //nanoseconds pMetadataStream >> pMetadata.mMtime >> lNotUsedUint; // nanoseconds pMetadataStream >> lNotUsedInt >> lNotUsedUint; // status change time break; } case cRecordCommonV3: { qint64 lNotUsedInt; quint64 lNotUsedUint; QString lNotUsedString; pMetadataStream >> lNotUsedUint >> pMetadata.mMode; pMetadataStream >> pMetadata.mUid >> lNotUsedString; // user name pMetadataStream >> pMetadata.mGid >> lNotUsedString; // group name pMetadataStream >> lNotUsedInt; // device number pMetadataStream >> pMetadata.mAtime >> lNotUsedUint; //nanoseconds pMetadataStream >> pMetadata.mMtime >> lNotUsedUint; // nanoseconds pMetadataStream >> lNotUsedInt >> lNotUsedUint; // status change time pMetadataStream >> pMetadata.mSize; break; } case cRecordSymlinkTarget: { pMetadataStream >> pMetadata.mSymlinkTarget; break; } default: { if(lTag != cRecordEnd) { QByteArray lNotUsed; pMetadataStream >> lNotUsed; } break; } } } while(lTag != cRecordEnd); } catch(int) { return 1; } return 0; // success } quint64 calculateChunkFileSize(const git_oid *pOid, git_repository *pRepository) { quint64 lLastChunkOffset = 0; quint64 lLastChunkSize = 0; uint lMode; do { git_tree *lTree; if(0 != git_tree_lookup(&lTree, pRepository, pOid)) { return 0; } ulong lEntryCount = git_tree_entrycount(lTree); const git_tree_entry *lEntry = git_tree_entry_byindex(lTree, lEntryCount - 1); quint64 lEntryOffset; if(!offsetFromName(lEntry, lEntryOffset)) { git_tree_free(lTree); return 0; } lLastChunkOffset += lEntryOffset; pOid = git_tree_entry_id(lEntry); lMode = git_tree_entry_filemode(lEntry); git_tree_free(lTree); } while(S_ISDIR(lMode)); git_blob *lBlob; if(0 != git_blob_lookup(&lBlob, pRepository, pOid)) { return 0; } lLastChunkSize = static_cast(git_blob_rawsize(lBlob)); git_blob_free(lBlob); return lLastChunkOffset + lLastChunkSize; } bool offsetFromName(const git_tree_entry *pEntry, quint64 &pUint) { bool lParsedOk; pUint = QString::fromUtf8(git_tree_entry_name(pEntry)).toULongLong(&lParsedOk, 16); return lParsedOk; } void getEntryAttributes(const git_tree_entry *pTreeEntry, uint &pMode, bool &pChunked, const git_oid *&pOid, QString &pName) { pMode = git_tree_entry_filemode(pTreeEntry); pOid = git_tree_entry_id(pTreeEntry); pName = QString::fromUtf8(git_tree_entry_name(pTreeEntry)); pChunked = false; if(pName.endsWith(QStringLiteral(".bupl"))) { pName.chop(5); } else if(pName.endsWith(QStringLiteral(".bup"))) { pName.chop(4); pMode = DEFAULT_MODE_FILE; pChunked = true; } } QString vfsTimeToString(git_time_t pTime) { QDateTime lDateTime; lDateTime.setSecsSinceEpoch(pTime); return lDateTime.toLocalTime().toString(QStringLiteral("yyyy-MM-dd hh:mm")); } kup-backup-0.9.1/kioslave/vfshelpers.h000066400000000000000000000025511420500356400177340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef VFSHELPERS_H #define VFSHELPERS_H #include #include class QBuffer; #include #define DEFAULT_MODE_DIRECTORY 0040755 #define DEFAULT_MODE_FILE 0100644 class VintStream: public QObject { Q_OBJECT public: VintStream(const void *pData, int pSize, QObject *pParent); VintStream &operator>>(qint64 &pInt); VintStream &operator>>(quint64 &pUint); VintStream &operator>>(QString &pString); VintStream &operator>>(QByteArray &pByteArray); protected: QByteArray mByteArray; QBuffer *mBuffer; }; struct Metadata { Metadata() {} Metadata(qint64 pMode); qint64 mMode; qint64 mUid; qint64 mGid; qint64 mAtime; qint64 mMtime; qint64 mSize; //negative if invalid QString mSymlinkTarget; static qint64 mDefaultUid; static qint64 mDefaultGid; static bool mDefaultsResolved; }; int readMetadata(VintStream &pMetadataStream, Metadata &pMetadata); quint64 calculateChunkFileSize(const git_oid *pOid, git_repository *pRepository); bool offsetFromName(const git_tree_entry *pEntry, quint64 &pUint); void getEntryAttributes(const git_tree_entry *pTreeEntry, uint &pMode, bool &pChunked, const git_oid *&pOid, QString &pName); QString vfsTimeToString(git_time_t pTime); #endif // VFSHELPERS_H kup-backup-0.9.1/logo.png000066400000000000000000000133711420500356400152350ustar00rootroot00000000000000PNG  IHDR>a pHYs;;̶tEXtSoftwarewww.inkscape.org<IDATx]yt} E4oieE9#l)M&i6MqvcűW[?&eK"eI6%4:(eS@ .vpCLJ7ٙf~ dAdAdAdAW ^{-^d8s8+-  zC A:CTFF\ȨpCЀBr,eX$l >+ :N Uq.,`5Y,1>>iZ,PAkBee9JKKl(t:u: XX k8k.Ŕ afj,/+MX +WV8s d+Ғh4*.J:@ӡ%EECCCI-OTNZ,//pu% F#6 o2| R,oC5hӋL#5q,,,$< #:? )]S)Wx˻v]GDXCPYi ˗+2L̙O02<ttrEٌl%%Xj%lqih8ay&d4Q[Niv1hkkɓ}ĜӉq P׊vشi*| 6 YFFDK!=rccc;<!>7XL,rrsQ\cE'ӝy?(T{ee8yHr0|;W|5łXu`U@> ayNhĪU+z*\.tvv Y*߁o?ؼQd9Kr099Jz\@~2Y8|50؈@H-_"ॸɄ͍[P_ íp&s / 09 ZY4 x<:;YnކlG&_cpKi`0͍XW[wy':)<0 ;7'G9zfmUEQO7A ܶK;c&?RG-Zޯλ "OIJB,Z5 0̢8w?SNyg_Ǻu>R);c#''Jٳxgz&I^}[KǮIJJ@rsssR+_-@@0͆u )yi+~ ~-Yoyx-?ЭGͺuT^oۇ)LORo!۷PЀ|y`0tȑxbibaZU#@ mn;z\.^/VW*;x϶Xԯ8Z4C7|:#wqohl. |x3ZCҏ>RaЫ;0@~-_al T>.pgJo8hnΝèl^džT'?"_z?ǧ~*^/Y=L%ж?8F+WeX!&ٌ5kRvvtPxސ@>`jz{{)Z/uu0ǣA }rl6'$E&|ǒ%TN#"g8n $AG۟ՌPH\qwr8a<(.FD9 rW" +>߽`ρq0:bzj*rGμOAw{08x4iA D 0CKX>f WVP|ǿꩄtBc'{`pK~i@AH!h$EETw$|BFGFg-((g˞K. UУb٠R?00pEϝ"<033ORwFE?#Z6]uőr!Xj8NWTX~M< s6R pW:y sfAN>#Vwuv\Q@[ۇ3^[S@A&Sl-_ W$-TɓpŵE?GHdq{:ꢞa/=yZ-P B$`tʕ+Q\7{^oBDPF!^#!F7@H/+{oXҲlo.tuvx[[ڑ8A=WV++)BBWѯVTW5Rhq Wػw/GF'0PC1o7%VqO(|% 8㨪JC+gC 9@!w0ryarb"j˔*CZ~v3 G{  _mn xyř.Hj@)RRtw5k/WO>)̖҂?TkjjooJnyTh? wKcjr*ONN/ ^ЀSAO>~5oBX0 GgEk!ZWgN/ݎnZ/[oL7RQ]-/ܶs'**ŋuݎ7^ P~=6݀kkjZkn}}}Qtwusuu5oICXA~8 5kG? 8_ Nxގvt:TTVbŊ(,*BAAr.Evv6XV8ranΉ adxg~?nXp߮]hjbhU5&}{b@Ann.57u8{h9Ñpj܌[wk~i `INVnmGŠ_i+7od X>b_wiX 333 n6vN8ׇa'%X4߷ %@ 0f8B)!6 333*PYYB9}##(xeaΆbAaa!KPRR5k K,PkBElDg'7lPMDn ;fe}Պ)^$7dXѼ}i"':;Z @,Vd}S2/o"^[u)kAGOO,O Yۇ:6!Z0?7dX -?Z/2AgwO5_/'T ,I~GwOO!/@P8!H-@AvvvJ2,wq*?EH $HL%0aJN~{O7hjI z/nL .Ys*A hк0&GB2H9qU2@{w{5}TSpW_x@JB,8 xdI8^roIV@\K"OH2@ X{h^@ 2Q0;|=hN.@ ]}'cQ Xż˅O)A>B»zq=1dB!W=@Eg7~@"âSi"HM zm8c10,?l`#K`h ͟H(  G  "0,?pU9tXJr\*@]V ¯4/GF K%}2ž)aJ5,~>@G#WαXK,h46mMh~+>{O% <FQ2b\ò& , IrERu?J^!HTJ*ՍĪM AF* GL45OO%H5˗h #22`4@X 1|/ 2 2 2 2 BJF>-IENDB`kup-backup-0.9.1/org.kde.kup.appdata.xml000066400000000000000000000466651420500356400200650ustar00rootroot00000000000000 org.kde.kup org.kde.systemsettings org.kde.plasmashell FSFAP GPL-2.0+ Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup Kup xxKupxx K 备份 Kup Backup scheduler for KDE's Plasma desktop Planificador de la còpia de seguretat per a l'escriptori Plasma del KDE Planificador de la còpia de seguretat per a l'escriptori Plasma del KDE Sicherungsplaner für die KDE-Plasma-Arbeitsfläche Backup scheduler for KDE's Plasma desktop Organizador de copias de seguridad para el escritorio KDE Plasma KDE Plasma töölaua varukoopiate tegemise ajakava Babes-kopia antolatzailea KDEko Plasma mahaigainerako Varmuuskopioajastin KDE:n Plasma-työpöydälle Planificateur de sauvegarde pour le bureau Plasma de KDE Penjadwalan cadangan untuk desktop Plasma KDE Pianificatore di copie di sicurezza per il desktop Plasma di KDE KDE Plasma 데스크톱 백업 스케줄러 Planner van back-ups voor Plasma bureaublad van KDE Planowanie kopi zapasowej dla pulpitu KDE Plazma Agendamento de cópias de segurança para a área de trabalho do Plasma Agendador de backups para a área de trabalho Plasma da KDE Planificator de copii de rezervă pentru biroul KDE Plasma Planer izdelave varnostnih kopij za namizje KDE Plasma Schemaläggning av säkerhetskopiering för KDE:s Plasma-skrivbord Засіб планування резервного копіювання для стільниці Плазми KDE xxBackup scheduler for KDE's Plasma desktopxx KDE Plasma 桌面的定期备份工具 KDE Plasma 桌面的備份排程工具

Kup can help you remember to keep up-to-date backups of your personal files. It provides:

El Kup ajuda a recordar el mantenir actualitzades les còpies de seguretat dels fitxers personals. Proporciona:

El Kup ajuda a recordar el mantindre actualitzades les còpies de seguretat dels fitxers personals. Proporciona:

Kup kann Ihnen dabei helfen, aktuelle Sicherungen Ihrer persönlichen Dateien aufzubewahren. Es bietet folgendes:

Kup can help you remember to keep up-to-date backups of your personal files. It provides:

Kup puede ayudarle a mantener copias de seguridad actualizadas de sus archivos personales. Proporciona:

Kup aitab sul meeles pidada vajadust varundada aegsasti oma isiklikke faile. See võimaldab:

Kup-ek zure fitxategi propioen babes-kopiak egunean mantentzea gogoratzen lagunduko dizu. Hau ematen du:

Kup auttaa varmuuskopioimaan henkilökohtaiset tiedostot säännöllisesti. Se tarjoaa:

Kup peut vous aider à vous conserver vos sauvegardes à jour pour vos fichiers personnels. Il propose :

Kup bisa membantu kamu mengingatkan agar pencadangan tetap terbarukan pada file-file personalmu. Ini menyediakan:

Kup può aiutarti a ricordare di mantenere aggiornate le copie di sicurezza dei tuoi file personali. Fornisce:

Kup을 사용하면 개인 파일 최신 백업을 유지할 수 있습니다. 기능:

Kup kan u helpen te herinneren om up-to-date back-ups van uw persoonlijke bestanden te maken. Het biedt:

Kup przypomina o posiadaniu świeżych kopii zapasowych twoich plików osobistych: Zapewnia:

O Kup pode ajudá-lo a recordar-se das cópias de segurança actualizadas dos seus ficheiros pessoais. Oferece:

O Kup pode ajudá-lo a lembrar de manter backups atualizados de seus arquivos pessoais. Ele fornece:

Kup vam lahko pomaga pri spominjanju nenehnega zagotavljanja varnostnih kopij svojih osebnih datotek.Zagotavlja:

Kup kan hjälpa till att komma ihåg att ha aktuella säkerhetskopior av personliga filer. Det tillhandahåller:

Kup може допомогти вам у підтриманні актуальності резервних копій ваших особистих файлів. Програма має такі можливості:

xxKup can help you remember to keep up-to-date backups of your personal files. It provides:xx

K 备份可以提醒您及时备份个人数据。它提供了:

  • Incremental backup archive with the use of "bup".
  • Arxiu per a còpia de seguretat incremental emprant «bup».
  • Arxiu per a còpia de seguretat incremental emprant «bup».
  • Inkrementelles Sicherungsarchiv mit der Verwendung von „bup“.
  • Incremental backup archive with the use of "bup".
  • Copias de seguridad incrementales usando «bup».
  • Inkrementvarundamist "bup" abil.
  • Babes-kopia artxibo inkrementalak «bup» erabiliz.
  • Inkrementaalisen varmuuskopioarkiston bup-ohjelman avulla.
  • Une archive incrémentale de sauvegarde avec l'utilisation de « bup ».
  • Arsip cadangan tambahan dengan penggunaan "bup".
  • Archivi di copie di sicurezza incrementali mediante l'uso di «bup».
  • "bup"을 사용한 증분 백업 압축 파일 생성.
  • Incrementeel back-uparchief met gebruik van "bup".
  • Przyrostowe archiwa kopii zapasowych z użyciem "bup".
  • Cópias de segurança incrementais com o uso do "bup".
  • Arquivo de backup incremental com o uso do "bup".
  • Inkrementalno varnostno arhiviranje z uporabo programa "bup".
  • Inkrementellt säkerhetskopieringsarkiv med användning av "bup".
  • Створення нарощувальних резервних копій за допомогою програми bup.
  • xxIncremental backup archive with the use of "bup".xx
  • bup 增量备份归档功能。
  • 使用「bup」增量備份封存檔。
  • Synchronized folders with the use of "rsync".
  • Carpetes sincronitzades emprant «rsync».
  • Carpetes sincronitzades emprant «rsync».
  • Abgleich von Ordnern mit Hilfe von „rsync“.
  • Synchronised folders with the use of "rsync".
  • Carpetas sincronizadas usando «rsync»
  • Kataloogide sünkroonimist "rsync" abil.
  • Karpeta sinkronizatuak «rsync» erabiliz.
  • Synkronoidut kansiot rsync-ohjelman avulla.
  • Dossiers synchronisés avec l'utilisation de « rsync ».
  • Folder-folder yang tersinkronisasikan dengan penggunaan "rsync".
  • Cartelle sincronizzate mediante l'uso di «rsync».
  • "rsync"를 사용한 폴더 동기화.
  • Gesynchroniseerde mappen met gebruik van "rsync".
  • Synchronizowane katalogi z użyciem "rsync".
  • Pastas sincronizadas com o uso do "rsync".
  • Pastas sincronizadas com o uso do "rsync".
  • Dosare sincronizate prin folosirea „rsync”.
  • Sinhronizirane mape z uporabo programa "rsync".
  • Synkroniserade kataloger med användning av "rsync".
  • Синхронізація тек за допомогою програми rsync.
  • xxSynchronized folders with the use of "rsync".xx
  • rsync 同步文件夹功能。
  • 使用「rsync」同步資料夾。
  • Support for local filesystem or external usb storage.
  • Suport per a sistema de fitxers local o emmagatzematge extern USB.
  • Suport per a sistema de fitxers local o emmagatzematge extern USB.
  • Unterstützung für lokale Dateisysteme oder externen USB-Speicher.
  • Support for local filesystem or external USB storage.
  • Soporte para sistema de archivos local o almacenaje USB externo.
  • Salvestamist kohalikku failisüsteemi või välisele USB-pulgale.
  • Fitxategi-sistema lokaleko edo kanpoko USBko biltegiratze euskarria.
  • Paikallisten tiedostojärjestelmien ja ulkoisten USB-tallennusvälineiden tuen.
  • La prise en charge du système local de fichiers ou sur un stockage USB externe.
  • Dukungan untuk filesystem lokal atau penyimpanan usb eksternal.
  • Supporto per file i filesystem locali oppure per le memorie usb esterne.
  • 로컬 파일 시스템 및 외장형 USB 저장소 지원.
  • Ondersteuning voor een lokale bestandssysteem of externe usb-opslag.
  • Obsługę miejscowych systemów plików lub zewnętrznych pamięci USB
  • Suporte para o sistema de ficheiros local ou para o armazenamento externo em USB.
  • Suporte a sistema de arquivos local e armazenamento USB externo.
  • Podporo lokalnemu datotečnemu sistemu ali zunanji hrambi USB.
  • Stöd för lokala filsystem eller extern USB-lagring
  • Підтримка локальних файлових систем та зовнішніх сховищ даних USB.
  • xxSupport for local filesystem or external usb storage.xx
  • 支持本机文件系统或外部 USB 存储。
  • Monitor availability of backup destinations, like for example a mounted network storage.
  • Supervisa la disponibilitat de les destinacions per a còpia de seguretat, com ara un emmagatzematge de xarxa muntat.
  • Supervisa la disponibilitat de les destinacions per a còpia de seguretat, com ara un emmagatzematge de xarxa muntat.
  • Überwachung der Verfügbarkeit von Sicherungszielen, wie z. B. einem eingehänten Netzwerkspeicher.
  • Monitor availability of backup destinations, like for example a mounted network storage.
  • Monitorización de disponibilidad de destinos para las copias de seguridad, como por ejemplo almacenamiento de red.
  • Varundamise sihtkohtade saadavuse jälgimist, mis on oluline näiteks ühendamist vajava võrgusalvesti puhul.
  • Gainbegiratu babes-kopia jomugen eskuragarritasuna, muntatutako sareko biltegiratze bat esaterako.
  • Varmuuskopioinnin kohteiden kuten verkkojakojen saavutettavuuden valvonnan.
  • Surveillance de la disponibilité des destinations de sauvegarde, comme par exemple, un stockage monté en réseau.
  • Memantau ketersediaan tujuan cadangan, seperti misalnya tempat penyimpanan jaringan yang terkaitkan.
  • Controllo della disponibilità delle destinazioni della copia di sicurezza, ad esempio delle unità di memorizzazione di rete montate.
  • 마운트된 네트워크 저장소 등 백업 대상의 사용 가능 여부 모니터링.
  • Beschikbaarheid van bestemmingen voor back-ups te monitoren, zoals bijvoorbeeld voor aangekoppelde netwerkopslag.
  • Monitorowanie dostępności miejsc tworzenia kopii zapasowych, takich jak np. podpięte sieciowe urządzenie przechowywania.
  • Monitorização da disponibilidade dos destinos das cópias de segurança, como por exemplo um armazenamento na rede montado.
  • Monitore a disponibilidade dos destinos de backup, como, por exemplo, um armazenamento de rede montado.
  • Nadzoruje razpoložljivost rezervnih ciljev varnostnih kopij, kot je na primer nameščen omrežni disk.
  • Övervakning av säkerhetskopieringsmål, som exempelvis monterad nätverkslagring.
  • Спостереження за доступністю місць зберігання резервних копій, зокрема змонтованих сховищ даних у локальній мережі.
  • xxMonitor availability of backup destinations, like for example a mounted network storage.xx
  • 监控备份目标可用性,比如挂载的网络存储。
  • Integration into KDE's Plasma desktop.
  • Integració amb l'escriptori Plasma del KDE.
  • Integració amb l'escriptori Plasma del KDE.
  • Integration in die KDE-Plasma-Arbeitsfläche.
  • Integration into KDE's Plasma desktop.
  • Integración con el escritorio KDE Plasma.
  • Lõimimist KDE Plasma töölauga.
  • KDEren Plasma mahaigainean bateratzea.
  • Integroinnin KDE:n Plasma-työpöydälle.
  • Intégration avec le bureau Plasma de KDE.
  • Integrasi ke dalam desktop Plasma KDE.
  • Integrazione col desktop Plasma di KDE.
  • KDE Plasma 데스크톱 통합.
  • Integratie in het bureaublad Plasma van KDE.
  • Współpracę z pulpitem KDE Plazmy.
  • Integração na área de trabalho Plasma do KDE.
  • Integração com a área de trabalho Plasma.
  • Integrarea în biroul KDE Plasma.
  • Integracija v namizje KDE Plasma.
  • Integration med KDE:s Plasma-skrivbord.
  • Інтеграція із стільницею Плазми KDE.
  • xxIntegration into KDE's Plasma desktop.xx
  • KDE Plasma 桌面整合。
  • 整合至 KDE Plasma 桌面
kup https://cdn.kde.org/screenshots/kup/kup.png KDE Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> xxSimon Persson <simon.persson@mykolab.com>xx Simon Persson <simon.persson@mykolab.com> Simon Persson <simon.persson@mykolab.com> KDE System
kup-backup-0.9.1/plasmoid/000077500000000000000000000000001420500356400153725ustar00rootroot00000000000000kup-backup-0.9.1/plasmoid/contents/000077500000000000000000000000001420500356400172275ustar00rootroot00000000000000kup-backup-0.9.1/plasmoid/contents/ui/000077500000000000000000000000001420500356400176445ustar00rootroot00000000000000kup-backup-0.9.1/plasmoid/contents/ui/FullRepresentation.qml000066400000000000000000000070511420500356400242070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL import QtQuick 2.2 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras PlasmaComponents.Page { Layout.minimumWidth: units.gridUnit * 12 Layout.minimumHeight: units.gridUnit * 12 header: PlasmaExtras.PlasmoidHeading { visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading) RowLayout { anchors.fill: parent Item { Layout.fillWidth: true } PlasmaComponents.ToolButton { icon.name: "view-refresh" onClicked: plasmoid.action("reloadKup").trigger() PlasmaComponents.ToolTip { text: plasmoid.action("reloadKup").text } } PlasmaComponents.ToolButton { icon.name: "configure" onClicked: plasmoid.action("configure").trigger() PlasmaComponents.ToolTip { text: plasmoid.action("configure").text } } } } Item { anchors.fill: parent anchors.topMargin: units.smallSpacing * 2 focus: true PlasmaExtras.Heading { width: parent.width level: 3 opacity: 0.6 text: getCommonStatus("no plan reason", "") visible: planCount == 0 } ColumnLayout { anchors.fill: parent PlasmaExtras.ScrollArea { Layout.fillWidth: true Layout.fillHeight: true ListView { model: planCount delegate: planDelegate boundsBehavior: Flickable.StopAtBounds } } } Component { id: planDelegate Column { width: parent.width spacing: theme.defaultFont.pointSize RowLayout { width: parent.width Column { Layout.fillWidth: true PlasmaExtras.Heading { level: 3 text: getPlanStatus(index, "description") } PlasmaExtras.Heading { level: 4 text: getPlanStatus(index, "status heading") } PlasmaExtras.Paragraph { text: getPlanStatus(index, "status details") } } PlasmaCore.IconItem { source: getPlanStatus(index, "icon name") Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredWidth: units.iconSizes.huge Layout.preferredHeight: units.iconSizes.huge } } Flow { width: parent.width spacing: theme.defaultFont.pointSize PlasmaComponents.Button { text: i18nd("kup", "Save new backup") visible: getPlanStatus(index, "destination available") && !getPlanStatus(index, "busy") onClicked: startOperation(index, "save backup") } PlasmaComponents.Button { text: i18nd("kup", "Prune old backups") visible: getPlanStatus(index, "bup type") && getPlanStatus(index, "destination available") onClicked: startOperation(index, "remove backups") } PlasmaComponents.Button { text: i18nd("kup", "Show files") visible: getPlanStatus(index, "destination available") onClicked: startOperation(index, "show backup files") } PlasmaComponents.Button { text: i18nd("kup", "Show log file") visible: getPlanStatus(index, "log file exists") onClicked: startOperation(index, "show log file") } } } } } function getPlanStatus(planNumber, key){ return backupPlans.data["plan " + planNumber.toString()][key]; } function startOperation(i, name) { var service = backupPlans.serviceForSource(i.toString()); var operation = service.operationDescription(name); service.startOperationCall(operation); } } kup-backup-0.9.1/plasmoid/contents/ui/Main.qml000066400000000000000000000037111420500356400212450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL import QtQuick 2.0 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.kquickcontrolsaddons 2.0 as KQCAddons Item { Plasmoid.switchWidth: units.gridUnit * 10 Plasmoid.switchHeight: units.gridUnit * 10 Plasmoid.toolTipMainText: getCommonStatus("tooltip title", "Error") Plasmoid.toolTipSubText: getCommonStatus("tooltip subtitle", "No connection") Plasmoid.icon: getCommonStatus("tooltip icon name", "kup") Plasmoid.status: getCommonStatus("tray icon active", false) ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus PlasmaCore.DataSource { id: backupPlans engine: "backups" connectedSources: sources onSourceAdded: { disconnectSource(source); connectSource(source); } onSourceRemoved: { disconnectSource(source); } } function getCommonStatus(key, def){ var result = backupPlans.data["common"][key]; if(result === undefined) { result = def; } return result; } function action_configure() { KQCAddons.KCMShell.openSystemSettings("kcm_kup"); } function action_reloadKup() { var service = backupPlans.serviceForSource("daemon"); var operation = service.operationDescription("reload"); service.startOperationCall(operation); } property int planCount: backupPlans.data["common"]["plan count"] Plasmoid.fullRepresentation: FullRepresentation {} Plasmoid.compactRepresentation: PlasmaCore.IconItem { source: "kup" width: units.iconSizes.medium; height: units.iconSizes.medium; MouseArea { anchors.fill: parent onClicked: plasmoid.expanded = !plasmoid.expanded } } Component.onCompleted: { plasmoid.removeAction("configure"); plasmoid.setAction("configure", i18n("&Configure Kup..."), "configure"); plasmoid.setAction("reloadKup", i18n("&Reload backup plans"), "view-refresh"); } } kup-backup-0.9.1/plasmoid/metadata.desktop000066400000000000000000000050221420500356400205440ustar00rootroot00000000000000[Desktop Entry] Type=Service Icon=kup X-KDE-PluginInfo-Author=Simon Persson X-KDE-PluginInfo-Email=simon.persson@mykolab.com X-KDE-PluginInfo-Name=org.kde.kupapplet X-KDE-PluginInfo-Version=1.0 X-KDE-PluginInfo-Website=https://github.com/spersson/kup X-KDE-PluginInfo-Category=System Information X-KDE-PluginInfo-License=GPL X-KDE-PluginInfo-EnabledByDefault=true X-KDE-ServiceTypes=Plasma/Applet X-Plasma-API=declarativeappletscript X-Plasma-MainScript=ui/Main.qml X-Plasma-NotificationArea=true X-Plasma-NotificationAreaCategory=SystemServices X-Plasma-DBusActivationService=org.kde.kupdaemon Name=Backup Status Name[ca]=Estat de la còpia de seguretat Name[ca@valencia]=Estat de la còpia de seguretat Name[cs]=Stav zálohy Name[de]=Sicherungsstatus Name[en_GB]=Backup Status Name[es]=Estado de la copia de seguridad Name[et]=Varundamise olek Name[eu]=Babes-kopiaren egoera Name[fi]=Varmuuskopion tila Name[fr]=État de sauvegarde Name[it]=Stato della copia di sicurezza Name[ko]=백업 상태 Name[nl]=Status van back-up Name[pl]=Stan kopii zapasowej Name[pt]=Estado da Cópia de Segurança Name[pt_BR]=Status do backup Name[sk]=Stav záloh Name[sl]=Stanje varnostnih kopij Name[sv]=Säkerhetskopieringsstatus Name[uk]=Стан резервного копіювання Name[x-test]=xxBackup Statusxx Name[zh_CN]=备份状态 Name[zh_TW]=備份狀態 Comment=Displays status of backup plans Comment[ca]=Mostra l'estat dels plans per a còpia de seguretat Comment[ca@valencia]=Mostra l'estat dels plans per a còpia de seguretat Comment[de]=Status der Sicherungspläne anzeigen Comment[en_GB]=Displays status of backup plans Comment[es]=Muestra el estado de las planificaciones de las copias de seguridad Comment[et]=Varukoopiakavade oleku näitamine Comment[eu]=Azaldu babes-kopia egitasmoen egoera Comment[fi]=Näyttää varmuuskopiointisuunnitelmien tilan Comment[fr]=Affiche l'état des plans de sauvegarde Comment[it]=Visualizza lo stato dei piani delle copie di sicurezza Comment[ko]=백업 계획 상태 표시 Comment[nl]=Status van back-up-plannen tonen Comment[pl]=Wyświetla stan planów kopii zapasowej Comment[pt]=Mostra o estado dos planos de cópias de segurança Comment[pt_BR]=Mostra o status dos planos de backup Comment[sl]=Prikaže stanje planov varnostnega kopiranja Comment[sv]=Visar status för säkerhetskopieringsplaner Comment[uk]=Показує стан планів резервного копіювання Comment[x-test]=xxDisplays status of backup plansxx Comment[zh_CN]=显示备份计划的状态 Comment[zh_TW]=顯示備份計畫狀態 kup-backup-0.9.1/purger/000077500000000000000000000000001420500356400150665ustar00rootroot00000000000000kup-backup-0.9.1/purger/CMakeLists.txt000066400000000000000000000011211420500356400176210ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2021 Simon Persson # # SPDX-License-Identifier: GPL-2.0-or-later set(purger_SRCS purger.cpp main.cpp ) ecm_qt_declare_logging_category(purger_SRCS HEADER kuppurger_debug.h IDENTIFIER KUPPURGER CATEGORY_NAME kup.purger DEFAULT_SEVERITY Warning EXPORT kup DESCRIPTION "Kup Purger" ) add_executable(kup-purger ${purger_SRCS}) target_link_libraries(kup-purger Qt5::Widgets KF5::I18n KF5::CoreAddons KF5::XmlGui ) ########### install files ############### install(TARGETS kup-purger ${INSTALL_TARGETS_DEFAULT_ARGS}) kup-backup-0.9.1/purger/main.cpp000066400000000000000000000036021420500356400165170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "purger.h" #include #include #include #include #include #include int main(int pArgCount, char **pArgArray) { QApplication lApp(pArgCount, pArgArray); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); KLocalizedString::setApplicationDomain("kup"); KAboutData lAbout(QStringLiteral("kuppurger"), xi18nc("@title", "Purger"), QStringLiteral("0.9.1"), i18n("Purger for bup archives."), KAboutLicense::GPL, i18n("Copyright (C) 2013-2021 Simon Persson")); lAbout.addAuthor(i18n("Simon Persson"), i18n("Maintainer"), "simon.persson@mykolab.com"); lAbout.setTranslator(xi18nc("NAME OF TRANSLATORS", "Your names"), xi18nc("EMAIL OF TRANSLATORS", "Your emails")); KAboutData::setApplicationData(lAbout); //this calls qApp.setApplicationName, setVersion, etc. QCommandLineParser lParser; lParser.addOption(QCommandLineOption(QStringList() << QStringLiteral("b") << QStringLiteral("branch"), i18n("Name of the branch to be opened."), QStringLiteral("branch name"), QStringLiteral("kup"))); lParser.addPositionalArgument(QStringLiteral(""), i18n("Path to the bup repository to be opened.")); lAbout.setupCommandLine(&lParser); lParser.process(lApp); lAbout.processCommandLine(&lParser); QStringList lPosArgs = lParser.positionalArguments(); if(lPosArgs.isEmpty()) { return 1; } QString lRepoPath = QDir(lPosArgs.takeFirst()).absolutePath(); if(lRepoPath.isEmpty()) { return 1; } auto lPurger = new Purger(lRepoPath, lParser.value(QStringLiteral("branch"))); lPurger->show(); return QApplication::exec(); } kup-backup-0.9.1/purger/purger.cpp000066400000000000000000000124611420500356400171020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "purger.h" #include "kuppurger_debug.h" #include #include #include #include #include #include #include #include Purger::Purger(QString pRepoPath, QString pBranchName, QWidget *pParent) : KMainWindow(pParent), mRepoPath(std::move(pRepoPath)), mBranchName(std::move(pBranchName)) { setWindowIcon(QIcon::fromTheme(QStringLiteral("kup"))); KToolBar *lAppToolBar = toolBar(); lAppToolBar->addAction(KStandardAction::quit(this, SLOT(close()), this)); mDeleteAction = KStandardAction::deleteFile(this, SLOT(purge()), this); lAppToolBar->addAction(mDeleteAction); mListWidget = new QListWidget(); auto lSplitter = new QSplitter(); lSplitter->addWidget(mListWidget); mTextEdit = new QTextEdit(); mTextEdit->setReadOnly(true); lSplitter->addWidget(mTextEdit); setCentralWidget(lSplitter); mCollectProcess = new KProcess(); mCollectProcess->setOutputChannelMode(KProcess::SeparateChannels); *mCollectProcess << QStringLiteral("bup"); *mCollectProcess << QStringLiteral("-d") << mRepoPath; *mCollectProcess << QStringLiteral("gc") << QStringLiteral("--unsafe") << QStringLiteral("--verbose"); connect(mCollectProcess, &KProcess::readyReadStandardError, [this] { auto lLogText = QString::fromUtf8(mCollectProcess->readAllStandardError()); qCInfo(KUPPURGER) << lLogText; mTextEdit->append(lLogText); }); connect(mCollectProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(purgeDone(int,QProcess::ExitStatus))); mListProcess = new KProcess(); mListProcess->setOutputChannelMode(KProcess::SeparateChannels); *mListProcess << QStringLiteral("bup"); *mListProcess << QStringLiteral("-d") << mRepoPath; *mListProcess << QStringLiteral("ls") << QStringLiteral("--hash") << mBranchName; connect(mListProcess, &KProcess::readyReadStandardOutput, [this] { KFormat lFormat; const auto lLines = QString::fromUtf8(mListProcess->readAllStandardOutput()).split(QChar::LineFeed); for(const QString &lLine: lLines) { qCDebug(KUPPURGER) << lLine; const auto lHash = lLine.left(40); if(!lHash.isEmpty() && lHash != QStringLiteral("0000000000000000000000000000000000000000")) { const auto lTimeStamp = lLine.mid(41); if(mHashes.contains(lHash)) { auto lItem = mHashes.value(lHash); lItem->setWhatsThis(lItem->whatsThis() + QChar::LineFeed + lTimeStamp); } else { const auto lDateTime = QDateTime::fromString(lTimeStamp, QStringLiteral("yyyy-MM-dd-HHmmss")); const auto lDisplayText = lFormat.formatRelativeDateTime(lDateTime, QLocale::ShortFormat); auto lItem = new QListWidgetItem(lDisplayText, mListWidget); lItem->setWhatsThis(lTimeStamp); //misuse of field, for later use when removing lItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); lItem->setCheckState(Qt::Unchecked); mHashes.insert(lHash, lItem); } } } }); connect(mListProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(listDone(int,QProcess::ExitStatus))); fillListWidget(); } QSize Purger::sizeHint() const { return {800, 600}; } void Purger::fillListWidget() { QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); mListWidget->clear(); mHashes.clear(); mDeleteAction->setEnabled(false); mListProcess->start(); } void Purger::listDone(int, QProcess::ExitStatus) { QGuiApplication::restoreOverrideCursor(); mDeleteAction->setEnabled(true); } void Purger::purge() { qCInfo(KUPPURGER) << "Starting purge operation."; mDeleteAction->setEnabled(false); bool lAnythingRemoved = false; for(int i=0; i < mListWidget->count(); ++i) { auto lItem = mListWidget->item(i); qCInfo(KUPPURGER) << lItem->text() << lItem->whatsThis() << lItem->checkState(); if(lItem->checkState() == Qt::Checked) { const auto lTimeStamps = lItem->whatsThis().split(QChar::LineFeed); for(const QString &lTimeStamp: lTimeStamps) { KProcess lRemoveProcess; lRemoveProcess.setOutputChannelMode(KProcess::SeparateChannels); lRemoveProcess << QStringLiteral("bup"); lRemoveProcess << QStringLiteral("-d") << mRepoPath << QStringLiteral("rm"); lRemoveProcess << QStringLiteral("--unsafe") << QStringLiteral("--verbose"); lRemoveProcess << QString("%1/%2").arg(mBranchName).arg(lTimeStamp); qCInfo(KUPPURGER) << lRemoveProcess.program(); if(lRemoveProcess.execute() == 0) { lAnythingRemoved = true; qCInfo(KUPPURGER) << "Successfully removed snapshot"; } const auto lLogText = QString::fromUtf8(lRemoveProcess.readAllStandardError()); qCInfo(KUPPURGER) << lLogText; mTextEdit->append(lLogText); } } } if(lAnythingRemoved) { QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); qCInfo(KUPPURGER) << mCollectProcess->program(); mCollectProcess->start(); } else { mDeleteAction->setEnabled(true); } } void Purger::purgeDone(int pExitCode, QProcess::ExitStatus pExitStatus) { qCInfo(KUPPURGER) << pExitCode << pExitStatus; QGuiApplication::restoreOverrideCursor(); mDeleteAction->setEnabled(true); const auto lLogText = QString::fromUtf8(mCollectProcess->readAllStandardError()); qCInfo(KUPPURGER) << lLogText; mTextEdit->append(lLogText); fillListWidget(); } kup-backup-0.9.1/purger/purger.h000066400000000000000000000015461420500356400165510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef PURGER_H #define PURGER_H #include #include #include #include #include class Purger : public KMainWindow { Q_OBJECT public: explicit Purger(QString pRepoPath, QString pBranchName, QWidget *pParent = nullptr); QSize sizeHint() const override; protected slots: void fillListWidget(); void listDone(int, QProcess::ExitStatus); void purge(); void purgeDone(int, QProcess::ExitStatus); protected: QListWidget *mListWidget {}; QTextEdit *mTextEdit {}; KProcess *mCollectProcess {}; KProcess *mListProcess {}; QHash mHashes; QAction *mDeleteAction {}; QString mRepoPath; QString mBranchName; }; #endif // PURGER_H kup-backup-0.9.1/settings/000077500000000000000000000000001420500356400154225ustar00rootroot00000000000000kup-backup-0.9.1/settings/backupplan.cpp000066400000000000000000000202231420500356400202450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "backupplan.h" #include "kuputils.h" #include #include #include #include #include #include BackupPlan::BackupPlan(int pPlanNumber, KSharedConfigPtr pConfig, QObject *pParent) :KCoreConfigSkeleton(std::move(pConfig), pParent), mPlanNumber(pPlanNumber) { setCurrentGroup(QString(QStringLiteral("Plan/%1")).arg(mPlanNumber)); addItemString(QStringLiteral("Description"), mDescription, xi18nc("@label Default name for a new backup plan, %1 is the number of the plan in order", "Backup plan %1", pPlanNumber)); QStringList lDefaultIncludeList; lDefaultIncludeList << QDir::homePath(); addItemStringList(QStringLiteral("Paths included"), mPathsIncluded, lDefaultIncludeList); QStringList lDefaultExcludeList; lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.cache"); lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.bup"); lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.thumbnails"); lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/Trash"); lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/baloo"); lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/TelegramDesktop/tdata/temp"); lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.config/Riot/Cache"); QMutableStringListIterator i(lDefaultExcludeList); while(i.hasNext()) { ensureNoTrailingSlash(i.next()); } addItemStringList(QStringLiteral("Paths excluded"), mPathsExcluded, lDefaultExcludeList); addItemInt(QStringLiteral("Backup type"), mBackupType); addItemInt(QStringLiteral("Backup version"), mBackupVersion); addItemInt(QStringLiteral("Schedule type"), mScheduleType, 2); addItemInt(QStringLiteral("Schedule interval"), mScheduleInterval, 1); addItemInt(QStringLiteral("Schedule interval unit"), mScheduleIntervalUnit, 3); addItemInt(QStringLiteral("Usage limit"), mUsageLimit, 25); addItemBool(QStringLiteral("Ask first"), mAskBeforeTakingBackup, true); addItemInt(QStringLiteral("Destination type"), mDestinationType, 1); addItem(new KCoreConfigSkeleton::ItemUrl(currentGroup(), QStringLiteral("Filesystem destination path"), mFilesystemDestinationPath, QUrl::fromLocalFile(QDir::homePath() + QStringLiteral("/.bup")))); addItemString(QStringLiteral("External drive UUID"), mExternalUUID); addItemPath(QStringLiteral("External drive destination path"), mExternalDestinationPath, i18n("Backups")); addItemString(QStringLiteral("External volume label"), mExternalVolumeLabel); addItemULongLong(QStringLiteral("External volume capacity"), mExternalVolumeCapacity); addItemString(QStringLiteral("External device description"), mExternalDeviceDescription); addItemInt(QStringLiteral("External partition number"), mExternalPartitionNumber); addItemInt(QStringLiteral("External partitions count"), mExternalPartitionsOnDrive); addItemBool(QStringLiteral("Show hidden folders"), mShowHiddenFolders); addItemBool(QStringLiteral("Generate recovery info"), mGenerateRecoveryInfo); addItemBool(QStringLiteral("Check backups"), mCheckBackups); addItemBool(QStringLiteral("Exclude patterns"), mExcludePatterns); addItemString(QStringLiteral("Exclude patterns file path"), mExcludePatternsPath); addItemDateTime(QStringLiteral("Last complete backup"), mLastCompleteBackup); addItemDouble(QStringLiteral("Last backup size"), mLastBackupSize); addItemDouble(QStringLiteral("Last available space"), mLastAvailableSpace); addItemUInt(QStringLiteral("Accumulated usage time"), mAccumulatedUsageTime); load(); } void BackupPlan::setPlanNumber(int pPlanNumber) { mPlanNumber = pPlanNumber; QString lGroupName = QString(QStringLiteral("Plan/%1")).arg(mPlanNumber); foreach(KConfigSkeletonItem *lItem, items()) { lItem->setGroup(lGroupName); } } void BackupPlan::removePlanFromConfig() { config()->deleteGroup(QString(QStringLiteral("Plan/%1")).arg(mPlanNumber)); } void BackupPlan::copyFrom(const BackupPlan &pPlan) { mDescription = i18nc("default description of newly duplicated backup plan", "%1 (copy)", pPlan.mDescription); mPathsIncluded = pPlan.mPathsIncluded; mPathsExcluded = pPlan.mPathsExcluded; mBackupType = pPlan.mBackupType; mScheduleType = pPlan.mScheduleType; mScheduleInterval = pPlan.mScheduleInterval; mScheduleIntervalUnit = pPlan.mScheduleIntervalUnit; mUsageLimit = pPlan.mUsageLimit; mAskBeforeTakingBackup = pPlan.mAskBeforeTakingBackup; mDestinationType = pPlan.mDestinationType; mFilesystemDestinationPath = pPlan.mFilesystemDestinationPath; mExternalUUID = pPlan.mExternalUUID; mExternalDestinationPath = pPlan.mExternalDestinationPath; mExternalVolumeLabel = pPlan.mExternalVolumeLabel; mExternalDeviceDescription = pPlan.mExternalDeviceDescription; mExternalPartitionNumber = pPlan.mExternalPartitionNumber; mExternalPartitionsOnDrive = pPlan.mExternalPartitionsOnDrive; mExternalVolumeCapacity = pPlan.mExternalVolumeCapacity; mShowHiddenFolders = pPlan.mShowHiddenFolders; mGenerateRecoveryInfo = pPlan.mGenerateRecoveryInfo; mCheckBackups = pPlan.mCheckBackups; } QDateTime BackupPlan::nextScheduledTime() { Q_ASSERT(mScheduleType == 1); if(!mLastCompleteBackup.isValid()) return QDateTime(); //plan has never been run return mLastCompleteBackup.addSecs(scheduleIntervalInSeconds()); } qint64 BackupPlan::scheduleIntervalInSeconds() { Q_ASSERT(mScheduleType == 1); switch(mScheduleIntervalUnit) { case 0: return mScheduleInterval * 60; case 1: return mScheduleInterval * 60 * 60; case 2: return mScheduleInterval * 60 * 60 * 24; case 3: return mScheduleInterval * 60 * 60 * 24 * 7; default: return 0; } } BackupPlan::Status BackupPlan::backupStatus() { if(!mLastCompleteBackup.isValid()) { return BAD; } if(mScheduleType == MANUAL) { return NO_STATUS; } qint64 lStatus = 5; //trigger BAD status if schedule type is something strange qint64 lInterval = 1; switch(mScheduleType) { case INTERVAL: lStatus = mLastCompleteBackup.secsTo(QDateTime::currentDateTimeUtc()); lInterval = scheduleIntervalInSeconds(); break; case USAGE: lStatus = mAccumulatedUsageTime; lInterval = mUsageLimit * 3600; break; } if(lStatus < lInterval) return GOOD; if(lStatus < lInterval * 3) return MEDIUM; return BAD; } QString BackupPlan::iconName(Status pStatus) { switch(pStatus) { case GOOD: return QStringLiteral("security-high"); case MEDIUM: return QStringLiteral("security-medium"); case BAD: return QStringLiteral("security-low"); case NO_STATUS: break; } return QString(); } QString BackupPlan::absoluteExcludesFilePath() { if(QDir::isRelativePath(mExcludePatternsPath)) { return QDir::homePath() + QDir::separator() + mExcludePatternsPath; } return mExcludePatternsPath; } void BackupPlan::usrRead() { //correct the time spec after default read routines. mLastCompleteBackup.setTimeSpec(Qt::UTC); QMutableStringListIterator lExcludes(mPathsExcluded); while(lExcludes.hasNext()) { ensureNoTrailingSlash(lExcludes.next()); } QMutableStringListIterator lIncludes(mPathsIncluded); while(lIncludes.hasNext()) { ensureNoTrailingSlash(lIncludes.next()); } } QString BackupPlan::statusText() { QLocale lLocale; KFormat lFormat(lLocale); QString lStatus; if(mLastCompleteBackup.isValid()) { QDateTime lLocalTime = mLastCompleteBackup.toLocalTime(); lStatus += i18nc("%1 is fancy formatted date", "Last saved: %1", lFormat.formatRelativeDate(lLocalTime.date(), QLocale::LongFormat)); if(mLastBackupSize > 0.0) { lStatus += '\n'; lStatus += i18nc("%1 is storage size of archive", "Size: %1", lFormat.formatByteSize(mLastBackupSize)); } if(mLastAvailableSpace > 0.0) { lStatus += '\n'; lStatus += i18nc("%1 is free storage space", "Free space: %1", lFormat.formatByteSize(mLastAvailableSpace)); } } else { lStatus = xi18nc("@label", "This backup plan has never been run."); } return lStatus; } kup-backup-0.9.1/settings/backupplan.h000066400000000000000000000036551420500356400177240ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef BACKUPPLAN_H #define BACKUPPLAN_H #include class Schedule; class BackupPlan : public KCoreConfigSkeleton { public: BackupPlan(int pPlanNumber, KSharedConfigPtr pConfig, QObject *pParent = nullptr); int planNumber() const {return mPlanNumber;} virtual void setPlanNumber(int pPlanNumber); QString statusText(); void removePlanFromConfig(); void copyFrom(const BackupPlan &pPlan); QString mDescription; QStringList mPathsIncluded; QStringList mPathsExcluded; enum BackupType {BupType = 0, RsyncType}; qint32 mBackupType{}; qint32 mBackupVersion{}; enum ScheduleType {MANUAL=0, INTERVAL, USAGE}; qint32 mScheduleType{}; qint32 mScheduleInterval{}; qint32 mScheduleIntervalUnit{}; qint32 mUsageLimit{}; // in hours bool mAskBeforeTakingBackup{}; qint32 mDestinationType{}; QUrl mFilesystemDestinationPath; QString mExternalUUID; QString mExternalDestinationPath; QString mExternalVolumeLabel; QString mExternalDeviceDescription; int mExternalPartitionNumber{}; int mExternalPartitionsOnDrive{}; qulonglong mExternalVolumeCapacity{}; bool mShowHiddenFolders{}; bool mGenerateRecoveryInfo{}; bool mCheckBackups{}; bool mExcludePatterns{}; QString mExcludePatternsPath; QDateTime mLastCompleteBackup; // Size of the last backup in bytes. double mLastBackupSize{}; // Last known available space on destination double mLastAvailableSpace{}; // How long has Kup been running since last backup (s) quint32 mAccumulatedUsageTime{}; virtual QDateTime nextScheduledTime(); virtual qint64 scheduleIntervalInSeconds(); enum Status {GOOD, MEDIUM, BAD, NO_STATUS}; Status backupStatus(); static QString iconName(Status pStatus); QString absoluteExcludesFilePath(); protected: void usrRead() override; int mPlanNumber; }; #endif // BACKUPPLAN_H kup-backup-0.9.1/settings/kupsettings.cpp000066400000000000000000000010261420500356400205050ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kupsettings.h" #include #include "backupplan.h" KupSettings::KupSettings(KSharedConfigPtr pConfig, QObject *pParent) : KCoreConfigSkeleton(std::move(pConfig), pParent) { setCurrentGroup(QStringLiteral("Kup settings")); addItemBool(QStringLiteral("Backups enabled"), mBackupsEnabled); addItemInt(QStringLiteral("Number of backups"), mNumberOfPlans, 0); } kup-backup-0.9.1/settings/kupsettings.h000066400000000000000000000010421420500356400201500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPSETTINGS_H #define KUPSETTINGS_H #include #include class KupSettings : public KCoreConfigSkeleton { Q_OBJECT public: explicit KupSettings(KSharedConfigPtr pConfig, QObject *pParent = nullptr); // Common enable of backup plans bool mBackupsEnabled{}; // Number of backup plans configured int mNumberOfPlans{}; }; #endif // KUPSETTINGS_H kup-backup-0.9.1/settings/kuputils.cpp000066400000000000000000000010521420500356400200040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "kuputils.h" #include void ensureTrailingSlash(QString &pPath) { if(!pPath.endsWith(QDir::separator())) { pPath.append(QDir::separator()); } } void ensureNoTrailingSlash(QString &pPath) { while(pPath.endsWith(QDir::separator())) { pPath.chop(1); } } QString lastPartOfPath(const QString &pPath) { return pPath.section(QDir::separator(), -1, -1, QString::SectionSkipEmpty); } kup-backup-0.9.1/settings/kuputils.h000066400000000000000000000005511420500356400174540ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Simon Persson // // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #ifndef KUPUTILS_H #define KUPUTILS_H class QString; void ensureTrailingSlash(QString &pPath); void ensureNoTrailingSlash(QString &pPath); QString lastPartOfPath(const QString &pPath); #endif // KUPUTILS_H