pax_global_header00006660000000000000000000000064144073043170014515gustar00rootroot0000000000000052 comment=f750259951a528e422d575943f7584bb17787ed6 rsbackup-10.0/000077500000000000000000000000001440730431700132475ustar00rootroot00000000000000rsbackup-10.0/.clang-format000066400000000000000000000066061440730431700156320ustar00rootroot00000000000000# We require clang-source 6.0 or later. # See also src/check-source.in. --- Language: Cpp AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlines: Right AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: MultiLine BinPackArguments: true BinPackParameters: true BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false AfterStruct: false AfterUnion: false AfterExternBlock: false BeforeCatch: false BeforeElse: false IndentBraces: false SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Attach BreakBeforeInheritanceComma: false BreakInheritanceList: AfterColon BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: AfterColon BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true ColumnLimit: 80 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: true ForEachMacros: - foreach - Q_FOREACH - BOOST_FOREACH IncludeBlocks: Preserve IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 - Regex: '^(<|"(gtest|gmock|isl|json)/)' Priority: 3 - Regex: '.*' Priority: 1 IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: false IndentPPDirectives: None IndentWidth: 2 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: true MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None ObjCBinPackProtocolList: Auto ObjCBlockIndentWidth: 2 ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 19 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Right ReflowComments: true SortIncludes: false SortUsingDeclarations: true SpaceAfterCStyleCast: false SpaceAfterTemplateKeyword: true SpaceBeforeAssignmentOperators: true SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: false SpaceBeforeInheritanceColon: false SpaceBeforeParens: Never SpaceBeforeRangeBasedForLoopColon: false SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp11 TabWidth: 8 UseTab: Never ... rsbackup-10.0/.dir-locals.el000066400000000000000000000003161440730431700157000ustar00rootroot00000000000000;;; Directory Local Variables ;;; See Info node `(emacs) Directory Variables' for more information. ((c++-mode (indent-tabs-mode) (fill-column . 79) (comment-column . 40) (c-basic-offset . 2))) rsbackup-10.0/.gitignore000066400000000000000000000022771440730431700152470ustar00rootroot00000000000000debian/files debian/debhelper.log debian/rsbackup debian/rsbackup-graph debian/substvars *.tar.gz *.deb *~ Makefile.in Makefile /INSTALL aclocal.m4 autom4te.cache config.aux config.h config.h.in config.log config.status /configure *.[oa] *.o.tmp .coverity .deps .gdb_history stamp-h1 src/rsbackup src/rsbackup-graph src/test-action src/test-base64 src/test-check src/test-color src/test-command src/test-confbase src/test-database src/test-date src/test-device src/test-directory src/test-eventloop src/test-globfiles src/test-host src/test-indent src/test-io src/test-lock src/test-namelt src/test-parsefloat src/test-parseinteger src/test-parsetimeinterval src/test-progress src/test-prunedecay src/test-regexp src/test-select src/test-split src/test-subprocess src/test-timespec src/test-tolines src/test-unicode src/test-volume src/check-source tests/w-* doc/*.1.html doc/*.5.html doc/rsbackup-manual.html doc/CHANGES.html doc/README.html debian/substvars.* tools/rsbackup.cron tools/rsbackup-mount tools/rsbackup-snapshot-hook tools/rsbackup.daily tools/rsbackup.hourly tools/rsbackup.monthly tools/rsbackup.weekly products doc/internal *.gcno *.gcda *.gcov GPATH GRTAGS GSYMS GTAGS *.trs *.log *.tgz /.vscode rsbackup-10.0/.travis.yml000077500000000000000000000011321440730431700153600ustar00rootroot00000000000000os: - linux dist: xenial addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-6 - g++-6 - lynx - sqlite3 - libsqlite3-dev - libboost-system-dev - libboost-filesystem-dev - libboost-dev - pkg-config - libpangomm-1.4-dev - libcairomm-1.0-dev - python-xattr - acl language: cpp compiler: - gcc - clang install: - if test -e scripts/travis/$TRAVIS_OS_NAME/$CC/install; then scripts/travis/$TRAVIS_OS_NAME/$CC/install; fi script: - autoreconf -si - scripts/travis/$TRAVIS_OS_NAME/$CC/configure - make - VERBOSE=true make check rsbackup-10.0/COPYING000066400000000000000000001045131440730431700143060ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . rsbackup-10.0/Doxyfile000066400000000000000000002007231440730431700147610ustar00rootroot00000000000000# Doxyfile 1.7.1 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project # # All text after a hash (#) is considered a comment and will be ignored # The format is: # TAG = value [value, ...] # For lists items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (" ") #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file # that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See # http://www.gnu.org/software/libiconv for the list of possible encodings. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or a sequence of words surrounded # by quotes) that should identify the project. PROJECT_NAME = rsbackup # The PROJECT_NUMBER tag can be used to enter a project or revision number. # This could be handy for archiving the generated documentation or # if some version control system is used. PROJECT_NUMBER = # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. # If a relative path is entered, it will be relative to the location # where doxygen was started. If left blank the current directory will be used. OUTPUT_DIRECTORY = doc/internal # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create # 4096 sub-directories (in 2 levels) under the output directory of each output # format and will distribute the generated files over these directories. # Enabling this option can be useful when feeding doxygen a huge amount of # source files, where putting all generated files in the same directory would # otherwise cause performance problems for the file system. CREATE_SUBDIRS = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # The default language is English, other supported languages are: # Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, # Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, # Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English # messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, # Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, # Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will # include brief member descriptions after the members that are listed in # the file and class documentation (similar to JavaDoc). # Set to NO to disable this. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend # the brief description of a member or function before the detailed description. # Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator # that is used to form the text in various listings. Each string # in this list, if found as the leading text of the brief description, will be # stripped from the text and the result after processing the whole list, is # used as the annotated text. Otherwise, the brief description is used as-is. # If left blank, the following values are used ("$name" is automatically # replaced with the name of the entity): "The $name class" "The $name widget" # "The $name file" "is" "provides" "specifies" "contains" # "represents" "a" "an" "the" ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # Doxygen will generate a detailed section even if there is only a brief # description. ALWAYS_DETAILED_SEC = YES # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full # path before files name in the file list and in the header files. If set # to NO the shortest path that makes the file name unique will be used. FULL_PATH_NAMES = YES # If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag # can be used to strip a user-defined part of the path. Stripping is # only done if one of the specified strings matches the left-hand part of # the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the # path to strip. STRIP_FROM_PATH = # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of # the path mentioned in the documentation of a class, which tells # the reader which header file to include in order to use a class. # If left blank only the name of the header file containing the class # definition is used. Otherwise one should specify the include paths that # are normally passed to the compiler using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter # (but less readable) file names. This can be useful is your file systems # doesn't support long names like on DOS, Mac, or CD-ROM. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen # will interpret the first line (until the first dot) of a JavaDoc-style # comment as the brief description. If set to NO, the JavaDoc # comments will behave just like regular Qt-style comments # (thus requiring an explicit @brief command for a brief description.) JAVADOC_AUTOBRIEF = NO # If the QT_AUTOBRIEF tag is set to YES then Doxygen will # interpret the first line (until the first dot) of a Qt-style # comment as the brief description. If set to NO, the comments # will behave just like regular Qt-style comments (thus requiring # an explicit \brief command for a brief description.) QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen # treat a multi-line C++ special comment block (i.e. a block of //! or /// # comments) as a brief description. This used to be the default behaviour. # The new default is to treat a multi-line C++ comment block as a detailed # description. Set this tag to YES if you prefer the old behaviour instead. MULTILINE_CPP_IS_BRIEF = NO # If the INHERIT_DOCS tag is set to YES (the default) then an undocumented # member inherits the documentation from any documented member that it # re-implements. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce # a new page for each member. If set to NO, the documentation of a member will # be part of the file/class/namespace that contains it. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. # Doxygen uses this value to replace tabs by spaces in code fragments. TAB_SIZE = 8 # This tag can be used to specify a number of aliases that acts # as commands in the documentation. An alias has the form "name=value". # For example adding "sideeffect=\par Side Effects:\n" will allow you to # put the command \sideeffect (or @sideeffect) in the documentation, which # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. ALIASES = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C # sources only. Doxygen will then generate output that is more tailored for C. # For instance, some of the names that are used will be different. The list # of all members will be omitted, etc. OPTIMIZE_OUTPUT_FOR_C = NO # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java # sources only. Doxygen will then generate output that is more tailored for # Java. For instance, namespaces will be presented as packages, qualified # scopes will look different, etc. OPTIMIZE_OUTPUT_JAVA = NO # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources only. Doxygen will then generate output that is more tailored for # Fortran. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for # VHDL. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given extension. # Doxygen has a built-in mapping, but you can override or extend it using this # tag. The format is ext=language, where ext is a file extension, and language # is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C, # C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make # doxygen treat .inc files as Fortran files (default is PHP), and .f files as C # (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions # you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. EXTENSION_MAPPING = # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should # set this tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); v.s. # func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. # Doxygen will parse them like normal C++ but will assume all classes use public # instead of private inheritance when no explicit protection keyword is present. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate getter # and setter methods for a property. Setting this option to YES (the default) # will make doxygen to replace the get and set methods by a property in the # documentation. This will only work if the methods are indeed getting or # setting a simple type. If this is not the case, or you want to show the # methods anyway, you should set this option to NO. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. DISTRIBUTE_GROUP_DOC = NO # Set the SUBGROUPING tag to YES (the default) to allow class member groups of # the same type (for instance a group of public functions) to be put as a # subgroup of that type (e.g. under the Public Functions section). Set it to # NO to prevent subgrouping. Alternatively, this can be done per class using # the \nosubgrouping command. SUBGROUPING = YES # When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum # is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically # be useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. TYPEDEF_HIDES_STRUCT = NO #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. # Private class members and static file members will be hidden unless # the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES EXTRACT_ALL = NO # If the EXTRACT_PRIVATE tag is set to YES all private members of a class # will be included in the documentation. EXTRACT_PRIVATE = YES # If the EXTRACT_STATIC tag is set to YES all static members of a file # will be included in the documentation. EXTRACT_STATIC = YES # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) # defined locally in source files will be included in the documentation. # If set to NO only classes defined in header files are included. EXTRACT_LOCAL_CLASSES = YES # This flag is only useful for Objective-C code. When set to YES local # methods, which are defined in the implementation section but not in # the interface are included in the documentation. # If set to NO (the default) only methods in the interface are included. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base # name of the file that contains the anonymous namespace. By default # anonymous namespace are hidden. EXTRACT_ANON_NSPACES = NO # If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members of documented classes, files or namespaces. # If set to NO (the default) these members will be included in the # various overviews, but no documentation section is generated. # This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. # If set to NO (the default) these classes will be included in the various # overviews. This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all # friend (class|struct|union) declarations. # If set to NO (the default) these declarations will be included in the # documentation. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. # If set to NO (the default) these blocks will be appended to the # function's detailed documentation block. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation # that is typed after a \internal command is included. If the tag is set # to NO (the default) then the documentation will be excluded. # Set it to YES to include the internal documentation. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate # file names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen # will show members with their full class and namespace scopes in the # documentation. If set to YES the scope will be hidden. HIDE_SCOPE_NAMES = NO # If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen # will put a list of the files that are included by a file in the documentation # of that file. SHOW_INCLUDE_FILES = YES # If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen # will list include files with double quotes in the documentation # rather than with sharp brackets. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES (the default) then a tag [inline] # is inserted in the documentation for inline members. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen # will sort the (detailed) documentation of file and class members # alphabetically by member name. If set to NO the members will appear in # declaration order. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the # brief documentation of file, namespace and class members alphabetically # by member name. If set to NO (the default) the members will appear in # declaration order. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen # will sort the (brief and detailed) documentation of class members so that # constructors and destructors are listed first. If set to NO (the default) # the constructors will appear in the respective orders defined by # SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. # This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO # and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. SORT_MEMBERS_CTORS_1ST = NO # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the # hierarchy of group names into alphabetical order. If set to NO (the default) # the group names will appear in their defined order. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to # NO (the default), the class list will be sorted only by class name, # not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the # alphabetical list. SORT_BY_SCOPE_NAME = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or # disable (NO) the todo list. This list is created by putting \todo # commands in the documentation. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or # disable (NO) the test list. This list is created by putting \test # commands in the documentation. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable (YES) or # disable (NO) the bug list. This list is created by putting \bug # commands in the documentation. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or # disable (NO) the deprecated list. This list is created by putting # \deprecated commands in the documentation. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional # documentation sections, marked by \if sectionname ... \endif. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines # the initial value of a variable or define consists of for it to appear in # the documentation. If the initializer consists of more lines than specified # here it will be hidden. Use a value of 0 to hide initializers completely. # The appearance of the initializer of individual variables and defines in the # documentation can be controlled using \showinitializer or \hideinitializer # command in the documentation regardless of this setting. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated # at the bottom of the documentation of classes and structs. If set to YES the # list will mention the files that were used to generate the documentation. SHOW_USED_FILES = YES # Set the SHOW_FILES tag to NO to disable the generation of the Files page. # This will remove the Files entry from the Quick Index and from the # Folder Tree View (if specified). The default is YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the # Namespaces page. # This will remove the Namespaces entry from the Quick Index # and from the Folder Tree View (if specified). The default is YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command , where is the value of # the FILE_VERSION_FILTER tag, and is the name of an input file # provided by doxygen. Whatever the program writes to standard output # is used as the file version. See the manual for examples. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed # by doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. The create the layout file # that represents doxygen's defaults, run doxygen with the -l option. # You can optionally specify a file name after the option, if omitted # DoxygenLayout.xml will be used as the name of the layout file. LAYOUT_FILE = #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated # by doxygen. Possible values are YES and NO. If left blank NO is used. QUIET = YES # The WARNINGS tag can be used to turn on/off the warning messages that are # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. WARNINGS = YES # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. WARN_IF_UNDOCUMENTED = YES # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. WARN_IF_DOC_ERROR = YES # This WARN_NO_PARAMDOC option can be abled to get warnings for # functions that are documented, but have no documentation for their parameters # or return value. If set to NO (the default) doxygen will only warn about # wrong or incomplete parameter documentation, but not about the absence of # documentation. WARN_NO_PARAMDOC = NO # The WARN_FORMAT tag determines the format of the warning messages that # doxygen can produce. The string should contain the $file, $line, and $text # tags, which will be replaced by the file and line number from which the # warning originated and the warning text. Optionally the format may contain # $version, which will be replaced by the version of the file (if it could # be obtained via FILE_VERSION_FILTER) WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning # and error messages should be written. If left blank the output is written # to stderr. WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag can be used to specify the files and/or directories that contain # documented source files. You may enter file names like "myfile.cpp" or # directories like "/usr/src/myproject". Separate the files or directories # with spaces. INPUT = src # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # also the default input encoding. Doxygen uses libiconv (or the iconv built # into libc) for the transcoding. See http://www.gnu.org/software/libiconv for # the list of possible encodings. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank the following patterns are tested: # *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx # *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 FILE_PATTERNS = # The RECURSIVE tag can be used to turn specify whether or not subdirectories # should be searched for input files as well. Possible values are YES and NO. # If left blank NO is used. RECURSIVE = NO # The EXCLUDE tag can be used to specify files and/or directories that should # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used select whether or not files or # directories that are symbolic links (a Unix filesystem feature) are excluded # from the input. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. Note that the wildcards are matched # against the file with absolute path, so to exclude all test directories # for example use the pattern */test/* EXCLUDE_PATTERNS = # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or # directories that contain example code fragments that are included (see # the \include command). EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank all files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude # commands irrespective of the value of the RECURSIVE tag. # Possible values are YES and NO. If left blank NO is used. EXAMPLE_RECURSIVE = NO # The IMAGE_PATH tag can be used to specify one or more files or # directories that contain image that are included in the documentation (see # the \image command). IMAGE_PATH = # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command , where # is the value of the INPUT_FILTER tag, and is the name of an # input file. Doxygen will then use the output that the filter program writes # to standard output. # If FILTER_PATTERNS is specified, this tag will be # ignored. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. # Doxygen will compare the file name with each pattern and apply the # filter if there is a match. # The filters are a list of the form: # pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further # info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER # is applied to all files. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will be used to filter the input files when producing source # files to browse (i.e. when SOURCE_BROWSER is set to YES). FILTER_SOURCE_FILES = NO #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will # be generated. Documented entities will be cross-referenced with these sources. # Note: To get rid of all source code in the generated output, make sure also # VERBATIM_HEADERS is set to NO. SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body # of functions and classes directly in the documentation. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct # doxygen to hide any special comment blocks from generated source code # fragments. Normal C and C++ comments will always remain visible. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES # then for each documented function all documented # functions referencing it will be listed. REFERENCED_BY_RELATION = NO # If the REFERENCES_RELATION tag is set to YES # then for each documented function all documented entities # called/used by that function will be listed. REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES (the default) # and SOURCE_BROWSER tag is set to YES, then the hyperlinks from # functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will # link to the source code. # Otherwise they will link to the documentation. REFERENCES_LINK_SOURCE = YES # If the USE_HTAGS tag is set to YES then the references to source code # will point to the HTML generated by the htags(1) tool instead of doxygen # built-in source browser. The htags tool is part of GNU's global source # tagging system (see http://www.gnu.org/software/global/global.html). You # will need version 4.8.6 or higher. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen # will generate a verbatim copy of the header file for each class for # which an include is specified. Set to NO to disable this. VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index # of all compounds will be generated. Enable this if the project # contains a lot of classes, structs, unions or interfaces. ALPHABETICAL_INDEX = YES # If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then # the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns # in which this list will be split (can be a number in the range [1..20]) COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all # classes will be put under the same header in the alphabetical index. # The IGNORE_PREFIX tag can be used to specify one or more prefixes that # should be ignored while generating the index headers. IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES (the default) Doxygen will # generate HTML output. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `html' will be used as the default path. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for # each generated HTML page (for example: .htm,.php,.asp). If it is left blank # doxygen will generate files with .html extension. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a personal HTML header for # each generated HTML page. If it is left blank doxygen will generate a # standard header. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a personal HTML footer for # each generated HTML page. If it is left blank doxygen will generate a # standard footer. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading # style sheet that is used by each HTML page. It can be used to # fine-tune the look of the HTML output. If the tag is left blank doxygen # will generate a default style sheet. Note that doxygen will try to copy # the style sheet file to the HTML output directory, so don't put your own # stylesheet in the HTML output directory as well, or it will be erased! HTML_STYLESHEET = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. # Doxygen will adjust the colors in the stylesheet and background images # according to this color. Hue is specified as an angle on a colorwheel, # see http://en.wikipedia.org/wiki/Hue for more information. # For instance the value 0 represents red, 60 is yellow, 120 is green, # 180 is cyan, 240 is blue, 300 purple, and 360 is red again. # The allowed range is 0 to 359. HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of # the colors in the HTML output. For a value of 0 the output will use # grayscales only. A value of 255 will produce the most vivid colors. HTML_COLORSTYLE_SAT = 100 # The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to # the luminance component of the colors in the HTML output. Values below # 100 gradually make the output lighter, whereas values above 100 make # the output darker. The value divided by 100 is the actual gamma applied, # so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, # and 100 does not change the gamma. HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. HTML_TIMESTAMP = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. For this to work a browser that supports # JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox # Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). HTML_DYNAMIC_SECTIONS = NO # If the GENERATE_DOCSET tag is set to YES, additional index files # will be generated that can be used as input for Apple's Xcode 3 # integrated development environment, introduced with OSX 10.5 (Leopard). # To create a documentation set, doxygen will generate a Makefile in the # HTML output directory. Running make will produce the docset in that # directory and running "make install" will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find # it at startup. # See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html # for more information. GENERATE_DOCSET = NO # When GENERATE_DOCSET tag is set to YES, this tag determines the name of the # feed. A documentation feed provides an umbrella under which multiple # documentation sets from a single provider (such as a company or product suite) # can be grouped. DOCSET_FEEDNAME = "Doxygen generated docs" # When GENERATE_DOCSET tag is set to YES, this tag specifies a string that # should uniquely identify the documentation set bundle. This should be a # reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen # will append .docset to the name. DOCSET_BUNDLE_ID = org.doxygen.Project # When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify # the documentation publisher. This should be a reverse domain-name style # string, e.g. com.mycompany.MyDocSet.documentation. DOCSET_PUBLISHER_ID = org.doxygen.Publisher # The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES, additional index files # will be generated that can be used as input for tools like the # Microsoft HTML help workshop to generate a compiled HTML help file (.chm) # of the generated HTML documentation. GENERATE_HTMLHELP = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can # be used to specify the file name of the resulting .chm file. You # can add a path in front of the file if the result should not be # written to the html output directory. CHM_FILE = # If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can # be used to specify the location (absolute path including file name) of # the HTML help compiler (hhc.exe). If non-empty doxygen will try to run # the HTML help compiler on the generated index.hhp. HHC_LOCATION = # If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag # controls if a separate .chi index file is generated (YES) or that # it should be included in the master .chm file (NO). GENERATE_CHI = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING # is used to encode HtmlHelp index (hhk), content (hhc) and project file # content. CHM_INDEX_ENCODING = # If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag # controls whether a binary table of contents is generated (YES) or a # normal table of contents (NO) in the .chm file. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members # to the contents of the HTML help documentation and to the tree view. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated # that can be used as input for Qt's qhelpgenerator to generate a # Qt Compressed Help (.qch) of the generated HTML documentation. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can # be used to specify the file name of the resulting .qch file. # The path specified is relative to the HTML output folder. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#namespace QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#virtual-folders QHP_VIRTUAL_FOLDER = doc # If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to # add. For more information please see # http://doc.trolltech.com/qthelpproject.html#custom-filters QHP_CUST_FILTER_NAME = # The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see # # Qt Help Project / Custom Filters. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's # filter section matches. # # Qt Help Project / Filter Attributes. QHP_SECT_FILTER_ATTRS = # If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can # be used to specify the location of Qt's qhelpgenerator. # If non-empty doxygen will try to run qhelpgenerator on the generated # .qhp file. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files # will be generated, which together with the HTML files, form an Eclipse help # plugin. To install this plugin and make it available under the help contents # menu in Eclipse, the contents of the directory containing the HTML and XML # files needs to be copied into the plugins directory of eclipse. The name of # the directory within the plugins directory should be the same as # the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before # the help appears. GENERATE_ECLIPSEHELP = NO # A unique identifier for the eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have # this name. ECLIPSE_DOC_ID = org.doxygen.Project # The DISABLE_INDEX tag can be used to turn on/off the condensed index at # top of each HTML page. The value NO (the default) enables the index and # the value YES disables it. DISABLE_INDEX = NO # This tag can be used to set the number of enum values (range [1..20]) # that doxygen will group on one line in the generated HTML documentation. ENUM_VALUES_PER_LINE = 4 # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. # If the tag value is set to YES, a side panel will be generated # containing a tree-like index structure (just like the one that # is generated for HTML Help). For this to work a browser that supports # JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). # Windows users are probably better off using the HTML help feature. GENERATE_TREEVIEW = NO # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # used to set the initial width (in pixels) of the frame in which the tree # is shown. TREEVIEW_WIDTH = 250 # When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open # links to external symbols imported via tag files in a separate window. EXT_LINKS_IN_WINDOW = NO # Use this tag to change the font size of Latex formulas included # as images in the HTML documentation. The default is 10. Note that # when you change the font size after a successful doxygen run you need # to manually remove any form_*.png images from the HTML output directory # to force them to be regenerated. FORMULA_FONTSIZE = 10 # Use the FORMULA_TRANPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are # not supported properly for IE 6.0, but are supported on all modern browsers. # Note that when changing this option you need to delete any form_*.png files # in the HTML output before the changes have effect. FORMULA_TRANSPARENT = YES # When the SEARCHENGINE tag is enabled doxygen will generate a search box # for the HTML output. The underlying search engine uses javascript # and DHTML and should work on any modern browser. Note that when using # HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets # (GENERATE_DOCSET) there is already a search function so this one should # typically be disabled. For large projects the javascript based search engine # can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be # implemented using a PHP enabled web server instead of at the web client # using Javascript. Doxygen will generate the search PHP script and index # file to put on the web server. The advantage of the server # based approach is that it scales better to large projects and allows # full text search. The disadvances is that it is more difficult to setup # and does not have live searching capabilities. SERVER_BASED_SEARCH = NO #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- # If the GENERATE_LATEX tag is set to YES (the default) Doxygen will # generate Latex output. GENERATE_LATEX = NO # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `latex' will be used as the default path. LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. If left blank `latex' will be used as the default command name. # Note that when enabling USE_PDFLATEX this option is only used for # generating bitmaps for formulas in the HTML output, but not in the # Makefile that is written to the output directory. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to # generate index for LaTeX. If left blank `makeindex' will be used as the # default command name. MAKEINDEX_CMD_NAME = makeindex # If the COMPACT_LATEX tag is set to YES Doxygen generates more compact # LaTeX documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_LATEX = NO # The PAPER_TYPE tag can be used to set the paper type that is used # by the printer. Possible values are: a4, a4wide, letter, legal and # executive. If left blank a4wide will be used. PAPER_TYPE = a4wide # The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX # packages that should be included in the LaTeX output. EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a personal LaTeX header for # the generated latex document. The header should contain everything until # the first chapter. If it is left blank doxygen will generate a # standard header. Notice: only use this tag if you know what you are doing! LATEX_HEADER = # If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated # is prepared for conversion to pdf (using ps2pdf). The pdf file will # contain links (just like the HTML output) instead of page references # This makes the output suitable for online browsing using a pdf viewer. PDF_HYPERLINKS = YES # If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of # plain latex in the generated Makefile. Set this option to YES to get a # higher quality PDF documentation. USE_PDFLATEX = YES # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. # command to the generated LaTeX files. This will instruct LaTeX to keep # running if errors occur, instead of asking the user for help. # This option is also used when generating formulas in HTML. LATEX_BATCHMODE = NO # If LATEX_HIDE_INDICES is set to YES then doxygen will not # include the index chapters (such as File Index, Compound Index, etc.) # in the output. LATEX_HIDE_INDICES = NO # If LATEX_SOURCE_CODE is set to YES then doxygen will include # source code with syntax highlighting in the LaTeX output. # Note that which sources are shown also depends on other settings # such as SOURCE_BROWSER. LATEX_SOURCE_CODE = NO #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- # If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output # The RTF output is optimized for Word 97 and may not look very pretty with # other RTF readers or editors. GENERATE_RTF = NO # The RTF_OUTPUT tag is used to specify where the RTF docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `rtf' will be used as the default path. RTF_OUTPUT = rtf # If the COMPACT_RTF tag is set to YES Doxygen generates more compact # RTF documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_RTF = NO # If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated # will contain hyperlink fields. The RTF file will # contain links (just like the HTML output) instead of page references. # This makes the output suitable for online browsing using WORD or other # programs which support those fields. # Note: wordpad (write) and others do not support links. RTF_HYPERLINKS = NO # Load stylesheet definitions from file. Syntax is similar to doxygen's # config file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an rtf document. # Syntax is similar to doxygen's config file. RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- # If the GENERATE_MAN tag is set to YES (the default) Doxygen will # generate man pages GENERATE_MAN = NO # The MAN_OUTPUT tag is used to specify where the man pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `man' will be used as the default path. MAN_OUTPUT = man # The MAN_EXTENSION tag determines the extension that is added to # the generated man pages (default is the subroutine's section .3) MAN_EXTENSION = .3 # If the MAN_LINKS tag is set to YES and Doxygen generates man output, # then it will generate one additional man file for each entity # documented in the real man page(s). These additional files # only source the real man page, but without them the man command # would be unable to find the correct page. The default is NO. MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- # If the GENERATE_XML tag is set to YES Doxygen will # generate an XML file that captures the structure of # the code including all documentation. GENERATE_XML = NO # The XML_OUTPUT tag is used to specify where the XML pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `xml' will be used as the default path. XML_OUTPUT = xml # If the XML_PROGRAMLISTING tag is set to YES Doxygen will # dump the program listings (including syntax highlighting # and cross-referencing information) to the XML output. Note that # enabling this will significantly increase the size of the XML output. XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will # generate an AutoGen Definitions (see autogen.sf.net) file # that captures the structure of the code including all # documentation. Note that this feature is still experimental # and incomplete at the moment. GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- # If the GENERATE_PERLMOD tag is set to YES Doxygen will # generate a Perl module file that captures the structure of # the code including all documentation. Note that this # feature is still experimental and incomplete at the # moment. GENERATE_PERLMOD = NO # If the PERLMOD_LATEX tag is set to YES Doxygen will generate # the necessary Makefile rules, Perl scripts and LaTeX code to be able # to generate PDF and DVI output from the Perl module output. PERLMOD_LATEX = NO # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # nicely formatted so it can be parsed by a human reader. # This is useful # if you want to understand what is going on. # On the other hand, if this # tag is set to NO the size of the Perl module output will be much smaller # and Perl will parse it just the same. PERLMOD_PRETTY = YES # The names of the make variables in the generated doxyrules.make file # are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. # This is useful so different doxyrules.make files included by the same # Makefile don't overwrite each other's variables. PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- # If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will # evaluate all C-preprocessor directives found in the sources and include # files. ENABLE_PREPROCESSING = YES # If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro # names in the source code. If set to NO (the default) only conditional # compilation will be performed. Macro expansion can be done in a controlled # way by setting EXPAND_ONLY_PREDEF to YES. MACRO_EXPANSION = NO # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES # then the macro expansion is limited to the macros specified with the # PREDEFINED and EXPAND_AS_DEFINED tags. EXPAND_ONLY_PREDEF = NO # If the SEARCH_INCLUDES tag is set to YES (the default) the includes files # in the INCLUDE_PATH (see below) will be search if a #include is found. SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by # the preprocessor. INCLUDE_PATH = # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the # directories. If left blank, the patterns specified with FILE_PATTERNS will # be used. INCLUDE_FILE_PATTERNS = # The PREDEFINED tag can be used to specify one or more macro names that # are defined before the preprocessor is started (similar to the -D option of # gcc). The argument of the tag is a list of macros of the form: name # or name=definition (no spaces). If the definition and the = are # omitted =1 is assumed. To prevent a macro definition from being # undefined via #undef or recursively expanded use the := operator # instead of the = operator. PREDEFINED = # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then # this tag can be used to specify a list of macro names that should be expanded. # The macro definition that is found in the sources will be used. # Use the PREDEFINED tag if you want to use a different macro definition. EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then # doxygen's preprocessor will remove all function-like macros that are alone # on a line, have an all uppercase name, and do not end with a semicolon. Such # function macros are typically used for boiler-plate code, and will confuse # the parser if not removed. SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- # The TAGFILES option can be used to specify one or more tagfiles. # Optionally an initial location of the external documentation # can be added for each tagfile. The format of a tag file without # this location is as follows: # # TAGFILES = file1 file2 ... # Adding location for the tag files is done as follows: # # TAGFILES = file1=loc1 "file2 = loc2" ... # where "loc1" and "loc2" can be relative or absolute paths or # URLs. If a location is present for each tag, the installdox tool # does not have to be run to correct the links. # Note that each tag file must have a unique name # (where the name does NOT include the path) # If a tag file is not located in the directory in which doxygen # is run, you must also specify the path to the tagfile here. TAGFILES = # When a file name is specified after GENERATE_TAGFILE, doxygen will create # a tag file that is based on the input files it reads. GENERATE_TAGFILE = # If the ALLEXTERNALS tag is set to YES all external classes will be listed # in the class index. If set to NO only the inherited external classes # will be listed. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed # in the modules index. If set to NO, only the current project's groups will # be listed. EXTERNAL_GROUPS = YES # The PERL_PATH should be the absolute path and name of the perl script # interpreter (i.e. the result of `which perl'). PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- # If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will # generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base # or super classes. Setting the tag to NO turns the diagrams off. Note that # this option is superseded by the HAVE_DOT option below. This is only a # fallback. It is recommended to install and use dot, since it yields more # powerful graphs. CLASS_DIAGRAMS = YES # You can define message sequence charts within doxygen comments using the \msc # command. Doxygen will then run the mscgen tool (see # http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the # documentation. The MSCGEN_PATH tag allows you to specify the directory where # the mscgen tool resides. If left empty the tool is assumed to be found in the # default search path. MSCGEN_PATH = # If set to YES, the inheritance and collaboration graphs will hide # inheritance and usage relations if the target is undocumented # or is not a class. HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz, a graph visualization # toolkit from AT&T and Lucent Bell Labs. The other options in this section # have no effect if this option is set to NO (the default) HAVE_DOT = NO # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is # allowed to run in parallel. When set to 0 (the default) doxygen will # base this on the number of processors available in the system. You can set it # explicitly to a value larger than 0 to get control over the balance # between CPU load and processing speed. DOT_NUM_THREADS = 0 # By default doxygen will write a font called FreeSans.ttf to the output # directory and reference it in all dot files that doxygen generates. This # font does not include all possible unicode characters however, so when you need # these (or just want a differently looking font) you can specify the font name # using DOT_FONTNAME. You need need to make sure dot is able to find the font, # which can be done by putting it in a standard location or by setting the # DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory # containing the font. DOT_FONTNAME = # The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. # The default size is 10pt. DOT_FONTSIZE = 10 # By default doxygen will tell dot to use the output directory to look for the # FreeSans.ttf font (which doxygen will put there itself). If you specify a # different font using DOT_FONTNAME you can set the path where dot # can find it using this tag. DOT_FONTPATH = # If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect inheritance relations. Setting this tag to YES will force the # the CLASS_DIAGRAMS tag to NO. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect implementation dependencies (inheritance, containment, and # class references variables) of the class with other documented classes. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen # will generate a graph for groups, showing the direct groups dependencies GROUP_GRAPHS = YES # If the UML_LOOK tag is set to YES doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. UML_LOOK = NO # If set to YES, the inheritance and collaboration graphs will show the # relations between templates and their instances. TEMPLATE_RELATIONS = NO # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT # tags are set to YES then doxygen will generate a graph for each documented # file showing the direct and indirect include dependencies of the file with # other documented files. INCLUDE_GRAPH = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and # HAVE_DOT tags are set to YES then doxygen will generate a graph for each # documented header file showing the documented files that directly or # indirectly include this file. INCLUDED_BY_GRAPH = YES # If the CALL_GRAPH and HAVE_DOT options are set to YES then # doxygen will generate a call dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable call graphs # for selected functions only using the \callgraph command. CALL_GRAPH = NO # If the CALLER_GRAPH and HAVE_DOT tags are set to YES then # doxygen will generate a caller dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable caller # graphs for selected functions only using the \callergraph command. CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # will graphical hierarchy of all classes instead of a textual one. GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES # then doxygen will show the dependencies a directory has on other directories # in a graphical way. The dependency relations are determined by the #include # relations between the files in the directories. DIRECTORY_GRAPH = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. Possible values are png, jpg, or gif # If left blank png will be used. DOT_IMAGE_FORMAT = png # The tag DOT_PATH can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found in the path. DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the # \dotfile command). DOTFILE_DIRS = # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of # nodes that will be shown in the graph. If the number of nodes in a graph # becomes larger than this value, doxygen will truncate the graph, which is # visualized by representing a node as a red box. Note that doxygen if the # number of direct children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note # that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. DOT_GRAPH_MAX_NODES = 50 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the # graphs generated by dot. A depth value of 3 means that only nodes reachable # from the root by following a path via at most 3 edges will be shown. Nodes # that lay further from the root node will be omitted. Note that setting this # option to 1 or 2 may greatly reduce the computation time needed for large # code bases. Also note that the size of a graph can be further restricted by # DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. MAX_DOT_GRAPH_DEPTH = 0 # Set the DOT_TRANSPARENT tag to YES to generate images with a transparent # background. This is disabled by default, because dot on Windows does not # seem to support this out of the box. Warning: Depending on the platform used, # enabling this option may lead to badly anti-aliased labels on the edges of # a graph (i.e. they become hard to read). DOT_TRANSPARENT = NO # Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) # support this, this feature is disabled by default. DOT_MULTI_TARGETS = YES # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # generate a legend page explaining the meaning of the various boxes and # arrows in the dot generated graphs. GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES (the default) Doxygen will # remove the intermediate dot files that are used to generate # the various graphs. DOT_CLEANUP = YES rsbackup-10.0/Makefile.am000066400000000000000000000026151440730431700153070ustar00rootroot00000000000000# Copyright © 2011, 2014, 2015, 2017 Richard Kettlewell. # # 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 . SUBDIRS=src tests doc tools EXTRA_DIST=scripts/fakeshell.sh \ debian/changelog debian/control debian/copyright \ debian/doc.rsbackup \ debian/rsbackup.conffiles debian/rsbackup.postinst debian/rules \ debian/rsbackup.postrm \ debian/CHECKLIST.md \ debian/watch \ debian/upstream/signing-key.asc \ debian/NEWS \ README.md dist_noinst_SCRIPTS=scripts/txt2src scripts/htmlman scripts/dist \ scripts/travis/linux/clang/configure \ scripts/travis/linux/gcc/configure scripts/travis/osx/clang/configure \ scripts/travis/osx/clang/install format: cd $(srcdir) && clang-format -i */*.h */*.cc echo-distdir: @echo $(distdir) echo-version: @echo $(VERSION) clean-local: rm -f */*.gcno rm -f */*.gcda rm -f */*.gcov rsbackup-10.0/README.md000066400000000000000000000037031440730431700145310ustar00rootroot00000000000000rsbackup ======== [![Build Status](https://api.travis-ci.com/ewxrjk/rsbackup.svg?branch=master)](https://travis-ci.com/github/ewxrjk/rsbackup) rsbackup backs up your computer(s) to removable hard disks. The backup is an ordinary filesystem tree, and hard links between repeated backups are used to save space. Old backups are automatically pruned after a set period of time. Installation ------------ ### Dependencies * [rsync](http://samba.anu.edu.au/rsync/) * [SQLite](http://www.sqlite.org/) * [Boost](http://www.boost.org/) * [Cairomm](https://www.cairographics.org/cairomm/) and [Pangomm](https://github.com/GNOME/pangomm) (optional) * A C++11 compiler ### Platforms On Debian/Ubuntu systems, [get rsbackup.deb](http://www.greenend.org.uk/rjk/rsbackup) and install that. Please see [Platform Support](https://github.com/ewxrjk/rsbackup/wiki#platform-support) for platform-specific notes. ### Building To build from source: autoreconf -si # only if you got it from git ./configure make check sudo make install Documentation ------------- Read [the tutorial manual](http://www.greenend.org.uk/rjk/rsbackup/rsbackup-manual.html) first. For reference information, see the man page: man rsbackup Bugs ---- Report bugs via [Github](https://github.com/ewxrjk/rsbackup/issues). Licence ------- Copyright © 2010-2020 Richard Kettlewell 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 . rsbackup-10.0/acinclude.m4000066400000000000000000000020731440730431700154420ustar00rootroot00000000000000# Copyright © 2011 Richard Kettlewell. # # 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 . AC_DEFUN([RJK_GCOV],[ GCOV=${GCOV:-true} AC_ARG_WITH([gcov], [AS_HELP_STRING([--with-gcov], [Enable coverage testing])], [[if test $withval = yes; then CXXFLAGS="${CXXFLAGS} -O0 -fprofile-arcs -ftest-coverage" GCOV=`echo $CXX | sed s'/[cg]++/gcov/;s/ .*$//'`; fi]]) AC_SUBST([GCOV],[$GCOV]) ]) rsbackup-10.0/configure.ac000077500000000000000000000123021440730431700155360ustar00rootroot00000000000000# Copyright © Richard Kettlewell. # # 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 . AC_PREREQ([2.61]) AC_INIT([rsbackup], [10.0], [rjk@greenend.org.uk]) AC_CONFIG_AUX_DIR([config.aux]) AM_INIT_AUTOMAKE([foreign]) m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) AC_CONFIG_SRCDIR([src/Conf.cc]) AM_CONFIG_HEADER([config.h]) AC_CANONICAL_HOST AC_LANG([C++]) AC_PROG_CXX AM_PROG_AR AC_PROG_RANLIB AC_SET_MAKE AC_C_BIGENDIAN AC_PATH_PROGS([CLANG_FORMAT],[clang-format-6.0 clang-format-7 clang-format-8 clang-format],[clang-format]) case "$host_os" in *bsd* ) CPPFLAGS="${CPPFLAGS} -isystem /usr/local/include" LDFLAGS="${LDFLAGS} -L/usr/local/lib" ;; linux-gnu ) # For backtrace to work properly LDFLAGS="${LDFLAGS} -rdynamic" ;; esac AC_CHECK_HEADERS([paths.h execinfo.h]) case "$host" in *apple-darwin* ) # Use system sqlite3 AC_SUBST([SQLITE3_CFLAGS],[]) AC_SUBST([SQLITE3_LIBS],[-lsqlite3]) ;; * ) PKG_CHECK_MODULES([SQLITE3],[sqlite3]) ;; esac boost="-lboost_filesystem -lboost_system" AC_SUBST([BOOST_LIBS],[${boost}]) AC_CHECK_LIB([pthread],[pthread_create]) AC_CHECK_LIB([iconv],[iconv_open]) AC_CHECK_LIB([rt],[clock_gettime]) AC_CHECK_LIB([execinfo],[backtrace]) AC_CACHE_CHECK([type of second argument to iconv()], [rjk_cv_iconv_inptr],[ AC_COMPILE_IFELSE( [AC_LANG_PROGRAM([#include ],[ iconv_t cd = 0; char *in, *out; size_t n; iconv(cd, &in, &n, &out, &n); ])], [rjk_cv_iconv_inptr="char **"], [AC_COMPILE_IFELSE( [AC_LANG_PROGRAM([#include ],[ iconv_t cd = 0; const char *in; char *out; size_t n; iconv(cd, &in, &n, &out, &n); ])], [rjk_cv_iconv_inptr="const char **"], [rjk_cv_iconv_inptr="unknown"])]) ]) case "$rjk_cv_iconv_inptr" in unknown ) AC_MSG_WARN([cannot determine argument type, winging it]) AC_DEFINE([ICONV_FIXUP],[],[fixup string for second argument to iconv()]) ;; "char **" ) AC_DEFINE([ICONV_FIXUP],[],[fixup string for second argument to iconv()]) ;; "const char **" ) AC_DEFINE_UNQUOTED([ICONV_FIXUP],[(${rjk_cv_iconv_inptr})],[fixup string for second argument to iconv()]) ;; esac AC_CHECK_LIB([pthread], [pthread_create], [AC_SUBST(LIBPTHREAD,[-lpthread])], [missing_libraries="$missing_libraries libpthread"]) AC_CACHE_CHECK([for Cairomm CFLAGS],[rjk_cv_cairomm_cflags],[ rjk_cv_cairomm_cflags="$(pkg-config --silence-errors --cflags cairomm-1.0)" ]) AC_CACHE_CHECK([for Cairomm LIBS],[rjk_cv_cairomm_libs],[ rjk_cv_cairomm_libs="$(pkg-config --silence-errors --libs cairomm-1.0)" ]) AC_SUBST([CAIROMM_CFLAGS],[$rjk_cv_cairomm_cflags]) AC_SUBST([CAIROMM_LIBS],[$rjk_cv_cairomm_libs]) AC_CACHE_CHECK([for Pangomm CFLAGS],[rjk_cv_pangomm_cflags],[ rjk_cv_pangomm_cflags="$(pkg-config --silence-errors --cflags pangomm-1.4)" ]) AC_CACHE_CHECK([for Pangomm LIBS],[rjk_cv_pangomm_libs],[ rjk_cv_pangomm_libs="$(pkg-config --silence-errors --libs pangomm-1.4)" ]) AC_CACHE_CHECK([for pango CFLAGS],[rjk_cv_pango_cflags],[ rjk_cv_pango_cflags="$(pkg-config --silence-errors --cflags pango)" ]) AC_CACHE_CHECK([for pango LIBS],[rjk_cv_pango_libs],[ rjk_cv_pango_libs="$(pkg-config --silence-errors --libs pango)" ]) AC_SUBST([PANGOMM_CFLAGS],["$rjk_cv_pangomm_cflags $rjk_cv_pango_cflags"]) AC_SUBST([PANGOMM_LIBS],["$rjk_cv_pangomm_libs $rjk_cv_pango_libs"]) AC_DEFINE([_GNU_SOURCE], [1], [use GNU extensions]) RJK_GCOV if test "x$GXX" = xyes; then CXXFLAGS="-std=gnu++11 -Wall -W -Werror -Wpointer-arith -Wwrite-strings ${CXXFLAGS}" fi CXXFLAGS="${CXXFLAGS} ${CXXFLAGS_EXTRA}" # 1. Glibmm uses C++14 features # 2. Which Clang moans about even in gnu++11 mode. # 3. But you can disable the moan with a -Wno... option. # 4. But GCC errors if it encounters some _other_ warning. # # 2 and 4 are probably bugs. Anyway the fix is: AC_CACHE_CHECK([whether Glibmm uses features from the future], [rjk_cv_glibmm_future], [save_CXXFLAGS="${CXXFLAGS}" CXXFLAGS="${CXXFLAGS} ${rjk_cv_pangomm_cflags}" AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include ]])], [rjk_cv_glibmm_future=no], [rjk_cv_glibmm_future=yes]) CXXFLAGS="${save_CXXFLAGS}" ]) if test $rjk_cv_glibmm_future = yes; then CXXFLAGS="${CXXFLAGS} -Wno-c++14-extensions" fi # Reinforce GNU++11, since pangomm and cairomm think they can override # it(!) if test "x$GXX" = xyes; then AC_SUBST([FINAL_CXXFLAGS],[-std=gnu++11]) fi AH_BOTTOM([#ifdef __GNUC__ # define attribute(x) __attribute__(x) #else # define attribute(x) #endif]) AC_CONFIG_FILES([Makefile src/Makefile tests/Makefile doc/Makefile tools/Makefile]) AC_CONFIG_FILES([src/check-source], [chmod +x src/check-source]) AC_OUTPUT rsbackup-10.0/debian/000077500000000000000000000000001440730431700144715ustar00rootroot00000000000000rsbackup-10.0/debian/CHECKLIST.md000066400000000000000000000050461440730431700163310ustar00rootroot00000000000000# Debian standards checklist Things that need attention are listed below. Their presence here doesn't mean a change is definitely required, but it should be looked into. ## 3.9.7 * 12.3 recommend to ship additional documentation for package "pkg" in a separate package "pkg-doc" and install it into "/usr/share/doc/pkg". _extra docs are small, not going to do this_ * See also 4.0.0.0 12.3 (*-doc deps should be at most Recommends) ## 3.9.8 Nothing relevant. ## 4.0.0 * 4.3 "config.sub" and "config.guess" should be updated at build time or replaced with the versions from autotools-dev. _not going to do this manually if it's not important enough for auto* packages to do it automatically_ * 4.9.1 New "DEB\_BUILD\_OPTIONS" tag, "nodoc", which says to suppress documentation generation (but continue to build all binary packages, even documentation packages, just let them be mostly empty). _not going to do this while there is no -doc package_ ## 4.0.1 Nothing relevant. ## 4.1.0 * ~~4.11 If upstream provides OpenPGP signatures, including the upstream signing key as "debian/upstream/signing-key.asc" in the source package and using the "pgpsigurlmangle" option in "debian/watch" configuration to indicate how to find the upstream signature for new releases is recommended.~~ * 4.15 Packages should build reproducibly when certain factors are held constant; see 4.15 for the list. _patches accepted within reason_ ## 4.1.1 Nothing relevant. ## 4.1.2 Nothing relevant. ## 4.1.3 * ~~5.6.26 URLs given in "VCS-*" headers should use a scheme that provides confidentiality ("https", for example) if the VCS repository supports it. "[vcs-field-uses-insecure-uri]" _should add VCS headers_~~ ## 4.1.4 Nothing relevant. ## 4.1.5 Nothing relevant. ## 4.2.0 * ~~4.9 The package build should be as verbose as reasonably possible. This means that "debian/rules" should pass to the commands it invokes options that cause them to produce verbose output.~~ * ~~12.7 Upstream release notes, when available, should be installed as "/usr/share/doc/package/NEWS.gz". Upstream changelogs may be made available as "/usr/share/doc/package/changelog.gz".~~ _CHANGES already dumped as changelog_ ## 4.2.1 Nothing relevant. ## 4.3.0 * ~~10.1 Binaries should be stripped using "strip --strip-unneeded --remove- section=.comment --remove-section=.note" (as dh\_strip already does).~~ ## 4.4.0 * Recommended to use `dh`. _not going to do (upstream), current build works fine_ ## 4.4.1 Nothing relevant. ## 4.5.0 Nothing relevant. ## 4.5.1 Nothing relevant. ## 4.6.0 Nothing relevant. rsbackup-10.0/debian/NEWS000066400000000000000000000006621440730431700151740ustar00rootroot00000000000000rsbackup (3.1-2) unstable; urgency=low Old logfiles from rsbackup versions older than 2.0 (e.g. 1.1-4 shipped with jessie) are not supported by rsbackup versions 7.0 and greater. You must use a version of rsbackup between versions 2.0 and 6.0 to upgrade these old logfiles. This includes those versions of rsbackup shipped with stretch and buster. -- Matthew Vernon Sun, 16 Feb 2020 14:31:39 +0000 rsbackup-10.0/debian/changelog000066400000000000000000000216201440730431700163440ustar00rootroot00000000000000rsbackup (10.0) stable; urgency=medium * Release 10.0 -- Richard Kettlewell Fri, 24 Mar 2023 11:25:55 +0000 rsbackup (9.0) stable; urgency=medium * Release 9.0 -- Richard Kettlewell Sat, 12 Nov 2022 12:04:18 +0000 rsbackup (8.0-1) unstable; urgency=medium * New upstream version (Closes: #1010487) -- Matthew Vernon Thu, 20 Oct 2022 18:47:26 +0100 rsbackup (8.0) stable; urgency=medium * Release 8.0 -- Richard Kettlewell Sat, 16 Jan 2021 11:02:39 +0000 rsbackup (7.0) stable; urgency=medium * Release 7.0 -- Richard Kettlewell Fri, 29 May 2020 10:39:02 +0100 rsbackup (6.0-2) unstable; urgency=medium * Source-only upload so this can migrate to testing -- Matthew Vernon Sun, 16 Feb 2020 14:43:06 +0000 rsbackup (6.0-1) unstable; urgency=medium * New upstream release. -- Matthew Vernon Sat, 28 Sep 2019 16:51:19 +0100 rsbackup (6.0) unstable; urgency=medium * Release 6.0 -- Richard Kettlewell Sat, 07 Sep 2019 09:38:31 +0100 rsbackup (5.1-1) unstable; urgency=medium * New upstream version (Closes: #908644) -- Matthew Vernon Mon, 08 Oct 2018 20:26:07 +0100 rsbackup (5.1) stable; urgency=medium * Release 5.1 -- Richard Kettlewell Fri, 27 Jul 2018 18:34:15 +0100 rsbackup (5.0-2) unstable; urgency=medium * Upstream patch to workaround sigc++ build failure on GCC 8 (Closes: #897852) -- Matthew Vernon Sun, 22 Jul 2018 12:21:27 +0100 rsbackup (5.0-1) unstable; urgency=medium * New upstream version -- Matthew Vernon Sat, 24 Feb 2018 12:54:43 +0000 rsbackup (5.0) stable; urgency=medium * Release 5.0 -- Richard Kettlewell Sun, 28 Jan 2018 13:36:10 +0000 rsbackup (4.0-1) unstable; urgency=medium * New upstream version * Upstream patch to build with gcc 7 (Closes: #853647) -- Matthew Vernon Sat, 26 Aug 2017 14:22:30 +0100 rsbackup (4.0) stable; urgency=medium * Release 4.0 -- Richard Kettlewell Sun, 26 Mar 2017 17:16:52 +0100 rsbackup (3.1-3) unstable; urgency=low * Patch from Reiner Herrmann to make build reproducible (Closes: #842546) -- Matthew Vernon Sun, 30 Oct 2016 13:35:01 +0000 rsbackup (3.1-2) unstable; urgency=low * Sacrifice a version number to dgit -- Matthew Vernon Thu, 30 Jun 2016 16:37:44 +0100 rsbackup (3.1-1) unstable; urgency=low * New upstream version (Closes: #811750, #810335) -- Matthew Vernon Thu, 30 Jun 2016 14:03:57 +0100 rsbackup (3.1) unstable; urgency=medium * Release 3.1 -- Richard Kettlewell Sun, 24 Jan 2016 10:59:06 +0000 rsbackup (3.0-2) unstable; urgency=low * Upload again with orig this time (dgit needs a new version number to do this) -- Matthew Vernon Fri, 01 Jan 2016 12:58:18 +0000 rsbackup (3.0-1) unstable; urgency=low * New upstream version (Closes: #777394) * patch from Maria Valentina Marin to fix mtimes before building binary packages (reproducible-builds effort) (Closes: #793716) -- Matthew Vernon Tue, 29 Dec 2015 14:57:43 +0000 rsbackup (3.0) unstable; urgency=low * Release 3.0 -- Richard Kettlewell Sat, 19 Dec 2015 11:45:55 +0000 rsbackup (2.1) unstable; urgency=low * Release 2.1 -- Richard Kettlewell Sat, 08 Aug 2015 15:21:23 +0100 rsbackup (2.0) unstable; urgency=low * Release 2.0 -- Richard Kettlewell Fri, 13 Feb 2015 17:40:14 +0000 rsbackup (1.2) unstable; urgency=low * Release 1.2. -- Richard Kettlewell Sun, 28 Dec 2014 15:13:59 +0000 rsbackup (1.1-4) unstable; urgency=low * Patch from Jonathan Wiltshire to use install rather than cp for post{inst,rm}, making build less sensitive to source file permissions (Closes: #774013) -- Matthew Vernon Sun, 28 Dec 2014 19:10:11 +0000 rsbackup (1.1-3) unstable; urgency=low * Clean up /ets/rsbackup on purge (thanks to Andreas Beckmann for the bug report, and Richard Kettlewell (upstream) for the patch (Closes: #773181) -- Matthew Vernon Tue, 16 Dec 2014 21:42:19 +0000 rsbackup (1.1-2) unstable; urgency=low * Make cronjobs exit quietly if rsbackup.cron not present (i.e. package removed not purged) (Closes: #766455) -- Matthew Vernon Thu, 23 Oct 2014 11:35:09 +0100 rsbackup (1.1-1) unstable; urgency=low * New upstream version * Incorporate 1.0-2 changelog entry -- Matthew Vernon Tue, 21 Oct 2014 20:28:44 +0100 rsbackup (1.1) unstable; urgency=low * More accurate error messages about missing devices. * 'include' command skips filenames starting '#'. * Stricter checking of host and volume names. * Better fsck handling in the snapshot hook script. -- Richard Kettlewell Thu, 31 Jul 2014 20:19:24 +0100 rsbackup (1.0-2) unstable; urgency=low * binary-arch should depend on binary-${PACKAGE}, not binary-indep (Closes: #760609) -- Matthew Vernon Wed, 17 Sep 2014 23:06:06 +0100 rsbackup (1.0-1) unstable; urgency=low * Initial upload to Debian (Closes: #744305) * Bump standards version (no changes needed) * Maintainer set to a mailing list * Lintian-inspired improvements to rules file -- Matthew Vernon Sat, 05 Jul 2014 14:47:25 +0100 rsbackup (1.0) unstable; urgency=low * New --dump-config option to verify configuration file parse. * New --check option to rsbackup-mount. * Configuration files are now read in a fixed order (issue #8). * The --force option no longer implies the --verbose option. * Minor bug fixes. -- Richard Kettlewell Sat, 21 Jun 2014 13:24:19 +0100 rsbackup (0.4.4) unstable; urgency=low * Fix post-backup hook environment -- Richard Kettlewell Sat, 19 Apr 2014 18:43:06 +0100 rsbackup (0.4.3) unstable; urgency=low * Build Debian packages with hardening flags * Minor build/test improvements. -- Richard Kettlewell Mon, 13 Jan 2014 21:27:34 +0000 rsbackup (0.4.2) unstable; urgency=low * --retire no longer fails if a host directory does not exist. * Fix recalculation of per-device backup counts. -- Richard Kettlewell Fri, 25 Oct 2013 17:52:32 +0100 rsbackup (0.4.1) unstable; urgency=low * Release 0.4.1 -- Richard Kettlewell Sun, 19 May 2013 16:59:07 +0100 rsbackup (0.4) unstable; urgency=low * Release 0.4 -- Richard Kettlewell Sat, 02 Feb 2013 16:58:02 +0000 rsbackup (0.3) unstable; urgency=low * Release 0.3 -- Richard Kettlewell Sat, 24 Mar 2012 13:49:30 +0000 rsbackup (0.2) unstable; urgency=low * Release 0.2 * Rewritten in C++ * Text format report * --retire and --retire-device replace --prune-unknown * New --warn-* options * New always-up host directive * No longer create .incomplete files * Delete stray files in retried backups -- Richard Kettlewell Sat, 07 May 2011 12:05:34 +0100 rsbackup (0.1) unstable; urgency=low * Release 0.1 * 'nicely' option in rsbackup.defaults * Fixed permissions on installed files * Build script fixes from Peter Maydell -- Richard Kettlewell Sun, 03 Apr 2011 11:35:49 +0100 rsbackup (0.0.109) unstable; urgency=low * rsbackup.cron now prunes all volumes (if it prunes anything), not just the ones scheduled for backup at that moment. -- Richard Kettlewell Sat, 05 Feb 2011 11:38:02 +0000 rsbackup (0.0.107) unstable; urgency=low * For lower-frequency cronjobs, add --wait so the backup is reliably run. -- Richard Kettlewell Sun, 02 Jan 2011 12:28:13 +0000 rsbackup (0.0.100) unstable; urgency=low * Testing new cron arrangement. -- Richard Kettlewell Sun, 03 Oct 2010 10:42:41 +0100 rsbackup (0.0.99) unstable; urgency=low * Don't try to include deleted logfiles in the report. -- Richard Kettlewell Sun, 03 Oct 2010 10:45:55 +0100 rsbackup (0.0.98) unstable; urgency=low * Add min-backups setting to control how many backups per store are guaranteed to be kept. -- Richard Kettlewell Wed, 22 Sep 2010 12:23:30 +0100 rsbackup (0.0.97) unstable; urgency=low * Don't prune the last backup of a volume on each store (rather than the last backup of a volume). -- Richard Kettlewell Wed, 22 Sep 2010 11:48:29 +0100 rsbackup (0.0) unstable; urgency=low * Initial debian/*. -- Richard Kettlewell Fri, 07 May 2010 13:04:16 +0100 rsbackup-10.0/debian/control000066400000000000000000000031411440730431700160730ustar00rootroot00000000000000Source: rsbackup Maintainer: Debian rsbackup maintainers Uploaders: Matthew Vernon Priority: optional Standards-Version: 3.9.6.0 Section: admin Homepage: https://www.greenend.org.uk/rjk/rsbackup/ Vcs-Git: https://github.com/ewxrjk/rsbackup Build-Depends: lynx|lynx-cur,devscripts,sqlite3,libsqlite3-dev,libboost-system-dev,libboost-filesystem-dev,libboost-dev,pkg-config,libpangomm-1.4-dev,libcairomm-1.0-dev,xattr,acl Package: rsbackup Architecture: any Section: admin Depends: ${shlibs:Depends},rsync Recommends: openssh-server,openssh-client Description: rsync-based backup utility Backups are stored as complete filesystem trees on a (perhaps external) hard disk. Multiple backups use hard links between identical files to save space. . Backups may be taken from multiple machines (over SSH) and stored to multiple disks. . Backups may be made automatically, i.e. without relying on the operator to remember to make a backup. Package: rsbackup-graph Architecture: any Section: admin Depends: ${shlibs:Depends},rsbackup Recommends: openssh-server,openssh-client Description: Graphics for rsync-based backup utility Backups are stored as complete filesystem trees on a (perhaps external) hard disk. Multiple backups use hard links between identical files to save space. . Backups may be taken from multiple machines (over SSH) and stored to multiple disks. . Backups may be made automatically, i.e. without relying on the operator to remember to make a backup. . This package generates graphical representations of currently available backups. rsbackup-10.0/debian/copyright000066400000000000000000000015371440730431700164320ustar00rootroot00000000000000Copyright © 2010-2015 Richard Kettlewell 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 . On Debian GNU/Linux systems, the complete text of the GNU General Public License can be found in ‘/usr/share/common-licenses/GPL-3’ or in the dpkg source as the file ‘COPYING’ rsbackup-10.0/debian/doc.rsbackup000066400000000000000000000003651440730431700167760ustar00rootroot00000000000000Document: rsbackup Title: rsbackup Documentation Author: Richard Kettlewell Abstract: rsync-based backup system Section: System/Administration Format: HTML Index: /usr/share/doc/rsbackup/rsbackup-docs.html Files: /usr/share/doc/rsbackup/*.html rsbackup-10.0/debian/rsbackup.conffiles000066400000000000000000000002521440730431700201740ustar00rootroot00000000000000/etc/rsbackup/config /etc/rsbackup/defaults /etc/rsbackup/devices /etc/cron.hourly/rsbackup /etc/cron.daily/rsbackup /etc/cron.weekly/rsbackup /etc/cron.monthly/rsbackup rsbackup-10.0/debian/rsbackup.postinst000077500000000000000000000014441440730431700201160ustar00rootroot00000000000000#! /bin/sh # Copyright © 2010 Richard Kettlewell. # # 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 . set -e case "$1" in configure ) if [ ! -e /etc/rsbackup/local ]; then touch /etc/rsbackup/local fi ;; esac rsbackup-10.0/debian/rsbackup.postrm000077500000000000000000000014721440730431700175600ustar00rootroot00000000000000#! /bin/sh # Copyright © 2014 Richard Kettlewell. # # 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 . set -e case "$1" in purge ) rm -f /etc/rsbackup/local [ -d /etc/rsbackup ] && rmdir --ignore-fail-on-non-empty /etc/rsbackup ;; esac rsbackup-10.0/debian/rules000077500000000000000000000131371440730431700155560ustar00rootroot00000000000000#! /usr/bin/make -f # Copyright © 2010-2018 Richard Kettlewell. # # 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 . INSTALL=install STRIPARGS=--strip-unneeded --remove-section=.comment --remove-section=.note export DEB_BUILD_MAINT_OPTIONS = hardening=+all DPKG_EXPORT_BUILDFLAGS = 1 include /usr/share/dpkg/buildflags.mk export SOURCE_DATE_EPOCH = $(shell date -d "$$(dpkg-parsechangelog --count 1 -SDate)" +%s) # Debian apparently wants reproducible builds but won't execute them # in a consistent environment, so every project has to clean up after # it. export LC_ALL=C.UTF-8 build-arch: build build-indep: build build: [ -e configure ] || autoreconf -si ./configure --prefix=/usr --mandir=/usr/share/man --disable-silent-rules ${CONFIGURE_EXTRA} $(MAKE) clean-rsbackup: rm -rf debian/rsbackup binary-rsbackup: build rm -rf debian/rsbackup mkdir -p debian/rsbackup/DEBIAN mkdir -p debian/rsbackup/etc/rsbackup/hosts.d mkdir -p debian/rsbackup/etc/cron.hourly mkdir -p debian/rsbackup/etc/cron.daily mkdir -p debian/rsbackup/etc/cron.weekly mkdir -p debian/rsbackup/etc/cron.monthly mkdir -p debian/rsbackup/usr/share/doc/rsbackup mkdir -p debian/rsbackup/var/log/backup mkdir -p debian/rsbackup/usr/share/doc-base mkdir -p debian/rsbackup/usr/bin mkdir -p debian/rsbackup/usr/share/man/man1 mkdir -p debian/rsbackup/usr/share/man/man5 cp debian/rsbackup.conffiles debian/rsbackup/DEBIAN/conffiles install debian/rsbackup.postinst debian/rsbackup/DEBIAN/postinst install debian/rsbackup.postrm debian/rsbackup/DEBIAN/postrm install -m 755 tools/rsbackup.hourly debian/rsbackup/etc/cron.hourly/rsbackup install -m 755 tools/rsbackup.daily debian/rsbackup/etc/cron.daily/rsbackup install -m 755 tools/rsbackup.weekly debian/rsbackup/etc/cron.weekly/rsbackup install -m 755 tools/rsbackup.monthly debian/rsbackup/etc/cron.monthly/rsbackup cp tools/rsbackup.config debian/rsbackup/etc/rsbackup/config cp tools/rsbackup.defaults debian/rsbackup/etc/rsbackup/defaults cp tools/rsbackup.devices debian/rsbackup/etc/rsbackup/devices cp debian/changelog debian/rsbackup/usr/share/doc/rsbackup/changelog.Debian cp debian/NEWS debian/rsbackup/usr/share/doc/rsbackup/NEWS.Debian cp debian/doc.rsbackup debian/rsbackup/usr/share/doc-base/rsbackup cp README.md debian/rsbackup/usr/share/doc/rsbackup/. cp doc/CHANGES.md debian/rsbackup/usr/share/doc/rsbackup/changelog gzip -9nv debian/rsbackup/usr/share/doc/rsbackup/* cp doc/*.html doc/*.css debian/rsbackup/usr/share/doc/rsbackup/. rm -f debian/rsbackup/usr/share/doc/rsbackup/*.prefix.html rm -f debian/rsbackup/usr/share/doc/rsbackup/*.in.html cp debian/copyright debian/rsbackup/usr/share/doc/rsbackup/. $(INSTALL) -m 755 src/rsbackup debian/rsbackup/usr/bin/rsbackup $(MAKE) -C tools install DESTDIR=$(shell pwd)/debian/rsbackup $(INSTALL) -m 644 doc/rsbackup.1 \ doc/rsbackup.cron.1 \ doc/rsbackup-mount.1 \ doc/rsbackup-snapshot-hook.1 \ debian/rsbackup/usr/share/man/man1/ $(INSTALL) -m 644 doc/rsbackup.5 debian/rsbackup/usr/share/man/man5/ strip $(STRIPARGS) debian/rsbackup/usr/bin/rsbackup gzip -9nv debian/rsbackup/usr/share/man/man*/* dpkg-shlibdeps -Tdebian/substvars.rsbackup \ debian/rsbackup/usr/bin/* cd debian/rsbackup && \ find -name DEBIAN -prune -o -type f -print \ | sed 's/^\.\///' \ | sort | xargs md5sum > DEBIAN/md5sums dpkg-gencontrol -isp -prsbackup -Pdebian/rsbackup \ -Tdebian/substvars.rsbackup chown -R root:root debian/rsbackup chmod -R g-ws debian/rsbackup find debian/rsbackup -newermt "@$$SOURCE_DATE_EPOCH" -print0 | \ xargs -0r touch --no-dereference --date="@$$SOURCE_DATE_EPOCH" dpkg --build debian/rsbackup .. clean-rsbackup-graph: rm -rf debian/rsbackup-graph binary-rsbackup-graph: build rm -rf debian/rsbackup-graph mkdir -p debian/rsbackup-graph/DEBIAN mkdir -p debian/rsbackup-graph/usr/bin mkdir -p debian/rsbackup-graph/usr/share/man/man1 mkdir -p debian/rsbackup-graph/usr/share/doc ln -s rsbackup \ debian/rsbackup-graph/usr/share/doc/rsbackup-graph $(INSTALL) -m 755 src/rsbackup-graph \ debian/rsbackup-graph/usr/bin/rsbackup-graph $(INSTALL) -m 644 doc/rsbackup-graph.1 \ debian/rsbackup-graph/usr/share/man/man1/ strip $(STRIPARGS) debian/rsbackup-graph/usr/bin/rsbackup-graph gzip -9nv debian/rsbackup-graph/usr/share/man/man*/* dpkg-shlibdeps -Tdebian/substvars.rsbackup-graph \ debian/rsbackup-graph/usr/bin/* cd debian/rsbackup-graph && \ find -name DEBIAN -prune -o -type f -print \ | sed 's/^\.\///' \ | xargs md5sum > DEBIAN/md5sums dpkg-gencontrol -isp -prsbackup-graph -Pdebian/rsbackup-graph \ -Tdebian/substvars.rsbackup-graph chown -R root:root debian/rsbackup-graph chmod -R g-ws debian/rsbackup-graph find debian/rsbackup-graph -newermt "@$$SOURCE_DATE_EPOCH" -print0 | \ xargs -0r touch --no-dereference --date="@$$SOURCE_DATE_EPOCH" dpkg --build debian/rsbackup-graph .. binary: binary-arch binary-indep binary-arch: binary-rsbackup binary-rsbackup-graph binary-indep: clean: clean-rsbackup clean-rsbackup-graph rm -f debian/files rm -f debian/debhelper.log [ ! -f Makefile ] || $(MAKE) distclean rsbackup-10.0/debian/source/000077500000000000000000000000001440730431700157715ustar00rootroot00000000000000rsbackup-10.0/debian/source/options000066400000000000000000000001201440730431700174000ustar00rootroot00000000000000extend-diff-ignore=Makefile\.in|configure|aclocal\.m4|config\.aux|config\.h\.in rsbackup-10.0/debian/upstream/000077500000000000000000000000001440730431700163315ustar00rootroot00000000000000rsbackup-10.0/debian/upstream/signing-key.asc000066400000000000000000001056471440730431700212620ustar00rootroot00000000000000 KҧU4F>sM‰Ml\-|-U$ii<ҩ{~~k*G?&ap<>L*VeJ88wY-Q!_=$'wė飨M.c_/+B1 }}EOfƒfKyZ^1Z_lɾp.EX3U2 ,-:?8|hi_[~&/ vթvKY>89;Richard Kettlewell (day-to-day usage) :$   U3 8t H›^$Nifeː4MytgҭNͲG҉xscԝyL(E.6˚Sզ}MD Eک`sD)%nJxo*ؕEm7rwV6+B,q0E_RDض={]ْ@BsK=Ph|\0o~,ϓ@0K5L2##Wơ9hZ̬ ݾFeX|xɁ-͇<7!U3N    8t0;˰EϹaË";=!ι5;}ef0_X=b½ B:p|Pï|Z[f>zp#y㰐 و9G^3Ε;:b=Ѫ.'U7Ր;H3EwIJ@F&tfsѯ"?nɃ ziiˎ7tK?޺URvO"U]  SNMFjhoy0!>k!arEaEԀXD}+.h] 1Ҥn}lq*gIU࿝LY' L7ޒwXiu)"z۬۴܄+4%- e G)d5fςJ3bZ.> FB0b~3A*_7_{|TdE!;t.k(0Ge5N}9m8vtPH UsL0T;auS #ߤxǦ ?tEr.$Y, EcvJ( 8i u{hu᠉5' xRQh+'mb6VndG'z 51;H6x/&AQMMbG%NUf%0 *LU>mi ,M>lpDt>*4d3󼜋{I/_1tuEգ!+xUضyKMOrv!x XG!ZƬvS2Q~єunF EL0U&B+.ry|ۯX=eJbAeml3wx~j߭Ia}2̇2a@ D1EC>,aELdNփ`}=3=N~[ 偧o{'-9 _F[+Ru ޵v)OR|] TK ˼ oL%*L/P8XkNJ8o ?I^ލ2ͳQ76˦"́ uhR y?ʿ6`/$@;]ڿ*`H`><~Ruy)Q6R8Ӭd2[J^(9+e,$-=xmv ^q ֶ>f Uf%O 9#H 9-=̅͆un)oq&bDڬ6G4ӉTIovLci$-=3oeEG0(cja&;*j[47OKH^=a~){ƍ'sKŨq k3\ޛQ3?p-~h$?ڽ(s=S<ڠӀ*{YrL1%r|JL ۋS@nu5|2nTJa<̉Ugxg Z`y6b4?ϡPNjm#n!MC1?1 jby'XL~+{7RvK*Wc(AgZ:jԎ MG?Ox39s㈴B9M%ОgK ݏLiDEiIw1W~B9]DYsiƘ@bu¦gߕW̗*?`D8Bu&Sidca>[ifPJH%s.+*-}x Y(k%k)4X??_ãsqf܃۴(Richard Kettlewell 7!   U3 8tﳸl"1căQ@Ws*릲ۘko#K̭6W[Z.eϑ2zg %YC#_8ǒ/E>Ɍ>/Ve!v{X$'cr EWDD,5a@5F/CM/M(^ 슎0qk†CXLm >5Wa/b9aJ@#/ K$"?E ^QΈFKҨ 6 [6cdāA)D ܣ8**)pPhyLlC #t7!Kҧ    8t: Tt?9"2ArA,|OvW}*Cn@gL7n+Vx.VϥC5q'`8P[t LQF5*ld0xf١( +`mA 0twgw^enm{7W=l'r ^<&WP-D\ձK.Vp7q0*g۳5"yu:ξUJ@Sb|nÑIKO.P 1^P& P *SUVgrG{cv:!)?T* ^.h--"Zb}JB&U< W (-xxU3¯5??E@ =H)Έrb^;}&f`y?iá'i4.Lri`v˲1aaÆP>UYe=x.k='!p(8w8-rOb|cG9"Bm w?dlM^P gaO?%:$   Kҫ] 8tfvlHGw %ҿ6i*EsTđGMaS`rsjҠ̂H}'86)sN-草LF1"?aX5F*6eQYsh +\"9Qf6uTSG|VLfem`B q7^2V$vͮZYĶi'vG|} Emp1޶ UjܥH30#lFDZYFig;N?U]  SNMU_$.ؼXʞd#OHl v9@+)ۅJ862Ib-`{Q oWQe *@Ӝ2.gnXoɬC}Rnw&Wlx6aҥ}HUؓZ >kƧ&0e)ɒʿ'NvA L|h{6GYqRXV9nl!`!Fy7O5M݁2ͨ+y R`4N.⪷݉ U^Mv jcGkkK .9ڪŞ^؂$axc)V)U&f 蟙|y_X.ϓDJqVLgh)HBά~j v!'' E qOʞAxbH\ QA1 _)[HX\RlNmm9A~ (D '"rXh&w~̰c[1Կ5Olx6g*L :#9z^\nsB+ED1%iPz^܈5 @{'('jz-t!(@stAY»R3>Uf%0 *LU/ov/&>9;Ȫ N"=z䩷ֱ-gd5+K(iFlt\> ֒=ֶw׻E<|!3p%}MiVMe} {=iRgān,ҟ0[snϏtC<֦諆^iw&B%&$YC %Kt%k9ݛ]`&8K!`^bX-L%\Y+[+Ѯr*[>Y+h~816B7VcAARpi7C$IX ܙjvH@h`b Gḛr!JLaTH畽o #̏g\#0#|g1#fy-K$<Zz2>[8ԣkNFؠ}yIs~ङ2qI=0YfIix4H~]?WFӳLNr#_|R#AwߎZlx$ p|e ?ى<||Q!jJ8y4(?`\',CQ|nˇ8s-DfB{O&xaN赟?'$gl[sM!Q ;KIڜ5*Kd Ƨc2t}Ugxg Z`.&+m辇9qk 9HzLw2JVh)^ku?3OUR5ZESC^њg"GؑEC0TqUB8Lн_Bޛ _UλnSk-@EGs? {6lV Y(Hf C8-B~PSramCr.0BV)솩y@^n$@FKTe/NGOw,:Z;s9KFf0z]_YMކyN7XEZk>?6^svz]6w,;t&It@e?YcJ" 0ЩpIV LW2Richard Kettlewell N08Y1 sfere.greenend.org.uk mail domain is going away 8t#luӟnHH X]~[=̻D>̫}Bu&:uV2ڨOMJ,;JVr_8Z}utLWj?bңO)JպYYo)!xJ[w[8'VpW:k.9}}2ma]7{v ב rtlz?\%f)ycDV.:G,Q1[2 I/b G g<G06%XT|{\8n;vȡ;EɉD@ZV-Pi U$U1 Z` JtLy]]4٫E6yCKVv2bCnX3v`}ÜuG4eҢYk zF?JL}ZNc`Œzք;&$x3Esp<{ _`֕ywHs=.01DXynkwT`@讘n9O^OlqX#%xBICQ>]vT*s˅o_Qc0V>87qu\ʁ 27Ҵ։Uf%0 *LU{d,fq݇:uL9jfFC[5FI`l5R܃i7FP=8<9~#~XLQQl m'\>ć3܏=!4Nq#(~ilרr¤A~<޵]/m+A֓~vR,kkzм֘Qng3]ȯ_ZЯf:Lj<%B?Kz.qȫ1U*1C }QZ|=Ugxg Z`jjN [!_:)c"~r^3lE*89qy$)(0VnX}>Hz΍jȝ|.CuC,nDzi=U/r|3bGY}?(ķf0Y,4cI{eMx5[(im̃kʗTP5Y`/k< JI~Yw7Qv|js3Ve. kT܋wKkj-׊l&| F$.ϲ'v mWʉU 95}P =wGL 9Y^d9|ve5?1kSICX'!_ʁ*+N~EF|m |"F5> oE]z.UˠѮI>İ}]ѕ7]jV}_.TWy(K}Y͓x[js{ִ]lژQ ru $]A敹'U8]?)L Ħ rŽSoԛC4x%g_HH\n\x.;]!ZcѩNt@$vE*V?! 4r Բt(3]KUqzQ1"r#>'^\.Ϣv/PlBML<dFKv6a9L=A#W2\fy_AZI[#pGd023+,Ŵ(Richard Kettlewell U3¯5??b ?%*Tq JD%}qBLhUl1>vsp޾q(')oszjhGO|t ~QmӔgu,W,y.PJb߷k#Δ`+0ÏEqaı\97+ / ֠2"!xȪw@!tc;m{amٓ;Q)kg/h3jŒ/Wm#tW|@2? n^WŹnc67!KҪ    8tztH9WvZf%ݱJ~۽&Oͼ\mZFxQ a SU^\ߠGL챬A 8t"ɶ\T⥇O|E*UФlZP6=gdHjէI8F>Fg:Q }Ͼ,2G9[T/h։Ӽw=^,J1OH-'*RPba 8^ɞNƵJbXxU:$O1n2|f" 5eKan.7lVcB ?]3݆bTiJ +/CX%! `'i=[2|$!@4FU]  SNMX12a!(ý"\yq*;-n5m qκ|R9E2#NaZ,+rz-px&q9*B"wɎ0?ҏf zrMmb (4XaҲPn4:68cwKWvIpKk*1AauݻL1d*iJA̩U;q}3x9t}L@DdHS/U:MߔZU<@ U^Mv jcȶ#UF^ Ʉ|}U2ģIYJ&ڸXU:~X) HWp>У^AYkBYCݾ-3>T= )Ey: U<,BkY1O %ޅ<=~.e(W<Ұ\zYF,d#tP@km&yMPaAOju>LJ8$̳9k2hQ JuHL;nK}Wo*ڬ~ͻ9ֻqJC*!=g՚hkX ;*U'):7ѦŢ[tOz  ,}G(BX?IOɮ(B" haA'ddE"I&AwJ峵 ް.nQĬl9ֵhb +˜~ouD́`v=d+A,Jx8Ŝ=RuMERfB )Y,rpAoJ9׃͉Uf%0 *LUھf΢(il1yݫhzaC0ȱűaΊzW/-Gާƪ 9;d\]yvkA'-XjI ~z6rޝ+VJF;d/C96; AE vriC6$[o k Z BgI`apʢyO`D=R#Gv8 9%ԇ7n @{ [DV6Bb8AΕUUwPF*XhnA'Ws@O rFT8WybEHR8PZ'r3Ŭl忱o4.UȲ2)=蔮xiWX{?ga=`*jsbrQt˂p7>E+l9wD8RʨUX4⎼hG{ YB)0 ѽcUf%O 9#H 9ƷOg(u^tE#ᅐrlv3)]H"h}qDj>e=ݡ#X5  Nձ"}V] J"abuL<ֽ\Z^-eIo礑pܳMAm'y5CmZVp"Z(cEea6{Y=dX^a"\7䀦i $I1=J&DUgxg Z`oum5ZG;үӘZ#Q!Gi"lMsz(PpMnT~uFpqW@cj kZzxvހL5-ɡ%4? J+XM0 :Ɠiz6Vŷ!ubA׃_E)9J.ut:I2WNB,Kbc ۹㷧9|}igD+'_:\Kp|/,U 95}P iW4dR{~ ݪl%Y36.DʁͭAg}|/=aSG_֖pNJ@ u_Z(EMҼVD&׃.YsԌ )&kZz ibyr;,`@2^KM鷊@ y,rx+鵀o8A w2&c_%X_65qGK-n+lA45:# / Z\S8 mӏw[dP$[Ԍz^G-a%¦;lt@:(ؐp> A 8#CzD1h3dkI^/Z\XĸK+ssC^+B ,3Է7F& pHK_?6Cc|GQƔxZ 0<$8qNitDr k h`cZ΅z%ctVaʻڇ" Wu W| %uVmah\~:Ko {@ f^Tn\uJ^E5xue)pJU_Rꍃv@f: qWz8󋨳 u.\'L>+G`:a;MpZ[ {-V0fmҠ]ҫ2 &?4yHȣeUצO@㦪2voe\2?~\or%Rqn\3>V" W%Tu W|8%VCk(K(Z@zϞ(Z+'$܎ݺP/_\Gz%q#@W ΄"dVep]e% a|MsU_v9$Xa#=O>텻z)+T" Xu W|Q۴o-fp^PfUMs`ہ(`bғn1$D.O,A| 6 aM eILLOv3.op2VjfR2g9q%1' RC_xM P jQ<| <#H4J_=ǥ72&$|tdJ/J %sd MgP21||G5!\ufk| Zt" X~u W|eN.r)ƋFoteN:~T`xgo0g+q45U{y*T]rACUaG&c Ee*2).T)շ5s)k PXx \QLeJ5~9h,/EwISA\QDJϡ[=mV;7,[^2ԘDdr޳S %ZL* %" X+~u W|iUCM}笓_6`R֫q&O4f Da}FYITJ7pEZ-~=wKRJf%Z0C7=&3" XMcu W|O0UՏ"u39=פGciSyn GǔMh ^ q{Kg)`yK:oGv**v)CA<^w࣓ }~|4%oYV%PgA-RnsH17a =iwD$xt Vvp@w,M<(u=uۇ|_ xľ0Gz+hIWvq,[ M>_UuvH<WtrJFIFHHDExifII*  (1 2iSAMSUNGSM-N910FHHGIMP 2.8.142015:04:19 15:39:20"0220 |b0100 d2015:04:19 15:20:402015:04:19 15:20:40dd0100  Z@Phttp://ns.adobe.com/xap/1.0/ 2560 1440 SAMSUNG SM-N910F Left-bottom 72 72 Inch N910FXXU1ANK4 2015:04:19 15:20:40 Centered f/1.9 Normal program Exif Version 2.2 2015:04:19 15:20:40 2015:04:19 15:20:40 1.85 EV (f/1.9) Center-weighted average 1.6 mm 98 bytes undefined data FlashPix Version 1.0 sRGB 2560 1440 Auto exposure Auto white balance 22 Portrait C  0:*,#0E=IGD=CALVm]LQhRAC_`hqu{|{J\wmx{vC88vOCOvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv  <9k|K;8㧜=4|JC LAge)U25$$w=,@39|!( )J&iei'{^iIOł* W*,AꫭyNMQQ Sc=@'D5=qz{'rz7Y7SDen2'C͞\MlȵT22=^Ld$FNK$ 鹽ꁅ@0hj@^mw2=Q@1{ 1:9Nc=EY M9:26:ENs.NسXQئR᳷:W3Tՙє Nc#]tXNszf&nj ;joZ\720Z夹T)1賟L/њQ` M7Y;qmfK5) 8VpS:\atFҳ"`$:FJU0$:~Խhepu^w]`HŘH4EPR) %; گz"CK KtZ@G[@ @/yu59\ZPY:z U5;f]ejJ2*QYU8᙭6Y]P"*Jj\  @TܸJWI.RaUTbnvh,LL% !012@A"3$k/FBLSFr%6jgȔkR} j&J}Jri,~U(Ⅳ?jU%V).8oJ.0*?S9ťM4ՄʀUgvhQ[#Bд"$&K-ل  Q+-NtFﰰ %2u MbVI v bj}`݂h9HEʕH탷*' fFd ~?pqULJx߻10QJ+R0-kZ֦FY ð́M*lQiZbպBԧxoKV!r@_bB]vBtd ا-?Jڿg 3FH vnE5SĴftB ,!iPEN &h7z*7mxft16US4fD-4q=i0%lq|JnEJ[( O`8JwU fe2qv (*TAb/E *(+JҴBwe9R$xMf 0ߡ_Y^OKRԧ! ŒF 90@P` ?*.TXQudxn<(=v$)QEq-8F_`?$!0@ 1P`Aaq?mC]jN-u5uY6K\ ;ə5 ocha5%H9~#ɟ%!1 AQq0a@?!$bYdSoЙcСG*c#ND?+lJb N~l t6#}#xjUmwZIK6䇼&d)쟤Aɺb&K7űIv5ɦt3KT21Ee$bE4\%\46cGHJ{v&Y= c< ևKUbX_=ĝ(!1AQa q0?"x ;#Y'U%vex^mԻHBtW1bQ !ssASOdA1fb^; ^P|:X`S㘒ψ"ز)kW(FU^ ek4=yS;ੂګn% };` &j@vj% {PR̰Sp A&%r g_xPީoqx+ Vvڅ%S2x`ߣH(DgNol`^ZF2ZQ;|ZV !u2_\hH X42/NXQt+ =s|SSҩ(eZRR%:eAɨl: %~аx.7QQr @w|j/İd="A7*9ɨ OE!qr NmjU_sZgw[sy p]xMg^KW {J/$[H(n W.4߼#Վ@g{ Z^aVi)[/mlEFRˬ L(u "*cjbjf,RW rٴa@bŶ<T\LB+\_ɨ 07fy_xV! I(0犖9T1 Ca![Zc 8y`CJ J@hzw-HamP15w/UppDlmq. J8 TEmm`HF9-c[˷.V1A.alI4ȵ~D^"ү 3аTܥpՌ[|1,jlNGITRCVu.WiUo]tc 7,Ŏo*)3D''e7W5y蜥kO( ]8Ǵ+q[p"'iR@Jcw2/FǩNؗVHs0Y@ @:0ZAл&?9op M(*L2P-R'Rdl"q(} b-%$CS9wBK$m%AB:t3 '(۫3w*jvy~pek w j"d bgA)C6_\> @)Qf;ͺ.AOib;!b~-\3 uS!P5ItG.30vxh4D;JC[3LJ bXz ʄf^!)bAS|i4Iİe3Q߆G ^!R W֡5a4 lf^_E"9;&]j",=:_Aab Mv]Mpb_Mq,E"7 (}W( EXeTKsKJiL"Oى7!U3    8t_CSɊRWʛ]  =y"c$JD<09 +65b38p9TCw,%01y7K A_)l}5.f 2dύQ p-k";RvtZf,jy>#s_2 eGz҃~NIϫgw`4d ÒdY~ӎXj`߰ၯae 9jj8 VY~RU!z@_đnSRDиs |ٿ=F<<7 TmHGH49JaU 95}P o6ꘔB\lVnm4`Lx8Yˤ?q@ލ_Z\^ci >Ʉtyۧk"g;M PCz ^e?/K0[ϹܛڀNc(\r3AQ١xbP%UWM#>D'#C8+n y5I Dy?{>)Ce4<:3/y@u5 (N-+Ɇc]FkTϗ5n_票2uDps* Iuwpo U{0/%_"V=Fv!T;XDFAH2?@7AJs}͢'Wĩ3;' h<xpBs^P(*W-pq(VI,ⶑP{[Y7N5U_- 7#HR/xN$(L)`~p5S.AW7Qu ,ƂIݳ$}WIǃs]y %ucl*v%Richard Kettlewell 7!V     8tWjt6QqY5ʒV T6I&nmMEd#~I Z􇴨]-G]Ȫ8dHpy> /Nw)܀0gT D(9sqt6cbDq]J#W oyݫXX]:,`u S;e) ަl~6;M_"]+܉:†? 'lqVu 1sN5~Q%l? 6OVn Z`0D2,#ߑ:b{:GA,懽F!Ŏfz/&SuTT (R֝m6CQMWPqRt,?'|0D|b ]n(YaLíz" Wu W|sOwl_^(]>ړ< 1Ӝ]2)D^:[ 1K2<R COʶ^q}Q0Е`:M?)nDKyt @p\meIf:mJo{z+:OaOvI3A_WO#) K? Sd/7V'JK.l= 4;$ȱ s,PDw]xi֬Xg\(-!?" W%Tu W|):Aخfs6{afy I u#q;WP9d\iSd[|Ď?]__Hm<>Z,y~CW[di{Ԝ5B`7azsUL }:Ƙ8BJ34Eheͽ7K~ٍY0zO/E#-ԺV3I1dVOkRW!pt%O՚ $CltG!&d}3t" Wu W|(2;2S7Lh-E(CLD_U151r'!٤*}YU@NTG*8sfIΔ= ޠ?]C4s',(2f_i'٦윘mRK<:GEVP- j^&ﰦ(ٍĖL&N*^A5YR Yfŗ" X~u W|)rЦIB4-N# u1հl1T3E^9Y~^S$VTkK<\}-|N<7oV  -]dH^1.#>YcY_끄O!$Hi)g؎5}Pa9͹ы}7vѦౌ% -K'Z-O]$QB71_<|*ބ@"EL<G#6թr_88" X+~u W|u I~F21e(F&SO:eXy!٪-EnnPWz0 -࿲/j:F+tGY-$R>3.^%QX̼jip7sCh7-PSϹRuv,-gLjBf6q7%t 2ܹd|-` Đ X >Z%@ !#@ue,̉" X_u W|ČL^Tٙj-I/Y[y=J5RIx-Ijw\c~iSl*kY]9ږn+SX`Bt2a61_cEիTU>jhoK*\'z7^\JDx"ʋ*|nH"U\jdS}䞴)#Aڄ٢I)ҧrh9C9EjҰfK䠗YXf" Xqu W|e^whwҔpǦ9AolҤHBP;tF9+0H;RAowm lLkW.d)L]߽TPZThaGx }m,[΅!yJZZӏ;eg[4];18 Bs,'o4)!m'v U1$ e].cW%H}cќaԇW$`MW@'z0Cn*W{O" XMcu W|ry3W:w@\zЂM_47p,plD3deӨQA6Dm2 •bLeR?/i]2ǭX.f@g gj,{[". 0&؅p4]] @SdI[.U>}yAKBl7> /l*uڮq/ <_nLߤw^82;Bjvx c" Xu W|@OaV)MH\8%g0^٠QZx͘F\c9FSkPJ~0,'Jĸ^S #ͦ%a}=5oq 3d*m" Xu W|ѝ(,'(n^s$++R7ъ4Y2ai_\1;@"J3rOQ"hZ+ |l'jlPJ/TbmjR`QeThK'&#"ͱN>Q-%]9%c(66r ؘF|ۭ+TB\&[3b|$yjd_zϿn6O\zˁL{#{v;{ a]<4qVPpJ+AfTDc&Richard Kettlewell Vf Z` j休Tqp~E噝wYGplJ0']5ҡRQ.t LHڟ%'A0F(8@w^HYuQ#(RNMwY !t;|p'1U(%ơFmX#Hx634/$Q|B/{Ђz(mھ\ץVxՉ!%vb@@lƨ}WkLVM]b3杸Fnjr{7!V/|    8ttAͰx'ݞ`w|)y VH3m39?&ky04א0VeI/*џ?wwfô܍8(|51 O6ik8H1P(eÍ٤5nv iqOBj+Pqdn͇Pԁ=C jWrM¢4hx(X"د'(jQ-R|q$Fv9aeoH'Richard Kettlewell Vf Z`3tZl9@ArCyM"m ?NR"+ֵxi0c|YA履FLhhԼkk;eEww˜Ӈdw_4@fT @6*bYKU|B0R}ҹ]FGЕ2/u2cU;90q\Mμ˴džI`C@.)#KWH*;V.Ye<0`̠ e7!V/    8trWB?5-2!0|sk+su+!;:߅|0E0[_ @K)M OS~l(~v*YPh^k^Ycs_/5eGADtS#wrwC_. g/d}al[K|Rh#X 3 F)3sZc$a1pA`Ъ^z'i Qbl(Richard Kettlewell 7!Vo#    8tcr*S 1~ʷՓړF?!JJ]VD>i'Eځ1lCrHerH.ESUW@ aWtElt4 ݹVpÈDpҮLػVuOxzCO3- >!7P~lRut,;B(zn 21 R|Ue: /=/ػbrFf@62l3E&  btaX$Richard Kettlewell 7!V"-z    8t=J2@q*F> Evp)c=}$lj툕jfbE_D%Z噖B9?eT"ZFҢD- lvå@`$Cs+E֝1~\`J*rܸ4c(!'7&?#l 7!W|^9eAA`Y <|T sH]a,U9]GwV?_l [ o>U x5WpJ  KҨC>@6&-B`kWmY5.Jjtt҂6!D=MN\z݅yIFaÛ9B(x7u rY3|g{][4A=!Т;?dz]M"gDt!pINoMf;81'~mLT\S;Y8)E2,na8d_Ä'JȔ~@{m|k&}]XFsh5MJ> e^hq@! 'feqM_= Jt7}Ƿ) vW9̝nbY\=[ZZВr( t"UV^=LP[hF2̀8#xT_Įm]" !yg⪣2mf$.ޓ=Ʌ,QT`{3Cwh$&řS3 UxRE4OX]&ÈT3dOi֐,9z >k_b `z KҨ[zh,#b_ 'PKN9m+?))/%T:^| Ib j-07$CCAL{$w-nc*jwB<W6&£d[KX` 1ˣùj-F=/g5mCQU-iTK{fl jYmrt.Ӭ4I ,o(?Ξ@ ^e@Xځ]aEֹ#ԇs KҨ[ 8tqR<>9)d8c%ThKEoڠяOzeh\<4Z- Pn~.cQG/S E UL)F،^qgW*R@%:P8l{L0)Ȫ)m/!>tMW3k>#ԋ8-r?!WfvwnR Gٴs穾C<2 n2d՗wM=61@߹DCG*8 rrsbackup-10.0/debian/watch000066400000000000000000000001521440730431700155200ustar00rootroot00000000000000version=4 opts=pgpsigurlmangle=s/$/.asc/ https://www.greenend.org.uk/rjk/rsbackup/ rsbackup-(.+)\.tar\.gz rsbackup-10.0/doc/000077500000000000000000000000001440730431700140145ustar00rootroot00000000000000rsbackup-10.0/doc/CHANGES.md000066400000000000000000000525321440730431700154150ustar00rootroot00000000000000# Changes To rsbackup Please see [rsbackup in git](https://github.com/ewxrjk/rsbackup) for detailed change history. ## Changes In rsbackup 10.0 * Pass `--open-noatime` to rsync by default. Fixes [issue #101](https://github.com/ewxrjk/rsbackup/issues/101). * `rsbackup-snapshot-hook` now uses `dmsetup` rather than `udevadm` to query logical volumes, which should stop it spuriously rejecting some of them. [Fixes issue #72](https://github.com/ewxrjk/rsbackup/issues/72). * `rsbackup-snapshot-hook` should now cope with volume group names containing hyphens. * A volume can be temporarily opted out of hardlinking between backups, using a `.nolink` file. * **Incompatible change**: `rsync-timeout`, the old name for `backup-job-timeout`, has been removed. ## Changes In rsbackup 9.0 * A new `--check-unexpected` option lists unexpected files found on backup devices. Fixes [issue #95](https://github.com/ewxrjk/rsbackup/issues/95). * Underway backups are properly logged in the database. Fixes [issue #94](https://github.com/ewxrjk/rsbackup/issues/94). * Improved new device documentation. Fixes [issue #93](https://github.com/ewxrjk/rsbackup/issues/93). * New `--latest` option to find the latest available backup. Fixes [issue #92](https://github.com/ewxrjk/rsbackup/issues/92). * Number parsing in the config file was slightly tightened. Fixes [issue #99](https://github.com/ewxrjk/rsbackup/issues/99). * The database is updated more quickly after a backup is pruned. Fixes [issue #98](https://github.com/ewxrjk/rsbackup/issues/98). * **Incompatible change**: The old names for hook directives are removed. `pre-` and `post-access-hook` must be replaced with `pre-` and `post-device hook`, and `pre-` and `post-backup-hook` must be replaced with `pre-` and `post-volume-hook`. * **Incompatible change**: The parameters for `decay-limit`, `decay-start`, `decay-window`, `hook-timeout`, `keep-prune-logs`, `max-age`, `min-interval`, `prune-age`, `prune-logs`, `rsync-timeout` and `ssh-timeout` now _require_ the units suffix introduced in release 7.0. * Old logfiles from releases before 2.0 are no longer detected. * Pruning logs created by versions of rsbackup older than 2.0 are no longer automatically removed. ## Changes In rsbackup 8.0 * The names of hosts, volumes, devices and stores are now ordered in a way that respects integer values embedded in them. Fixes [issue #76](https://github.com/ewxrjk/rsbackup/issues/76). * A new `rsync-remote` directive allows control over the remote rsync command. * The pruning log in the backup report is now displayed in a less verbose form. Fixes [issue #69](https://github.com/ewxrjk/rsbackup/issues/69). * Warnings about unknown hosts and volumes are more informative. Fixes [issue #79](https://github.com/ewxrjk/rsbackup/issues/79). * The `rsync --link-dest` option is used in a more subtle way. The most recent complete backup is always used if possible, but if there are more recent incomplete backups then the latest of those is used as well. Fixes [issue #77](https://github.com/ewxrjk/rsbackup/issues/77). * The `decay-scale` prune parameter is no longer constrained to be an integer. Fixes [issue #87](https://github.com/ewxrjk/rsbackup/issues/87). * The `rsync-timeout` directive has been renamed `backup-job-timeout`. **Advance warning**: In some future version the old name will be removed. * A new `rsync-io-timeout` directive is added, corresponding to the `rsync --timeout` directive. Fixes [issue #80](https://github.com/ewxrjk/rsbackup/issues/80). * `rsbackup-graph --fonts` will list known font families. Fixes [issue #81](https://github.com/ewxrjk/rsbackup/issues/81). * Documentation around `--forget-only` improved. Fixes [issue #90](https://github.com/ewxrjk/rsbackup/issues/90). * Prunes interrupted by the `prune-timeout` directive only generate diagnostics if `--verbose` is used. Fixes [issue #89](https://github.com/ewxrjk/rsbackup/issues/89). * Subprocess execution no longer leaks file descriptors. Fixes [issue #83](https://github.com/ewxrjk/rsbackup/issues/83). * Stale material in the man page for `rsbackup.cron` removed. Fixes [issue #88](https://github.com/ewxrjk/rsbackup/issues/88). ## Changes In rsbackup 7.0 * The `--force` option is extended to override backup policies. * In `/etc/rsbackup/defaults`, the `hourly`, `daily`, `weekly` and `monthly` settings are gone. Instead, use backup policies. Fixes [issue #59](https://github.com/ewxrjk/rsbackup/issues/59). * The hook directives are renamed, to (`pre`,`post`)`-`(`device`,`volume`)`-hook`. **Advance warning**: In some future version the old names will be removed. * `pre-volume-hook` and `post-volume-hook` can be suppressed (e.g. in a volume that has inherited a hook from its host) by passing an empty command.. Fixes [issue #71](https://github.com/ewxrjk/rsbackup/issues/71). * `RSBACKUP_STATUS`, `RSBACKUP_DEVICE` and `RSBACKUP_STORE` are no longer provided to volume hooks. `pre-volume-hook` output is no longer logged as part of a backup record. * `pre-volume-hook` is now run only once, before all backups of a volume, and `post-volume-hook` is now run only once, after all backups of a volume. Fixes [issue #17](https://github.com/ewxrjk/rsbackup/issues/17). * A new `rsync-link-dest` directive allows use of the `rsync --link-dest` option to be suppressed, for instance to deal with volumes which only have constantly changing files. Fixes [issue #70](https://github.com/ewxrjk/rsbackup/issues/70). * The parameters for `decay-limit`, `decay-start`, `decay-window`, `hook-timeout`, `keep-prune-logs`, `max-age`, `min-interval`, `prune-age`, `prune-logs`, `rsync-timeout` and `ssh-timeout` now include a units suffix. **Advance warning**: In some future version the suffix will be mandatory. * A new `prune-timeout` directive allows an upper bound to be placed on the amount of time spent pruning, to avoid pruning 'crowding out' other activity. Fixes [issue #66](https://github.com/ewxrjk/rsbackup/issues/66). * **Incompatible change**: old logfiles from releases before 2.0 are no longer upgraded. Instead, if such logfiles are detected, an error is reported. You must use a release between 2.0 and 6.0 to upgrade such log files before version 7.0 will run. In a future release even the detection of the old logfiles will be removed. * **Incompatible change**: the `always-up` directive has been removed. Instead, use `host-check always-up`. ## Changes In rsbackup 6.0 * **Incompatible change**: the `min-backups` and `prune-age` directives have been removed. Instead, use `prune-parameter min-backups` and `prune-parameter prune-age`. * **Incompatible change**: the arguments to the `public`, `always-up` and `check-mounted` directives are now mandatory. * **Incompatible change**: the deprecated `colors` and `report-prune-logs` directives have been removed. Use `colors-good`, `colors-bad` and `report` instead. * **Incompatible change**: the `exec` prune policy now uses absolute timestamps instead of ages. This is a side-effect of fixing [issue #49](https://github.com/ewxrjk/rsbackup/issues/49) (see below). * Concurrent backup of distinct hosts to distinct devices. See the `group` directive for fine-grained control and [issue #18](https://github.com/ewxrjk/rsbackup/issues/18) for discussion. * More conservative CSS syntax is used, improving interoperability with AquaMail. Fixes [issue #52](https://github.com/ewxrjk/rsbackup/issues/52). * Line lengths in encoded images are bounded, improving interoperability with Exim. Fixes [issue #53](https://github.com/ewxrjk/rsbackup/issues/53). * Images in emails use attachments rather than inline `data:` URLs. This improves interoperability with GMail. Fixes [issue #54](https://github.com/ewxrjk/rsbackup/issues/54). * New `database` directive controls the database filename. Fixes [issue #55](https://github.com/ewxrjk/rsbackup/issues/55). * The backup size is now included in the report. Fixes [issue #51](https://github.com/ewxrjk/rsbackup/issues/51). * Backups can now happen more than once per day. See the `backup-policy` directive for fine-grained control over backup frequency. The default remains one backup per day but backup filenames have changed to reflect the possibility of more than one. Fixes [issue #49](https://github.com/ewxrjk/rsbackup/issues/49). * Removal of old prune logs was broken and has been re-enabled, and volume retire no longer tries to remove pruned backups. Fixes [issue #56](https://github.com/ewxrjk/rsbackup/issues/56). ## Changes In rsbackup 5.1 * Store directories are now normally required to be mount points. See the description of `store` and `store-pattern` in [rsbackup(5)](rsbackup.5.html) and `--unmounted-store` in [rsbackup(1)](rsbackup.1.html) for options to restore the previous behavior. Fixes [issue #42](https://github.com/ewxrjk/rsbackup/issues/42). * Minor build fixes. ## Changes In rsbackup 5.0 * **Incompatible change**: Configuration file parsing has changed slightly, with stricter rules about indentation. See [rsbackup(5)](rsbackup.5.html) for details. * **Incompatible change**: The default location for snapshots has changed to `/var/lib/rsbackup/snapshots`. Fixes [issue #28](https://github.com/ewxrjk/rsbackup/issues/28). **If you use snapshots you must adjust your configuration.** * New `host-check` directive controlling how to test whether hosts are up or down. Fixes [issue #26](https://github.com/ewxrjk/rsbackup/issues/26). **Advance warning**: the old `always-up` directive is now deprecated and will produce a warning. In some future version it will be removed. * ACLs and extended attributes are now backed up. Note that the options used assume a modern version of `rsync`, and are not supported by the version installed under macOS; also this feature can also cause some trouble with Windows filesystems. Set the `rsync-extra-options` as discussed in [rsbackup(5)](rsbackup.5.html) to work around this. Fixes [issue #37](https://github.com/ewxrjk/rsbackup/issues/37) and [issue #41](https://github.com/ewxrjk/rsbackup/issues/41). * The `--retire` option now always requests confirmation from the user. Fixes [issue #38](https://github.com/ewxrjk/rsbackup/issues/38). * New `--forget-only` option used with `--retire` to drop database records for backups without deleting the backups themselves. Fixes [issue #41](https://github.com/ewxrjk/rsbackup/issues/41). * `pre-backup-hook` scripts may now exit with a distinct exit status to indicate a transient failure, equivalent to a `check-file` or `check-mounted` test failing. Addresses [issue #43](https://github.com/ewxrjk/rsbackup/issues/43). ## Changes In rsbackup 4.0 * A new tool, `rsbackup-graph`, has been introduced. This generates a graphical representation of available backups. * The `colors` directive is now split into `colors-good` and `colors-bad` which can take either RGB or HSV parameters. **Advance warning**: the old `colors` directive is now deprecated and will produce a warning. In some future version it will be removed. * Report contents can now be parameterized using the new `report` directive. **Advance warning**: the old `report-prune-logs` directive is now deprecated and will produce a warning. In some future version it will be removed. * Configuration file documentation has been moved to a new man page, [rsbackup(5)](rsbackup.5.html). * Various minor bugs have been fixed. ## Changes In rsbackup 3.1 * Don’t throw exceptions from destructors. Addresses [Debian bug #811705](https://bugs.debian.org/811705). * Fix error handling in character encoding translation. * Patch from Maria Valentina Marin to use consistent mtimes during `.deb` build. Fixes [Debian bug #793716](https://bugs.debian.org/793716). * Stop cron scripts exiting nonzero if `.deb` is removed. Fixes [Debian bug #810335](https://bugs.debian.org/810335). * Patch from Jonathon Wiltshire to use `install` rather than `cp` during `.deb` builds. * Correct distribution of scripts. * Add missing `.deb` build dependencies. ## Changes In rsbackup 3.0 * Pruning now supports selectable, and pluggable, pruning policies. See the `PRUNING` section of the man page for further information. The default behavior matches previous versions. [Fixes issue #7](https://github.com/ewxrjk/rsbackup/issues/7). **Advance warning**: the `min-backups` and `prune-age` directives are now deprecated in their current form and will produce a warning. In some future version they will be removed. Instead, use `prune-parameter min-backups` and `prune-parameter prune-age`. * **Advance warning**: the `public`, `always-up`, `check-mounted` and `traverse` directives now take an explicit boolean argument. Using them without an argument is now deprecated (but has not changed in meaning). In some future version the argument will become mandatory. * Removal of backups (when pruning, or retiring a volume) now parallelizes removal across devices. [Fixes issue #24](https://github.com/ewxrjk/rsbackup/issues/24). * The `rsync-timeout` and `hook-timeout` directives are now inherited, as documented. `ssh-timeout` becomes inherited too. The `sendmail` directive is now documented. * Host and volume names may no longer start with “`-`”. * `--dump-config --verbose` now annotates its output. Some options missed by `--dump-config` are now output. * A C++11 compiler and Boost are now required. ## Changes In rsbackup 2.1 * `rsbackup.cron` will always run the prune and report steps, even if the earlier steps fail. * `rsbackup-snapshot-hook` copes better with aliases for logical volumes. [Fixes issue #23](https://github.com/ewxrjk/rsbackup/issues/23). * Pruning logs in the report are now limited by the `report-prune-logs` configuration setting. ## Changes In rsbackup 2.0 * **Incompatible change**: pre-backup and post-backup hooks are now run even in `--dry-run` mode. The environment variables `RSBACKUP_ACT` can be used by the script to distinguish the two situations. `rsbackup-snapshot-hook` has been modified accordingly. [Fixes issue #9](https://github.com/ewxrjk/rsbackup/issues/9). * **Incompatible change**: The log format has changed. The old per-backup logfiles are gone, replaced by a SQLite database. Old installations are automatically upgraded. [Fixes issue #11](https://github.com/ewxrjk/rsbackup/issues/11). * New `check-mounted` option verifies that a volume is mounted before backing it up. [Fixes issue #13](https://github.com/ewxrjk/rsbackup/issues/13). * New `store-pattern` option allows stores to be specified by a glob pattern instead of individually. [Fixes issue #5](https://github.com/ewxrjk/rsbackup/issues/5). * New `stylesheet` and `colors` options allow operator control of the stylesheet and coloration in the HTML version of the report. [Fixes issue #6](https://github.com/ewxrjk/rsbackup/issues/6). * The semantics of `lock` are now documented. [Fixes issue #20](https://github.com/ewxrjk/rsbackup/issues/20). * Shell scripts supplied with `rsbackup` no longer depend on Bash. * Dashes are now allowed in hostnames. [Fixes issue #21](https://github.com/ewxrjk/rsbackup/issues/21). * The order in which hosts are backed up can now be controlled with the `priority` option. [Fixes issue #19](https://github.com/ewxrjk/rsbackup/issues/19). * Reports now include counts of various error/warning conditions in the summary section; email reports reflect these in the subject line. The `always-up` option is slightly modified: backups of always-up hosts are attempted, resulting in error logs, even if they do not seem to be available. [Fixes issue #22](https://github.com/ewxrjk/rsbackup/issues/22). * New `--database` option allows the path to the database to be overridden. ## Changes In rsbackup 1.2 * Quoting and completeness fixes to `--dump-config` option. * OSX builds work again. * The cron scripts no longer attempt to run `rsbackup.cron` when it has been removed. Fixes [Debian bug #766455](https://bugs.debian.org/766455). * Some fixes to Debian packaging. ## Changes In rsbackup 1.1 * Error messages about missing unavailable devices with `--store` are now more accurate. [Fixes issue #10](https://github.com/ewxrjk/rsbackup/issues/10). * The `include` command now skips filenames that start with `#`. [Fixes issue #12](https://github.com/ewxrjk/rsbackup/issues/12). * The command-line parser now rejects invalid host and volume names (rather than accepting ones that will never match anything). Zero-length device, host and volume names are now rejected (in all contexts). * The test suite has been expanded, and supports concurrent execution if a sufficiently recent version of Automake is used. [Fixes issue #14](https://github.com/ewxrjk/rsbackup/issues/14). * `rsbackup-snapshot-hook` no longer fails if `fsck` finds and fixes errors. It is also now tested. [Fixes issue #15](https://github.com/ewxrjk/rsbackup/issues/15). ## Changes In rsbackup 1.0 * New `--dump-config` option to verify configuration file parse. * New `--check` option to `rsbackup-mount`. * Configuration files are now read in a fixed order ([issue #8](https://github.com/ewxrjk/rsbackup/issues/8)). * The `--force` option no longer implies the `--verbose` option. (This was a bug.) * Minor bug fixes. ## Changes In rsbackup 0.4.4 * Correct `RSBACKUP_STATUS` value passed to post-backup hook. (Bug spotted by Jacob Nevins.) ## Changes In rsbackup 0.4.2 * `--retire` no longer fails if a host directory has already been removed. * Fixed recalculation of per-device backup counts, visible as self-inconsistent reports when generated in the same invocation of `rsbackup` as some other operation. ## Changes In rsbackup 0.4.1 * Fix a crash with the `--html` option (Jon Amery). * Fix to `--prune-incomplete` option, which wouldn't work in the absence of some other option (Jacob Nevins). ## Changes In rsbackup 0.4 * The new `pre-access-hook` and `post-access-hook` options support running “hook” scripts before and after any access to backup storage devices. * The new `pre-backup-hook` and `post-backup-hook` options support running “hook” scripts before and after a backup. Although these can be used for any purpose, the motivation is to enable the creation of LVM snapshots of the subject filesystems (and their destruction afterwards), resulting in more consistent backups. The supplied hook script only knows about the Linux logical volume system. * The new `devices` option allows a host or volume to be restricted to a subset of devices, identified by a filename glob pattern. * The new `rsync-timeout` option allows a time limit to be imposed on a backup. * The new `check-file` option allows backups of a volume to be suppressed when it is not available (for instance, because it is only sometimes mounted). * `--verbose` (and therefore `--dry-run`) is now more verbose. * `--text` and `--html` now accept `-` to write to standard output. * Improved error reporting. * Minor bug fixes and portability and build script improvements. * `rsbackup-mount` now supports unencrypted devices and separate key material files (contributed by Matthew Vernon). ## Changes In rsbackup 0.3 * `--prune` honours command-line selections again. * The “oldest” backup for a host with no backups now shows up as “none” rather than “1980-01-01”. * The new `--logs` option controls which logfiles are included in the HTML report. The default is to only include the logfile of the most recent backup if it failed. Also, if the most recent attempt to backup a volume to a given device failed, its heading is highlighted (in red). * The tool scripts now have proper `--version` options. Single-letter command line options are now supported (in fact they existed in many cases already but weren’t documented). * Retiring a volume no longer makes a (futile and harmless!) attempt to remove `.` and `..`. * The `.incomplete` files used by the Perl script to indicate partial backups are now created by the C++ version too. They are created both before starting a backup and before pruning it. rsbackup itself does not rely on them itself but they are an important hint to the operator when doing bulk restores. * Logfiles of backups where pruning has commenced are now updated to reflect this, so that they will not be counted as viable backups in the report. * Error output from failed backups is now more visible. The old behaviour can be restored with the `--no-errors` option. * Missing or misconfigured stores are now reported in more detail. If it looks like a store is present but misconfigured (for instance, wrong permissions), this is always reported as an error. If it looks like the store is absent then this is only reported if `--warn-store` is given, but if _no_ configured store is present then the problems found with all configured stores are listed. The documentation on how to set up stores has also been clarified. * Prune logs now include detail about why a backup was eligible for pruning. ## Changes In rsbackup 0.2 `rsbackup` has been rewritten in C++. The behaviour is largely same except as follows: * New `--text` option generates a plaintext version of the report. In addition the email report includes both the text and HTML versions. * `--prune-unknown` is removed. It is replaced by `--retire`, which is used to remove backups of volumes (and hosts) that are no longer in use and `--retire-device` which is used to remove logs for devices that are no longer in use. * The `rsync` command now includes the `--delete` option, meaning that interrupted backups no longer include stray files from the first attempt. * `.incomplete` files are no longer created. Instead the logs are used to distinguish complete from incomplete backups. * Various `--warn-` options to control what is warned about. * New `always-up` option to indicate that a host is expected to always be available to back up. * There are now test scripts. rsbackup-10.0/doc/CHANGES.prefix.html000066400000000000000000000003771440730431700172550ustar00rootroot00000000000000 Changes To rsbackup rsbackup-10.0/doc/Makefile.am000066400000000000000000000033171440730431700160540ustar00rootroot00000000000000# Copyright © 2011, 2012, 2014, 2015 Richard Kettlewell. # # 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 . man_MANS=rsbackup.1 rsbackup.cron.1 rsbackup-mount.1 rsbackup-snapshot-hook.1 \ rsbackup.5 rsbackup-graph.1 EXTRA_DIST=$(man_MANS) rsbackup-debian.html disk-encryption.html \ rsbackup-docs.html \ rsbackup-manual.in.html rsbackup.css CHANGES.md testing.md \ CHANGES.prefix.html README.prefix.html \ partial-transfer.md MANHTML=rsbackup.1.html rsbackup.cron.1.html rsbackup-mount.1.html rsbackup-snapshot-hook.1.html rsbackup.5.html rsbackup-graph.1.html all: rsbackup-manual.html html: $(MANHTML) CHANGES.html README.html rsbackup-manual.html: rsbackup-manual.in.html Makefile rm -f $@.new sed 's/_version_/${VERSION}/g' < ${srcdir}/rsbackup-manual.in.html > $@.new chmod 444 $@.new mv -f $@.new $@ %.html: % $(top_srcdir)/scripts/htmlman $^ CHANGES.html: CHANGES.md exec > $@ && cat CHANGES.prefix.html && markdown --html4tags $^ README.html: ../README.md exec > $@ && cat README.prefix.html && markdown --html4tags $^ clean-local: rm -f *.new CHANGES.html README.html distclean-local: rm -f rsbackup-manual.html rm -f $(MANHTML) rsbackup-10.0/doc/README.prefix.html000066400000000000000000000003731440730431700171360ustar00rootroot00000000000000 rsbackup README rsbackup-10.0/doc/disk-encryption.html000066400000000000000000000171601440730431700200310ustar00rootroot00000000000000 Setting Up An Encrypted Disk

Setting Up An Encrypted Disk

This isn’t part of rsbackup proper, but since you might well want to encrypt your backups, the setup process is sketched here.

Threat Model

The main threat I’m interested in protecting against is that a petty criminal comes into possession of my backup disk either by stealing it from an offsite location, or because I manage to lose it somehow. I don’t want them able to read my email or gain access to website passwords (of which I have far too many to remember, and most of them don’t protect anything especially important).

I’m not especially interested in protecting my backups from the NSA. If an organisation with government-level resources were to take an interest in me, then realistically they have better tools available than stealing my backups.

The reason I document this is that, if you are interested in defending against better-resourced attacks than I am (for instance if you live in a country with a highly repressive government), the details below may not be suitable for you.

The Tools

The device mapper is the Linux kernel’s framework for creating virtual block devices. In this case, we are interested in creating a virtual block device that has the plain text corresponding to encrypted data on a physical block device.

dm-crypt is the low-level means of achieving this: writes to the virtual block device are encrypted and forwarded to the underlying physical device, and similarly reads from the virtual block devices are forwarded to the physical device and the content decrypted. The plain text will not be available if the correct key has not been supplied.

Linux Unified Key Setup (LUKS) is a specific encryption scheme that we’ll use.

cryptsetup is a command line tool we’ll use to set things up.

Setting Up An Encrypted Device

First create the partition to encrypt, using your favourite disk partitioner, lvcreate, or whatever. Any pre-existing contents will be destroyed, so take a backup if there is anything important there. I’ll assume below that the device name for the partition it /dev/sdb1, but obviously you should change this to whatever device you are using.

To create the LUKS data structures and establish a key:

# cryptsetup luksFormat /dev/sdb1

WARNING!
========
This will overwrite data on /dev/sdb1 irrevocably.

Are you sure? (Type uppercase yes): YES
Enter LUKS passphrase:
Verify passphrase:
Command successful.

As you can see, you must choose a passphrase. The default encryption key is 128 bits long, so it’s worth using a longer password than the traditional 8 characters, provided you can actually remember it. (A password you can’t remember is no use whatsoever.)

Note: cryptsetup also supports reading the key from a file. If keeping the key file safe somewhere (a couple of well-hidden USB sticks, say) is easier than remembering a suitably long passphrase, that might be more appropriate.

Note: cryptsetup offers a variety of cipher specifications. You may wish to review the available options and consult the cryptsetup FAQ rather than accepting the default.

At this point the encrypted partition exists but does not have any filesystem in it and the underlying plaintext is not accessible. It’s possible to detect the format:

# cat /dev/sdb1 | file -
/dev/stdin: LUKS encrypted file, ver 1 [aes, cbc-essiv:sha256, sha1] UUID: c3ad50a5-a379-4e72-9f92-cacf592

The next step is to create a virtual block device with the plaintext:

# cryptsetup luksOpen /dev/sdb1 backup3
Enter LUKS passphrase:
key slot 0 unlocked.
Command successful.
# ls -l /dev/mapper/backup3
brw-rw---- 1 root disk 254, 7 2010-03-14 15:54 /dev/mapper/backup3

You will need to re-enter the passphrase you chose earlier.

At this point you can create a filesystem:

# mkfs -j -Lbackup3 /dev/mapper/backup3
mke2fs 1.41.3 (12-Oct-2008)
Filesystem label=backup3
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
62336 inodes, 248870 blocks
12443 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=255852544
8 block groups
32768 blocks per group, 32768 fragments per group
7792 inodes per group
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376

Writing inode tables: done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 29 mounts or
180 days, whichever comes first.  Use tune2fs -c or -i to override.

When I did this on a 4TB USB2-attached disk, it took about 40 minutes. So you might want to go away and do something else.

By default the filesystem will be regularly fsck’d. You can suppress this, if you want:

# tune2fs -c0 -i0 /dev/mapper/backup3

It’s now possible to mount the new filesystem:

# mount /dev/mapper/backup3 /mnt
# really ls -l /mnt
total 16
drwx------ 2 root root 16384 2010-03-14 15:55 lost+found
richard@araminta:~$ df -h /mnt
Filesystem             Size  Used Avail Use% Mounted on
/dev/mapper/backup3    957M  1.2M  908M   1% /mnt

So now you can create files, take backups, etc.

Note that just because the disk is encrypted does not imply that other users of the system cannot get at its contents while it’s mounted. The normal file permission rules apply.

To unmount and detach the disk:

# umount /mnt
# cryptsetup luksClose backup3
rsbackup-10.0/doc/partial-transfer.md000066400000000000000000000026341440730431700176210ustar00rootroot00000000000000Partial Transfer ================ What is the ‘partial transfer’ warning about? It corresponds to an error status of 24 from `rsync`, which documents it as ‘Partial transfer due to vanished source files’. Within `rsync`, this is: #define RERR_VANISHED 24 /* file(s) vanished on sender side */ ...and: { RERR_VANISHED , "some files vanished before they could be transferred" }, ...and: /* VANISHED is not an error, only a warning */ if (code == RERR_VANISHED) { rprintf(FWARNING, "rsync warning: %s (code %d) at %s(%d) [%s=%s]\n", name, code, file, line, who_am_i(), RSYNC_VERSION); } else { rprintf(FERROR, "rsync error: %s (code %d) at %s(%d) [%s=%s]\n", name, code, file, line, who_am_i(), RSYNC_VERSION); } It is set during cleanup: if (exit_code == 0) { if (code) exit_code = code; if (io_error & IOERR_DEL_LIMIT) exit_code = RERR_DEL_LIMIT; if (io_error & IOERR_VANISHED) exit_code = RERR_VANISHED; if (io_error & IOERR_GENERAL || got_xfer_error) exit_code = RERR_PARTIAL; } `IOERR_VANISHED` is set in three places. The general idea seems to be that the file (or link or whatever) formerly existed (e.g. in a directory listing) but has now gone away. So this reflects either a backup target changing ‘underfoot’, or strange filesystem semantics. rsbackup-10.0/doc/rsbackup-debian.html000066400000000000000000000101571440730431700177400ustar00rootroot00000000000000 rsbackup and Debian

rsbackup and Debian

The Debian package of rsbackup has some extra features. You could use these on other systems but on Debian they are set up to work automatically.

/etc/rsbackup/config

In the .deb package, the default /etc/rsbackup/config is as follows:

# Location of lockfile
lock /var/run/rsbackup.lock

# User configuration
include /etc/rsbackup/local

# Hosts
include /etc/rsbackup/hosts.d

Normally you would not edit this file. Instead, put local configuration in /etc/rsbackup/local and a file for each host to back up into /etc/rsbackup/hosts.d.

/etc/rsbackup/defaults

There is an extra configuration file for the cron jobs, /etc/rsbackup/defaults. The default is as follows:

#
# Set backup=hourly|daily|weekly|monthly to control frequency of
# backup attempts. (Use backup policies for fine-grained control over
# when backups happen.)
#
backup=hourly

#
# Set report=hourly|daily|weekly|monthly to control frequency of
# email reports.  (Hourly is probably a bit much!)  Only effective
# if email is not "".
#
report=daily

#
# Set email=ADDRESS to have the report emailed to that address.
#
email=root

#
# Set prune=hourly|daily|weekly|monthly|never to control frequency of
# automated pruning of old backups
#
prune=daily

#
# Set prune_incomplete=hourly|daily|weekly|monthly|never to control
# frequency of automated pruning of incomplete backups
#
prune_incomplete=weekly

#
# Prefix to the rsbackup command
# Use 'nice' and/or 'ionice' here
#
nicely=
  • Set backup to control the frequency of backup attempts. Normally this can be left at hourly and the frequency of each host and volume’s backups controlled via backup policies.

  • Set email to the destination address for reports from the daily backup run, or comment out the line entirely to suppress email reports. If you want reports at some other frequency than daily, modify report.

  • Modify prune to control the frequency of pruning of old backups. It is recommended to leave this as daily, since deleting a week or more’s worth of backups takes a very long time.

  • Modify prune_incomplete to control the frequency of deleting incomplete backups. It is recommended to keep this at a lower frequency than you take backups, as otherwise rsync will not be able to use incomplete backups to optimize new ones.

  • Modify nicely to control the priority of rsbackup. For example, you might use:

    nicely="nice ionice -3"
rsbackup-10.0/doc/rsbackup-docs.html000066400000000000000000000042501440730431700174430ustar00rootroot00000000000000 rsbackup

rsbackup

Getting Started

Reference Information

Miscellaneous

  • README
  • Change history
  • rsbackup home page
  • rsbackup on Github
    • rsbackup-10.0/doc/rsbackup-graph.1000066400000000000000000000045051440730431700170130ustar00rootroot00000000000000.TH rsbackup-graph 1 .\" Copyright (c) 2011, 2012, 2014, 2015 Richard Kettlewell .\" .\" 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 . .SH NAME rsbackup-graph \- graphical representation of rsbackup state .SH SYNOPSIS \fBrsbackup\-graph\fR [\fIOPTIONS\fR] [\fB\-\-\fR] [\fISELECTOR\fR...] .SH DESCRIPTION \fBrsbackup\-graph\fR generates a graphical representation of the current set of backups known to \fBrsbackup\fR. .PP For configuration options which affect this program, see the section \fBGraph Directives\fR in \fBrsbackup\fR(5). .SH OPTIONS .TP .B \-\-config \fIPATH\fR, \fB\-c \fIPATH The path to the configuration file. The default is .IR /etc/rsbackup/config . .TP .B \-\-database\fR, \fB\-D \fIPATH Override the path to the backup database. .TP .B \-\-output\fR, \fB\-o \fIPATH Set the output path. To write to standard output, use \fB\-o -\fR. The default is \fIrsbackup.png\fR. .TP .B \-\-fonts\fR, \fB-F Lists the known font families to standard output. .TP .B \-\-help\fR, \fB\-h Display a usage message. .TP .B \-\-version\fR, \fB\-V Display the version number. .SS "Volume Selection" The list of selectors on the command line determines what subset of the known volumes are displayed. The following selectors are possible: .TP 16 .I HOST Select all volumes for the host. .TP .IR HOST : VOLUME Select the volume. .TP .BI \- HOST Deselect all volumes for the host. .TP .BI \- HOST : VOLUME Deselect the volume. .TP .B * Select all volumes. .PP If no hosts or volumes are specified on the command line then all volumes are selected for display. .SH "SEE ALSO" \fBrsbackup\fR(1), \fBrsbackup.cron\fR(1), \fBrsbackup\-mount\fR(1), \fBrsbackup\-snapshot\-hook\fR(1), \fBrsync\fR(1), \fBrsbackup\fR(5) .SH AUTHOR Richard Kettlewell rsbackup-10.0/doc/rsbackup-manual.in.html000066400000000000000000000775501440730431700204120ustar00rootroot00000000000000 rsbackup

      rsbackup

      Contents

      1. Overview

      rsbackup backs up your computer(s) to removable hard disks. The backup is an ordinary filesystem tree, and hard links between repeated backups are used to save space. Old backups are automatically pruned after a set period of time.

      This guide describes how to set up and manage rsbackup. See the man pages for the command and the configuration file for detailed reference information.

      2. Setting Up

      2.1 Installation

      The systems you want to back up are called clients. The system that has the backup hard disk(s) attached to it is called the server, and it is on this system that the rsbackup program runs. The server can itself be a client.

      Each client must have an SSH server and rsync installed. For Debian and Ubuntu systems it should be sufficient to install them as follows (if you don’t have them already):

      apt-get install openssh-server rsync

      The server requires rsync and an SSH client. Again for Debian:

      apt-get install openssh-client rsync

      For other platforms, you must consult their documentation, or install them from source:

      2.1.1 SSH Setup

      The server’s root login needs to be able to SSH to each of the clients’ root logins without having to enter a password or confirm a key hash. You should consult the SSH documentation to set this up, but the general procedure, assuming you use RSA keys and OpenSSH, is as follows. If you are sufficiently familiar with SSH to do this without further documentation, skip to the next section.

      On the server create an SSH key with:

      sudo ssh-keygen

      When asked for a passphrase, just hit return (but see below). Then copy ~root/.ssh/id_rsa.pub to each of the clients and append it to their ~root/.ssh/authorized_keys. At the same time, retrieve the clients’ host key hashes with:

      ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key.pub

      As root on the server, ssh to each of the clients and verify their host keys hashes.

      2.1.2 Installing rsbackup

      To install rsbackup, go to www.greenend.org.uk/rjk/rsbackup and download the source code:

      wget http://www.greenend.org.uk/rjk/rsbackup/rsbackup-_version_.tar.gz
      tar xf rsbackup-_version_.tar.gz
      cd rsbackup-_version_
      ./configure
      make
      sudo make install

      (You will probably need to change the version number.)

      On Debian systems you can use the pre-built .deb files:

      wget http://www.greenend.org.uk/rjk/rsbackup/rsbackup__version__amd64.deb
      sudo dpkg -i rsbackup__version__amd64.deb

      (You will probably need to change the version number and perhaps the architecture.)

      At this point it should be possible to read the man page, which contains reference information:

      man rsbackup

      2.1.3 Variations

      • What If The Server Is Also A Client?

        If you want to backup the backup server itself then you don’t need to set up the server to be able to SSH to itself. See below for how to configure this.

      • What If I’m Not The Superuser?

        rsbackup does not actually depend on being the superuser, although of course its functionality will be limited if it isn’t. However you could for instance use it to back up your home directory to your portable USB disk. The setup is the same except that you do it for your personal login rather than root and therefore don’t use sudo.

      • What If I Don’t Like Empty Passphrases?

        In this case you will have to find some other way of making the server’s private SSH key available when backups run. This is outside the scope of this document.

      2.2 Configuration

      rsbackup reads a configuration file from /etc/rsbackup/config. (Use the --config option to override this, if you prefer another location.) You will need to enter some global settings and then describe the backup clients.

      2.2.1 Backup Storage

      First you should define where backups will be stored. This guide will assume you use removable hard disks, but you can use permanently online backups too.

      For each distinct backup device you need to define two things. The first is the mount point that the device will appear at. For example, if you have two backup disks and the mount points are /backup1 and /backup2 you would write the following:

      store /backup1
      store /backup2

      The second is to define device names. Device names correspond to the contents of a single-line file called device-id in the root of the backup device. For instance, if you called your devices backup1 and backup2 you would write the following:

      device backup1
      device backup2

      Of course, you must also create these files! For example:

      echo backup1 > /backup1/device-id
      echo backup2 > /backup2/device-id

      rsbackup does not mind if the devices share a mount point (and only one is present at a time); any device may use any mount point as far as it’s concerned. You will probably find it more convenient to give them separate mount points though. If more than one device is mounted when you make a backup, backups will be made to all of them. You can use the --store option to select just one.

      Although it would of course be convenient for users to be able to access backups of their files directly, it would also mean that they could go “back in time” past permission changes or deletions of private files belonging to one another. Therefore, the top directory of your backup devices should (usually) be owned by root and mode 0700 (i.e. -rwx------). By default, rsbackup will insist on this, although you can use the public directive to change this behaviour.

      chmod 700 /backup1 /backup2

      Remember to update /etc/updatedb.conf to exclude your backup devices. Otherwise updatedb will spend ever-increasing amounts of time indexing your backups.

      2.2.2 Pruning Parameters

      Next you may want to define some pruning parameters. These can be overridden for each volume you back up, so by defining global ones you are only setting defaults.

      The first one you might want to set is the maximum age of the most recent backup. If any volume’s most recent backup is older than this many days then it will show up as red in the backup status report. The default is 3 days. To reduce it to (for instance) 1 day:

      max-age 1

      The second parameter to choose is the age at which backups are automatically pruned, i.e. deleted. The default is 366 days, ensuring that you will be able to “go back” up to a year. If you only wanted to go back a month you could reduce it as follows:

      prune-parameter prune-age 31

      Remember, these are defaults and can be overridden on a per-host or per-volume basis.

      There are many other settings described in the man page. They will not be covered here.

      2.2.3 Defining What To Back Up

      The rest of the configuration file will define what to back up (and what to exclude).

      There are two ways to organise your configuration file.

      1. You can put all the configuration for all the hosts in the main configuration file.

      2. You can put each host’s definitions in a file of its own and include them all. To do this put a line at the end of your configuration file as follows

        include /etc/rsbackup/hosts.d

        Then for each host create a file named after the host in this directory and use it to store the host’s configuration, as described below.

        The the Debian packaging of rsbackup uses this approach.

      For each host to back up, you should write a host stanza. This will contain some host level settings and then a volume stanza for each part of the host’s filesystem back up.

      Here is an example host stanza:

      host sfere
        volume root /
        volume boot /boot
        volume home /home
          prune-parameter prune-age 366
          exclude /*/Desktop/Trash/*
          exclude /*/.local/share/Trash/*
          exclude /*/.mozilla/*/Cache/*
          exclude /*/.ccache
        volume var /var
          exclude /tmp/*

      The meaning of this is as follows:

      • The first line contains the name of the host. This would normally be its DNS name (see below for an example of where it is not).

      • Each of the volume lines contains the name of a volume on the host and the path to that volume. By default, rsbackup will assume that each volume corresponds to a (mounted) filesystem and therefore not backup files from other filesystems.

      • In this case there are four volumes. root and boot are quite simple: all the files in them will be backed up.

      • home, however, is more complex. Firstly, it has a prune-age setting to ensure that it is kept for longer than the default lifetime. Secondly, it excludes various trash and cache directories.

        In the first three cases, it does this by backing up the directory but not its contents; they will be empty on the backup device. In the fourth case, it does not even backup the directory. Note that the exclusion patterns are rooted at the path to the volume - they are not absolute path names. (Consult the rsync documentation for --exclude for more information about these patterns.)

      • /var is similar to home in that a temporary directory is excluded.

      An important note: the indentation is not significant to rsbackup - only to the reader. Anything that comes after a host directive and before the next host or volume directive is considered part of that host. Similarly anything that comes after a volume directive and before the next host or volume directive is considered part of that volume.

      This example shows how to back up a host where the name differs from the DNS name. The important part is the hostname directive:

      host lith
        hostname chymax
        volume lith /Volumes/Lith
          exclude "Temporary Internet Files"
          exclude /RECYCLER
          exclude /pagefile.sys
          exclude "/Documents and Settings/*/Local Settings/Temp"
          exclude "/System Volume Information/_restore*"

      What is actually going on here is that lith is really the Windows partition on chymax. The computer usually runs Unix, with the Windows partition mounted for convenience. So to get lith’s files, it is necessary to ssh to chymax.

      This example shows how to back up without using SSH at all:

      host araminta
        hostname localhost
        volume root /
        volume boot /boot
        volume home /home
          prune-parameter prune-age 366
          exclude /*/Desktop/Trash/*
          exclude /*/.local/share/Trash/*
          exclude /*/.mozilla/*/Cache/*
          exclude /*/.ccache
        volume var /var
          exclude /tmp/*
        volume news /var/spool/news
          prune-parameter prune-age 14

      In this case, araminta is actually the backup server, so using SSH would mean SSHing to itself. The hostname localhost is special-cased to avoid using SSH at all.

      Here is example of backing up a laptop:

      host kakajou
        max-age 7
        volume users /Users
          prune-parameter prune-age 366
          exclude /*/.Trash/*
          exclude /*/Library/Caches
          exclude /*/Library/VirtualBox/Machines/*/Snapshots
          exclude /*/Library/VirtualBox/**.vdi
        volume local /local
          prune-parameter prune-age 366
        volume etc /etc
        volume sw /sw

      This host is usually asleep or not even in the house, so opportunities to back it up are rare. Therefore it has a host-wide max-age setting.

      2.2.4 macOS-Specific Configuration

      The version of rsync supplied by Apple does not support the options used by modern versions for backing up extended attributes and ACLs. If you have not installed a modern version of rsync then you must configure rsbackup to use suitable options instead:

      rsync‐extra‐options ‐‐extended‐attributes

      Note that this will not back up ACLs.

      This is only suitable for local backups. For remote backups the situation is worse still. If backing up a macOS host from a host with a modern rsync, or vice versa, extended attributes and ACLs cannot be backed up at all. The directive must then be set as follows:

      rsync-extra-options

      Note that this will not back up ACLs or extended attributes.

      2.2.5 Windows-Specific Configuration

      When backing up a Windows filesystem it can happen that attributes in the Windows filesystem do not fit in the backup filesystem; if this happens you may see errors like this:

      rsync: rsync_xal_set: lsetxattr(""/backup7/host/volume/2018-02-04/path/to/file"","attrname") failed: No space left on device (28)
      rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1668) [generator=3.1.2]

      In that case the affected volumes must disable attribute backup and ACL backup as follows:

      rsync-extra-options

      Note that this will not back up ACLs or extended attributes.

      3. Manual Backups

      3.1 Initial Backup

      Before you actually make a backup, you should do a “dry run” to verify that rsbackup does what you expect.

      rsbackup --backup --dry-run

      This will print out the commands that it would run without actually executing them. It’s also a good way of verifying that the syntax of the configuration is correct.

      Once you’re happy with the output, you can try making an initial backup:

      rsbackup --backup --verbose

      The --verbose option makes rsbackup report what it is doing as it progresses. In normal use you would omit it but it’s useful when setting up and crucial when debugging.

      Depending on how much data you have (and how fast your disks and network are) the initial backup may take a very long time. I did my initial backups inside an instance of screen so that they couldn’t be affected by logging out, etc.

      For each backup of each volume, an entry will be made in /var/log/backup/backups.db detailing what was backed up and recording any error output.

      3.2 Pruning Old Backups

      Pruning refers to deleting a volume’s old backups. During pruning, a backup will be deleted if it is older than the volume’s max-age setting, with the proviso that the most recent min-backups backups on each store will not be deleted.

      To prune any backups that are due to be deleted:

      rsbackup --prune --verbose

      The details of what is pruned are logged to files in /var/log/backup.

      There are several possible pruning policies, which determine which backups are selected for deletion. See the section on pruning in rsbackup(5) for details.

      3.3 Failed Backups

      If a backup fails then it will be left in an incomplete state. You can tell rsbackup to pick up where it left off simply by running it again on the same day; if however you leave it until another day then that backup will never be completed. To delete any incomplete backups, therefore:

      rsbackup --prune-incomplete --verbose

      Backups are not pruned immediately because even if they are incomplete, the portion that succeeded may be useful to reduce the amount of data copied when you retry.

      3.4 Retired Volumes

      When you take a volume or host out of service, you need to tell rsbackup about this. The first part of this is to remove the corresponding volume or host sections in the configuration file. If you don’t do this then rsbackup will keep on trying to backup the obsolete volume or host.

      The second part is to delete old backups for the volume and their corresponding records. If you don’t do this then the report will complain about them. This can be done with the --retire option:

      rsbackup --retire HOST:VOLUME

      ...or:

      rsbackup --retire HOST

      All backups on available devices and their corresponding records will be deleted (possibly a slow process).

      If you want to keep (say) the last backup for the volume then you should at least rename it aside, otherwise --retire will delete it; you may prefer to tar it up and compress it.

      It is also possible to keep the backups themselves, but remove the record of them, making them invisible to rsbackup but preserving the data for archival. To do this, use --forget-only together with the --retire option.

      3.5 Status Reports

      You can generate an HTML status report to a disk file:

      rsbackup --html status.html

      This will show:

      • Any backups to unknown devices. (See below.)
      • For each volume, the dates of the oldest backup, how many backups exist. The latter is red if there are any devices with no backups of the volume.
      • For each volume’s backup to a each device, the date of the newest backup on the device and the total number of backups on the device. The former is red if it violates the max-age setting for the volume and the latter is red if there are no backups of the volume on the device.
      • The rsync output for the last backup, if it failed. You can use the --logs option to make the logfile section more verbose.
      • Logfiles for pruning.

      If you prefer plain text you can use --text instead of --html.

      You can also request that the report by sent by email, with the --email option. This is intended for use when automating backups. The email contains both the plain text and the HTML versions, most mail clients should be able to pick whichever they are best able to display.

      4. Automatic Backups

      If you installed from a .deb then have a look at the Debian-specific documentation.

      Manual backups might be perfectly adequate for you. However, computers are often better at remembering to perform scheduled tasks than humans are, so it may be better to run your backups as a cron job. For example, to run your backups, with pruning and a report, at 1am every day you might use the following crontab line:

      0 1 * * * rsbackup --backup --prune --email root

      This will automatically do a backup every night, prune any out-of-date backups, and mail a report to the system administrator.

      You might want to add (for example) a weekly prune of incomplete backups:

      0 8 * * 0 rsbackup --prune-incomplete

      5. Snapshots

      If you use Linux LVM then you may prefer to snapshot filesystems while they are being backed up, so that there is no possibility of files changing half way through the backup. This can be achieved by adding the following lines to your configuration file:

      pre-volume-hook rsbackup-snapshot-hook
      post-volume-hook rsbackup-snapshot-hook

      Then for each volume that is to be snapshotted, create /snap/VOLUME on the target host. The volume name must be the one used by rsbackup, for instance root, boot, home or var in the first example above. Before each volume is backed up, a snapshot will automatically be created; it will be removed again after the backup is complete or on failure.

      There is a man page for rsbackup-snapshot-hook detailing what it does and the options it can take.

      6. Device Management

      6.1 Lost Devices

      Hard disks get lost or stolen, and fail. In this case rsbackup needs to be told that one of its devices has gone away. The first part of this is to delete the corresponding device directive in the configuration file (and the store directive, if that’s unique to the device).

      The second part is to delete records for backups to the device. If you don’t do this then the report will complain about them. This can be done with the --retire-device option:

      rsbackup --retire-device NAME

      You can do these steps in either order, but if you delete the records first, you will be ask if you are sure. You can override this with the --force option.

      --retire-device will never delete actual backups, only the backup records.

      6.2 Upgrading Devices

      If a backup device gets full you have several options:

      • Reduce the number of backups to be kept on it by lowering prune-age. But sooner or later you may reach the point where you just cannot keep backups as long as you like.

      • Introduce an entirely new, bigger device and take the old device out of service, either keeping it against a rainy day or destroying it as described above.

      • Copy all its contents to a new, bigger device, keeping the same device name. Remember to delete the old device-id file, or confusion may follow!

      7. Restoring

      Backups aren’t worth anything if you can’t restore, course.

      7.1 Manual Restoration

      The backup has the same layout, permissions etc as the original system, so it’s perfectly possible to simply copy files from a backup directory to their proper location. This is the most convenient way if you want to rescue only a small number of files.

      Be careful to get file ownership right. The backup is stored with the same numeric user and group ID as the original system used. Put another way, the relationship between usernames and user IDs (and group names and group IDs) on the backup disk reflects the client, not the server (or any other machine the disk might be attached to).

      Until a backup is completed, a corresponding .incomplete file will exist. Check for such a file before restoring any given backup.

      7.2 Restoring With rsync

      You can do bulk restores with rsync. For example, supposing that host chymax has a volume called users in which user home directories are backed up, and user rjk wants their entire home directory to be restored:

      rsync -aSHAXz --numeric-ids /store/chymax/users/2010-04-01/rjk/. chymax:~rjk/.

      -a means recursive into directories; preserve symlinks, permissions, modification times, groups, owners, device files and special files. -S means handle sparse files efficiently. -H means preserve hard links. -z means compress data for transfer; you might want to omit this if your CPU is slow.

      --numeric-ids is important as backups are stored with the same numeric user and group IDs as the original system; no translation via name is performed.

      8. Links, etc

      rsbackup-10.0/doc/rsbackup-mount.1000066400000000000000000000046711440730431700170600ustar00rootroot00000000000000.TH rsbackup-mount 1 .\" Copyright (c) 2011, 2012, 2014 Richard Kettlewell .\" .\" 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 . .SH NAME rsbackup-mount \- mount encrypted or unencrypted disks used by rsbackup .SH SYNOPSIS \fBrsbackup-mount\fR [\fIOPTIONS\fR] [\fB\-\-\fR] [\fIDEVICES\fR] .SH DESCRIPTION \fBrsbackup-mount\fR mounts and unmounts encrypted or unencrypted partitions used as backup stores by \fBrsbackup\fR(1). .SH OPTIONS .TP .B \-\-unmount\fR, \fB\-u Unmount devices. (The default is to mount devices.) .TP .B \-\-check\fR, \fB\-c Check for available devices that aren't open. .TP .B \-\-dry\-run\fR, \fB\-n Do nothing, instead print the commands that would be executed. .TP .B \-\-key\-file \fIPATH\fR, \fB\-k \fIPATH Pass the argument \fB\-\-key\-file\fR \fIPATH\fR to \fBcryptsetup\fR. .TP .B \-\-help\fR, \fB\-h Display usage message. .TP .B \-\-version\fR, \fB\-V Display version string. .SH CONFIGURATION Configuration is read from \fI/etc/rsbackup/devices\fR. This is a shell script fragment. It should set \fBdevices\fR to a (space-separated) list of device names and for each device name \fID\fR, define \fID\fR\fB_uuid\fR to its UUID. To specify that a device is unencrypted (i.e. that it just requires mounting or unmounting), define \fID\fR\fB_plain\fR as a non-empty string for that device. (Have a look in \fI/dev/disk/by-uuid\fR to find device UUIDs). .SS Example .in +4n .EX # List names of devices here devices="backup3 backup4" # For each device, define DEVICE_uuid # e.g. devicename_uuid=8f4171f0-007d-4083-a40c-407e5f9c24dd backup3_uuid=8f4171f0-007d-4083-a40c-407e5f9c24dd backup4_uuid=80a1ca0d-d07a-4195-9da1-8cbb9fbc11b9 # For any device that isn't encrypted, define DEVICE_plain # e.g. devicename_plain=1 backup3_plain=1 .EE .in .SH "SEE ALSO" \fBrsbackup\fR(1), \fBrsbackup.cron\fR(1) .SH AUTHOR Richard Kettlewell rsbackup-10.0/doc/rsbackup-snapshot-hook.1000066400000000000000000000042241440730431700205050ustar00rootroot00000000000000.TH rsbackup 1 .\" Copyright (c) 2012, 2014, 2017 Richard Kettlewell .\" .\" 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 . .SH NAME rsbackup-snapshot-hook \- rsync-based backup utility .SH SYNOPSIS \fBrsbackup-snapshot-hook\fR [\fIOPTIONS\fR] .SH DESCRIPTION \fBrsbackup-snapshot-hook\fR creates and removes LVM snapshots when configured as a pre- or post-volume-hook for \fBrsbackup\fR(1). .PP Snapshots will only be created for volumes that have a corresponding directory in the snapshot directory. For instance, a snapshot will be created for the volume \fBroot\fR if \fI/var/lib/rsbackup/snapshots/root\fR exists. (Use the \fB\-\-snaps\fR option to change the snapshot directory.) Such volumes must be stored in a logical volume. .PP Leftover snapshots are always unmounted and removed before creation. .PP Snapshots are given only a fraction of the size of the original volume. Use the \fB\-\-divisor\fR to change this. .PP After creation snapshots are fsck'd and mounted. If an error occurs in any part of the creation process, the partially snapshot created snapshot may be left in place (and ultimately be torn down when the post-volume-hook is run). .SH OPTIONS .TP .B \-\-snaps\fR, \fB\-s\fI PATH Set the path to the snapshot directory. The default is \fI/var/lib/rsbackup/snapshots\fR. .TP .B \-\-divisor\fR, \fB\-d\fI DIVISOR Set the snapshot size ratio. The default is 5. .TP .B \-\-help\fR, \fB\-h Display usage message. .TP .B \-\-version\fR, \fB\-V Display version string. .SH "SEE ALSO" \fBrsbackup\fR(1), \fBlvm\fR(8) .SH AUTHOR Richard Kettlewell rsbackup-10.0/doc/rsbackup.1000066400000000000000000000404151440730431700157140ustar00rootroot00000000000000.TH rsbackup 1 .\" Copyright (c) Richard Kettlewell .\" .\" 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 . .SH NAME rsbackup \- rsync-based backup utility .SH SYNOPSIS \fBrsbackup\fR [\fIOPTIONS\fR] [\fB\-\-\fR] [\fISELECTOR\fR...] .br \fBrsbackup \-\-retire [\fIOPTIONS\fR] [\fB\-\-\fR] [\fISELECTOR\fR...] .br \fBrsbackup \-\-retire\-device [\fIOPTIONS\fR] [\fB\-\-\fR] \fIDEVICE\fR... .SH DESCRIPTION \fBrsbackup\fR backs up files from one or more (remote) destinations to a single backup storage directory, preserving their contents, layout, ownership, permissions, timestamps and hardlink structure. .PP Incremental backups are achieved by hard-linking identical files within successive backups of the same files. .PP See \fBrsbackup\fR(5) for details of the configuration file. .SH OPTIONS .SS "Action Options" At least one of these options must be specified. When multiple actions are specified, they are executed in the order shown below. .TP .B \-\-backup\fR, \fB\-b Make a backup of the selected volumes. At most one backup of a given volume will be made per day. .TP .B \-\-retire\-device Retire the named devices. Retiring a device only means deleting the records of it. Files on the device itself are not touched. .IP If the device is still listed in the configuration file then you will be asked whether you really want to retire it; you can suppress this check with the \fB\-\-force\fR option. .TP .B \-\-retire Retire the named hosts and volumes. Retiring a volume means deleting any available backups for the volume and the records of them. Records corresponding to backups on unavailable devices are not removed. .IP If you just want to remove backup records for retired volumes but want to keep the backups, also use the \fB-\-forget\-only\fR option (see below). .IP Since this command deletes backups, you will be prompted to confirm it. You can suppress this check with the \fB\-\-force\fR option. .TP .B \-\-forget\-only With \fB\-\-retire\fR, suppresses deletion of backups, and instead just drops database records for the hosts and volumes affected. .TP .B \-\-prune\fR, \fB\-p Prune old backups of selected volumes. See \fBrsbackup\fR(5) for details how how pruning is controlled. .TP .BR \-\-prune\-incomplete, \fB\-P Prune incomplete backups of selected volumes. Any backups that failed before completion will be removed. .TP .B \-\-html \fIPATH\fR, \fB\-H \fIPATH Write an HTML report to \fIPATH\fR. The report covers all volumes, not just selected ones. \fIPATH\fR can be \fB\-\fR to write to standard output. .TP .B \-\-text \fIPATH\fR, \fB\-T \fIPATH Write a plain text report to \fIPATH\fR. The report covers all volumes, not just selected ones. \fIPATH\fR can be \fB\-\fR to write to standard output. .TP .B \-\-email \fIADDRESS\fR, \fB\-e \fIADDRESS Email a report to \fIADDRESS\fR. The contents is equivalent to the output of \fB\-\-text\fR and \fB\-\-html\fR. .TP .B \-\-check\-unexpected List unexpected files on backup devices to standard output. Must not be combined with any other action option. .IP Note that this option does not verify that the backups are good. It just detects unexpected files on currently-mounted backup devices. .TP .B \-\-latest Prints out the path to the latest complete backup for each selected volume. .TP .B \-\-dump\-config Writes the parsed configuration file to standard output. Must not be combined with any other action option. .IP With \fB\-\-verbose\fR, the configuration file is annotated with descriptive comments. .SS "General Options" .TP .B \-\-config \fIPATH\fR, \fB\-c \fIPATH The path to the configuration file. The default is .IR /etc/rsbackup/config . .TP .B \-\-store \fIPATH\fR, \fB\-s \fIPATH Specify the destination directory to back up to. Using this option (possibly more than once) is equivalent to removing the \fBstore\fR directives from the configuration file and replacing them with the paths give in \fB\-\-store\fR options. .IP This option implicitly enables the \fB\-\-warn\-store\fR option. .TP .B \-\-unmounted\-store \fIPATH\fR Equivalent to \fB\-\-store\fR except that the store does not have to be a mount point. .TP .B \-\-verbose\fR, \fB\-v Enable verbose mode. Various messages will be displayed to report progress and the rsync \fB\-\-quiet\fR option is suppressed. .TP .B \-\-dry\-run\fR, \fB\-n Enable dry-run mode. Commands will be displayed but nothing will actually be done. .TP .B \-\-force\fR, \fB\-f Suppress checks made when retiring devices and volumes, and overrides backup policies. .TP .B \-\-null\fR, \fB-0 For the \-\-check\-unexpected option, terminate filenames with a null, rather than newline. .TP .B \-\-wait\fR, \fB\-w Waits rather than giving up if another copy of \fBrsbackup\fR is running. .TP .B \-\-database\fR, \fB\-D \fIPATH Override the path to the backup database. .TP .B \-\-help\fR, \fB\-h Display a usage message. .TP .B \-\-version\fR, \fB\-V Display the version number. .SS "Report Verbosity" .TP .B \-\-logs \fIVERBOSITY\fR Controls which logfiles for a given volume/device pair to include in the report. The possible values of \fIVERBOSITY\fR are: .RS .TP .B all Includes all nonempty logfiles, even if the backup succeeded. .TP .B errors Includes all error logfiles. .TP .B recent Includes only the most recent error logfile. .TP .B latest Includes only the latest logfile, even if the backup succeeded. .TP .B failed Includes only the most recent logfile but only if that attempt failed. This is the default. .RE .SS "Warning Options" .TP .B \-\-warn\-unknown Display warnings for unknown devices, hosts and volumes. (Warnings will always be included in the report, this refers to runtime error output.) .TP .B \-\-warn\-store Display warnings for unsuitable store directories and unavailable devices. .TP .B \-\-warn\-unreachable Display warnings for unreachable hosts. .TP .B \-\-no\-warn\-partial Suppress warnings for rsync "partial transfer" diagnostics (which are on by default). .TP .B \-\-warn\-all\fR, \fB\-W Enable all \fB\-\-warn\-\fR options. .TP .B \-\-no\-errors Suppress display of errors from rsync. .SS "Volume Selection" The list of selectors on the command line determines what subset of the known volumes are backed up, pruned or retired. The following selectors are possible: .TP 16 .I HOST Select all volumes for the host. .TP .IR HOST : VOLUME Select the volume. .TP .BI \- HOST Deselect all volumes for the host. .TP .BI \- HOST : VOLUME Deselect the volume. .TP .B * Select all volumes. .PP If no hosts or volumes are specified on the command line then all volumes are selected for backing up or pruning. For retiring, you must explicitly select hosts or volumes to retire and only positive selections are possible. .SH "BACKUP LIFECYCLE" .SS "Adding A New Host" To add a new host create a \fBhost\fR entry for it in the configuration file. .PP To back up the local host, specify \fBhostname localhost\fR. Otherwise you can usually omit \fBhostname\fR. .PP You may want to set host-wide values for \fBprune\-parameter prune\-age\fR, \fBmax\-age\fR and \fBprune\-parameter min\-backups\fR. .PP A host with no volumes has no effect. .SS "Adding A New Volume" To add a new volume create a \fBvolume\fR entry for it in the relevant \fBhost\fR section of the configuration file. .PP Add \fBexclude\fR options to skip files you don't want to back up. This might include temporary files and the contents of "trash" directories. .PP If the volume contains mount points, and you want to back up the contents of the subsiduary filesystems, then be sure to include the \fBtraverse\fR option. .PP You may want to set per-volume values for \fBprune\-parameter prune\-age\fR, \fBmax\-age\fR and \fBprune\-parameter min\-backups\fR. .SS "Adding A New Device" To add a new device: .IP 1. 4 Format and mount it. .IP 2. 4 chown it to root. .IP 3. 4 chmod it to mode 0700. .IP 4. 4 Create a \fIdevice\-id\fR file containing its name in its top-level directory. .IP 5. 4 Add a \fBdevice\fR entry for it in the configuration file, .IP 6. 4 If you do not use \fBstore\-pattern\fR, add a \fBstore\fR entry in the configuration file mentioning its usual mount point. .SS "Making Backups" To backup up all available volumes to all available devices: .PP .in +4n .EX rsbackup \-\-backup .EE .in .PP You will probably want to automate this. To only back up a limited set of volumes specify selection arguments on the command line. .SS "Pruning Backups" To prune old backups: .PP .in +4n .EX rsbackup \-\-prune \-\-prune\-incomplete .EE .in .PP You will probably want to automate this. .PP An "incomplete backup" occurs when a backup of a volume fails or is interrupted before completion. They are not immediately deleted because \fBrsync\fR may be able to use the files already transferred to save effort on subsequent backups on the same day, or (if there are no complete backups to use for this purpose) later days. .SS "Retiring A Host" Retiring a host means removing all backups for it. The suggested approach is to remove configuration for it and then use \fBrsbackup \-\-retire \fIHOST\fR to remove its backups too. You can do this the other way around but you will be prompted to check you really meant to remove backups for a host still listed in the configuration file. .PP If any of the backups for the host are on a retired device you should retire that device first. .SS "Retiring A Volume" Retiring a volume means removing all backups for it. It is almost the same as retiring a whole host but the command is \fBrsbackup \-\-retire \fIHOST\fB:\fIVOLUME\fR. .PP You can retire multiple hosts and volumes in a single command. .SS "Retiring A Device" Retiring a device just means removing the records for it. Use \fBrsbackup \-\-retire\-device \fIDEVICE\fR to do this. The contents of the device are not modified; if you want that you must do it manually. .PP You can retire multiple devices in a single command. .SH RESTORING Restore costs extra l-) .SS "Manual Restore" The backup has the same layout, permissions etc as the original system, so it's perfectly possible to simply copy files from a backup directory to their proper location. .PP Be careful to get file ownership right. The backup is stored with the same numeric user and group ID as the original system used. .PP Until a backup is completed, or while one is being pruned, a corresponding \fB.incomplete\fR file will exist. Check for such a file before restoring any given backup. .SS "Restoring With rsync" Supposing that host \fBchymax\fR has a volume called \fBusers\fR in which user home directories are backed up, and user \fBrjk\fR wants their entire home directory to be restored, an example restore command might be: .PP .in +4n .EX rsync \-aSHAXz \-\-numeric\-ids /store/chymax/users/2010-04-01/rjk/. chymax:~rjk/. .EE .in .PP You could add the \fB\-\-delete\fR option if you wanted to restore to exactly the status quo ante, or at the opposite extreme \fB\-\-existing\fR if you only wanted to restore files that had been deleted. .PP You might prefer to rsync back into a staging area and then pick files out manually. .SS "Restoring with tar" You could tar up a backup directory (or a subset of it) and then untar it on the target. Remember to use the \fB\-\-numeric\-owner\fR option to tar. .SH "STORE VALIDITY" A store may be in the following states: .IP \fBavailable The store can be used for a backup. .IP \fBunavailable The store cannot be used for a backup. Normally this does not generate an error but \fB\-\-warn\-store\fR can be used to report warnings for all unavailable stores, and if no store is available then the problems with the unavailable stores are described. .IP \fBbad The store cannot be used for a backup. This always generates an error message, but does not prevent backups to other stores taking place. .IP "\fBfatally broken" The store cannot be used for a backup. The program will be terminated. .PP The states are recognized using the following tests (in this order): .IP \(bu If the store path does not exist, the store is bad. .IP \(bu If the store does not have a \fBdevice\-id\fR file then it is unavailable. If it has one but reading it raises an error then it is bad. .IP \(bu If the store's \fBdevice\-id\fR file contains an unknown device name then it is bad. .IP \(bu If the store's \fBdevice\-id\fR file names the same device as some other store then it is fatally broken. .IP \(bu If the store is not owned by \fBroot\fR then it is bad. This check can be overridden with the \fBpublic\fR directive. .IP \(bu If the store can be read or written by group or world then it is bad. This check can be overridden with the \fBpublic\fR directive. .SH "LINK TARGETS" In order to minimize storage requirements, when a backup is made, \fBrsbackup\fR selects recent backups to use as link targets. Where possible, \fBrsync\fR will create hardlinks between the new backup and the link target, instead of making a new copy of an unchanged file. .PP At present up to two backups may be selected as link targets, according to the following rules: .IP \(bu If the \fB.nolink\fR file exists for the volume (see below) then no link target is used, overriding the rules below. .IP \(bu The most recent complete backup, if there is one, is used. .IP \(bu If the most recent backup is incomplete, that is used. .SH FILES .TP .I /etc/rsbackup/config Configuration file. See \fBrsbackup\fR(5) .TP .I LOGS/backups.db The backup records. See \fBSCHEMA\fR below. .TP .I STORE/HOST/VOLUME/YYYY\-MM\-DD One backup for a volume. .TP .I STORE/HOST/VOLUME.nolink If this file exists then no existing backup will be considered as a hardlink target until a new backup has succeeded (at which point it is deleted). .TP .I STORE/HOST/VOLUME/YYYY\-MM\-DD.incomplete Flag file for an incomplete backup. .SH SCHEMA .I backups.db is a SQLite database. It contains a single table with the following definition: .in +4n .EX CREATE TABLE backup ( host TEXT, volume TEXT, device TEXT, id TEXT, time INTEGER, pruned INTEGER, rc INTEGER, status INTEGER, log BLOB, PRIMARY KEY (host,volume,device,id) ) .EE .in Each row represents a backup. The meanings of the fields are as follows: .TP 10 .B host The name of the host the backup was taken from. .TP .B volume The name of the volume the backup was taken from. .TP .B device The name of the device the backup was written to. .TP .B id The unique identifier for the backup. Currently this is the date the backup was made, in the format YYYY-MM-DD but this may be changed in the future. .TP .B time The time that the backup was started, as a \fBtime_t\fR. .TP .B pruned The time that backup pruning started (if it is underway) or finished (if it is complete), as a \fBtime_t\fR. .TP .B rc The exit status of the backup process. 0 means success. .TP .B status Status of this backup. See below. .TP .B log The log output of \fBrsync\fR(1) and hooks. If the backup status is pruning or pruned (see below) then this contains the reason for the pruning. .PP Possible status values are: .TP .B 0 Unknown status. Not normally seen. .TP .B 1 The backup is underway, or \fBrsbackup\fR was interrupted. .TP .B 2 Backup is complete. .TP .B 3 Backup has failed. .TP .B 4 Pruning has started. .TP .B 5 Pruning has completed. .PP \fBrsbackup\fR is not designed with concurrent access to this table in mind. Therefore it is recommended that you only modify its contents when the program is not running. .SH "HISTORICAL BEHAVIOR" Older versions of \fBrsbackup\fR stored the logs for each backup in a separate file. If such files are encountered then \fBrsbackup\fR will automatically populate \fIbackups.db\fR from them and then delete them. .PP Older versions of \fBrsbackup\fR logged pruning information to a pruning logfile. These files will be deleted at the same rate as records of pruned backups in the database. They are not included in the report. .SH "SEE ALSO" \fBrsbackup\-graph\fR(1), \fBrsbackup.cron\fR(1), \fBrsbackup\-mount\fR(1), \fBrsbackup\-snapshot\-hook\fR(1), \fBrsync\fR(1), \fBrsbackup\fR(5) .SH AUTHOR Richard Kettlewell rsbackup-10.0/doc/rsbackup.5000066400000000000000000000775441440730431700157350ustar00rootroot00000000000000.TH rsbackup 5 .\" Copyright (c) 2011, 2012, 2014-20 Richard Kettlewell .\" .\" 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 . .SH NAME /etc/rsbackup/config \- configuration for rsync-based backup utility .SH DESCRIPTION This describes the configuration file syntax for for \fBrsbackup\fR(1). .SH "SYNTAX" .SS "Line Splitting" Line are split into space-separated words. To include spaces in a word, quote it using "double quotes". Quotes and backslashes within quoted strings are escaped with backslashes (and cannot appear in an unquoted word). .SS "Comments and Blank Lines" Anything after the first (unquoted) "#" to appear on a line is ignored. .PP Lines with no words on (whether they are completely empty, or contain just spaces, or have a "#" before any non-space characters) are ignored (and do not have to follow the indentation rules below). .SS "Directives and Stanzas" The first word of a line is called a directive. The remaining words, if any, form its arguments. .PP A stanza consists of a directive introducing the stanza followed by zero or more directives within the stanza. These must be indented, consistently, relative to the directive that introduced the stanza. .PP A configuration file contains global directives (which must not be indented) and one or more host stanzas. Each host stanza contains one or more volume stanzas. .PP Global directives may appear after host stanzas (and host directives after volume stanzas) provided they are indented correctly. .SS "Time Intervals" A time interval, denoted \fIINTERVAL\fR below, can be either a raw integer, or an integer with the suffix "s", "m", "h" or "d" for seconds, minutes, hours or days respectively. .PP If there is no suffix then the interpretation is contextual. This behavior is deprecated; suffixes will become mandatory in future. .SH "GLOBAL DIRECTIVES" Global directives control some general aspect of the program. .TP .B database \fIPATH\fR The path to the backup database. By default this is \fILOGS\fB/backups.db\fR where \fILOGS\fR is controlled by the \fBlogs\fR directive below. .TP .B device \fIDEVICE\fR Names a device. This can be used multiple times. The store must have a file called \fISTORE\fB/device\-id\fR which contains a known device name. Backups will only be made to known devices. .IP When a device is lost or destroyed, remove its device entry and use the \-\-prune\-unknown option to delete records of backups on it. .IP Device names may contain letters, digits, dots and underscores. .TP .B include \fIPATH\fR Include another file as part of the configuration. If \fIPATH\fR is a directory then the files within it are included (excluding dotfiles, backup and recovery files). .TP .B keep\-prune\-logs \fIINTERVAL\fR The time period to keep records of pruned backups for. The default is 31 days. .TP .B lock \fIPATH\fR Enable locking. If this directive is present then \fIPATH\fR will be used as a lockfile for operations that change anything (\-\-backup, \-\-prune, etc). .IP The lock is made by opening \fIPATH\fR and calling \fBflock\fR(2) on it with \fBLOCK_EX\fR. .TP .B logs \fIPATH\fR The directory to store logfiles and backup records. The default is \fI/var/log/backup\fR. .TP .B post\-device\-hook \fICOMMAND\fR... A command to execute after all backup and prune operations. This is executed only once per invocation of \fBrsbackup\fR. A backup is still considered to have succeeded even if the post-access hook fails (i.e. exits nonzero). See \fBHOOKS\fR below. .TP .B pre\-device\-hook \fICOMMAND\fR... A command to execute before anything that accesses any backup devices (i.e. backup and prune operations). This is executed only once per invocation of \fBrsbackup\fR and if it fails (i.e. exits nonzero) then \fBrsbackup\fR terminates immediately. See \fBHOOKS\fR below. .TP .B prune\-timeout \fIINTERVAL The maximum amount of time to spend pruning, in a single invocation. 0 means that there is no limit (which is the default). .IP Note that, if this is directive is used, prune operations timing out are considered to be normal behavior, and the exit status will be 0. Most of the diagnostics relating to timeouts are suppressed unless the \fB\-v\fR option is used. .TP .B public true\fR|\fBfalse If true, backups are public. Normally backups must only be accessible by the calling user. This directive suppresses the check. .TP .B store \fR[\fB--mounted|--no-mounted\fR] \fIPATH\fR A path at which a backup device may be mounted. This can be used multiple times. .IP With the \fB--mounted\fR option (which is the default), \fIPATH\fR must be a mount point. With \fB--no-mounted\fR it need not be a mount point. .TP .B store\-pattern \fR[\fB-mounted|-nomounted\fR] \fIPATTERN\fR A \fBglob\fR(7) pattern matching paths at which a backup device may be mounted. This can be used multiple times. .IP See the description of \fBstore\fR above for the meanings of the options. .SS "Report Directives" These are global directives that affect only the HTML report. .TP .B color\-bad \fICOLOR The color used to represent bad states (no sufficiently recent backup) in the report. See below for the interpretation of \fICOLOR\fR. .TP .B color\-good \fICOLOR The color used to represent good states (a recent backup) in the report. .TP .B report \fR[\fB+\fR] \fR[\fIKEY\fR][\fB:\fIVALUE\fR][\fB?\fICONDITION\fR] ... Defines the report contents. The arguments to this directive are a sequence of keys, optionally parameterized by a value and/or a condition. .IP If the first argument is a \fB+\fR then the arguments are added to the current configuration; otherwise they replace it. .IP The possible keys, with values where appropriate, are: .RS .TP .B generated A timestamp stating when the report was generated. .TP .B history\-graph A graphic showing the backups available for each volume. This only works if \fBrsbackup\-graph\fR(1) is installed. .TP .B h1:\fIHEADING .TP .B h2:\fIHEADING .TP .B h3:\fIHEADING Headings at levels 1, 2 and 3. .TP .B logs A list of logs of failed backups. .TP .B p:\fIPARAGRAPH A paragraph of text. .TP .B prune\-logs\fR[\fB:\fIDAYS\fR] A list of logs of pruned backups. .IP \fIDAYS\fR is the number of days of pruning logs to put in the report. The default is 3. .TP .B summary A table summarizing the backups available for each volume. .TP .B title:\fITITLE The document title. .TP .B warnings A list of warning messages. .PP If a condition is specified then the key is only used if the condition is true. The possible conditions are: .TP .B warnings True if there are any warnings to display (i.e. if the \fBwarnings\fR key is nonempty). .PP Within a \fIVALUE\fR the following sequences undergo substitution: .TP .B \e\fICHAR Replaced with the single character \fICHAR\fR. .TP .B ${\fIVARIABLE\fB} Replaced with the value of the environment variable \fIVARIABLE\fR, if it is set. .PP The following environment variables are set: .TP .B RSBACKUP_CTIME The local date and time in \fBctime\fR(3) format. .TP .B RSBACKUP_DATE The local date in YYYY\-MM\-DD format. .PP The default is equivalent to: .PP .RS .in +4n .EX report "title:Backup report (${RSBACKUP_DATE})" report + "h1:Backup report (${RSBACKUP_DATE})" report + h2:Warnings?warnings warnings report + "h2:Summary" summary report + history\-graph report + h2:Logfiles logs report + "h3:Pruning logs" prune\-logs report + "p:Generated ${RSBACKUP_CTIME}" .EE .in .RE .RE .TP .B sendmail \fIPATH\fR The path to the executable to use for sending email. The default is platform-dependent but typically \fI/usr/sbin/sendmail\fR. The executable should support the \fB\-t\fR, \fB\-oee\fR, \fB\-oi\fR and \fB\-odb\fR options. .TP .B stylesheet \fIPATH The path to the stylesheet to use in the HTML report. If this is absent then a built-in default stylesheet is used. .SS "Graph Directives" These are global directives that affect the output of \fBrsbackup\-graph\fR(1). .TP .B color\-graph\-background \fICOLOR The background color. See below for the interpretation of \fICOLOR\fR. .TP .B color\-graph\-foreground \fICOLOR The foreground color, i.e. for text. .TP .B color\-month\-guide \fICOLOR The color for the vertical month guides. .TP .B color\-host\-guide \fICOLOR The color for the horizontal guides between hosts. .TP .B color\-volume\-guide \fICOLOR The color for the horizontal guides between volumes. .TP .B device\-color\-strategy \fISTRATEGY The strategy to use for picking device colors. .IP A strategy is a name and a sequence of parameters, all of which are optional. .IP The possible strategies are: .RS .TP .B equidistant\-value \fIHUE SATURATION MINVALUE MAXVALUE Colors are picked with chosen hue and saturation, with values equally spaced within a range. .IP The default hue is 0 and the default saturation is 1. The default value range is from 0 to 1. .TP .B equidistant\-hue \fIHUE SATURATION VALUE Colors are picked with chosen saturation and value and equally spaced hues, starting from \fIHUE\fR. .IP The default starting hue is 0 and the default saturation and value are 1. .PP The default strategy is equivalent to: .RS .in +4n .EX device\-color\-strategy equidistant\-value 120 0.75 .EE .in .RE .RE .TP .B horizontal\-padding \fIPIXELS The number pixels to place between horizontally adjacent elements. The default is 8. .TP .B vertical\-padding \fIPIXELS The number pixels to place between vertically adjacent elements. The default is 2. .TP .B host\-name\-font \fIFONT The font description used for host names. See below for the interpretation of \fIFONT\fR. .TP .B volume\-name\-font \fIFONT The font description used for volume names. .TP .B device\-name\-font \fIFONT The font description used for device names. .TP .B time\-label\-font \fIFONT The font description used for time labels. .TP .B graph\-layout \fR[\fB+\fR] \fR\fIPART\fR\fB:\fICOLUMN\fB,\fIROW\fR[\fB:\fIHV\fR] ... .RS Defines the graph layout. .PP The arguments to this directive are a sequence of graph component specifications of the form \fIPART\fR\fB:\fICOLUMN\fB,\fIROW\fR[\fB:\fIHV\fR], where: .TP .I PART The name of this component. The following parts are recognized: .RS .TP .B host\-labels The host name labels for the graph. This is expected to be in the same row as \fBcontent\fR. .TP .B volume\-labels The volume name labels for the graph. This is expected to be in the same row as \fBcontent\fR. .TP .B content The graph content. .TP .B time\-labels The time labels for the graph. This is expected to be in the same column as \fBcontent\fR. .TP .B device\-key The key mapping device names to colors. .RE .TP .I COLUMN The column number for this component. 0 is the leftmost column. .TP .I ROW The row number for this component. 0 is the top row. .TP .I HV The (optional) justification specification for this component. .I H may be one of the following: .RS .TP .B L Left justification. .TP .B C Centre justification. .TP .B R Right justification. .PP .I V may be one of the following: .TP .B T Top justification. .TP .B C Centre justification. .TP .B B Bottom justification. .RE .PP Parts may be repeated or omitted. .PP The default layout is equivalent to: .PP .RS .in +4n .EX graph\-layout host\-labels:0,0 graph\-layout + volume\-labels:1,0 graph\-layout + content:2,0 graph\-layout + time\-labels:2,1 graph\-layout + device\-key:2,3:RC .EE .in .RE .RE .SS Colors \fICOLOR\fR may be one of the following: .TP .I DECIMAL\fR or \fB0x\fIRRGGBB An integer value representing an RGB triple. It is most convenient to use hexadecimal. For example, black is \fB0x000000\fR, red is \fB0xFF0000\fR, and so on. .TP .B rgb \fIRED GREEN BLUE Three numbers in the range 0 to 1 representing red, green and blue components. .TP .B hsv \fIHUE SATURATION VALUE \fIHUE\fR chooses between different primary colors and mixtures of them. 0 represents red, 120 represents green and 240 represents blue; intermediate values represent mixed hues. .IP Normally it would be in the range 0 <= \fIHUE\fR < 360, but values outside this range are mapped into it. .IP \fISATURATION\fR is a number in the range 0 to 1 and (roughly) represents how colorful the color is. 0 is a shade of grey and 1 is maximally colorful. .IP \fIVALUE\fR is a number in the range 0 to 1 and represents the brightness of the color. .IP See https://en.wikipedia.org/wiki/HSL_and_HSV for a fuller discussion of these terms. .SS Fonts \fIFONT\fR is a Pango font description. The syntax is "[\fIFAMILY-LIST\fR] [\fISTYLE-OPTIONS\fR] [\fISIZE\fR]" where: .TP .I FAMILY-LIST A comma-separate list of font families. These necessarily depend on the fonts installed locally but Pango recognizes \fBmonospace\fR, \fBsans\fR and and \fBserif\fR as generic family names. .IP To get a list of Pango fonts: .IP .in +4n .EX rsbackup-graph --fonts .EE .in .TP .I STYLE-OPTIONS A whitespace-separated list of style, variant, weight, stretch and gravity options. .IP The possible style options are \fBroman\fR (the default), \fBoblique\fR and \fBitalic\fB. .IP The possible variant options are \fBsmall\-caps\fR. .IP The possible weight options are \fBthin\fB, \fBultra\-light\fR, \fBlight\fR, \fBsemi\-light\fB, \fBbook\fR, \fBregular\fR (the default), \fBmedium\fR, \fBsemi\-bold\fR, \fBbold\fR, \fBultra\-bold\fR, \fBheavy\fR and \fBultra\-heavy\fR. .IP The possible stretch options are \fBultra\-condensed\fR, \fBcondensed\fR, \fBsemi\-condensed\fR, \fBsemi\-expanded\fR, \fBexpanded\fR and \fBultra\-expanded\fR. .IP The possible gravity options are \fBsouth\fR (the default), \fBnorth\fR, \fBeast\fR and \fBwest\fR. .TP .I SIZE The font size in points, or \fIPIXELS\fR\fBpx\fR for a font size in pixels. .PP The details of the syntax are entirely under the control of the Pango library; for full details you must consult its documentation or source code. .SH "INHERITABLE DIRECTIVES" Inheritable directives control an aspect of one or more backups. They can be specified at the global level or in a \fBhost\fR or \fBvolume\fR stanza (see below). If one appears in multiple places then volume settings override host settings and host settings override global settings. .TP .B backup\-parameter \fINAME\fR \fIVALUE\fR Set a parameter for the backup policy. See \fBBACKUP POLICIES\fR below. .TP .B backup\-parameter \-\-remove \fINAME\fR Remove a parameter for the backup policy. See \fBBACKUP POLICIES\fR below. .TP .B backup\-policy \fINAME\fR The backup policy to use. See \fBBACKUP POLICIES\fR below. .TP .B hook\-timeout \fIINTERVAL How long to wait before concluding a hook has hung. The default is 0, which means to wait indefinitely. .TP .B host\-check always-up Assume that the host is always up. .TP .B host\-check ssh Check whether the host is up using SSH. This is the default host check behavior. .TP .B host\-check command \fICOMMAND\fR... Check whether the host is up by executing a command. The name of the host will be appended to the command line. If it exits with status 0 the host is assumed to be up. If it exits with nonzero status the host is assumed to be down. .TP .B max\-age \fIINTERVAL\fR The maximum age of the most recent backup before you feel uncomfortable. The default is 3 days, meaning that if a volume hasn't been backed up in the last 3 days it will have red ink in the HTML report. .TP .B post\-volume\-hook \fICOMMAND\fR... A command to execute after finishing backups of a volume, or after they failed. A backup is still considered to have succeeded even if the post-backup hook fails (exits nonzero). See \fBHOOKS\fR below. .IP The hook can be suppressed with an empty \fICOMMAND\fR (e.g. if you have a global hook and which to suppress it for a single volume). .TP .B pre\-volume\-hook \fICOMMAND\fR... A command to execute before starting a backups of a volume. If this hook fails (i.e. exits nonzero) then the backups are not made and the post-volume-hook will not be run. See \fBHOOKS\fR below. .IP The hook can be suppressed with an empty \fICOMMAND\fR (e.g. if you have a global hook and which to suppress it for a single volume). .IP This hook can override the source path for the volume by writing a new source path to standard output. .TP .B prune\-parameter \fINAME\fR \fIVALUE\fR Set a parameter for the pruning policy. See \fBPRUNING\fR below. .TP .B prune\-parameter \-\-remove \fINAME\fR Remove a parameter for pruning policy. .TP .B prune\-policy \fINAME\fR The pruning policy to use. See \fBPRUNING\fR below. .TP .B backup\-job\-timeout \fIINTERVAL How long to wait before concluding rsync has hung. The default is 0, which means to wait indefinitely. .TP .B rsync\-command \fICOMMAND The command to execute to make a backup. The default is \fBrsync\fR. .TP .B rsync\-base\-options \fIOPTIONS \fR... The options to supply to the rsync command. The default is \fB--archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats\fR. .TP .B rsync\-extra\-options \fIOPTIONS \fR... Additional options to supply to the rsync command. The default is \fB--xattrs --acls --open-noatime\fR. .IP See \fBPLATFORMS\fR for how to use this directive when backing up macOS or Windows platforms. .TP .B rsync\-io\-timeout \fIINTERVAL The I/O timeout (passed as \fB\-\-timeout\fR) to \fBrsync\fR. The default is 0, meaning no timeout. .TP .B rsync\-link\-dest \fBtrue\fR|\fBfalse If true, use rsync's \fB\-\-link\-dest\fR option to save space in backups. The default is \fBtrue\fR. .TP .B rsync\-remote \fBCOMMAND\fR If nonempty, passed to \fBrsync\fR as the \fB\-\-rsync\-path\fR option. .TP .B ssh\-timeout \fIINTERVAL How long to wait before concluding a host is down. The default is 60 seconds. .SH "HOST DIRECTIVES" A host stanza is started by a \fBhost\fR directive. .TP .B host \fIHOST\fR Introduce a host stanza. The name is used for the backup directory for this host. .PP The following directives, and \fBvolume\fR stanzas (see below), can appear in a host stanza: .TP .B devices \fIPATTERN\fR A \fBglob\fR(3) pattern restricting the devices that this host will be backed up to. .IP Note that only backup creation honors this restriction. Pruning and retiring do not. .TP .B group \fIGROUP\fR The concurrency group for this host. The default is the name from the host stanza. See \fBCONCURRENCY\fR below. .TP .B hostname \fIHOSTNAME\fR The SSH hostname for this host. The default is the name from the host stanza. .IP The hostname \fBlocalhost\fR is treated specially: it is assumed to always be identical to the local system, so files will be read from the local filesystem. .TP .B priority \fIINTEGER\fR The priority of this host. Hosts are backed up in descending priority order. The default priority is 0. .TP .B user \fIUSERNAME\fR The SSH username for this host. The default is not to supply a username. .PP In addition, inheritable directives can appear in a host stanza, and override any appearance of them at the global level. .PP The contents of a host stanza must be indented consistently relative to the \fBhost\fR directive that introduces it. .PP Remote hosts are accessed by SSH. The user \fBrsbackup\fR runs as must be able to connect to the remote host (and without a password being entered if it is to be run from a cron job or similar). .SH "VOLUME DIRECTIVES" A volume stanza is started by a \fBvolume\fR directive. It can only appear within a host stanza. .TP .B volume \fIVOLUME PATH\fR Introduce a volume stanza. The name is used for the backup directory for this volume. The path is the absolute path on the host. .PP The following directives can appear in a volume stanza: .TP .B check\-file \fIPATH\fR Checks that \fIPATH\fR exists before backing up the volume. \fIPATH\fR may be either an absolute path or a relative path (to the root of the volume). It need not be inside the volume though the usual use would be to check for a file which is always present there. .IP This check is done before executing the \fBpre\-volume\-hook\fR, so it applies to the real path to the volume, not the rewritten path. .TP .B check\-mounted true\fR|\fBfalse If true, checks that the volume's path is a mount point before backing up the volume. .IP This check is done before executing the \fBpre\-volume\-hook\fR, so it applies to the real path to the volume, not the rewritten path. .IP Note that if multiple \fBcheck\-\fR options are used, all checks must pass for the volume to be backed up. .TP .B exclude \fIPATTERN\fR An exclusion for this volume. The pattern is passed to the rsync \fB\-\-exclude\fR option. This directive may appear multiple times per volume. .IP See the rsync man page for full details. .TP .B traverse true\fR|\fBfalse If true, traverse mount points. This suppresses the rsync \fB\-\-one\-file\-system\fR option. .PP In addition, inheritable directives can appear in a volume stanza, and override any appearance of them at the host or global level. .PP The contents of a volume stanza must be indented consistently relative to the \fBvolume\fR directive that introduces it. .SH "BACKUP POLICIES" Backup policies determine when a backup is made. The available policies are listed below. The default policy is \fBdaily\fR. .SS always This policy creates a backup at every opportunity. .SS daily This policy creates at most one backup per calendar day, as understood in local time. .SS interval This policy enfores a minimum interval between backups. The following backup parameters are supported: .TP .B min\-interval \fIINTERVAL The minimum interval between backups. .PP The \fB--force\fR option can be used to override backup policies, forcing all selected volumes to be backed up unconditionally. .SH PRUNING This is process of removing old backups (using the \fB\-\-prune\fR option). The pruning policy used to determine which backups to remove is set with the inheritable \fBprune\-policy\fR directive, and parameters to the policy set via the \fBprune\-parameter\fR directive. .PP The available policies are listed below. The default policy is \fBage\fR. .SS age This policy deletes backups older than a minimum age, provided a minimum number of backups on a device remain available. The following pruning parameters are supported: .TP .B min\-backups \fIBACKUPS The minimum number of backups of the volume to maintain on the device. Pruning will never cause the number of backups to fall below this value. The default (and minimum) is 1. .TP .B prune\-age \fIINTERVAL The age after backups become eligible for pruning. Only backups more than this many days old will be pruned. The default is 366 days and the minimum is 1 day. .PP For backwards compatibility, these values can also be set using the directives of the same name. This will be disabled in a future version. .SS decay This policy thins out backups older than a minimum age, using a configurable decay pattern that arranges to keep a declining number of backups with age. .PP The idea is that backup history is partitioned into a series of windows. Each window is a fixed multiple of the size of the previous one. The pruning policy arranges that only one backup (per device) is preserved within each window. .PP For example, with the default configuration, the first window is 1 day long and will contain one backup. The second window is two days long and again, only contains one backup. The third window is four days long, and so on. .PP The effect is that the density of backups over time decays exponentially. .PP See .UR https://www.greenend.org.uk/rjk/rsbackup/decay.pdf decay.pdf .UE for more information. .PP The following pruning parameters are supported: .TP .B decay\-start \fIINTERVAL The age after backups become eligible for pruning. Only backups more than this many days old will be pruned. The default is 1 day and the minimum is 1 day. .TP .B decay\-limit \fIINTERVAL The age after which backups are always pruned. Backups older than this will always be pruned unless this would leave no backups at all. The default is 366 days and the minimum is 1 day. .TP .B decay\-scale \fISCALE The scale at which the decay window is expanded. The default is 2 and the (exclusive) minimum is 1. .TP .B decay\-window \fIINTERVAL The size of the decay window. The default is 1 day and the minimum is 1 day. .SS exec This policy executes a subprogram with parameters and additional information supplied in the environment. .PP The following parameters are supported: .TP .B path The path to the subprogram to execute. .PP Any additional parameters are supplied to the subprogram via environment variables, prefixed with \fBPRUNE_\fR. Additionally the following environment variables are set: .TP .B PRUNE_DEVICE The name of the device containing the backup. .TP .B PRUNE_HOST The name of the host. .TP .B PRUNE_ONDEVICE The list of backups on the device, by timestamp. This list excludes any that have already been scheduled for pruning. .TP .B PRUNE_TOTAL The total number of backups of this volume on any device. Note that it does not include backups on other devices that have just been selected for pruning by another call to the subprogram. .TP .B PRUNE_VOLUME The name of the volume. .PP These environment variables all override any parameters with clashing names. .PP The output should be a list of backups to prune, one per line (in any order). Each line should contain the timestamp of the backup to prune (i.e. the same value as appeared in \fBPRUNE_ONDEVICE\fR), followed by a colon, followed by the reason that this backup is to be pruned. .PP As a convenience, if the argument to \fBprune\-policy\fR starts with \fB/\fR then the \fBexec\fR policy is chosen with the policy name as the \fBpath\fR parameter. .SS never This policy never deletes any backups. .SH HOOKS A hook is a command executed by \fBrsbackup\fR just before or just after some action. The command is passed directly to \fBexecvp\fR(3); to use a shell command, therefore, either wrap it in a script or invoke the shell with the \fB\-c\fR option. .PP All hooks are run in \fB\-\-dry\-run\fR mode. Hook scripts must honor \fBRSBACKUP_ACT\fR which will be set to \fBfalse\fR in this mode and \fBtrue\fR otherwise. .SS "Device Hooks" Device hooks are executed (once) before doing anything that will access backup devices (even just to read them). .PP The following environment variables are set when a device hook is executed: .TP .B RSBACKUP_ACT Set to \fBfalse\fR in \fB\-\-dry\-run\fR mode and \fBtrue\fR otherwise. .TP .B RSBACKUP_DEVICES A space-separated list of known device names. .TP .B RSBACKUP_HOOK The name of the hook (i.e. \fBpre\-device\-hook\fR, etc). This allows a single hook script to serve as the implementation for multiple hooks. .PP Device hooks used to be called access hooks. .SS "Volume Hooks" Pre-volume hooks are executed before all the backups of a volume, and post-volume hooks after all backups of the volume. Possible uses for volume hooks include snapshotting volumes or mounting volumes. .PP When a volume hook is executed, the environment variables listed in \fBENVIRONMENT\fR below are set, along with the following: .TP .B RSBACKUP_HOOK The name of the hook (i.e. \fBpre\-volume\-hook\fR, etc). This allows a single hook script to serve as the implementation for multiple hooks. .PP The exit status of the \fBpre\-volume\-hook\fR is interpreted as follows: .TP .B 0 The hook succeeded. The backup will be attempted. .TP .B 75 The volume is temporarily unavailable. The backup will not be attempted, as if \fBcheck\-file\fR or \fBcheck-mounted\fR had failed. .TP .I anything else Something went wrong. The backup will be treated as failed, as if it had been attempted and \fBrsync\fR had failed. .PP See \fBrsbackup\-snapshot\-hook\fR(1) for a hook program that can be used to back up from Linux LVM snapshots. .PP Volume hooks used to be called backup hooks. .SH ENVIRONMENT When a hook or \fBrsync\fR are executed, the following environment variables are set: .TP .B RSBACKUP_ACT Set to \fBfalse\fR in \fB\-\-dry\-run\fR mode and \fBtrue\fR otherwise. .TP .B RSBACKUP_HOST The name of the host. .TP .B RSBACKUP_GROUP The name of the concurrency group. See the \fBgroup\fR directive. .TP .B RSBACKUP_SSH_HOSTNAME The SSH hostname of the host. .IP Recall that \fBrsbackup\fR treats the hostname \fBlocalhost\fR specially. If the hook also needs to do so then it must duplicate this logic. .TP .B RSBACKUP_SSH_TARGET The SSH hostname and username combined for passing to \fBssh\fR(1). .IP This will be \fIusername\fB@\fIhostname\fR or just \fIhostname\fR depending on whether a SSH username was set. .TP .B RSBACKUP_SSH_USERNAME The SSH username of the host. If no SSH username was set, this variable will not be set. .TP .B RSBACKUP_VOLUME The name of the volume. .TP .B RSBACKUP_VOLUME_PATH The path to the volume. .SH CONCURRENCY Any given device only gets used for one thing at a time; it will never happen that two backups, or two prunes, access the same device. .PP No concurrency group will ever have more than one backup made from it any a time. Normally a concurrency group is just a single host, but the \fBgroup\fR directive can be used to add multiple hosts to a single group (for instance, if they share physical hardware). .PP No two hooks will be executed concurrently, even if they apply to different concurrency groups and different devices. However, a hook may execute while a backup (for a different concurrency group and a different device) is executing. .SH NOTES .SS "Resource Control" Large backup jobs can have unreasonable impacts on kernel memory, evicting applications and cache data by the gigabyte just for single-use copies of backup data. .PP On Linux this problem can be addressed with with the memory cgroup controller. .PP First, a slice is created on each host (both the back server and client machines): .PP .in +4n .EX [Unit] Description=Memory-bound slice for rsbackup Before=slices.target [Slice] MemoryAccounting=true MemoryHigh=128M MemoryMax=256M .EE .in .PP Second, \fBrsbackup\fR is run with a memory use limit: .PP .in +4n .EX systemd-run --quiet --pipe --slice membound rsbackup --backup .EE .in .PP If you are using the Debian cron job then this can be configured in \fI/etc/rsbackup/defaults\fR: .PP .in +4n .EX nicely="systemd-run --quiet --pipe --slice membound" .EE .in .PP Finally, to control resource use on client machines, add the following to their \fBhost\fR sections: .PP .in +4n .EX rsync-remote "systemd-run --quiet --pipe --slice membound rsync" .EE .in .PP See also: \fBsystemd-run\fR(1), \fBsystemctl\fR(1), \fBsystemd.slice\fR(5), \fBsystemd.resource-control\fR(5), \fBrsbackup.cron\fR(1). .SS macOS Apple's \fBrsync\fR does not have the \-\-open-noatime option, and has a nonstandard option to enable backup of extended attributes. .PP For local backups you can configure \fBrsbackup\fR to backup extended attributes with a host-level directive: .PP .in +4n .EX rsync-extra-options --extended-attributes .EE .in .PP If backing up a macOS host from a host with a modern \fBrsync\fR, or vice versa, however, extended attributes and ACLs cannot be backed up at all. In that case the affected hosts must disable backup attribute and ACL backup as follows: .PP .in +4n .EX rsync-extra-options .EE .in .PP If an up-to-date \fBrsync\fR is used on macOS hosts, it can be left at the default. .SS Windows \fBrsbackup\fR does not run on Windows. However, it may be used to back up Windows filesystems. In this case it can happen that the attributes in the Windows filesystem do not fit in the backup filesystem; if this happens you may see errors like this: .PP .in +4n .EX rsync: rsync_xal_set: lsetxattr(""/backup7/host/volume/2018-02-04/path/to/file"","attrname") failed: No space left on device (28) rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1668) [generator=3.1.2] .EE .in .PP In that case the affected volumes must disable attribute backup and ACL backup as follows: .PP .in +4n .EX rsync-extra-options --open-noatime .EE .in .SH "SEE ALSO" \fBrsbackup\fR(1), \fBrsbackup\-graph\fR(1), \fBrsbackup.cron\fR(1), \fBrsbackup\-mount\fR(1), \fBrsbackup\-snapshot\-hook\fR(1), \fBrsync\fR(1), \fBrsbackup\fR(5) .SH AUTHOR Richard Kettlewell rsbackup-10.0/doc/rsbackup.cron.1000066400000000000000000000061511440730431700166530ustar00rootroot00000000000000.TH rsbackup.cron 1 .\" Copyright (c) 2011 Richard Kettlewell .\" .\" 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 . .SH NAME rsbackup.cron \- cron script for rsbackup .SH SYNOPSIS \fBrsbackup.cron\fR [\fIOPTIONS\fR] \fIFREQUENCY\fR .SH DESCRIPTION \fBrsbackup.cron\fR is invoked from cron to run automated backups, pruning and reporting. .PP \fIFREQUENCY\fR must be one of \fBhourly\fR, \fBdaily\fR, \fBweekly\fR or \fBmonthly\fR, corresponding to the frequency it is invoked at. .SH OPTIONS .TP .B \-\-dry\-run\fR, \fB-n Passes the \fB\-\-dry\-run\fR option to \fBrsbackup\fR. .TP .B \-\-verbose\fR, \fB-v Displays the \fBrsbackup\fR command before executing it. .TP .B \-\-help\fR, \fB-h Displays a usage message and terminates. .TP .B \-\-version\fR, \fB\-V Display version string. .SH CONFIGURATION Configuration is read from \fI/etc/rsbackup/defaults\fR. This is a shell script fragment and it should define the following variables: .TP .B backup Defines the frequency to \fIattempt\fR backups, i.e. to run \fBrsbackup --backup\fR. The default is \fBhourly\fR. .IP Use backup policies for full control over the actual backup frequency. .TP .B report Defines the frequency to send an email report. The default is \fBdaily\fR. .TP .B email Defines the destination address for the email report. .TP .B prune Defines the frequency of pruning old backups. The default is \fBdaily\fR. .TP .B prune_incomplete Defines the frequency of pruning incomplete backups. The default is \fBdweekly\fR. .SS Frequencies The possible frequences are \fBhourly\fR, \fBdaily\fB, \fBweekly\fR or \fBmonthly\fR. .SS Example .in +4n .EX # # Set backup=hourly|daily|weekly|monthly to control frequency of # backup attempts. (Use backup policies for fine-grained control over # when backups happen.) # backup=hourly # # Set report=hourly|daily|weekly|monthly to control frequency of # email reports. (Hourly is probably a bit much!) Only effective # if email is not "". # report=daily # # Set email=ADDRESS to have the report emailed to that address. # email=root # # Set prune=hourly|daily|weekly|monthly|never to control frequency of # automated pruning of old backups # prune=daily # # Set prune_incomplete=hourly|daily|weekly|monthly|never to control # frequency of automated pruning of incomplete backups # prune_incomplete=weekly # # Prefix to the rsbackup command # Use 'nice' and/or 'ionice' here. Remember to quote correctly. # nicely= .EE .in .SH "SEE ALSO" \fBrsbackup\fR(1), \fBrsbackup-mount\fR(1), \fBrsbackup\fR(5). .SH AUTHOR Richard Kettlewell rsbackup-10.0/doc/rsbackup.css000066400000000000000000000032351440730431700163430ustar00rootroot00000000000000/* Copyright © 2011 Richard Kettlewell. * * 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 http://www.gnu.org/licenses/. */ body { color: black; background-color: white } a:link, a:visited, a:active { color: blue; text-decoration: underline } h1 { background-color: #e0ffe0; padding: 0.2em } h2 { background-color: #e0e0e0; padding: 0.2em } h3 { text-decoration: underline } a.h3 { margin-left: 1em } h1,h2,h3,h4 { font-family: sans-serif } table { border-collapse: collapse } th { background-color: #e0e0e0; border-left: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0; border-right: 1px solid #e0e0e0 } td { border: 1px solid black; vertical-align: top; padding-left: 4px; padding-right: 4px } td.bad { background-color: #ff4040; color: #ffffff } td.good { background-color: #e0ffe0; color: #000000 } span.bad { color: #ff4040 } pre.log { background-color: #f0f0f0 } .example { border-left: 2px solid black; padding-left: 2px } div.volume { margin-left: 1em } .recent { color: #ff4040 } img.history { border: 1px solid black; padding: 2px } rsbackup-10.0/doc/testing.md000066400000000000000000000033021440730431700160110ustar00rootroot00000000000000rsbackup testing ================ rsbackup contains, broadly speaking, three collections of tests. Unit Tests ---------- These are `src/test-*.cc` and test individual aspects of the source code. Currently they are strongest on utility functions and classes and weakest on the functional logic of rsbackup. Missing things include: * The `Backup` class. * Error/warning reporting. * Store identification. * Device access hook invocation. * Most of the implementation of the following classes: * `Host` * `Volume` * `Conf` * Most of the hard bits of the `Subprocess` class. * Much of the `Command` class. * Error handling in the following classes: * `Directory` * `IO` * `Unicode` * Functional logic such as `MakeBackup.cc` and `Prune.cc`. Functional Tests ---------------- These can be found in the `tests` directory. They execute the `rsbackup` commands in various ways and verify that the behavior and output are as expected. Missing things include: * Sending email. * Responses to subprocess, IO and database errors. * Prompting. * Warning options. * Config file syntax errors. * Certain config file options. * Store permission checking. * Handling of ‘incomplete’ backups. * Removal of old pruning records. * Remote backups (needs a ‘fake’ `ssh`). Script Tests ------------ These are `tools/t-*` and test some of the scripts that accompany rsbackup. They use `scripts/fakeshell.sh` to mock commands invoked by the scripts. Missing: * `rsbackup-mount` * `rsbackup.cron` Running Tests ------------- Tests can be run via `make check` in the root directory, or in one of the individual directories listed above. To record test coverage information, configure with: ./configure --with-gcov rsbackup-10.0/docker/000077500000000000000000000000001440730431700145165ustar00rootroot00000000000000rsbackup-10.0/docker/fedora34/000077500000000000000000000000001440730431700161255ustar00rootroot00000000000000rsbackup-10.0/docker/fedora34/Dockerfile000066400000000000000000000005261440730431700201220ustar00rootroot00000000000000FROM fedora:34 RUN yum update -y && \ yum install -y \ acl \ autoconf \ automake \ boost-devel \ cairomm-devel \ diffutils \ findutils \ gcc-c++ \ git \ make \ pangomm-devel \ python3-devel \ python3-pip \ rsync \ sqlite-devel \ && \ yum clean all RUN pip3 install xattr ADD build /build VOLUME /src WORKDIR /src CMD /build rsbackup-10.0/docker/fedora34/build000077500000000000000000000001261440730431700171510ustar00rootroot00000000000000#!/bin/sh set -e autoreconf -si ./configure make clean make -j $(nproc) make -j check rsbackup-10.0/docker/rsbcentos7/000077500000000000000000000000001440730431700166075ustar00rootroot00000000000000rsbackup-10.0/docker/rsbcentos7/Dockerfile000066400000000000000000000005571440730431700206100ustar00rootroot00000000000000FROM centos:centos7 RUN yum update -y && \ yum install -y centos-release-scl && \ yum install -y \ autoconf \ automake \ boost-devel \ cairomm-devel \ devtoolset-7 \ make \ pangomm-devel \ python3-devel \ python3-pip \ git \ sqlite-devel \ && \ yum clean all RUN pip3 install xattr ADD build /build VOLUME /src WORKDIR /src CMD /build rsbackup-10.0/docker/rsbcentos7/build000077500000000000000000000001641440730431700176350ustar00rootroot00000000000000#!/bin/sh set -e . /opt/rh/devtoolset-7/enable autoreconf -si ./configure make clean make -j $(nproc) make -j check rsbackup-10.0/docker/rsbcentos8/000077500000000000000000000000001440730431700166105ustar00rootroot00000000000000rsbackup-10.0/docker/rsbcentos8/Dockerfile000066400000000000000000000006511440730431700206040ustar00rootroot00000000000000FROM centos:centos8 RUN dnf install -y 'dnf-command(config-manager)' RUN dnf config-manager --set-enabled PowerTools RUN yum update -y && \ yum install -y \ git \ autoconf \ automake \ sqlite-devel \ cairomm-devel \ pangomm-devel \ make \ boost-devel \ gcc-c++ \ python3-pip \ python3-devel \ rsync \ && \ yum clean all RUN pip3 install xattr ADD build /build VOLUME /src WORKDIR /src CMD /build rsbackup-10.0/docker/rsbcentos8/build000077500000000000000000000001261440730431700176340ustar00rootroot00000000000000#!/bin/sh set -e autoreconf -si ./configure make clean make -j $(nproc) make -j check rsbackup-10.0/docker/rsbdebian10/000077500000000000000000000000001440730431700166105ustar00rootroot00000000000000rsbackup-10.0/docker/rsbdebian10/Dockerfile000066400000000000000000000007161440730431700206060ustar00rootroot00000000000000FROM debian:buster RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ acl \ autoconf \ automake \ git \ libboost-dev \ libboost-filesystem-dev \ libboost-system-dev \ libcairomm-1.0-dev \ libpangomm-1.4-dev \ libsqlite3-dev \ lynx \ rsync \ sqlite3 \ xattr \ build-essential \ && \ apt-get clean ADD build /build VOLUME /src WORKDIR /src CMD /build rsbackup-10.0/docker/rsbdebian10/build000077500000000000000000000001261440730431700176340ustar00rootroot00000000000000#!/bin/sh set -e autoreconf -si ./configure make clean make -j $(nproc) make -j check rsbackup-10.0/docker/run000077500000000000000000000003221440730431700152450ustar00rootroot00000000000000#! /bin/sh set -e target=$1 shift docker build -t $target docker/$target docker run \ -it \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ -w /src \ $target:latest \ "$@" rsbackup-10.0/scripts/000077500000000000000000000000001440730431700147365ustar00rootroot00000000000000rsbackup-10.0/scripts/coverity000077500000000000000000000034011440730431700165260ustar00rootroot00000000000000#! /bin/bash # # Copyright (C) 2014-15 Richard Kettlewell # # 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 . set -e build=false submit=false while [ $# -gt 0 ]; do case "$1" in --submit ) submit=true shift ;; --build ) build=true shift ;; * ) echo >&2 "ERROR: unknown option $1" exit 1 ;; esac done name=rsbackup covpath=/usr/local/coverity/bin version="$(git describe --tags --dirty --always)" description="$(git describe --tags --dirty --always)" # .coverity should set token, email and project . .coverity echo "email=${email}" echo "file=@${name}.tgz" echo "version=${version}" echo "description=${description}" echo "project=${project}" if $build; then echo echo Building ... echo make distclean || true rm -rf cov-int ./configure "${covpath}/cov-build" --dir cov-int make -j$(nproc) make distclean tar czvf ${name}.tgz cov-int rm -rf cov-int fi if $submit; then echo echo Submitting ... echo curl --form token="${token}" \ --form email="${email}" \ --form file=@${name}.tgz \ --form version="${version}" \ --form description="${description}" \ "https://scan.coverity.com/builds?project=${project}" fi rsbackup-10.0/scripts/dist000077500000000000000000000054711440730431700156360ustar00rootroot00000000000000#! /bin/sh # # This file is part of rsbackup # Copyright (C) 2010, 2011, 2013-16 Richard Kettlewell # # 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 . set -e # s COMMAND... # # Echo a command then execute it. s() { echo "$@" >&2 "$@" } # r HOST COMMAND... # # Echo a command then execute it remotely. r() { h=$1 shift echo "$h:" "$@" >&2 case "$h" in chroot:* ) ( cd schroot -pc${h#chroot:} -- bash -c "$@" ) ;; * ) ssh $h "$@" ;; esac } # build HOST ARCH # # Create a .deb on HOST for architecture ARCH, then copy it back here # and add it to the list of build products. build() { host=$1 arch=$2 debs="" for b in $binpkgs; do debs="$debs ${b}_${debversion}_${arch}.deb" done echo echo "Build on $host for $arch" echo r $host "mkdir -p _builds" r $host "cd _builds && rm -rf ${source} ${archive} ${debs}" case "$host" in chroot:* ) cp ${archive} $HOME/_builds/. ;; * ) s scp ${archive} $host:_builds/. ;; esac r $host "cd _builds && tar xfz ${archive}" r $host "cd _builds/${source} && debian/rules build" r $host "cd _builds/${source} && fakeroot debian/rules binary" for deb in $debs; do case "$host" in chroot:* ) cp $HOME/_builds/$deb products/ ;; * ) s scp $host:_builds/$deb products/ ;; esac done echo echo "Built $debs" echo } rm -rf products mkdir products # Make sure auto*-generated files are up to date s autoreconf -si s ./configure # Build the source archive s make -C doc s make -C doc html s make -j$(nproc) distcheck srcpkg=rsbackup # source package name binpkgs="rsbackup rsbackup-graph" # binary packages version=$(make echo-version) # get version number debversion=$(dpkg-parsechangelog -ldebian/changelog -SVersion) source=${srcpkg}-${version} # source directory archive=${srcpkg}-${version}.tar.gz # tarball # Build .deb files s build araminta amd64 # buster cp ${archive} doc/*.html doc/*.css products/. rm -f products/*.in.html products/*.prefix.html mv products/CHANGES.html products/rsbackup-CHANGES.html lintian -i -I products/*.deb cd products for f in *.tar.gz *.deb; do echo echo "* Signing $f ..." echo gpg -a -b "$f" done cd .. ls -l products rsbackup-10.0/scripts/fakeshell.sh000066400000000000000000000166711440730431700172430ustar00rootroot00000000000000# Copyright © 2014 Richard Kettlewell. # # 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 . # # fake_init [OPTIONS] # # Creates fake workspace directory. # # Options: # --autoclean Clean up workspace on exit (default) # --no-autoclean Don't clean up worksapce on exit # fake_init() { local autoclean=true if [ ! -z "${fake_work}" ]; then echo "ERROR: fake_init already called" >&2 exit 1 fi while [ $# -gt 0 ]; do case "$1" in --autoclean ) autoclean=true shift ;; --no-autoclean ) autoclean=false shift ;; * ) echo "ERROR: fake_init: unknown option '$1'" >&2 exit 1 ;; esac done fake_work=`mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX` PATH="${fake_work}/faked:${PATH}" export PATH if $autoclean; then trap "fake_cleanup" EXIT trap "fake_cleanup 1" INT HUP TERM fi } # # fake_reset # # Clears the set of faked commands. Use this before each test. # fake_reset() { if [ -z "${fake_work}" ]; then echo "ERROR: fake_init not called" >&2 exit 1 fi rm -rf "${fake_work}/faked" "${fake_work}/checks" mkdir "${fake_work}/faked" "${fake_work}/checks" fake_failed=false } # # fake_cmd [OPTIONS] NAME [CMD] [--must-args ARGS] # # Create a faked command called NAME which executes CMD. # The default value of CMD is 'true'. # # Options: # --must-run This command must be run (see fake_check) # --must-not-run This command must not be run (see fake_check) # --must-args Require a specific argument sequence # fake_cmd() { local name cmd must_run must_not_run must_args argno limit must_run=false must_not_run=false must_args=false if [ ! -d "${fake_work}/faked" ]; then echo "ERROR: fake_reset not called" >&2 exit 1 fi while [ $# -gt 0 ]; do case "$1" in --must-run ) must_run=true shift ;; --must-not-run ) must_not_run=true shift ;; -* ) echo "ERROR: fake_cmd: unknown option '$1'" >&2 exit 1 ;; * ) break ;; esac done name="$1" shift if [ $# -gt 0 ] && [ "$1" != --must-args ]; then cmd="$1" shift else cmd=true fi if [ $# -gt 0 ] && [ "$1" = --must-args ]; then must_args=true shift else must_args=false fi echo "#! /usr/bin/env bash" > "${fake_work}/faked/${name}" echo "set -e" >> "${fake_work}/faked/${name}" # echo "echo \$0 \"\$@\" >&2" >> "${fake_work}/faked/${name}" if $must_run; then echo "ERROR: ${name}: was not run" > "${fake_work}/checks/${name}.run" echo "rm -f ${fake_work}/checks/${name}.run" \ >> "${fake_work}/faked/${name}" fi if $must_not_run; then echo "echo ERROR: ${name}: was run unexpectedly >> ${fake_work}/checks/${name}.errors" \ >> "${fake_work}/faked/${name}" fi if $must_args; then echo "if [ \$# != $# ]; then" >> "${fake_work}/faked/${name}" echo " echo ERROR: ${name}: expected $# args got \$# >> ${fake_work}/checks/${name}.errors" \ >> "${fake_work}/faked/${name}" echo "fi" >> "${fake_work}/faked/${name}" n=1 limit=$# while [ $n -le $limit ]; do echo "if [ \"\$$n\" != \"$1\" ]; then" \ >> "${fake_work}/faked/${name}" echo " echo ERROR: ${name}: arg $n: expected $1 got \$$n >> ${fake_work}/checks/${name}.errors" \ >> "${fake_work}/faked/${name}" echo "fi" >> "${fake_work}/faked/${name}" n=$(($n+1)) shift done fi echo "$cmd" >> "${fake_work}/faked/${name}" chmod +x "${fake_work}/faked/${name}" } # # fake_run [OPTIONS] [--] COMMAND ... # # Runs a command and checks its exit status. # Default is to insist that it exists with status 0. # # Options: # --must-exit STATUS must exit with a particular status # --must-output STRING must write a particular string to stdout # --must-output-empty must write nothing to stdout # # fake_run() { local must_exit must_output must_output_set must_output_empty status must_exit=0 must_output_set=false must_output_empty=false while [ $# -gt 0 ]; do case "$1" in --must-exit ) shift must_exit="$1" shift ;; --must-output ) shift must_output="$1" must_output_set=true shift ;; --must-output-empty ) must_output_empty=true shift ;; -- ) shift break ;; -* ) echo "ERROR: fake_run: unknown option '$1'" >&2 exit 1 ;; * ) break ;; esac done if $must_output_set || $must_output_empty; then if $must_output_empty; then touch "${fake_work}/expected" else echo "$must_output" > "${fake_work}/expected" fi set +e "$@" > "${fake_work}/got" status=$? set -e else set +e "$@" status=$? set -e fi if [ $status != $must_exit ]; then echo "ERROR: $1 exited with status $status (expected $must_exit)" >&2 exit 1 fi if $must_output_set || $must_output_empty; then if ! diff -ruN "${fake_work}/expected" "${fake_work}/got" \ > "${fake_work}/diff"; then echo "ERROR: $1 gave unexpected output" >&2 cat "${fake_work}/diff" >&2 exit 1 fi fi } # fake_check [OPTIONS] # # Reports any --must-* violations (see fake_cmd). # # Options: # --expected-fail Don't terminate on failure # fake_check() { local fatal failed fatal=true failed=false if [ ! -d "${fake_work}/faked" ]; then echo "ERROR: fake_reset not called" >&2 exit 1 fi while [ $# -gt 0 ]; do case "$1" in --expected-fail ) fatal=false shift ;; * ) echo "ERROR: fake_check: unknown option '$1'" >&2 exit 1 ;; esac done for f in "${fake_work}/checks/"*; do if [ -f "${f}" ]; then cat "${f}" >&2 failed=true fi done if $fatal && $failed; then exit 1 fi } # # fake_cleanup # # Remove workspace directory. Run by default on termination by default # (see fake_init). # fake_cleanup() { if [ ! -z "${fake_work}" ] && [ -d "${fake_work}" ]; then rm -rf "${fake_work}" unset fake_work fi if [ ! -z "$1" ]; then exit "$1" fi } rsbackup-10.0/scripts/host-check000077500000000000000000000013601440730431700167140ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2017 Richard Kettlewell. # # 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 . set -e touch $1.ran if [ -e $1.fail ]; then exit 1 fi exit 0 rsbackup-10.0/scripts/htmlman000077500000000000000000000041401440730431700163230ustar00rootroot00000000000000#! /bin/sh # # This file is part of DisOrder # Copyright (C) 2004, 2005, 2007, 2008, 2010 Richard Kettlewell # # 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 . # set -e stdhead=false extension="html" GNUSED=${GNUSED:-sed} while test $# -gt 0; do case "$1" in -stdhead ) stdhead=true ;; -extension ) shift extension=$1 ;; -- ) shift break ;; -* ) echo >&2 "ERROR: unknown option $1" exit 1 ;; * ) break ;; esac shift done for page; do title=$(basename $page) output=$page.$extension echo "$page -> $output" >&2 exec > $output.new echo "" echo " " if $stdhead; then echo "@quiethead@#" fi echo " $title" echo " " echo " " if $stdhead; then echo "@stdmenu{}@#" fi printf "
      "
        # this is kind of painful using only BREs
        nroff -Tascii -man "$page" | ${GNUSED} \
                              '1d;$d;
                               1,/./{/^$/d};
                               s/&/\&/g;
                               s//\>/g;
                               s/@/\@/g;
                               s!\(.\)\1!\1!g;
                               s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
                               s!_\(.\)!\1!g;
                               s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
                               s!<\1>!!g'
        echo "
      " if $stdhead; then echo "@credits" fi echo " " echo "" mv $output.new $output done rsbackup-10.0/scripts/systest000077500000000000000000000110631440730431700164030ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2012, 2014, 2017 Richard Kettlewell. # # 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 . set -e CLIENTS="deodand wampoon" WORK=${PWD}/systest.work TOOLS=${PWD}/tools SCRIPTS=${PWD}/scripts RSBACKUP=${PWD}/src/rsbackup x() { echo "EXEC:" "$@" >&2 "$@" } vg() { case "$1" in deodand ) echo deodand2 ;; wampoon | fanticule ) echo "$1" ;; * ) echo "" ;; esac } # Clean up leftovers clean() { for client in ${CLIENTS}; do vg=`vg $client` x ssh ${client} "umount /rsbackup || true" if [ "$vg" != "" ]; then x ssh ${client} "umount /var/lib/rsbackup/snapshots/rsbackup || true" x ssh ${client} "dd conv=nocreat if=/dev/zero of=/dev/${vg}/rsbackup || true" x ssh ${client} "lvremove -f ${vg}/rsbackup.snap || true" x ssh ${client} "lvremove -f ${vg}/rsbackup || true" x ssh ${client} "rmdir /rsbackup /var/lib/rsbackup/snapshots/rsbackup /var/lib/rsbackup/snapshots || true" else x ssh ${client} "rm -rf /rsbackup" fi done rm -rf ${WORK} } # Create subject volumes etc setup() { echo "Creating directories" mkdir -p ${WORK} ${WORK}/logs mkdir -m 0700 -p ${WORK}/store echo dummy > ${WORK}/store/device-id echo "Creating config" cat > ${WORK}/config <> ${WORK}/config <&2 "ERROR: host check for ${client} did not run" exit 1 fi done } backup_present() { if [ -d ${WORK}/store/$1/rsbackup/$(date +%F) ]; then : else echo >&2 "ERROR: backup for $1 missing" exit 1 fi } backup_absent() { if [ -d ${WORK}/store/$1/rsbackup/$(date +%F) ]; then echo >&2 "ERROR: backup for $1 unexpectedly present" exit 1 fi } backup() { clean_host_checks x touch wampoon.fail x ${RSBACKUP} --backup --verbose --config ${WORK}/config host_checks_ran backup_present deodand backup_absent wampoon x rm -f wampoon.fail x ${RSBACKUP} --backup --verbose --config ${WORK}/config host_checks_ran backup_present deodand backup_present wampoon clean_host_checks } verify() { for client in ${CLIENTS}; do echo "Verifying ${client}" diff -rqN --exclude systest.work . ${WORK}/store/${client}/rsbackup/$(date +%F)/. done } actions="clean setup backup verify clean" while [ $# -gt 0 ]; do case "$1" in --clean ) actions="clean" ;; --leave ) actions="clean setup backup verify" ;; --help ) cat <&2 exit 1 ;; esac shift done for action in $actions; do echo "ACTION: $action" $action echo done rsbackup-10.0/scripts/travis/000077500000000000000000000000001440730431700162465ustar00rootroot00000000000000rsbackup-10.0/scripts/travis/linux/000077500000000000000000000000001440730431700174055ustar00rootroot00000000000000rsbackup-10.0/scripts/travis/linux/clang/000077500000000000000000000000001440730431700204715ustar00rootroot00000000000000rsbackup-10.0/scripts/travis/linux/clang/configure000077500000000000000000000001371440730431700224010ustar00rootroot00000000000000#!/bin/sh set -e set -x export CXXFLAGS_EXTRA="-Wno-error=deprecated-declarations" ./configure rsbackup-10.0/scripts/travis/linux/gcc/000077500000000000000000000000001440730431700201415ustar00rootroot00000000000000rsbackup-10.0/scripts/travis/linux/gcc/configure000077500000000000000000000001621440730431700220470ustar00rootroot00000000000000#!/bin/sh set -e set -x export CXXFLAGS_EXTRA="-Wno-error=deprecated-declarations" ./configure CC=gcc-6 CXX=g++-6 rsbackup-10.0/scripts/travis/osx/000077500000000000000000000000001440730431700170575ustar00rootroot00000000000000rsbackup-10.0/scripts/travis/osx/clang/000077500000000000000000000000001440730431700201435ustar00rootroot00000000000000rsbackup-10.0/scripts/travis/osx/clang/configure000077500000000000000000000004201440730431700220460ustar00rootroot00000000000000#!/bin/sh set -e set -x export CXXFLAGS_EXTRA="-Wno-error=c++14-extensions -Wno-error=deprecated" # Needed to work around a bug in Homebrew # https://github.com/Homebrew/homebrew-core/issues/37873 export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig" ./configure rsbackup-10.0/scripts/travis/osx/clang/install000077500000000000000000000002711440730431700215370ustar00rootroot00000000000000#!/bin/sh set -e set -x brew unlink python@2 # workaround clash with python3 (which seems to be a pangomm dep) brew install pangomm cairomm libsigc++ clang-format brew link libsigc++@2 rsbackup-10.0/scripts/txt2src000077500000000000000000000016421440730431700163000ustar00rootroot00000000000000#! /usr/bin/perl -w # Copyright © 2011 Richard Kettlewell. # # 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 . use strict; my $symbol = shift; (print "char $symbol\[] =\n") or die "$!\n"; while(<>) { s/[\"\\]/\\$&/g; s/\n/\\n/g; (print " \"$_\"\n") or die "$!\n"; } (print ";\n") or die "$!\n"; (close STDOUT) or die "$!\n"; rsbackup-10.0/src/000077500000000000000000000000001440730431700140365ustar00rootroot00000000000000rsbackup-10.0/src/Action.cc000066400000000000000000000134531440730431700155700ustar00rootroot00000000000000// Copyright © 2015, 2016, 2020 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "EventLoop.h" #include "Action.h" #include #include #include #include #include /** @brief A reactor which cancels pruning after a timeout */ class ActionListTimeoutReactor: public Reactor { public: virtual void onTimeout(EventLoop *eventloop, const struct timespec &) override { timedOut = true; warning(WARNING_VERBOSE, "action list timed out, killing subprocesses"); eventloop->terminateSubprocesses(); } bool timedOut = false; }; void Action::done(EventLoop *, ActionList *) {} void ActionList::add(Action *a) { if(actions.find(a->name) != actions.end()) throw std::logic_error("duplicate action " + a->name); actions[a->name] = a; } void ActionList::setLimit(struct timespec &when) { limit = when; } void ActionList::go(bool wait_for_timeouts) { D("go"); ActionListTimeoutReactor timeout_reactor; if(limit.tv_sec) eventloop->whenTimeout(limit, &timeout_reactor); while(actions.size() > 0) { trigger(); eventloop->wait(wait_for_timeouts); } timedOut = timeout_reactor.timedOut; } void ActionList::trigger() { D("trigger"); Action *chosen = nullptr; if(limit.tv_sec) { struct timespec now; getMonotonicTime(now); if(now.tv_sec > limit.tv_sec || (now.tv_sec == limit.tv_sec && now.tv_nsec > limit.tv_nsec)) { // Cancel all pending actions std::vector cancel; for(auto it: actions) { Action *a = it.second; if(a->state == Action::Pending) cancel.push_back(a); } for(auto a: cancel) { warning(WARNING_VERBOSE, "action list timed out, cancelling %s", a->name.c_str()); cleanup(a, false, false); } return; } } for(auto it: actions) { Action *a = it.second; if(a->state != Action::Pending || blocked_by_resource(a) || blocked_by_dependency(a)) continue; if(failed_by_dependency(a)) { cleanup(a, false, false); return trigger(); } if(chosen == nullptr || chosen->priority < a->priority) chosen = a; } if(chosen) { chosen->state = Action::Running; for(std::string &r: chosen->resources) resources.insert(r); D("action %s starting", chosen->name.c_str()); chosen->go(eventloop, this); // Repeat in case there are more return trigger(); } } void ActionList::completed(Action *a, bool succeeded) { cleanup(a, succeeded, true); } void ActionList::cleanup(Action *a, bool succeeded, bool ran) { D("action %s %s", a->name.c_str(), succeeded ? "succeeded" : "failed"); auto it = actions.find(a->name); assert(it != actions.end()); if(it != actions.end()) { assert(a == it->second); if(ran) { assert(a->state == Action::Running); for(std::string &r: a->resources) resources.erase(r); a->state = succeeded ? Action::Succeeded : Action::Failed; } actions.erase(it); states[a->name] = succeeded ? Action::Succeeded : Action::Failed; if(ran) { a->done(eventloop, this); trigger(); } return; } } bool ActionList::blocked_by_resource(const Action *a) { for(auto &r: a->resources) if(contains(resources, r)) { D("action %s blocked by resource %s", a->name.c_str(), r.c_str()); return true; } return false; } bool ActionList::failed_by_dependency(const Action *a) { for(auto &p: a->predecessors) { if(p.flags & ACTION_GLOB) { for(auto it: states) { if(fnmatch(p.name.c_str(), it.first.c_str(), FNM_PATHNAME) != FNM_NOMATCH && it.second != Action::Succeeded) { assert(it.second == Action::Failed); D("action %s depends on success of failed action %s as %s", a->name.c_str(), it.first.c_str(), p.name.c_str()); return true; } } } else { auto d = states.find(p.name); if(d != states.end() // P completed or failed && (p.flags & ACTION_SUCCEEDED) // A needs P to have succeeded && d->second != Action::Succeeded) { // P failed assert(d->second == Action::Failed); D("action %s depends on success of failed action %s", a->name.c_str(), p.name.c_str()); return true; } } } return false; } bool ActionList::blocked_by_dependency(const Action *a) { for(auto &p: a->predecessors) { if(find(p) != actions.end()) { D("action %s blocked by dependency %s", a->name.c_str(), p.name.c_str()); return true; } else { // Sanity check if(!(p.flags & ACTION_GLOB)) { auto d = states.find(p.name); if(d == states.end()) throw std::logic_error(a->name + " follows unknown action " + p.name); } } } return false; } std::map::iterator ActionList::find(const ActionStatus &as) { if(as.flags & ACTION_GLOB) { auto it = actions.begin(); while(it != actions.end() && fnmatch(as.name.c_str(), it->first.c_str(), FNM_PATHNAME) == FNM_NOMATCH) ++it; return it; } else return actions.find(as.name); } rsbackup-10.0/src/Action.h000066400000000000000000000231631440730431700154310ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2015, 2016, 2020 Richard Kettlewell. // // 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 . #ifndef ACTION_H #define ACTION_H /** @file Action.h * @brief Sequencing of concurrent operations * * An @ref Action performs some task, while holding some collection of * resources. The task is executed within the context of an @ref EventLoop and * is initiated by @ref Action::go. Resources are registered using @ref * Action::uses. * * An @ref ActionList is an ordered container of @ref Action objects. Actions * are executed concurrently, with the restriction that no two actions can hold * the same resource concurrently. * * These objects are (intended to be) used wherever concurrency can be * exploited. Currently, this means @ref pruneBackups and @ref retireVolumes. */ #include #include #include #include class ActionList; class EventLoop; /** @brief Status of an action */ struct ActionStatus { /** @brief Action name */ std::string name; /** @brief Action flags * * @see Action::after */ unsigned flags; }; /** @brief Action must succeed * * @see Action::after */ #define ACTION_SUCCEEDED 0x0001 /** @brief Match action by globbing * * @see Action::after */ #define ACTION_GLOB 0x0002 /** @brief One action that may be initiated concurrently * * An @ref Action performs some task, while holding some collection of * resources. The task is executed within the context of an @ref EventLoop and * is initiated by @ref Action::go; it should call ActionList::completed when * it is finished. * * Actions require resources, which are identified by strings. No two * actions that require the same resource (as identified by string comparison) * are run concurrently. Resources are registered using @ref Action::uses. * * Actions have dependencies on other actions, identified either by * strings or by glob patterns. Furthermore the dependency may either be an * order-only dependency, which just controls sequencing, or may be a * dependency on the success of an action. Dependencies are registered using * @ref Action::after. * * Actions must be added to an @ref ActionList to be executed. */ class Action { public: /** @brief Constructor * @param name Action name */ Action(const std::string &name): name(name) {} /** @brief Destructor */ virtual ~Action() = default; /** @brief Specify a resource that this action uses * @param r Resource name * * Actions that use the same resource are not run concurrently. */ void uses(const std::string &r) { resources.push_back(r); } /** @brief Start the action * @param e Event loop * @param al Execution context * * Actions should not (normally) block but instead execute asynchronously * using event loop @p e. * * When it is complete it must call @ref ActionList::completed. */ virtual void go(EventLoop *e, ActionList *al) = 0; /** @brief Called when the action is complete * @param e Event loop * @param al Execution context * * The default implementation does nothing. */ virtual void done(EventLoop *e, ActionList *al); /** @brief Add a constraint that this action must follow another * @param name Name of action that this action must follow * @param flags Flags word * * The possible flag values are: * * * * * * * *
      @p flags valueMeaning
      @c 0@p name will be treated as an action name, which must * exist. This action will run after it and will only run if after @p name * has completed, whether or not it succeeded.
      @c ACTION_SUCCEEDED@p name will be treated as an action * name, which must exist. This action will run after it and will only run * if it @p name succeeded; if it failed, then this action will also fail, * without running.
      @c ACTION_GLOB@p name will be treated as a glob pattern. * This action will run after all matching actions (if there are any) have * completed, whether or not they succeeded.
      @c ACTION_SUCCEEDED|ACTION_GLOB@p name will be treated as * a glob pattern. This action will run after all matching actions (if there * are any) have succeeded. If at least one of them fails then this action * will also fail, without running, after they have all completed.
      * * Glob matching is done via fnmatch(3) and has the @c FNM_PATHNAME * flag set. */ void after(const std::string &name, unsigned flags) { predecessors.push_back({name, flags}); } /** @brief Set the action's priority * @param p New action priority * * Actions are dispatched in descending order of priority. The default * priority for a new action is 0. */ void set_priority(int p) { priority = p; } /** @brief Return the name of this action */ const std::string &get_name() const { return name; } /** @brief Possible states of an action */ enum State { /** @brief Action has not yet started */ Pending, /** @brief Action is underway */ Running, /** @brief Action completed successfully */ Succeeded, /** @brief Action completed unsuccessfully */ Failed, }; private: friend class ActionList; /** @brief Name of action */ std::string name; /** @brief Resources required by this action */ std::vector resources; /** @brief Predecessors of this action */ std::vector predecessors; /** @brief Current state */ State state = Pending; /** @brief Priority */ int priority = 0; }; /** @brief A collection of actions that are executed concurrently * * @ref Action "Actions" are executed concurrently, with the restriction that no * two actions can hold the same resource concurrently. * * When a new action is to be executed, the first action that has not been * started and does not contradict the restrictions above, is chosen for * execution. Actions are executed via Action::go; they should call * @ref ActionList::completed when they are finished. */ class ActionList { public: /** @brief Constructor * @param e Event loop */ ActionList(EventLoop *e): eventloop(e) {} /** @brief Set a time limit */ void setLimit(struct timespec &when); /** @brief Return true if the time limit was exceeded */ inline bool timeLimitExceeded() const { return timedOut; } /** @brief Add an action * @param a Action * * Adds an action to the end of the list. @p a must remain valid at least * until it has been completed, i.e. until @ref Action::done is called. */ void add(Action *a); /** @brief Initiate actions * @param wait_for_timeouts Whether event loop should wait for timeouts * * Returns when all actions are complete. * * This method repeatedly calls @ref EventLoop::wait, so if there are any * @ref Reactor objects attached to the event loop that do not belong to some * action, unexpected delays may result. */ void go(bool wait_for_timeouts = false); /** @brief Called when an action is complete * @param a Action that completed * @param succeeded @c true if the action succeeded, otherwise false * * It is the responsibility of @ref Action subclasses to call this method. * Normally it should not leave any @ref Reactor objects attached to the @ref * EventLoop when it does so (see the caveat at @ref Action::go). */ void completed(Action *a, bool succeeded); private: /** @brief Event loop */ EventLoop *eventloop; /** @brief Time limit */ struct timespec limit = {0, 0}; /** @brief Set when time limit triggered */ bool timedOut = false; /** @brief Remaining actions * * Includes in-progress actions. */ std::map actions; /** @brief Status of completed actions */ std::map states; /** @brief Start any new actions if possible */ void trigger(); /** @brief In-use resources */ std::set resources; /** @brief Called when an action is complete or skipped * @param a Action that completed * @param succeeded @c true if the action succeeded, otherwise false * @param ran @c true if the action actually ran */ void cleanup(Action *a, bool succeeded, bool ran); /** @brief Test whether an action is blocked by resource contention * @param a Action to check * @return @c true if action is blocked */ bool blocked_by_resource(const Action *a); /** @brief Test whether an action is blocked by a dependency * @param a Action to check * @return @c true if action is blocked */ bool blocked_by_dependency(const Action *a); /** @brief Test whether an action is failed by a dependency * @param a Action to check * @return @c true if action is failed */ bool failed_by_dependency(const Action *a); /** @brief Find an action by name or pattern * @param as Action name or pattern to find * @return Iterator pointing to action */ std::map::iterator find(const ActionStatus &as); }; #endif /* ACTION_H */ rsbackup-10.0/src/Backup.cc000066400000000000000000000074471440730431700155660ustar00rootroot00000000000000// Copyright © 2011, 2014-2016, 2019 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Device.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Store.h" #include "Database.h" #include "Utils.h" #include "Errors.h" #include #include #include // Return the path to this backup std::string Backup::backupPath() const { const Host *host = volume->parent; const Device *device = host->parent->findDevice(deviceName); const Store *store = device->store; assert(store != nullptr); return (store->path + PATH_SEP + host->name + PATH_SEP + volume->name + PATH_SEP + id); } void Backup::insert(Database &db, bool replace) const { const std::string command = replace ? "INSERT OR REPLACE" : "INSERT"; Database::Statement(db, (command + " INTO backup" " (host,volume,device,id,time,pruned,rc,status,log)" " VALUES (?,?,?,?,?,?,?,?,?)") .c_str(), SQL_STRING, &volume->parent->name, SQL_STRING, &volume->name, SQL_STRING, &deviceName, SQL_STRING, &id, SQL_INT64, (sqlite_int64)time, SQL_INT64, (sqlite_int64)pruned, SQL_INT, waitStatus, SQL_INT, status, SQL_STRING, &contents, SQL_END) .next(); } void Backup::update(Database &db) const { Database::Statement(db, "UPDATE backup SET rc=?,status=?,log=?,time=?,pruned=?" " WHERE host=? AND volume=? AND device=? AND id=?", SQL_INT, waitStatus, SQL_INT, status, SQL_STRING, &contents, SQL_INT64, (sqlite_int64)time, SQL_INT64, (sqlite_int64)pruned, SQL_STRING, &volume->parent->name, SQL_STRING, &volume->name, SQL_STRING, &deviceName, SQL_STRING, &id, SQL_END) .next(); } void Backup::remove(Database &db) const { Database::Statement(db, "DELETE FROM backup" " WHERE host=? AND volume=? AND device=? AND id=?", SQL_STRING, &volume->parent->name, SQL_STRING, &volume->name, SQL_STRING, &deviceName, SQL_STRING, &id, SQL_END) .next(); } void Backup::setStatus(int n) { if(status != n) { status = n; if(volume) volume->calculate(); } } Device *Backup::getDevice() const { return volume->parent->parent->findDevice(deviceName); } long long Backup::getSize() const { static std::regex size_regexp("Total file size: ([0-9,]+) bytes"); std::smatch mr; if(!std::regex_search(contents, mr, size_regexp)) return -1; std::string size_string; for(auto it = mr[1].first; it != mr[1].second; ++it) { auto ch = *it; if(isdigit(ch)) size_string += ch; } try { return parseInteger(size_string, 0, std::numeric_limits::max()); } catch(SyntaxError &e) { return -1; } } const char *const backup_status_names[] = {"unknown", "underway", "complete", "failed", "pruning", "pruned"}; rsbackup-10.0/src/Backup.h000066400000000000000000000117041440730431700154170ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014-2016 Richard Kettlewell. // // 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 . #ifndef BACKUP_H #define BACKUP_H /** @file Backup.h * @brief State of a backup */ #include #include "Date.h" class Database; class Volume; class Device; /** @brief Possible states of a backup */ enum BackupStatus { /** @brief %Backup status unknown */ UNKNOWN = 0, /** @brief %Backup is underway */ UNDERWAY = 1, /** @brief %Backup is complete */ COMPLETE = 2, /** @brief %Backup failed */ FAILED = 3, /** @brief Pruning is underway */ PRUNING = 4, /** @brief Pruning is complete */ PRUNED = 5 }; /** @brief Names of @ref BackupStatus constants */ extern const char *const backup_status_names[]; /** @brief Represents the status of one backup */ class Backup { /** @brief Status of this backup * * @see BackupStatus */ int status = UNKNOWN; public: /** @brief Wait status from @c rsync * * This is a wait status as returned by @c waitpid and similar functions. 0 * means the backup succeeded. It is meaningless if @ref Backup::status is * @ref UNKNOWN or @ref UNDERWAY. */ int waitStatus = 0; /** @brief Id of backup * * In the current implementation these are days represented by YYYY-MM-DD * format. However, IDs are treated as opaque strings, so this could be * changed. * * Note though that when importing logs from older versions of rsbackup (in * Conf::readState) the filename is assumed to start YYYY-MM-DD and this is * imported as both the time of the backup and its ID. */ std::string id; /** @brief Time of backup * * This reflects the time that the backup was started. */ time_t time = 0; /** @brief Time backup pruned * * The meaning of this member depends on the value of @ref Backup::status * * If the current status is @ref PRUNING then it reflects the time that it * was decided to prune the backup. * * If the current status is @ref PRUNED then it reflects the time that the * pruning operation completed. * * For any other status, the value is meaningless. */ time_t pruned = 0; /** @brief Device containing backup */ std::string deviceName; /** @brief Log contents */ std::string contents; /** @brief Volume backed up */ Volume *volume = nullptr; /** @brief Ordering on backups * @param that Other backup * @return @c true if this sorts earlier than @p that * * Backups are ordered by date first and by device name for backups of the * same date. */ inline bool operator<(const Backup &that) const { int c; if((c = time - that.time)) return c < 0; if((c = deviceName.compare(that.deviceName))) return c < 0; return false; } /** @brief Return path to backup */ std::string backupPath() const; /** @brief Return containing device * * This can be a null pointer if device is no longer mentioned in the * configuration file. In theory the operator should retire such devices, * but we can't enforce that. */ Device *getDevice() const; /** @brief Insert this backup into the database * @param db Database to update * @param replace Replace existing row if present */ void insert(Database &db, bool replace = false) const; /** @brief Update this backup in the database * @param db Database to update */ void update(Database &db) const; /** @brief Remove this backup from the database * @param db Database to update */ void remove(Database &db) const; /** @brief Retrieve status of this backup * @return Status (see @ref BackupStatus) */ inline int getStatus() const { return status; } /** @brief Return a size estimate for this backup * @return Size in bytes, or -1 if no estimate is available */ long long getSize() const; /** @brief Set the status of this backup * @param n New status (see @ref BackupStatus) * * Calls Volume::calculate if necessary. */ void setStatus(int n); }; /** @brief Comparison for backup pointers */ struct compare_backup { /** @brief Comparison operator * @param a A backup * @param b Another backup * @return @c true if @p a sorts earlier than @p b * * Backups are ordered by date first and by device name for backups of the * same date. */ bool operator()(Backup *a, Backup *b) const { return *a < *b; } }; #endif /* BACKUP_H */ rsbackup-10.0/src/BackupPolicy.cc000066400000000000000000000041351440730431700167350ustar00rootroot00000000000000// Copyright © 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "BackupPolicy.h" #include "Errors.h" #include BackupPolicy::BackupPolicy(const std::string &name) { if(!policies) policies = new policies_type(); (*policies)[name] = this; } const std::string &BackupPolicy::get(const Volume *volume, const std::string &name) const { auto it = volume->backupParameters.find(name); if(it != volume->backupParameters.end()) return it->second; else throw ConfigError("missing backup parameter '" + name + "'"); } const std::string &BackupPolicy::get(const Volume *volume, const std::string &name, const std::string &def) const { auto it = volume->backupParameters.find(name); if(it != volume->backupParameters.end()) return it->second; else return def; } const BackupPolicy *BackupPolicy::find(const std::string &name) { assert(policies != nullptr); // policies not statically initialized auto it = policies->find(name); if(it == policies->end()) throw ConfigError("unrecognized backup policy '" + name + "'"); return it->second; } void validateBackupPolicy(const Volume *volume) { const BackupPolicy *policy = BackupPolicy::find(volume->backupPolicy); policy->validate(volume); } BackupPolicy::policies_type *BackupPolicy::policies; rsbackup-10.0/src/BackupPolicy.h000066400000000000000000000047601440730431700166030ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2019 Richard Kettlewell. // // 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 . #ifndef BACKUPPOLICY_H #define BACKUPPOLICY_H /** @file BackupPolicy.h * @brief Definitions used by the backup policies */ #include #include class Volume; class Device; /** @brief Base class for backup policies */ class BackupPolicy { public: /** @brief Constructor * * Policies are automatically registered upon construction. */ BackupPolicy(const std::string &name); /** @brief Validate a backup policy * @param volume Volume to validate */ virtual void validate(const Volume *volume) const = 0; /** @brief Get a parameter value * @param volume Volume to get parameter from * @param name Name of parameter */ const std::string &get(const Volume *volume, const std::string &name) const; /** @brief Get a parameter value * @param volume Volume to get parameter from * @param name Name of parameter * @param def Default value */ const std::string &get(const Volume *volume, const std::string &name, const std::string &def) const; /** @brief Find a backup policy by name * @param name Name of policy * @return Backup policy */ static const BackupPolicy *find(const std::string &name); /** @brief Determine whether to back up a volume * @param volume Volume to consider * @param device Target device * @return @c return to back up and @c false to skip */ virtual bool backup(const Volume *volume, const Device *device) const = 0; private: /** @brief Type for @ref policies */ typedef std::map policies_type; /** @brief Map of policy names to implementations */ static policies_type *policies; }; /** @brief Validate the backup policy for a volume * @param volume Volume to validate */ void validateBackupPolicy(const Volume *volume); #endif /* BACKUPPOLICY_H */ rsbackup-10.0/src/BackupPolicyAlways.cc000066400000000000000000000021161440730431700201130ustar00rootroot00000000000000// Copyright © 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "BackupPolicy.h" /** @brief The @c always backup policy; backups are made at every opportunity. */ class BackupPolicyAlways: public BackupPolicy { public: BackupPolicyAlways(): BackupPolicy("always") {} void validate(const Volume *) const override {} bool backup(const Volume *, const Device *) const override { return true; } } backup_always; rsbackup-10.0/src/BackupPolicyDaily.cc000066400000000000000000000025771440730431700177300ustar00rootroot00000000000000// Copyright © 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Device.h" #include "BackupPolicy.h" /** @brief The @c daily backup policy; backups are made at most once per day. */ class BackupPolicyDaily: public BackupPolicy { public: BackupPolicyDaily(): BackupPolicy("daily") {} void validate(const Volume *) const override {} bool backup(const Volume *volume, const Device *device) const override { Date today = Date::today(); for(const Backup *backup: volume->backups) if(backup->getStatus() == COMPLETE && Date(backup->time) == today && backup->deviceName == device->name) return false; return true; } } backup_daily; rsbackup-10.0/src/BackupPolicyInterval.cc000066400000000000000000000032121440730431700204350ustar00rootroot00000000000000// Copyright © 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Device.h" #include "Utils.h" #include "Errors.h" #include "BackupPolicy.h" /** @brief The @c interval backup policy; backups are separate by a configurable * minimum interval. */ class BackupPolicyInterval: public BackupPolicy { public: BackupPolicyInterval(): BackupPolicy("interval") {} void validate(const Volume *volume) const override { if(parseTimeInterval(get(volume, "min-interval")) < 1) throw SyntaxError("min-interval too small"); } bool backup(const Volume *volume, const Device *device) const override { time_t now = Date::now(); int minInterval = parseTimeInterval(get(volume, "min-interval")); for(const Backup *backup: volume->backups) if(backup->getStatus() == COMPLETE && now - backup->time < minInterval && backup->deviceName == device->name) return false; return true; } } backup_interval; rsbackup-10.0/src/BulkRemove.cc000066400000000000000000000022731440730431700164240ustar00rootroot00000000000000// Copyright © 2011, 2012, 2015, 2016, 2019, 2020 Richard Kettlewell. // // 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 . #include #include "Subprocess.h" #include "Utils.h" #include "BulkRemove.h" #include "Conf.h" void BulkRemove::initialize(const std::string &path) { // Invoking rm makes more sense than re-implementing it. std::vector cmd = {globalConfig.rm, "-rf", path}; setCommand(cmd); reporting(globalWarningMask & WARNING_VERBOSE, false); // BulkRemoves only get created when the caller has committed to removing // things, so no point checking command.act here. } rsbackup-10.0/src/BulkRemove.h000066400000000000000000000033761440730431700162730ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2015, 2016 Richard Kettlewell. // // 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 . #ifndef BULKREMOVE_H #define BULKREMOVE_H /** @file BulkRemove.h * @brief Bulk remove operations * * Bulk removal happens in @ref pruneBackups and @ref retireVolumes. */ #include "Subprocess.h" /** @brief Bulk remove files and directories, as if by @c rm @c -rf. * * A @ref BulkRemove is a @ref Subprocess and therefore an @ref Action; it can * be invoked either with BulkRemove::runAndWait or as part of an @ref * ActionList. */ class BulkRemove: public Subprocess { public: /** @brief Constructor * @param name Action name */ BulkRemove(const std::string &name): Subprocess(name) {} /** @brief Constructor * @param name Action name * @param path Base path to remove * * The effect is equivalent to @c rm @c -rf. */ BulkRemove(const std::string &name, const std::string &path): Subprocess(name) { initialize(path); } /** @brief Initialize the bulk remover * @param path Base path to remove * * The effect is equivalent to @c rm @c -rf. */ void initialize(const std::string &path); }; #endif /* BULKREMOVE_H */ rsbackup-10.0/src/Check.cc000066400000000000000000000030411440730431700153600ustar00rootroot00000000000000// Copyright © 2011-13, 2019 Richard Kettlewell. // // 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 . #include #include "Command.h" #include "Errors.h" #include "Utils.h" #include "IO.h" #include #include #include bool check(const char *format, ...) { // --force overrides if(globalCommand.force) return true; char buffer[64]; for(;;) { // Display the prompt va_list ap; va_start(ap, format); IO::out.vwritef(format, ap); va_end(ap); IO::out.writef("\nyes/no> "); IO::out.flush(); // Get a yes/no answer if(!fgets(buffer, sizeof buffer, stdin)) { if(feof(stdin)) throw IOError("unexpected EOF reading stdin"); if(ferror(stdin)) throw IOError("reading stdin", errno); } std::string result = buffer; if(result == "yes\n") return true; if(result == "no\n") return false; IO::out.writef("Please answer 'yes' or 'no'.\n"); } } rsbackup-10.0/src/CheckBackups.cc000066400000000000000000000067431440730431700167050ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Command.h" #include "Conf.h" #include "Device.h" #include "Errors.h" #include "Host.h" #include "IO.h" #include "Store.h" #include "Utils.h" #include "Volume.h" static void checkDirectory(std::vector &unexpected, const std::string &root, const std::set &expected) { // Enumerate files Directory d; std::string f; std::set files; try { d.open(root); } catch(IOError &e) { return; } while(d.get(f)) { if(f == "." || f == "..") continue; if(expected.find(f) != expected.end()) continue; unexpected.push_back(root + "/" + f); } } static void checkTopLevelFiles(std::vector &unexpected, const Device *device) { std::set expected; expected.insert("device-id"); expected.insert("lost+found"); for(auto h: globalConfig.hosts) { expected.insert(h.first); } checkDirectory(unexpected, device->store->path, expected); } static void checkVolume(std::vector &unexpected, const Device *device, const Volume *volume) { std::set expected; for(auto backup: volume->backups) { if(backup->getDevice() == device) { expected.insert(backup->id); if(backup->getStatus() != COMPLETE) { expected.insert(backup->id + ".incomplete"); } } } checkDirectory(unexpected, device->store->path + "/" + volume->parent->name + "/" + volume->name, expected); } static void checkHost(std::vector &unexpected, const Device *device, const Host *host) { std::set expected; for(auto v: host->volumes) { expected.insert(v.first); } checkDirectory(unexpected, device->store->path + "/" + host->name, expected); for(auto v: host->volumes) { Volume *volume = v.second; checkVolume(unexpected, device, volume); } } static void checkDevice(std::vector &unexpected, const Device *device) { checkTopLevelFiles(unexpected, device); for(auto h: globalConfig.hosts) { const Host *host = h.second; checkHost(unexpected, device, host); } } void checkUnexpected() { // Load up log files globalConfig.readState(); // Find the devices to scan globalConfig.identifyDevices(Store::Enabled); std::vector unexpected; for(auto d: globalConfig.devices) { const Device *device = d.second; if(device->store) checkDevice(unexpected, device); } if(unexpected.size()) { warning(WARNING_ALWAYS, "%zu unexpected files found", unexpected.size()); for(auto it: unexpected) { IO::out.writef("%s%c", it.c_str(), globalCommand.eol); } } } rsbackup-10.0/src/Color.cc000066400000000000000000000032021440730431700154200ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include #include #include #include "Color.h" #include "Utils.h" Color Color::HSV(double h, double s, double v) { // https://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB h = fmod(h, 360); double c = v * s; double hp = h / 60; double x = c * (1 - fabs(fmod(hp, 2) - 1)); double r1, g1, b1; if(hp < 1) { r1 = c; g1 = x; b1 = 0; } else if(hp < 2) { r1 = x; g1 = c; b1 = 0; } else if(hp < 3) { r1 = 0; g1 = c; b1 = x; } else if(hp < 4) { r1 = 0; g1 = x; b1 = c; } else if(hp < 5) { r1 = x; g1 = 0; b1 = c; } else { r1 = c; g1 = 0; b1 = x; } double m = v - c; return Color(r1 + m, g1 + m, b1 + m); } std::ostream &operator<<(std::ostream &os, const Color &c) { boost::io::ios_all_saver save_state(os); os << std::hex << std::setw(6) << std::setfill('0') << static_cast(c); return os; } rsbackup-10.0/src/Color.h000066400000000000000000000120631440730431700152670ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef COLOR_H #define COLOR_H /** @file Color.h * @brief Representation and selection of colors * * The @ref Color class represents colors in RGB form. No attempt is made to * formalize it in terms of color space - you get whatever interpretation Cairo * and/or your HTML or image rendering clients use. It is used by the classes * of @ref Render.h and by the various color-setting configuration directives * (see @ref ColorDirective). * * The @ref ColorStrategy class and its subclasses provide a configurable means * of coloring a range of items different colors. It is used by @ref * Conf::deviceColorStrategy. */ #include #include #include /** @brief An RGB color * * Colors are accepted in three ways: * - as an RGB triple, with 0<=R,G,B<=1 (and this is the internal * representation) * - as an HSV triple, with 0<=H<360 and 0<=S,V<=1 * - as a 24-bit packed RGB integer, with 0<=R,G,B<=255 */ struct Color { /** @brief Constructor * @param r Red component * @param g Green component * @param b Blue component * * All components have values in the closed interval [0,1]. * For example, (1,0,0) represents red. */ Color(double r, double g, double b): red(r), green(g), blue(b) {} /** @brief Constructor * @param rgb 24-bit red/green/blue color * * The top 8 bits are red, the middle green and the bottom blue. * For example, 0xFF0000 represents red. */ Color(unsigned rgb = 0): red(component(rgb, 16)), green(component(rgb, 8)), blue(component(rgb, 0)) {} /** @brief Convert to integer form * @return 24-bit red/green/blue color value * * The top 8 bits are red, the middle green and the bottom blue. * For example, 0xFF0000 represents red. */ operator unsigned() const { return pack(red, 16) + pack(green, 8) + pack(blue, 0); } /** @brief Red component * * 0 <= red <= 1 */ double red; /** @brief Green component * * 0 <= green <= 1 */ double green; /** @brief Blue component * * 0 <= blue <= 1 */ double blue; /** @brief Convert from HSV * @param h Hue, 0<=h<360 * @param s Saturation, 0<=s<=1 * @param v Value, 0<=v<=1 * @return Color value * * @p h may actually be any value - it is reduced modulo 360. */ static Color HSV(double h, double s, double v); private: /** @brief Compute a component from integer form * @param n Integer form, 0 <= @p n <= 255 * @param shift Right shift (0, 8 or 16) * @return Component value, 0 <= value <= 1 */ static double component(unsigned n, unsigned shift) { return (double)((n >> shift) & 255) / 255.0; } /** @brief Pack a component into integer form * @param c Component, 0 <= @p c <= 1 * @param shift Left shift (0, 8 or 16) * @return Partial integer form */ static unsigned pack(double c, unsigned shift) { return static_cast(255.0 * c + 0x0.ffffffffp-1) << shift; } }; /** @brief Output a color * @param os Output stream * @param c Color * * Output @p c as a 6-digit hex value. */ std::ostream &operator<<(std::ostream &os, const Color &c); /** @brief A strategy for picking a small set of distinct colors */ class ColorStrategy { public: /** @brief Constructor * @param name Name of this strategy */ ColorStrategy(const char *name); /* @brief Destructor */ virtual ~ColorStrategy() = default; /** @brief Get a color * @param n Item number, 0 <= @p n < @p items * @param items Total number of items * @return Selected color */ virtual Color get(unsigned n, unsigned items) const = 0; /** @brief Get the description of this strategy */ virtual std::string description() const; /** @brief Create a color-picking strategy from configuration * @param name Name of strategy to use * @param params Strategy parameters * @param pos Index of first parameter * * The supported strategies are @c "equidistant-hue" (see @ref * EquidistantHue) and @c "equidistant-value" (see @ref EquidistantValue). * * The parameters correspond to the constructor arguments for the strategy * (generally with a bit of user-facing range-checking). */ static ColorStrategy *create(const std::string &name, std::vector ¶ms, size_t pos = 0); protected: /** @brief Name of this strategy */ const char *name; }; #endif /* COLOR_H */ rsbackup-10.0/src/ColorStrategy.cc000066400000000000000000000102751440730431700171530ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Color.h" #include "Utils.h" #include "Errors.h" #include #include #include ColorStrategy::ColorStrategy(const char *name): name(name) {} std::string ColorStrategy::description() const { return name; } /** @brief A color strategy that maximizes distance between hues */ class EquidistantHue: public ColorStrategy { public: /** @brief Constructor * @param h Starting hue * @param s Saturation * @param v Value */ EquidistantHue(double h = 0, double s = 1, double v = 1): ColorStrategy("equidistant-hue"), hue(fmod(h, 360)), saturation(s), value(v) {} Color get(unsigned n, unsigned items) const override { double h = hue + 360.0 * n / items; return Color::HSV(h, saturation, value); } std::string description() const override { std::stringstream ss; ss << name << ' ' << hue << ' ' << saturation << ' ' << value; return ss.str(); } /** @brief Starting hue */ double hue; /** @brief Saturation */ double saturation; /** @brief Value */ double value; }; /** @brief A color strategy that maximizes distance between values */ class EquidistantValue: public ColorStrategy { public: /** @brief Constructor * @param h Base hue * @param s Saturation * @param minv Minimum value * @param maxv Maximum value * */ EquidistantValue(double h = 0, double s = 1, double minv = 0, double maxv = 1): ColorStrategy("equidistant-value"), hue(fmod(h, 360)), saturation(s), minvalue(minv), maxvalue(maxv) {} Color get(unsigned n, unsigned items) const override { double value = minvalue + static_cast(n) / (items - 1) * (maxvalue - minvalue); return Color::HSV(hue, saturation, value); } std::string description() const override { std::stringstream ss; ss << name << ' ' << hue << ' ' << saturation; if(minvalue != 0 || maxvalue != 1) ss << ' ' << minvalue << ' ' << maxvalue; return ss.str(); } /** @brief Hue */ double hue; /** @brief Saturation */ double saturation; /** @brief Lowest value */ double minvalue; /** @brief Highest value */ double maxvalue; }; ColorStrategy *ColorStrategy::create(const std::string &name, std::vector ¶ms, size_t pos) { if(name == "equidistant-hue") { double h = 0, s = 1, v = 1; if(pos < params.size()) h = parseFloat(params[pos++]); if(pos < params.size()) s = parseFloat(params[pos++], 0, 1); if(pos < params.size()) v = parseFloat(params[pos++], 0, 1); if(pos < params.size()) throw SyntaxError("too many parameters for color strategy '" + name + "'"); return new EquidistantHue(h, s, v); } if(name == "equidistant-value") { double h = 0, s = 1, minv = 0, maxv = 1; if(pos < params.size()) h = parseFloat(params[pos++]); if(pos < params.size()) s = parseFloat(params[pos++], 0, 1); if(pos < params.size()) minv = parseFloat(params[pos++], 0, 1); if(pos < params.size()) maxv = parseFloat(params[pos++], 0, 1); if(minv >= maxv) throw SyntaxError("inconsistent parameters for color strategy '" + name + "'"); if(pos < params.size()) throw SyntaxError("too many parameters for color strategy '" + name + "'"); return new EquidistantValue(h, s, minv, maxv); } throw SyntaxError("unrecognized color strategy '" + name + "'"); } rsbackup-10.0/src/Command.cc000066400000000000000000000251521440730431700157300ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "Command.h" #include "Defaults.h" #include "Errors.h" #include "Conf.h" #include "IO.h" #include "Utils.h" #include #include // Long-only options enum { RETIRE_DEVICE = 256, RETIRE = 257, WARN_UNKNOWN = 258, WARN_STORE = 259, WARN_UNREACHABLE = 261, WARN_PARTIAL = 262, REPEAT_ERRORS = 263, NO_REPEAT_ERRORS = 264, NO_WARN_PARTIAL = 265, LOG_VERBOSITY = 266, DUMP_CONFIG = 267, FORGET_ONLY = 268, UNMOUNTED_STORE = 269, CHECK_UNEXPECTED = 270, LATEST = 271, }; const struct option Command::options[] = { {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"backup", no_argument, nullptr, 'b'}, {"html", required_argument, nullptr, 'H'}, {"text", required_argument, nullptr, 'T'}, {"email", required_argument, nullptr, 'e'}, {"prune", no_argument, nullptr, 'p'}, {"prune-incomplete", no_argument, nullptr, 'P'}, {"store", required_argument, nullptr, 's'}, {"unmounted-store", required_argument, nullptr, UNMOUNTED_STORE}, {"retire-device", no_argument, nullptr, RETIRE_DEVICE}, {"retire", no_argument, nullptr, RETIRE}, {"config", required_argument, nullptr, 'c'}, {"wait", no_argument, nullptr, 'w'}, {"force", no_argument, nullptr, 'f'}, {"dry-run", no_argument, nullptr, 'n'}, {"verbose", no_argument, nullptr, 'v'}, {"warn-unknown", no_argument, nullptr, WARN_UNKNOWN}, {"warn-store", no_argument, nullptr, WARN_STORE}, {"warn-unreachable", no_argument, nullptr, WARN_UNREACHABLE}, {"warn-partial", no_argument, nullptr, WARN_PARTIAL}, {"no-warn-partial", no_argument, nullptr, NO_WARN_PARTIAL}, {"errors", no_argument, nullptr, REPEAT_ERRORS}, {"no-errors", no_argument, nullptr, NO_REPEAT_ERRORS}, {"warn-all", no_argument, nullptr, 'W'}, {"debug", no_argument, nullptr, 'd'}, {"logs", required_argument, nullptr, LOG_VERBOSITY}, {"dump-config", no_argument, nullptr, DUMP_CONFIG}, {"database", required_argument, nullptr, 'D'}, {"forget-only", no_argument, nullptr, FORGET_ONLY}, {"check-unexpected", no_argument, nullptr, CHECK_UNEXPECTED}, {"null", no_argument, nullptr, '0'}, {"latest", no_argument, nullptr, LATEST}, {nullptr, 0, nullptr, 0}}; void Command::help() { IO::out.writef(helpString()); IO::out.close(); exit(0); } const char *Command::helpString() { return "Usage:\n" " rsbackup [OPTIONS] [--] [[-]HOST...] [[-]HOST:VOLUME...]\n" " rsbackup --retire [OPTIONS] [--] [HOST...] [HOST:VOLUME...]\n" " rsbackup --retire-device [OPTIONS] [--] DEVICES...\n" "\n" "At least one action option is required:\n" " --backup, -b Back up selected volumes (default: all)\n" " --html, -H PATH Write an HTML report to PATH\n" " --text, -T PATH Write a text report to PATH\n" " --email, -e ADDRESS Mail HTML report to ADDRESS\n" " --prune, -p Prune old backups of selected volumes " "(default: all)\n" " --prune-incomplete, -P Prune incomplete backups\n" " --retire Retire volumes (must specify at least " "one)\n" " --forget-only Retire from database but not disk (with " "--retire)\n" " --retire-device Retire devices (must specify at least " "one)\n" " --check-unexpected Check backup media for unexpected files\n" " --latest Display path to latest available backup\n" " --dump-config Dump parsed configuration\n" "\n" "Additional options:\n" " --logs all|errors|recent|latest|failed Log verbosity in report\n" " --store, -s DIR Override directory(s) to store backups in\n" " --unmounted-store DIR Override directory(s) to store backups in\n" " --config, -c PATH Set config file (default: " "/etc/rsbackup/config)\n" " --wait, -w Wait until running rsbackup finishes\n" " --force, -f Don't prompt when retiring\n" " --dry-run, -n Dry run only\n" " --verbose, -v Verbose output\n" " --debug, -d Debug output\n" " --database, -D PATH Override database path\n" " --null, -0 \\0-terminate filenames with " "--check-unexpected\n" " --help, -h Display usage message\n" " --version, -V Display version number\n" "\n" "Warning options:\n" " --warn-unknown Warn about unknown devices/volumes\n" " --warn-store Warn about bad stores/unavailable devices\n" " --warn-unreachable Warn about unreachable hosts\n" " --warn-partial Warn about partial transfers (default)\n" " --no-warn-partial Suppress warnings about partial transfers\n" " --warn-all, -W Enable all warnings\n" " --errors Display rsync errors (default)\n" " --no-errors Don't display rsync errors\n"; } void Command::version() { IO::out.writef("%s", VERSION); if(strlen(TAG) > 0) IO::out.writef(" (git: %s)", TAG); IO::out.writef("\n"); IO::out.close(); exit(0); } void Command::parse(int argc, const char *const *argv) { int n; // Override debug if(getenv("RSBACKUP_DEBUG")) globalDebug = true; // Parse options optind = 1; while((n = getopt_long(argc, (char *const *)argv, "+hVbH:T:e:pPs:c:wnfvdWD:0", options, nullptr)) >= 0) { switch(n) { case 'h': help(); case 'V': version(); case 'b': backup = true; break; case 'H': html = new std::string(optarg); break; case 'T': text = new std::string(optarg); break; case 'e': email = new std::string(optarg); break; case 'p': prune = true; break; case 'P': pruneIncomplete = true; break; case 's': stores.push_back(optarg); enable_warning(WARNING_STORE); break; case UNMOUNTED_STORE: unmountedStores.push_back(optarg); enable_warning(WARNING_STORE); break; case 'c': globalConfigPath = optarg; break; case 'w': wait = true; break; case 'n': act = false; enable_warning(WARNING_VERBOSE); break; case 'f': force = true; break; case 'v': enable_warning(WARNING_VERBOSE); break; case 'd': globalDebug = true; break; case 'D': globalDatabase = optarg; break; case RETIRE_DEVICE: retireDevice = true; break; case RETIRE: retire = true; break; case WARN_UNKNOWN: enable_warning(WARNING_UNKNOWN); break; case WARN_STORE: enable_warning(WARNING_STORE); break; case WARN_UNREACHABLE: enable_warning(WARNING_UNREACHABLE); break; case WARN_PARTIAL: enable_warning(WARNING_PARTIAL); break; case NO_WARN_PARTIAL: disable_warning(WARNING_PARTIAL); break; case REPEAT_ERRORS: enable_warning(WARNING_ERRORLOGS); break; case NO_REPEAT_ERRORS: disable_warning(WARNING_ERRORLOGS); break; case LOG_VERBOSITY: logVerbosity = getVerbosity(optarg); break; case 'W': enable_warning(static_cast(-1)); break; case DUMP_CONFIG: dumpConfig = true; break; case FORGET_ONLY: forgetOnly = true; break; case CHECK_UNEXPECTED: checkUnexpected = true; break; case LATEST: latest = true; break; case '0': eol = 0; break; default: exit(1); } } int commands = backup + !!html + !!text + !!email + prune + pruneIncomplete + retireDevice + retire + checkUnexpected + dumpConfig + latest; // Various options are incompatible with one another if(retire && retireDevice) throw CommandError("--retire and --retire-device cannot be used together"); if(backup && retire) throw CommandError("--retire and --backup cannot be used together"); if(backup && retireDevice) throw CommandError("--retire-device and --backup cannot be used together"); if(forgetOnly && !retire) throw CommandError("--forget-only may only be used with --retire"); if(commands > 1) { if(checkUnexpected) throw CommandError( "--check-unexpected cannot be used with any other action"); if(dumpConfig) throw CommandError("--dump-config cannot be used with any other action"); if(latest) throw CommandError("--latest cannot be used with any other action"); } // We have to do *something* if(commands == 0) throw CommandError("no action specified"); if(backup || prune || pruneIncomplete || retire || latest) { // Volumes to back up, prune, retire or report the latest if(optind < argc) { for(n = optind; n < argc; ++n) selections.add(argv[n]); if(latest) { for(auto &s: selections) { if(s.volume == "*") throw CommandError( "only explicit volumes may be specified for --latest"); } } } else { if(retire) throw CommandError("no volumes specified to retire"); if(latest) throw CommandError("no volumes specified for --latest"); } } if(retireDevice) { if(optind >= argc) throw CommandError("no devices specified to retire"); for(n = optind; n < argc; ++n) devices.push_back(argv[n]); } if(optind < argc) { if(checkUnexpected) throw CommandError("no arguments allowed to --check-unexpected"); if(dumpConfig) throw CommandError("no arguments allowed to --dump-config"); } } Command::LogVerbosity Command::getVerbosity(const std::string &v) { if(v == "all") return All; if(v == "errors") return Errors; if(v == "recent") return Recent; if(v == "latest") return Latest; if(v == "failed") return Failed; throw CommandError("invalid argument to --logs: " + v); } Command::~Command() { delete html; delete text; delete email; } Command globalCommand; std::string globalConfigPath = DEFAULT_CONFIG; std::string globalDatabase; rsbackup-10.0/src/Command.h000066400000000000000000000102571440730431700155720ustar00rootroot00000000000000// -*-C++-*- // Copyright © Richard Kettlewell. // // 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 . #ifndef COMMANDLINE_H #define COMMANDLINE_H /** @file Command.h * @brief Command-line parsing */ #include #include #include #include "Defaults.h" #include "Selection.h" struct option; /** @brief Represents the parsed command line */ class Command { public: Command() = default; Command(const Command &) = delete; Command &operator=(const Command &) = delete; ~Command(); /** @brief Verbosity of log summary in report */ enum LogVerbosity { All, Errors, Recent, Latest, Failed, }; /** @brief Parse command line arguments */ void parse(int argc, const char *const *argv); /** @brief @c --backup action * * The default is @c false. */ bool backup = false; /** @brief @c --prune action * * The default is @c false. */ bool prune = false; /** @brief @c --prune-incomplete action * * The default is @c false. */ bool pruneIncomplete = false; /** @brief @c --retire action * * The default is @c false. */ bool retire = false; /** @brief @c --retire-device action * * The default is @c false. */ bool retireDevice = false; /** @brief @c --dump-config action * * The default is @c false. */ bool dumpConfig = false; /** @brief @c --check-unexpected action * * The default is @c false. */ bool checkUnexpected = false; /** @brief @c --latest action * * The default is @c false. */ bool latest = false; /** @brief Output file for HTML report or null pointer */ std::string *html = nullptr; /** @brief Output file for text report or null pointer */ std::string *text = nullptr; /** @brief Address for email report or null pointer */ std::string *email = nullptr; /** @brief Explicitly specified stores */ std::vector stores; /** @brief Explicitly specified stores * * These ones don't have to be mount points. */ std::vector unmountedStores; /** @brief Wait if lock cannot be held * * The default is @c false. */ bool wait = false; /** @brief Actually do something * * i.e. opposite of @c --no-act * * The default is @c true. */ bool act = true; /** @brief Force retirement * * The default is @c false. */ bool force = false; /** @brief Line terminator for options that generate lists * * The default is a newline. */ int eol = '\n'; /** @brief Database-only retirement * * The default is @c false. */ bool forgetOnly = false; /** @brief Log summary verbosity */ LogVerbosity logVerbosity = Failed; /** @brief Devices selected for retirement */ std::vector devices; /** @brief Selections */ VolumeSelections selections; /** @brief Convert verbosity from string * @param v Verbosity string from command line * @return Enumeration value */ static LogVerbosity getVerbosity(const std::string &v); /** @brief Return the help string */ static const char *helpString(); /** @brief Option table * Used by getopt_long(3). */ static const struct option options[]; private: /** @brief Display help message and terminate */ [[noreturn]] void help(); /** @brief Display version string and terminate */ [[noreturn]] void version(); }; /** @brief Program command line */ extern Command globalCommand; /** @brief Path to config file * * This defaults to @ref DEFAULT_CONFIG. */ extern std::string globalConfigPath; /** @brief Database path */ extern std::string globalDatabase; #endif /* COMMANDLINE_H */ rsbackup-10.0/src/CompressTable.h000066400000000000000000000052571440730431700167630ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #ifndef COMPRESSTABLE_H #define COMPRESSTABLE_H /** @file src/CompressTable.h * @brief A compressable table */ /** @brief A compressable table * @param Value Value type represented by table cells. * * Any two rows in the table are eligible for combination if they * differ only in one column. */ template class Table { public: /** @brief Type of a cell in the table */ typedef std::set Cell; /** @brief Type of a row in the table */ class Row { public: /** @brief The cells of the row */ std::vector cells; /** @brief Add a new cell to the row * @param v Value to add */ void push_back(const Value &v) { Cell c; c.insert(v); cells.push_back(c); } /** @brief Return true if two rows can be merged * @param other Row to compare with */ bool mergeable(const Row &other) const { if(cells.size() != other.cells.size()) return false; size_t differences = 0; for(size_t i = 0; i < cells.size(); i++) if(cells[i] != other.cells[i]) ++differences; return differences <= 1; } /** @brief Merge another row into this one * @param other Source of merge */ void merge(const Row &other) { for(size_t i = 0; i < cells.size(); i++) { cells[i].insert(other.cells[i].begin(), other.cells[i].end()); } } }; /** @brief The rows of the table */ std::vector rows; /** @brief Add a new row to the table */ template void push_back(Container &row) { Row r; for(auto &v: row) { r.push_back(v); } rows.push_back(r); } /** @brief Compress the table */ void compress() { bool changed = false; do { changed = false; for(size_t i = 0; i + 1 < rows.size();) { if(rows[i].mergeable(rows[i + 1])) { rows[i].merge(rows[i + 1]); rows.erase(rows.begin() + i + 1); changed = true; } else i++; } } while(changed); } }; #endif rsbackup-10.0/src/Conf.cc000066400000000000000000000463231440730431700152420ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014-17, 2019, 2020 Richard Kettlewell. // // 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 . #include #include #include #include #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Store.h" #include "Errors.h" #include "IO.h" #include "Command.h" #include "Utils.h" #include "Database.h" #include "PrunePolicy.h" #include "ConfDirective.h" #include "Device.h" #include "Indent.h" #include "BackupPolicy.h" Conf::Conf() { std::vector args; args.push_back("120"); args.push_back("0.75"); deviceColorStrategy = ColorStrategy::create(DEFAULT_COLOR_STRATEGY, args); hostCheck.push_back("ssh"); } Conf::~Conf() { delete deviceColorStrategy; deviceColorStrategy = nullptr; delete db; db = nullptr; for(auto &d: devices) delete d.second; devices.clear(); for(auto &s: stores) delete s.second; stores.clear(); for(auto &h: hosts) delete h.second; hosts.clear(); } void Conf::write(std::ostream &os, int step, bool verbose) const { describe_type *d = verbose ? describe : nodescribe; d(os, "# ---- Inheritable directives ----", step); d(os, "", step); ConfBase::write(os, step, verbose); writeHostCheck(os, step, verbose); d(os, "# ---- Non-inheritable directives ----", step); d(os, "", step); d(os, "# Whether stores are public or private (default)", step); d(os, "# public true|false", step); os << indent(step) << "public " << (publicStores ? "true" : "false") << '\n'; d(os, "", step); d(os, "# Path to log directory", step); d(os, "# logs PATH", step); os << indent(step) << "logs " << quote(logs) << '\n'; d(os, "", step); d(os, "# Path to database", step); d(os, "# database PATH", step); if(database.size()) os << indent(step) << "database " << quote(database) << '\n'; d(os, "", step); d(os, "# Path to lock file", step); d(os, "# lock PATH", step); if(lock.size()) os << indent(step) << "lock " << quote(lock) << '\n'; d(os, "", step); d(os, "# Command to run before accessing backup devices", step); d(os, "# pre-device-hook COMMAND ...", step); if(preDevice.size()) os << indent(step) << "pre-device-hook " << quote(preDevice) << '\n'; d(os, "", step); d(os, "# Command to run after accessing backup devices", step); d(os, "# pre-device-hook COMMAND ...", step); if(postDevice.size()) os << indent(step) << "post-device-hook " << quote(postDevice) << '\n'; d(os, "", step); d(os, "# Names of backup devices", step); d(os, "# device NAME", step); for(auto &d: devices) os << "device " << quote(d.first) << '\n'; d(os, "", step); d(os, "# The time period to keep records of pruned backups for", step); d(os, "# keep-prune-logs INTERVAL", step); os << indent(step) << "keep-prune-logs " << formatTimeInterval(keepPruneLogs) << '\n'; d(os, "", step); d(os, "# The maximum time to spend pruning", step); d(os, "# prune-timeout INTERVAL", step); os << indent(step) << "prune-timeout " << formatTimeInterval(pruneTimeout) << '\n'; d(os, "", step); d(os, "# ---- Reporting ----", step); d(os, "", step); d(os, "# 'Good' and 'bad' colors for HTML report", step); d(os, "# color-good 0xRRGGBB", step); d(os, "# color-bad 0xRRGGBB", step); os << indent(step) << "color-good 0x" << colorGood << '\n' << indent(step) << "color-bad 0x" << colorBad << '\n'; d(os, "", step); d(os, "# Path to mail transport agent", step); d(os, "# sendmail PATH", step); os << indent(step) << "sendmail " << quote(sendmail) << '\n'; d(os, "", step); if(rm != DEFAULT_RM) { d(os, "# rm command", step); d(os, "# rm COMMAND", step); os << indent(step) << "rm " << quote(rm) << '\n'; d(os, "", step); } d(os, "# Stylesheet for HTML report", step); d(os, "# stylesheet PATH", step); if(stylesheet.size()) os << indent(step) << "stylesheet " << quote(stylesheet) << '\n'; d(os, "", step); d(os, "# Contents of report", step); d(os, "# report [+] KEY[:VALUE][?CONDITION]", step); d(os, "#", step); d(os, "# Keys:", step); d(os, "# generated -- generation time", step); d(os, "# history-graph -- graphical representation ofbackups", step); d(os, "# h1:HEADING -- level-1 heading", step); d(os, "# h2:HEADING -- level-2 heading", step); d(os, "# h3:HEADING -- level-3 heading", step); d(os, "# logs -- logs of failed backups", step); d(os, "# p:TEXT -- arbitrary text", step); d(os, "# prune-logs[:INTERVAL] -- pruning logs (default 3 days)", step); d(os, "# summary -- summary table", step); d(os, "# title:TITLE -- report title", step); d(os, "# warnings -- warning messages", step); d(os, "#", step); d(os, "# Conditions:", step); d(os, "# warnings -- true if there are warnings to display", step); writeVector(os, step, "report", report); d(os, "", step); d(os, "# ---- Graphs ----", step); d(os, "", step); d(os, "# Graph background color", step); d(os, "# color-graph-background 0xRRGGBB", step); os << indent(step) << "color-graph-background 0x" << colorGraphBackground << '\n'; d(os, "", step); d(os, "# Graph foreground color", step); d(os, "# color-graph-foreground 0xRRGGBB", step); os << indent(step) << "color-graph-foreground 0x" << colorGraphForeground << '\n'; d(os, "", step); d(os, "# Graph month guide color", step); d(os, "# color-month-guide 0xRRGGBB", step); os << indent(step) << "color-month-guide 0x" << colorMonthGuide << '\n'; d(os, "", step); d(os, "# Graph host guide color", step); d(os, "# color-host-guide 0xRRGGBB", step); os << indent(step) << "color-host-guide 0x" << colorHostGuide << '\n'; d(os, "", step); d(os, "# Graph volume guide color", step); d(os, "# color-volume-guide 0xRRGGBB", step); os << indent(step) << "color-volume-guide 0x" << colorVolumeGuide << '\n'; d(os, "", step); d(os, "# Strategy for picking device colors", step); d(os, "# device-color-strategy equidistant-value HUE", step); d(os, "# device-color-strategy equidistant-value HUE SATURATION", step); d(os, "# device-color-strategy equidistant-value HUE SATURATION MINVALUE " "MAXVALUE", step); d(os, "# device-color-strategy equidistant-hue HUE", step); d(os, "# device-color-strategy equidistant-hue HUE SATURATION VALUE", step); os << indent(step) << "device-color-strategy " << deviceColorStrategy->description() << '\n'; d(os, "", step); d(os, "# Horizontal padding", step); d(os, "# horizontal-padding PIXELS", step); os << indent(step) << "horizontal-padding " << horizontalPadding << '\n'; d(os, "", step); d(os, "# Vertical padding", step); d(os, "# vertical-padding PIXELS", step); os << indent(step) << "vertical-padding " << verticalPadding << '\n'; d(os, "", step); d(os, "# Minimum width of a backup indicator", step); d(os, "# backup-indicator-width PIXELS", step); os << indent(step) << "backup-indicator-width " << backupIndicatorWidth << '\n'; d(os, "", step); d(os, "# Minimum height of a backup indicator ", step); d(os, "# backup-indicator-height PIXELS", step); os << indent(step) << "backup-indicator-height " << backupIndicatorHeight << '\n'; d(os, "", step); d(os, "# Target width graph of graph", step); d(os, "# graph-target-width PIXELS", step); os << indent(step) << "graph-target-width " << graphTargetWidth << '\n'; d(os, "", step); d(os, "# Width of a backup indicator in the device key", step); d(os, "# backup-indicator-key-width PIXELS", step); os << indent(step) << "backup-indicator-key-width " << backupIndicatorKeyWidth << '\n'; d(os, "", step); d(os, "# Font description for host names", step); d(os, "# host-name-font FONT", step); os << indent(step) << "host-name-font " << hostNameFont << '\n'; d(os, "", step); d(os, "# Font description for volume names", step); d(os, "# volume-name-font FONT", step); os << indent(step) << "volume-name-font " << volumeNameFont << '\n'; d(os, "", step); d(os, "# Font description for device names", step); d(os, "# device-name-font FONT", step); os << indent(step) << "device-name-font " << deviceNameFont << '\n'; d(os, "", step); d(os, "# Font description for time labels", step); d(os, "# time-label-font FONT", step); os << indent(step) << "time-label-font " << timeLabelFont << '\n'; d(os, "", step); d(os, "# Layout", step); d(os, "# graph-layout [+] PART:COLUMN,ROW[:HV]", step); writeVector(os, step, "graph-layout", graphLayout); d(os, "", step); d(os, "# ---- Hosts to back up ----", step); for(auto &h: hosts) { os << '\n'; h.second->write(os, step, verbose); } } // Read the master configuration file plus anything it includes. void Conf::read() { readOneFile(globalConfigPath); } // Read one configuration file. Throws IOError if some file cannot be // read or ConfigError if the contents are bad. void Conf::readOneFile(const std::string &path) { ConfContext cc(this); Indent indenter; IO input; D("Conf::readOneFile %s", path.c_str()); input.open(path, "r"); std::string line; int lineno = 0; while(input.readline(line)) { ++lineno; // keep track of where we are cc.path = path; cc.line = lineno; try { size_t indent; split(cc.bits, line, &indent); if(!cc.bits.size()) // skip blank lines continue; // Consider all the possible commands const ConfDirective *d = ConfDirective::find(cc.bits[0]); if(d) { unsigned level = indenter.check(d->acceptable_levels, indent); switch(level) { case 0: throw SyntaxError("inconsistent indentation"); case LEVEL_TOP: cc.context = this; cc.host = nullptr; cc.volume = nullptr; break; case LEVEL_HOST: cc.context = cc.host; cc.volume = nullptr; break; case LEVEL_VOLUME: cc.context = cc.volume; break; default: throw std::logic_error("unexpected indent level"); } d->check(cc); d->set(cc); indenter.introduce(d->new_level); } else { throw SyntaxError("unknown command '" + cc.bits[0] + "'"); } } catch(SyntaxError &e) { // Wrap up in a ConfigError, which carries the path/line information. std::stringstream s; s << path << ":" << lineno << ": " << e.what(); throw ConfigError(s.str()); } } } // Implementation of the 'include' command. If PATH is a directory then // includes all the regular files it contains (excluding dotfiles and backups // but including symbolic links to regular files of any name), otherwise just // tries to read it. void Conf::includeFile(const std::string &path) { D("Conf::includeFile %s", path.c_str()); if(boost::filesystem::is_directory(path)) { std::vector files; Directory::getFiles(path, files); for(auto &name: files) { if(!name.size() || name.at(0) == '.' || name.at(0) == '#' || name.find('~') != std::string::npos) continue; std::string fullname = path + PATH_SEP + name; if(boost::filesystem::is_regular_file(fullname)) readOneFile(fullname); } } else readOneFile(path); } void Conf::validate() const { for(auto &h: hosts) for(auto &v: h.second->volumes) { validateBackupPolicy(v.second); validatePrunePolicy(v.second); } } // (De-)select all hosts void Conf::selectAll(bool sense) { for(auto &h: hosts) h.second->select(sense); } // (De-)select one host (or all if hostName="*") void Conf::selectHost(const std::string &hostName, bool sense) { if(hostName == "*") { selectAll(sense); } else { auto hosts_iterator = hosts.find(hostName); if(hosts_iterator == hosts.end()) throw CommandError("no such host as '" + hostName + "'"); hosts_iterator->second->select(sense); } } // (De-)select one volume (or all if volumeName="*") void Conf::selectVolume(const std::string &hostName, const std::string &volumeName, bool sense) { if(volumeName == "*") { selectHost(hostName, sense); } else { auto hosts_iterator = hosts.find(hostName); if(hosts_iterator == hosts.end()) throw CommandError("no such host as '" + hostName + "'"); Host *host = hosts_iterator->second; auto volumes_iterator = host->volumes.find(volumeName); if(volumes_iterator == host->volumes.end()) throw CommandError("no such volume as '" + hostName + ":" + volumeName + "'"); volumes_iterator->second->select(sense); } } void Conf::addHost(Host *h) { hosts[h->name] = h; } // Find a host by name Host *Conf::findHost(const std::string &hostName) const { auto it = hosts.find(hostName); return it != hosts.end() ? it->second : nullptr; } // Find a volume by name Volume *Conf::findVolume(const std::string &hostName, const std::string &volumeName) const { Host *host = findHost(hostName); return host ? host->findVolume(volumeName) : nullptr; } // Find a device by name Device *Conf::findDevice(const std::string &deviceName) const { auto it = devices.find(deviceName); return it != devices.end() ? it->second : nullptr; } // Read in logfiles void Conf::readState() { if(logsRead) return; std::string hostName, volumeName; std::vector files; const bool progress = (globalWarningMask & WARNING_VERBOSE) && isatty(2); std::vector upgraded; std::string log; // Read database contents // Better would be to read only the rows required, on demand. { Database::Statement stmt( getdb(), "SELECT host,volume,device,id,time,pruned,rc,status,log" " FROM backup", SQL_END); while(stmt.next()) { Backup backup; hostName = stmt.get_string(0); volumeName = stmt.get_string(1); backup.deviceName = stmt.get_string(2); backup.id = stmt.get_string(3); backup.time = stmt.get_int64(4); backup.pruned = stmt.get_int64(5); backup.waitStatus = stmt.get_int(6); backup.setStatus(stmt.get_int(7)); backup.contents = stmt.get_blob(8); addBackup(backup, hostName, volumeName); } } logsRead = true; if(progress) progressBar(IO::err, nullptr, 0, 0); } void Conf::addBackup(Backup &backup, const std::string &hostName, const std::string &volumeName, bool forceWarn) { const bool progress = (globalWarningMask & WARNING_VERBOSE) && isatty(2); unsigned warning_type = forceWarn ? WARNING_ALWAYS : WARNING_UNKNOWN; /* Don't keep pruned backups around */ if(backup.getStatus() == PRUNED) return; if(!contains(devices, backup.deviceName)) { if(!contains(unknownDevices, backup.deviceName)) { if(progress) progressBar(IO::err, nullptr, 0, 0); warning(warning_type, "unknown device %s", backup.deviceName.c_str()); unknownDevices.insert(backup.deviceName); ++globalConfig.unknownObjects; } return; } // Find the volume for this status record. If it cannot be found, we warn // about it once. Host *host = findHost(hostName); if(!host) { if(!contains(unknownHosts, std::pair{ hostName, backup.deviceName})) { if(progress) progressBar(IO::err, nullptr, 0, 0); warning(warning_type, "unknown host %s", hostName.c_str()); ++globalConfig.unknownObjects; unknownHosts.insert({hostName, backup.deviceName}); } return; } Volume *volume = host->findVolume(volumeName); if(!volume) { if(!contains(host->unknownVolumes, std::pair{ volumeName, backup.deviceName})) { if(progress) progressBar(IO::err, nullptr, 0, 0); warning(warning_type, "unknown volume %s:%s", hostName.c_str(), volumeName.c_str()); ++globalConfig.unknownObjects; host->unknownVolumes.insert({volumeName, backup.deviceName}); } return; } backup.volume = volume; // Attach the status record to the volume Backup *copy = new Backup(backup); bool inserted = volume->addBackup(copy); if(!inserted) delete copy; } // Create the mapping between stores and devices. void Conf::identifyDevices(int states) { if((devicesIdentified & states) == states) return; int found = 0; std::vector storeExceptions; for(auto &s: stores) { Store *store = s.second; if(!(store->state & states)) continue; try { store->identify(); ++found; } catch(UnavailableStore &unavailableStoreException) { warning(WARNING_STORE, "%s", unavailableStoreException.what()); storeExceptions.push_back(unavailableStoreException); } catch(FatalStoreError &fatalStoreException) { if(states == Store::Enabled) throw; else warning(WARNING_STORE, "%s", fatalStoreException.what()); } catch(BadStore &badStoreException) { if(states == Store::Enabled) error("%s", badStoreException.what()); } } if(!found && states == Store::Enabled) { error("no backup devices found"); if(!(globalWarningMask & WARNING_STORE)) for(size_t n = 0; n < storeExceptions.size(); ++n) IO::err.writef(" %s\n", storeExceptions[n].what()); } devicesIdentified |= states; } Database &Conf::getdb() { if(!db) { if(globalDatabase.size() == 0) { if(database.size() != 0) globalDatabase = database; else globalDatabase = logs + "/" DEFAULT_DATABASE; } if(globalCommand.act) { db = new Database(globalDatabase); if(!db->hasTable("backup")) createTables(); } else { try { db = new Database(globalDatabase, false); } catch(DatabaseError &) { // If we cannot open the database even with -n then we // create a throwaway in-memory database so that we can // make some progress in showing what we'd do anyway. db = new Database(":memory:"); createTables(true); } } } return *db; } void Conf::createTables(bool commitAnyway) { db->begin(); db->execute("CREATE TABLE backup (\n" " host TEXT,\n" " volume TEXT,\n" " device TEXT,\n" " id TEXT,\n" " time INTEGER,\n" " pruned INTEGER,\n" " rc INTEGER,\n" " status INTEGER,\n" " log BLOB,\n" " PRIMARY KEY (host,volume,device,id)\n" ")"); db->commit(commitAnyway); } ConfBase *Conf::getParent() const { return nullptr; } std::string Conf::what() const { return "system"; } Conf globalConfig; rsbackup-10.0/src/Conf.h000066400000000000000000000307261440730431700151040ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014-2016, 2019, 2020 Richard Kettlewell. // // 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 . #ifndef CONF_H #define CONF_H /** @file Conf.h * @brief Program configuration and state * * Configuration and state, which are not well-separated, are organized into a * tree structure. * * Nodes that take part in configuration inheritance derive from @ref ConfBase. * This class captures inheritable configuration and, in its constructor, * implements that inheritance from parent nodes. * * The root node has type @ref Conf. As well as the inheritable configuration * this contains the global configuration. * * The children of the @ref Conf node are of type @ref Host (a host that may be * backed up), @ref Device (a physical backup device) and @ref Store (a mount * point at which a backup device may be found). @ref Host participates in * configuration inheritance; the others do not. * * The children of the @ref Host nodes are all of type @ref Volume (a volume * within a host), which participates in configuration inheritance. Finally * the children of the @ref Volume nodes are of type @ref Backup (a single * backup of some value on some device at a particular time). */ #include #include #include #include #include "Defaults.h" #include "Color.h" #include "ConfBase.h" class Store; class Device; class Host; class Volume; class Database; class Backup; /** @brief Compare two strings as names * @param a First string * @param b Second string * @return true if @p a < @p b * * Comparison treats embedded numbers specially. */ bool namelt(const std::string &a, const std::string &b); /** @brief Function type to allow @ref namelt to be used by STL containers */ struct namelt_type { /** @brief Compare two strings as names * @param a First string * @param b Second string * @return true if @p a < @p b */ bool operator()(const std::string &a, const std::string &b) const { return namelt(a, b); } }; /** @brief Type of map from host names to hosts * * @see Conf::hosts */ typedef std::map hosts_type; /** @brief Type of map from store names to stores * * @see Conf::stores */ typedef std::map stores_type; /** @brief Type of map from device names to devices * * @see Conf::devices **/ typedef std::map devices_type; /** @brief Root node of the entire configuration and state of rsbackup. */ class Conf: public ConfBase { public: /** @brief Constructor */ Conf(); /** @brief Destructor */ virtual ~Conf(); /** @brief Map of host names to configuration */ hosts_type hosts; /** @brief Map of store names to configuration */ stores_type stores; /** @brief Map of device names to configuration */ devices_type devices; /** @brief Maximum device usage * * Corresponds to @c max-usage. Currently not implemented. */ int maxUsage = DEFAULT_MAX_USAGE; /** @brief Maximum file usage * * Corresponds to @c max-file-usage. Currently not implemented. */ int maxFileUsage = DEFAULT_MAX_FILE_USAGE; /** @brief Permit public stores */ bool publicStores = false; /** @brief Log directory */ std::string logs = DEFAULT_LOGS; /** @brief Database path */ std::string database; /** @brief Lockfile path */ std::string lock; /** @brief Age to keep pruning logs */ int keepPruneLogs = DEFAULT_KEEP_PRUNE_LOGS; /** @brief Maximum time to spend pruning */ int pruneTimeout = DEFAULT_PRUNE_TIMEOUT; /** @brief Path to @c sendmail */ std::string sendmail = DEFAULT_SENDMAIL; /** @brief @c rm command (overridden for testing only) */ std::string rm = DEFAULT_RM; /** @brief Pre-device-hook, run before a device is accessed */ std::vector preDevice; /** @brief Post-device-hook, run after a device is accessed */ std::vector postDevice; /** @brief Path to stylesheet for HTML report output * * If empty, a built-in one will be used. */ std::string stylesheet; /** @brief 'good' color code */ Color colorGood = COLOR_GOOD; /** @brief 'bad' color code */ Color colorBad = COLOR_BAD; /** @brief Foregroud color of graph */ Color colorGraphForeground = {0, 0, 0}; /** @brief Background color of graph */ Color colorGraphBackground = {1, 1, 1}; /** @brief Color of vertical bars repsenting months in graph */ Color colorMonthGuide = {0.96875, 0.96875, 0.96875}; /** @brief Color of horizontal lines between hosts in graph */ Color colorHostGuide = {0.875, 0.875, 0.875}; /** @brief Color of horizontal lines between volumes in graph */ Color colorVolumeGuide = {0.9375, 0.9375, 0.9375}; /** @brief Horizontal padding in graph */ double horizontalPadding = 8; /** @brief Vertical padding in graph */ double verticalPadding = 2; /** @brief Backup indicator width for one day */ double backupIndicatorWidth = 4; /** @brief Minimum backup indicator height */ double backupIndicatorHeight = 2; /** @brief Target graph width */ double graphTargetWidth = 0; /** @brief Backup indicator width in the device key */ double backupIndicatorKeyWidth = 16; /** @brief Strategy for picking device colors */ const ColorStrategy *deviceColorStrategy = nullptr; /** @brief Pango font description for host names */ std::string hostNameFont = "Normal"; /** @brief Pango font description for volume names */ std::string volumeNameFont = "Normal"; /** @brief Pango font description for device names */ std::string deviceNameFont = "Normal"; /** @brief Pango font description for time labels */ std::string timeLabelFont = "Normal"; /** @brief List of report sections */ std::vector report = {"title:Backup report (${RSBACKUP_DATE})", "h1:Backup report (${RSBACKUP_DATE})", "h2:Warnings?warnings", "warnings", "h2:Summary", "summary", "history-graph", "h2:Logfiles", "logs", "h3:Pruning logs", "prune-logs", "p:Generated ${RSBACKUP_CTIME}"}; /** @brief Layout of graph */ std::vector graphLayout = { "host-labels:0,0", "volume-labels:1,0", "content:2,0", "time-labels:2,1", "device-key:2,3:RC", }; /** @brief Read the master configuration file * @throws IOError if a file cannot be read * @throws ConfigError if the contents of a file are malformed */ void read(); /** @brief Validate a read configuration file */ void validate() const; /** @brief (De-)select one or more volumes * @param hostName Name of host containing volume to select * @param volumeName Name of volume to select or "*" for all * @param sense True to select, false to dselect */ void selectVolume(const std::string &hostName, const std::string &volumeName, bool sense = true); /** @brief Add a host * @param h New host * * The host name must not be in use. */ void addHost(Host *h); /** @brief Find a host by name * @param hostName Host to find * @return Host, or null pointer */ Host *findHost(const std::string &hostName) const; /** @brief Find a volume by name * @param hostName Name of host containing volume * @param volumeName Name of volume to find * @return Volume, or null pointer */ Volume *findVolume(const std::string &hostName, const std::string &volumeName) const; /** @brief Find a device by name * @param deviceName Name of device to find * @return Device, or null pointer */ Device *findDevice(const std::string &deviceName) const; /** @brief Read logfiles * * Safe to call multiple times - the second and subsequent calls are * ignored. */ void readState(); /** @brief Identify devices * @param states Bitmap of store states to consider * * Safe to call multiple times. * * After reading the config file: * - Conf::devices lists the configured devices (essentially, drives), but * each Device::store pointer is null. * - Conf::stores lists the configured stores (essentially, mount points), but * each Store::device pointer is null. * * This function is responsible for filling in those pointers. * * All stores which have a device present will have Store::device set to point * to the device and the corresponding Device::store points back the to store. * * Normally it is called with Store::Enabled. * The only use case for calling with Store::Disabled is to issue warnings * about disabled stores. */ void identifyDevices(int states); /** @brief Unrecognized device names found in logs * * Set by readState(). */ std::set unknownDevices; /** @brief Unrecognized host names found in logs, mapped to devices that * mention them * * Set by readState(). */ std::set> unknownHosts; /** @brief Total number of unknown objects * * Set by readState(). */ int unknownObjects = 0; /** @brief Get the database access object * @return Reference to database object * * Creates tables if they don't exist. */ Database &getdb(); ConfBase *getParent() const override; std::string what() const override; void write(std::ostream &os, int step, bool verbose) const override; private: /** @brief Read a single configuration file * @param path Path to file to read * @throws IOError if @p path cannot be read * @throws ConfigError if the contents of @p path are malformed */ void readOneFile(const std::string &path); /** @brief Read a configuration file or directory * @param path Path to file or directory to read * @throws IOError if a file cannot be read * @throws ConfigError if the contents of a file are malformed * * If @p path is a directory then the files in it are read (via @ref * readOneFile; there is no recursion). Dotfiles and backup files * (indicated by a trailing "~") are skipped. * * Otherwise the behavior is the same as @ref readOneFile(). */ void includeFile(const std::string &path); friend struct IncludeDirective; /** @brief (De-)select all hosts * @param sense @c true to select all hosts, @c false to deselect them all */ void selectAll(bool sense = true); /** @brief (De-)select a host * @param hostName Host to select or @c * * @param sense @c true to select hosts, @c false to deselect * * If @p hostName is @c * then all hosts are (de-)selected, as by @ref * selectAll(). */ void selectHost(const std::string &hostName, bool sense = true); /** @brief Set to @c true when logfiles have been read * Set by @ref readState(). */ bool logsRead = false; /** @brief Set to @c true when devices have been identified * Set by @ref identifyDevices(). */ int devicesIdentified = false; /** @brief Database access object */ Database *db = nullptr; /** @brief Create database tables */ void createTables(bool commitAnyway = false); /** @brief Validate and add a backup to a volume * @param backup Populated backup * @param hostName Host owning @p backup * @param volumeName Volume owning @p backup * @param forceWarn Force warnings on. * * Identifies the volume from the parameters, fills in @p Backup::volume, * and adds the backup to the volume's list of backups. If the backup * belongs to an unknown device, host or volume, logs this but does not add * it to anything. */ void addBackup(Backup &backup, const std::string &hostName, const std::string &volumeName, bool forceWarn = false); }; /** @brief Global configuration */ extern Conf globalConfig; #endif /* CONF_H */ rsbackup-10.0/src/ConfBase.cc000066400000000000000000000170051440730431700160300ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014-17, 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "ConfBase.h" #include "Utils.h" #include #include std::string ConfBase::quote(const std::string &s) { bool need_quote = false; if(!s.size()) need_quote = true; else if(s[0] == '#') need_quote = true; else { for(auto c: s) { if(isspace(c)) need_quote = true; if(c == '\\' || c == '"') need_quote = true; } } if(!need_quote) return s; std::stringstream ss; ss << '"'; for(auto c: s) { if(c == '\\' || c == '"') ss << '\\'; ss << c; } ss << '"'; return ss.str(); } std::string ConfBase::quote(const std::vector &vs) { std::stringstream ss; for(size_t n = 0; n < vs.size(); ++n) { if(n) ss << ' '; ss << quote(vs[n]); } return ss.str(); } std::string ConfBase::indent(int step) { return std::string(step, ' '); } void ConfBase::write(std::ostream &os, int step, bool verbose) const { describe_type *d = verbose && !getParent() ? describe : nodescribe; ConfBase *parent = getParent(); d(os, "# Maximum time with no backups before flagging host in report", step); d(os, "# max-age INTERVAL", step); os << indent(step) << "max-age " << formatTimeInterval(maxAge) << '\n'; d(os, "", 0); d(os, "# Backup policy for this " + what(), step); d(os, "# backup-policy always|daily|interval", step); os << indent(step) << "backup-policy " << backupPolicy << '\n'; d(os, "", 0); d(os, "# Backup parameters", step); d(os, "# backup-parameter NAME VALUE", step); d(os, "# backup-parameter --remove NAME", step); d(os, "# For parameters, see documentation for individual backup policies", step); for(auto &p: backupParameters) os << indent(step) << "backup-parameter " << quote(p.first) << ' ' << quote(p.second) << '\n'; // Any parameters set in our parent but not present here must have been // removed. if(parent) for(auto &p: parent->backupParameters) if(!contains(backupParameters, p.first)) os << indent(step) << "backup-parameter --remove " << quote(p.first) << '\n'; d(os, "", 0); d(os, "# Prune policy for this " + what(), step); d(os, "# prune-policy age|decay|exec|never", step); os << indent(step) << "prune-policy " << prunePolicy << '\n'; d(os, "", 0); d(os, "# Prune parameters", step); d(os, "# prune-parameter NAME VALUE", step); d(os, "# prune-parameter --remove NAME", step); d(os, "# For parameters, see documentation for individual pruning policies", step); for(auto &p: pruneParameters) os << indent(step) << "prune-parameter " << quote(p.first) << ' ' << quote(p.second) << '\n'; // Any parameters set in our parent but not present here must have been // removed. if(parent) for(auto &p: parent->pruneParameters) if(!contains(pruneParameters, p.first)) os << indent(step) << "prune-parameter --remove " << quote(p.first) << '\n'; d(os, "", 0); d(os, "# Command to run prior to making a backup", step); d(os, "# pre-volume-hook COMMAND ...", step); if(preVolume.size() || (parent && preVolume != parent->preVolume)) os << indent(step) << "pre-volume-hook" << (preVolume.size() ? " " : "") << quote(preVolume) << '\n'; d(os, "", 0); d(os, "# Command to run after making a backup", step); d(os, "# post-volume-hook COMMAND ...", step); if(postVolume.size() || (parent && postVolume != parent->postVolume)) os << indent(step) << "post-volume-hook" << (postVolume.size() ? " " : "") << quote(postVolume) << '\n'; d(os, "", 0); d(os, "# Maximum time to wait for rsync to complete", step); d(os, "# backup-job-timeout INTERVAL", step); if(backupJobTimeout) os << indent(step) << "backup-job-timeout " << formatTimeInterval(backupJobTimeout) << '\n'; d(os, "", 0); d(os, "# rsync internal timeout", step); d(os, "# rsync-io-timeout INTERVAL", step); if(rsyncIOTimeout) os << indent(step) << "rsync-io-timeout " << formatTimeInterval(rsyncIOTimeout) << '\n'; d(os, "", 0); d(os, "# Maximum time to wait before giving up on a host", step); d(os, "# ssh-timeout INTERVAL", step); os << indent(step) << "ssh-timeout " << formatTimeInterval(sshTimeout) << '\n'; d(os, "", 0); d(os, "# Maximum time to wait for a hook to complete", step); d(os, "# hook-timeout INTERVAL", step); if(hookTimeout) os << indent(step) << "hook-timeout " << formatTimeInterval(hookTimeout) << '\n'; d(os, "", 0); d(os, "# rsync command", step); d(os, "# rsync-command COMMAND", step); os << indent(step) << "rsync-command " << quote(rsyncCommand) << '\n'; d(os, "", 0); if(!rsyncLinkDest || (parent && rsyncLinkDest != parent->rsyncLinkDest)) { d(os, "# rsync file linking", step); d(os, "# rsync-link-dest true|false", step); os << indent(step) << "rsync-link-dest " << (rsyncLinkDest ? "true" : "false") << '\n'; d(os, "", 0); } d(os, "# rsync base options", step); d(os, "# rsync-base-options OPTION ...", step); os << indent(step) << "rsync-base-options"; for(auto &opt: rsyncBaseOptions) os << " " << quote(opt); os << "\n"; d(os, "", 0); d(os, "# rsync extra options", step); d(os, "# rsync-extra-options OPTION ...", step); os << indent(step) << "rsync-extra-options"; for(auto &opt: rsyncExtraOptions) os << " " << quote(opt); os << "\n"; d(os, "", 0); d(os, "# rsync remote command", step); d(os, "# rsync-remote COMMAND", step); if(rsyncRemote.size()) os << indent(step) << "rsync-remote " << quote(rsyncRemote) << '\n'; d(os, "", 0); } void ConfBase::writeHostCheck(std::ostream &os, int step, bool verbose) const { describe_type *d = verbose && !getParent() ? describe : nodescribe; d(os, "# Host check behavior", step); d(os, "# host-check ssh", step); d(os, "# host-check always-up", step); d(os, "# host-check command COMMAND ...", step); os << indent(step) << "host-check " << quote(hostCheck) << '\n'; d(os, "", 0); } void ConfBase::describe(std::ostream &os, const std::string &description, int step) { if(description.size()) os << indent(step) << description; os << '\n'; } void ConfBase::nodescribe(std::ostream &, const std::string &, int) {} void ConfBase::writeVector(std::ostream &os, int step, const std::string &directive, const std::vector &value) const { if(value.size()) { os << indent(step) << directive; int y = step + directive.size(); for(const auto &s: value) { if(y + s.size() >= 80) { os << '\n' << indent(step) << directive << " +"; y = step + directive.size() + 2; } const std::string q = quote(s); os << ' ' << q; y += q.size() + 1; } if(y) os << '\n'; } else os << indent(step) << directive << "\n"; } rsbackup-10.0/src/ConfBase.h000066400000000000000000000164741440730431700157030ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014-2017, 2019 Richard Kettlewell. // // 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 . #ifndef CONFBASE_H #define CONFBASE_H /** @file ConfBase.h * @brief Base class for program configuration and state */ #include #include #include #include "Defaults.h" /** @brief Base for Volume, Host and Conf * * Volume, Host and Conf share certain parameters, which are inherited from * container to contained. They are implemented in this class. */ class ConfBase { public: /** @brief Constructor with no parent * * All parameters are given their default values. * * @see @ref Defaults.h */ ConfBase() = default; ConfBase(const ConfBase &) = delete; ConfBase &operator=(const ConfBase &) = delete; /** @brief Constructor that inherits from a parent * @param parent Parent container */ ConfBase(ConfBase *parent): maxAge(parent->maxAge), backupPolicy(parent->backupPolicy), backupParameters(parent->backupParameters), prunePolicy(parent->prunePolicy), pruneParameters(parent->pruneParameters), preVolume(parent->preVolume), postVolume(parent->postVolume), backupJobTimeout(parent->backupJobTimeout), rsyncIOTimeout(parent->rsyncIOTimeout), rsyncCommand(parent->rsyncCommand), rsyncBaseOptions(parent->rsyncBaseOptions), rsyncExtraOptions(parent->rsyncExtraOptions), rsyncRemote(parent->rsyncRemote), rsyncLinkDest(parent->rsyncLinkDest), sshTimeout(parent->sshTimeout), hookTimeout(parent->hookTimeout), hostCheck(parent->hostCheck), devicePattern(parent->devicePattern) {} virtual ~ConfBase() = default; /** @brief Maximum comfortable age of most recent backup * * Corresponds to @c max-age. */ int maxAge = DEFAULT_MAX_AGE; /** @brief Name of pruning policy */ std::string backupPolicy = DEFAULT_BACKUP_POLICY; /** @brief Backup policy parameters */ std::map backupParameters; /** @brief Name of pruning policy */ std::string prunePolicy = DEFAULT_PRUNE_POLICY; /** @brief Pruning policy parameters */ std::map pruneParameters; /** @brief Pre-volume hook */ std::vector preVolume; /** @brief Post-volume hook */ std::vector postVolume; /** @brief backup job timeout */ int backupJobTimeout = 0; /** @brief rsync IO timeout */ int rsyncIOTimeout = 0; /** @brief rsync command */ std::string rsyncCommand = "rsync"; /** @brief rsync base options */ std::vector rsyncBaseOptions = { "--archive", // == -rlptgoD // --recursive recurse into directories // --links preserve symlinks // --perms preserve permissions // --times preserve modification times // --group preserve group IDs // --owner preserve user IDs // --devices preserve device files // --specials preserve special files "--sparse", // handle spare files efficiently "--numeric-ids", // don't remap UID/GID by name "--compress", // compress during file transfer "--fuzzy", // look for similar files "--hard-links", // preserve hard links "--delete", // delete extra files in destination "--stats", // generate file transfer stats }; /** @brief rsync extra options */ std::vector rsyncExtraOptions = { "--xattrs", // preserve extended attributes "--acls", // preserve ACLs "--open-noatime", // suppress atime changes }; /** @brief rsync @c --rsync-path override */ std::string rsyncRemote; /** @brief whether to enable --link-dest */ bool rsyncLinkDest = true; /** @brief Timeout to pass to SSH */ int sshTimeout = DEFAULT_SSH_TIMEOUT; /** @brief hook timeout */ int hookTimeout = 0; /** @brief Host check behavior */ std::vector hostCheck; /** @brief Device pattern to be used */ std::string devicePattern = "*"; /** @brief Write out the value of a vector directive * @param os Output stream * @param step Indent depth * @param directive Name of directive * @param value Value of directive */ void writeVector(std::ostream &os, int step, const std::string &directive, const std::vector &value) const; /** @brief Write this node to a stream * @param os Output stream * @param step Indent depth * @param verbose Include informative annotations */ virtual void write(std::ostream &os, int step, bool verbose) const; /** @brief Write the host-check configuration to a stream * @param os Output stream * @param step Indent depth * @param verbose Include informative annotations */ void writeHostCheck(std::ostream &os, int step, bool verbose) const; /** @brief Return the parent of this configuration node * @return Parent node or null pointer */ virtual ConfBase *getParent() const = 0; /** @brief Return a description of this node */ virtual std::string what() const = 0; protected: /** @brief Quote a string for use in the config file * @param s String to quote * @return Possibly quoted form of @p s */ static std::string quote(const std::string &s); /** @brief Quote a list of strings for use in the config file * @param vs String to quote * @return @p vs with appropriate quoting */ static std::string quote(const std::vector &vs); /** @brief Construct indent text * @param step Indent depth * @return String containing enough spaces */ static std::string indent(int step); /** @brief Type for description functions * @param os Output stream * @param description Description * @param step Indent depth * * See ConfBase::write and overloads for use. */ typedef void describe_type(std::ostream &, const std::string &, int); /** @brief Describe a configuration item * @param os Output stream * @param description Description * @param step Indent depth * * See ConfBase::write and overloads for use. */ static void describe(std::ostream &os, const std::string &description, int step); /** @brief No-op placeholder used instead of ConfBase::describe * @param os Output stream * @param description Description * @param step Indent depth * * See ConfBase::write and overloads for use. */ static void nodescribe(std::ostream &os, const std::string &description, int step); friend void test_quote(); friend void test_quote_vector(); friend void test_indent(); }; #endif /* CONFBASE_H */ rsbackup-10.0/src/ConfDirective.cc000066400000000000000000000710531440730431700170770ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014-20 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Device.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Store.h" #include "Errors.h" #include "Utils.h" #include "ConfDirective.h" #include // ConfDirective -------------------------------------------------------------- ConfDirective::ConfDirective(const char *name_, int min_, int max_, unsigned acceptable_levels_, unsigned new_level_): name(name_), acceptable_levels(acceptable_levels_), new_level(new_level_), min(min_), max(max_) { if(!directives) directives = new directives_type(); if(!aliases) aliases = new std::set(); assert((*directives).find(name) == (*directives).end()); (*directives)[name] = this; } void ConfDirective::alias(const char *name_) { std::string n(name_); assert(directives != nullptr); assert((*directives).find(n) == (*directives).end()); (*directives)[n] = this; assert(aliases != nullptr); aliases->insert(n); } const ConfDirective *ConfDirective::find(const std::string &name) { auto it = directives->find(name); return it == directives->end() ? nullptr : it->second; } void ConfDirective::check(const ConfContext &cc) const { int args = cc.bits.size() - 1; if(args < min) throw SyntaxError("too few arguments to '" + name + "'"); if(args > max) throw SyntaxError("too many arguments to '" + name + "'"); if(aliases->find(cc.bits[0]) != aliases->end()) warning(WARNING_DEPRECATED, "%s:%d: the '%s' directive is deprecated, use '%s' instead", cc.path.c_str(), cc.line, cc.bits[0].c_str(), name.c_str()); } bool ConfDirective::get_boolean(const ConfContext &cc) const { if(cc.bits.size() == 1) { warning(WARNING_DEPRECATED, "%s:%d: use '%s true' instead of '%s'", cc.path.c_str(), cc.line, name.c_str(), name.c_str()); return true; } else if(cc.bits[1] == "true") return true; else if(cc.bits[1] == "false") return false; else throw SyntaxError("invalid argument to '" + name + "' - only 'true' or 'false' allowed"); } void ConfDirective::extend(const ConfContext &cc, std::vector &conf) const { if(cc.bits[1] == "+") conf.insert(conf.end(), &cc.bits[2], &cc.bits[cc.bits.size()]); else conf.assign(&cc.bits[1], &cc.bits[cc.bits.size()]); } directives_type *ConfDirective::directives; std::set *ConfDirective::aliases; // HostOnlyDirective ---------------------------------------------------------- void HostOnlyDirective::check(const ConfContext &cc) const { if(cc.host == nullptr) throw SyntaxError("'" + name + "' command without 'host'"); if(cc.volume != nullptr && !(acceptable_levels & LEVEL_VOLUME)) throw SyntaxError("'" + name + "' inside 'volume'"); ConfDirective::check(cc); } // VolumeOnlyDirective -------------------------------------------------------- void VolumeOnlyDirective::check(const ConfContext &cc) const { if(cc.volume == nullptr) throw SyntaxError("'" + name + "' command without 'volume'"); ConfDirective::check(cc); } // ColorDirective ------------------------------------------------------------- void ColorDirective::check(const ConfContext &cc) const { int args = cc.bits.size() - 1; ConfDirective::check(cc); if(args > 1 && args < 4) throw SyntaxError("wrong number of arguments to '" + name + "'"); if(args == 4) { if(cc.bits[1] == "rgb" || cc.bits[1] == "hsv") ; // OK else throw SyntaxError("invalid color representation '" + cc.bits[1] + "'"); } } void ColorDirective::set(ConfContext &cc) const { int args = cc.bits.size() - 1; if(args == 4) { if(cc.bits[1] == "rgb") set_rgb(cc, 2); else if(cc.bits[1] == "hsv") set_hsv(cc, 2); } else set_packed(cc, 1, 0); } void ColorDirective::set_rgb(ConfContext &cc, size_t n) const { set(cc, Color(parseFloat(cc.bits[n], 0, 1), parseFloat(cc.bits[n + 1], 0, 1), parseFloat(cc.bits[n + 2], 0, 1))); } void ColorDirective::set_hsv(ConfContext &cc, size_t n) const { set(cc, Color::HSV(parseFloat(cc.bits[n]), parseFloat(cc.bits[n + 1], 0, 1), parseFloat(cc.bits[n + 2], 0, 1))); } void ColorDirective::set_packed(ConfContext &cc, size_t n, int radix) const { set(cc, Color(parseInteger(cc.bits[n], 0, 0xFFFFFF, radix))); } // Global directives ---------------------------------------------------------- /** @brief Parse arguments to the @c store directive * @param cc Configuration context * @param mounted Set to whether the store must be mounted * @return Index of the argument after the options */ size_t parseStoreArguments(const ConfContext &cc, bool &mounted) { mounted = true; size_t i = 1; while(i < cc.bits.size() && cc.bits[i][0] == '-') { if(cc.bits[i] == "--mounted") mounted = true; else if(cc.bits[i] == "--no-mounted") mounted = false; else throw SyntaxError("unrecognized store option"); ++i; } if(i >= cc.bits.size()) throw SyntaxError("missing store path"); return i; } /** @brief The @c store directive */ static const struct StoreDirective: public ConfDirective { StoreDirective(): ConfDirective("store", 1, INT_MAX) {} void set(ConfContext &cc) const override { bool mounted; size_t i = parseStoreArguments(cc, mounted); cc.conf->stores[cc.bits[i]] = new Store(cc.bits[i], mounted); } } store_directive; /** @brief The @c store-pattern directive */ static const struct StorePatternDirective: public ConfDirective { StorePatternDirective(): ConfDirective("store-pattern", 1, INT_MAX) {} void set(ConfContext &cc) const override { std::vector files; bool mounted; size_t i = parseStoreArguments(cc, mounted); globFiles(files, cc.bits[i], GLOB_NOCHECK); for(auto &file: files) cc.conf->stores[file] = new Store(file, mounted); } } store_pattern_directive; /** @brief The @c stylesheet directive */ static const struct StyleSheetDirective: public ConfDirective { StyleSheetDirective(): ConfDirective("stylesheet", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->stylesheet = cc.bits[1]; } } stylesheet_directive; /** @brief The @c color-good directive */ static const struct ColorGoodDirective: public ColorDirective { ColorGoodDirective(): ColorDirective("color-good") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorGood = c; } } color_good_directive; /** @brief The @c color-bad directive */ static const struct ColorBadDirective: public ColorDirective { ColorBadDirective(): ColorDirective("color-bad") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorBad = c; } } color_bad_directive; /** @brief The @c device directive */ static const struct DeviceDirective: public ConfDirective { DeviceDirective(): ConfDirective("device", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->devices[cc.bits[1]] = new Device(cc.bits[1]); } } device_directive; /** @brief The @c max-usage directive */ static const struct MaxUsageDirective: public ConfDirective { MaxUsageDirective(): ConfDirective("max-usage", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->maxUsage = parseInteger(cc.bits[1], 0, 100); } } max_usage_directive; /** @brief The @c max-file-usage directive */ static const struct MaxFileUsageDirective: public ConfDirective { MaxFileUsageDirective(): ConfDirective("max-file-usage", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->maxFileUsage = parseInteger(cc.bits[1], 0, 100); } } max_file_usage_directive; /** @brief The @c public directive */ static const struct PublicDirective: public ConfDirective { PublicDirective(): ConfDirective("public", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->publicStores = get_boolean(cc); } } public_directive; /** @brief The @c logs directive */ static const struct LogsDirective: public ConfDirective { LogsDirective(): ConfDirective("logs", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->logs = cc.bits[1]; } } logs_directive; /** @brief The @c database directive */ static const struct DatabaseDirective: public ConfDirective { DatabaseDirective(): ConfDirective("database", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->database = cc.bits[1]; } } database_directive; /** @brief The @c lock directive */ static const struct LockDirective: public ConfDirective { LockDirective(): ConfDirective("lock", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->lock = cc.bits[1]; } } lock_directive; /** @brief The @c sendmail directive */ static const struct SendmailDirective: public ConfDirective { SendmailDirective(): ConfDirective("sendmail", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->sendmail = cc.bits[1]; } } sendmail_directive; /** @brief The @c rm directive */ static const struct RmDirective: public ConfDirective { RmDirective(): ConfDirective("rm", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->rm = cc.bits[1]; } } rm_directive; /** @brief The @c pre-device-hook directive */ static const struct PreDeviceHookDirective: public ConfDirective { PreDeviceHookDirective(): ConfDirective("pre-device-hook", 1, INT_MAX) {} void set(ConfContext &cc) const override { cc.conf->preDevice.assign(cc.bits.begin() + 1, cc.bits.end()); } } pre_device_hook_directive; /** @brief The @c post-device-hook directive */ static const struct PostDeviceHookDirective: public ConfDirective { PostDeviceHookDirective(): ConfDirective("post-device-hook", 1, INT_MAX) {} void set(ConfContext &cc) const override { cc.conf->postDevice.assign(cc.bits.begin() + 1, cc.bits.end()); } } device; /** @brief The @c keep-prune-logs directive */ static const struct KeepPruneLogsDirective: public ConfDirective { KeepPruneLogsDirective(): ConfDirective("keep-prune-logs", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->keepPruneLogs = parseTimeInterval(cc.bits[1]); } } keep_prune_logs_directive; /** @brief The @c prune-timeout directive */ static const struct PruneTimeoutDirective: public ConfDirective { PruneTimeoutDirective(): ConfDirective("prune-timeout", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->pruneTimeout = parseTimeInterval(cc.bits[1]); } } prune_timeout_directive; /** @brief The @c include directive */ static const struct IncludeDirective: public ConfDirective { IncludeDirective(): ConfDirective("include", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->includeFile(cc.bits[1]); } } include_directive; /** @brief The color-graph-background directive */ static const struct ColorGraphBackgroundDirective: public ColorDirective { ColorGraphBackgroundDirective(): ColorDirective("color-graph-background") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorGraphBackground = c; } } color_graph_background_directive; /** @brief The color-graph-foreground directive */ static const struct ColorGraphForegroundDirective: public ColorDirective { ColorGraphForegroundDirective(): ColorDirective("color-graph-foreground") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorGraphForeground = c; } } color_graph_foreground_directive; /** @brief The color-month-guide directive */ static const struct ColorMonthGuideDirective: public ColorDirective { ColorMonthGuideDirective(): ColorDirective("color-month-guide") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorMonthGuide = c; } } color_month_guide_directive; /** @brief The color-host-guide directive */ static const struct ColorHostGuideDirective: public ColorDirective { ColorHostGuideDirective(): ColorDirective("color-host-guide") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorHostGuide = c; } } color_host_guide_directive; /** @brief The color-volume-guide directive */ static const struct ColorVolumeGuideDirective: public ColorDirective { ColorVolumeGuideDirective(): ColorDirective("color-volume-guide") {} void set(ConfContext &cc, const Color &c) const override { cc.conf->colorVolumeGuide = c; } } color_volume_guide_directive; /** @brief The device-color-strategy directive */ static const struct DeviceColorStrategyDirective: public ConfDirective { DeviceColorStrategyDirective(): ConfDirective("device-color-strategy", 1, INT_MAX) {} void set(ConfContext &cc) const override { ColorStrategy *nc = ColorStrategy::create(cc.bits[1], cc.bits, 2); delete cc.conf->deviceColorStrategy; cc.conf->deviceColorStrategy = nc; } } device_color_strategy_directive; /** @brief The horizontal-padding directive */ static const struct HorizontalPaddingDirective: public ConfDirective { HorizontalPaddingDirective(): ConfDirective("horizontal-padding") {} void set(ConfContext &cc) const override { cc.conf->horizontalPadding = parseFloat(cc.bits[1], 0, std::numeric_limits::max()); } } horizontal_padding_directive; /** @brief The vertical-padding directive */ static const struct VerticalPaddingDirective: public ConfDirective { VerticalPaddingDirective(): ConfDirective("vertical-padding") {} void set(ConfContext &cc) const override { cc.conf->verticalPadding = parseFloat(cc.bits[1], 0, std::numeric_limits::max()); } } vertical_padding_directive; /** @brief The backup-indicator-width directive */ static const struct BackupIndicatorWidthDirective: public ConfDirective { BackupIndicatorWidthDirective(): ConfDirective("backup-indicator-width") {} void set(ConfContext &cc) const override { cc.conf->backupIndicatorWidth = parseFloat(cc.bits[1], std::numeric_limits::min(), std::numeric_limits::max()); } } backup_indicator_width_directive; /** @brief The backup-indicator-height directive */ static const struct BackupIndicatorHeightDirective: public ConfDirective { BackupIndicatorHeightDirective(): ConfDirective("backup-indicator-height") {} void set(ConfContext &cc) const override { cc.conf->backupIndicatorHeight = parseFloat(cc.bits[1], std::numeric_limits::min(), std::numeric_limits::max()); } } backup_indicator_height_directive; /** @brief The graph-target-width directive */ static const struct GraphTargetWidthDirective: public ConfDirective { GraphTargetWidthDirective(): ConfDirective("graph-target-width") {} void set(ConfContext &cc) const override { cc.conf->graphTargetWidth = parseFloat(cc.bits[1], 0, std::numeric_limits::max()); } } graph_target_width_directive; /** @brief The backup-indicator-key-width directive */ static const struct BackupIndicatorKeyWidthDirective: public ConfDirective { BackupIndicatorKeyWidthDirective(): ConfDirective("backup-indicator-key-width") {} void set(ConfContext &cc) const override { cc.conf->backupIndicatorKeyWidth = parseFloat(cc.bits[1], std::numeric_limits::min(), std::numeric_limits::max()); } } backup_indicator_key_width_directive; /** @brief The host-name-font directive */ static const struct HostNameFontDirective: public ConfDirective { HostNameFontDirective(): ConfDirective("host-name-font", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->hostNameFont = cc.bits[1]; } } host_name_font_directive; /** @brief The volume-name-font directive */ static const struct VolumeNameFontDirective: public ConfDirective { VolumeNameFontDirective(): ConfDirective("volume-name-font", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->volumeNameFont = cc.bits[1]; } } volume_name_font_directive; /** @brief The device-name-font directive */ static const struct DeviceNameFontDirective: public ConfDirective { DeviceNameFontDirective(): ConfDirective("device-name-font", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->deviceNameFont = cc.bits[1]; } } device_name_font_directive; /** @brief The time-label-font directive */ static const struct TimeLabelFontDirective: public ConfDirective { TimeLabelFontDirective(): ConfDirective("time-label-font", 1, 1) {} void set(ConfContext &cc) const override { cc.conf->timeLabelFont = cc.bits[1]; } } time_label_font_directive; /** @brief The report directive */ static const struct ReportDirective: public ConfDirective { ReportDirective(): ConfDirective("report", 0, INT_MAX) {} void set(ConfContext &cc) const override { extend(cc, cc.conf->report); } } report_directive; /** @brief The graph-layout directive */ static const struct GraphLayoutDirective: public ConfDirective { GraphLayoutDirective(): ConfDirective("graph-layout", 0, INT_MAX) {} void set(ConfContext &cc) const override { extend(cc, cc.conf->graphLayout); } } graph_layout_directive; // Inheritable directives ----------------------------------------------------- /** @brief The @c max-age directive */ static const struct MaxAgeDirective: InheritableDirective { MaxAgeDirective(): InheritableDirective("max-age", 1, 1) {} void set(ConfContext &cc) const override { cc.context->maxAge = parseTimeInterval(cc.bits[1]); } } max_age_directive; /** @brief The @c backup-policy directive */ static const struct BackupPolicyDirective: InheritableDirective { BackupPolicyDirective(): InheritableDirective("backup-policy", 1, 1) {} void set(ConfContext &cc) const override { cc.context->backupPolicy = cc.bits[1]; } } backup_policy_directive; /** @brief The @c backup-parameter directive */ static const struct BackupParameterDirective: InheritableDirective { BackupParameterDirective(): InheritableDirective("backup-parameter", 2, 2) {} void check(const ConfContext &cc) const override { ConfDirective::check(cc); const std::string &name = (cc.bits[1] != "--remove" ? cc.bits[1] : cc.bits[2]); if(!valid(name)) throw SyntaxError("invalid backup-parameter name"); } void set(ConfContext &cc) const override { if(cc.bits[1] != "--remove") cc.context->backupParameters[cc.bits[1]] = cc.bits[2]; else cc.context->backupParameters.erase(cc.bits[2]); } /** @brief Test for valid @c backup-parameter names * @param name Candidate name * @return @c true if @name is valid */ static bool valid(const std::string &name) { return name.size() > 0 && name.at(0) != '-' && name.find_first_not_of(POLICY_PARAMETER_VALID) == std::string::npos; } } backup_parameter_directive; /** @brief The @c prune-policy directive */ static const struct PrunePolicyDirective: InheritableDirective { PrunePolicyDirective(): InheritableDirective("prune-policy", 1, 1) {} void set(ConfContext &cc) const override { if(cc.bits[1].size() > 0 && cc.bits[1].at(0) == '/') { cc.context->prunePolicy = "exec"; cc.context->pruneParameters["path"] = cc.bits[1]; } else cc.context->prunePolicy = cc.bits[1]; } } prune_policy_directive; /** @brief The @c prune-parameter directive */ static const struct PruneParameterDirective: InheritableDirective { PruneParameterDirective(): InheritableDirective("prune-parameter", 2, 2) {} void check(const ConfContext &cc) const override { ConfDirective::check(cc); const std::string &name = (cc.bits[1] != "--remove" ? cc.bits[1] : cc.bits[2]); if(!valid(name)) throw SyntaxError("invalid prune-parameter name"); } void set(ConfContext &cc) const override { if(cc.bits[1] != "--remove") cc.context->pruneParameters[cc.bits[1]] = cc.bits[2]; else cc.context->pruneParameters.erase(cc.bits[2]); } /** @brief Test for valid @c prune-parameter names * @param name Candidate name * @return @c true if @name is valid */ static bool valid(const std::string &name) { return name.size() > 0 && name.at(0) != '-' && name.find_first_not_of(POLICY_PARAMETER_VALID) == std::string::npos; } } prune_parameter_directive; /** @brief The @c pre-volume-hook directive */ static const struct PreVolumeHookDirective: InheritableDirective { PreVolumeHookDirective(): InheritableDirective("pre-volume-hook", 0, INT_MAX) {} void set(ConfContext &cc) const override { cc.context->preVolume.assign(cc.bits.begin() + 1, cc.bits.end()); } } pre_volume_hook_directive; /** @brief The @c post-volume-hook directive */ static const struct PostVolumeHookDirective: InheritableDirective { PostVolumeHookDirective(): InheritableDirective("post-volume-hook", 0, INT_MAX) {} void set(ConfContext &cc) const override { cc.context->postVolume.assign(cc.bits.begin() + 1, cc.bits.end()); } } post_volume_hook_directive; /** @brief The @c backup-job-timeout directive */ static const struct BackupJobTimeoutDirective: InheritableDirective { BackupJobTimeoutDirective(): InheritableDirective("backup-job-timeout", 1, 1) {} void set(ConfContext &cc) const override { cc.context->backupJobTimeout = parseTimeInterval(cc.bits[1]); } } backup_job_directive; /** @brief The @c rsync-io-timeout directive */ static const struct RsyncIOTimeoutDirective: InheritableDirective { RsyncIOTimeoutDirective(): InheritableDirective("rsync-io-timeout", 1, 1) {} void set(ConfContext &cc) const override { cc.context->rsyncIOTimeout = parseTimeInterval(cc.bits[1]); } } rsync_io_timeout_directive; /** @brief The @c hook-timeout directive */ static const struct HookTimeoutDirective: InheritableDirective { HookTimeoutDirective(): InheritableDirective("hook-timeout", 1, 1) {} void set(ConfContext &cc) const override { cc.context->hookTimeout = parseTimeInterval(cc.bits[1]); } } hook_timeout_directive; /** @brief The @c host-check directive */ static const struct HostCheckDirective: InheritableDirective { HostCheckDirective(): InheritableDirective("host-check", 1, INT_MAX, LEVEL_TOP | LEVEL_HOST) {} void set(ConfContext &cc) const override { if(cc.bits[1] == "ssh" || cc.bits[1] == "always-up") { if(cc.bits.size() != 2) throw SyntaxError("invalid host-check syntax"); } else if(cc.bits[1] == "command") { // OK } else { throw SyntaxError("unrecognized host-check type"); } cc.context->hostCheck.assign(cc.bits.begin() + 1, cc.bits.end()); } } host_check_directive; /** @brief The @c ssh-timeout directive */ static const struct SshTimeoutDirective: InheritableDirective { SshTimeoutDirective(): InheritableDirective("ssh-timeout", 1, 1) {} void set(ConfContext &cc) const override { cc.context->sshTimeout = parseTimeInterval(cc.bits[1]); } } ssh_timeout_directive; /** @brief The @c rsync-command directive */ static const struct RsyncCommandDirective: InheritableDirective { RsyncCommandDirective(): InheritableDirective("rsync-command", 1, 1) {} void set(ConfContext &cc) const override { cc.context->rsyncCommand = cc.bits[1]; } } rsync_command_directive; /** @brief The @c rsync-link-dest directive */ static const struct RsyncLinkDestDirective: InheritableDirective { RsyncLinkDestDirective(): InheritableDirective("rsync-link-dest", 1, 1) {} void set(ConfContext &cc) const override { cc.context->rsyncLinkDest = get_boolean(cc); } } rsync_link_dest_directive; /** @brief The @c rsync-base-options directive */ static const struct RsyncBaseOptionsDirective: InheritableDirective { RsyncBaseOptionsDirective(): InheritableDirective("rsync-base-options", 1, INT_MAX) {} void set(ConfContext &cc) const override { cc.context->rsyncBaseOptions.assign(cc.bits.begin() + 1, cc.bits.end()); } } rsync_base_options_directive; /** @brief The @c rsync-extra-options directive */ static const struct RsyncExtraOptionsDirective: InheritableDirective { RsyncExtraOptionsDirective(): InheritableDirective("rsync-extra-options", 0, INT_MAX) {} void set(ConfContext &cc) const override { cc.context->rsyncExtraOptions.assign(cc.bits.begin() + 1, cc.bits.end()); } } rsync_extra_options_directive; /** @brief The @c rsync-remote directive */ static const struct RsyncRemoteDirective: InheritableDirective { RsyncRemoteDirective(): InheritableDirective("rsync-remote", 1, 1) {} void set(ConfContext &cc) const override { cc.context->rsyncRemote = cc.bits[1]; } } rsync_remote_directive; // Host directives ------------------------------------------------------------ /** @brief The @c host directive */ static const struct HostDirective: public ConfDirective { HostDirective(): ConfDirective("host", 1, 1, LEVEL_TOP, LEVEL_HOST) {} void set(ConfContext &cc) const override { if(!Host::valid(cc.bits[1])) throw SyntaxError("invalid host name"); if(contains(cc.conf->hosts, cc.bits[1])) throw SyntaxError("duplicate host"); cc.context = cc.host = new Host(cc.conf, cc.bits[1]); cc.volume = nullptr; cc.host->hostname = cc.bits[1]; } } host_directive; /** @brief The @c group directive */ static const struct GroupDirective: public HostOnlyDirective { GroupDirective(): HostOnlyDirective("group", 1, 1) {} void set(ConfContext &cc) const override { cc.host->group = cc.bits[1]; } } group_directive; /** @brief The @c hostname directive */ static const struct HostnameDirective: public HostOnlyDirective { HostnameDirective(): HostOnlyDirective("hostname", 1, 1) {} void set(ConfContext &cc) const override { cc.host->hostname = cc.bits[1]; } } hostname_directive; /** @brief The @c priority directive */ static const struct PriorityDirective: public HostOnlyDirective { PriorityDirective(): HostOnlyDirective("priority", 1, 1) {} void set(ConfContext &cc) const override { cc.host->priority = parseInteger(cc.bits[1], std::numeric_limits::min(), std::numeric_limits::max()); } } priority_directive; /** @brief The @c user directive */ static const struct UserDirective: public HostOnlyDirective { UserDirective(): HostOnlyDirective("user", 1, 1) {} void set(ConfContext &cc) const override { cc.host->user = cc.bits[1]; } } user_directive; /** @brief The @c devices directive */ static const struct DevicesDirective: public HostOnlyDirective { DevicesDirective(): HostOnlyDirective("devices", 1, 1, LEVEL_HOST | LEVEL_VOLUME) {} void set(ConfContext &cc) const override { cc.context->devicePattern = cc.bits[1]; } } devices_directive; // Volume directives ---------------------------------------------------------- /** @brief The @c volume directive */ static const struct VolumeDirective: public HostOnlyDirective { VolumeDirective(): HostOnlyDirective("volume", 2, 2, LEVEL_HOST, LEVEL_VOLUME) {} void set(ConfContext &cc) const override { if(!Volume::valid(cc.bits[1])) throw SyntaxError("invalid volume name"); if(contains(cc.host->volumes, cc.bits[1])) throw SyntaxError("duplicate volume"); cc.context = cc.volume = new Volume(cc.host, cc.bits[1], cc.bits[2]); } } volume_directive; /** @brief The @c exclude directive */ static const struct ExcludeDirective: public VolumeOnlyDirective { ExcludeDirective(): VolumeOnlyDirective("exclude", 1, 1) {} void set(ConfContext &cc) const override { cc.volume->exclude.push_back(cc.bits[1]); } } exclude_directive; /** @brief The @c traverse directive */ static const struct TraverseDirective: public VolumeOnlyDirective { TraverseDirective(): VolumeOnlyDirective("traverse", 0, 1) {} void set(ConfContext &cc) const override { cc.volume->traverse = get_boolean(cc); } } traverse_directive; /** @brief The @c check-file directive */ static const struct CheckFileDirective: public VolumeOnlyDirective { CheckFileDirective(): VolumeOnlyDirective("check-file", 1, 1) {} void set(ConfContext &cc) const override { cc.volume->checkFile = cc.bits[1]; } } check_file_directive; /** @brief The @c check-mounted directive */ static const struct CheckMountedDirective: public VolumeOnlyDirective { CheckMountedDirective(): VolumeOnlyDirective("check-mounted", 1, 1) {} void set(ConfContext &cc) const override { cc.volume->checkMounted = get_boolean(cc); } } check_mounted_directive; rsbackup-10.0/src/ConfDirective.h000066400000000000000000000173461440730431700167460ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2014-17 Richard Kettlewell. // // 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 . #ifndef CONFDIRECTIVE_H #define CONFDIRECTIVE_H /** @file ConfDirective.h * @brief Configuration file parser support * * This file contains classes used during configuration parsing, including the * classes that define directives (@ref ConfDirective). */ /** @brief Bit indicating the top level of the configuration file */ #define LEVEL_TOP 1 /** @brief Bit indicating the host level of the configuration file */ #define LEVEL_HOST 2 /** @brief Bit indicating the volume level of the configuration file */ #define LEVEL_VOLUME 4 /** @brief Context for configuration file parsing * * This class captures the state for a configuration file that we are reading. * If it includes further configuration files, there will be further * ConfContext objects. */ struct ConfContext { /** @brief Constructor * * @param conf_ Root configuration node */ ConfContext(Conf *conf_): conf(conf_), context(conf_) {} /** @brief Root of configuration */ Conf *conf; /** @brief Current configuration node * * Could be a @ref Conf, @ref Host or @ref Volume. */ ConfBase *context; /** @brief Current host or null */ Host *host = nullptr; /** @brief Current volume or null */ Volume *volume = nullptr; /** @brief Parsed directive */ std::vector bits; /** @brief Containing filename */ std::string path; /** @brief Line number */ int line = -1; }; class ConfDirective; /** @brief Type of name-to-directive map */ typedef std::map directives_type; /** @brief Base class for configuration file directives * * Each configuration file directive has a corresponding subclass and a single * instance. */ class ConfDirective { public: /** @brief Constructor * * @param name_ Name of directive * @param min_ Minimum number of arguments * @param max_ Maximum number of arguments * @param acceptable_levels_ Acceptable indent levels * @param new_level_ New indent level introduced by this directive * * Directives are automatically registered in this constructor. * * The indent levels can contain @ref LEVEL_TOP, @ref LEVEL_HOST and @ref * LEVEL_VOLUME. */ ConfDirective(const char *name_, int min_ = 0, int max_ = INT_MAX, unsigned acceptable_levels_ = LEVEL_TOP, unsigned new_level_ = 0); /** @brief Add an alias */ void alias(const char *name_); /** @brief Name of directive */ const std::string name; /** @brief Check directive syntax * @param cc Context containing directive * * The base class implementation just checks the minimum and maximum number * of arguments. */ virtual void check(const ConfContext &cc) const; /** @brief Get a boolean parameter * @param cc Context containing directive * @return @c true or @c false * * Use in ConfDirective::set implementations for boolean-sense directives. */ bool get_boolean(const ConfContext &cc) const; /** @brief Set or extend a vector directive * @param cc Context containing directive * @param conf Configuration value to update */ void extend(const ConfContext &cc, std::vector &conf) const; /** @brief Act on a directive * @param cc Context containing directive */ virtual void set(ConfContext &cc) const = 0; /** @brief Find a directive * @param name Name of directive * @return Pointer to implementation or null pointer if not found */ static const ConfDirective *find(const std::string &name); /** @brief Acceptable indent levels */ const unsigned acceptable_levels; /** @brief Newly introduced level */ const unsigned new_level; private: /** @brief Minimum number of arguments */ int min; /** @brief Maximum number of arguments */ int max; /** @brief Map names to directives */ static directives_type *directives; /** @brief Deprecated aliases */ static std::set *aliases; }; /** @brief Base class for generally inheritable directives */ class InheritableDirective: public ConfDirective { public: /** @brief Constructor * * @param name_ Name of directive * @param min_ Minimum number of arguments * @param max_ Maximum number of arguments * @param acceptable_levels_ Acceptable indent levels * * The indent level can contain @ref LEVEL_TOP, @ref LEVEL_HOST and @ref * LEVEL_VOLUME. */ InheritableDirective(const char *name_, int min_ = 0, int max_ = INT_MAX, unsigned acceptable_levels_ = LEVEL_TOP | LEVEL_HOST | LEVEL_VOLUME): ConfDirective(name_, min_, max_, acceptable_levels_) {} }; /** @brief Base class for directives that can only appear in host or host/volume * context */ class HostOnlyDirective: public ConfDirective { public: /** @brief Constructor * * @param name_ Name of directive * @param min_ Minimum number of arguments * @param max_ Maximum number of arguments * @param acceptable_levels_ Acceptable indent levels * @param new_level_ New indent level introduced by this directive * * The indent levels can contain @ref LEVEL_TOP, @ref LEVEL_HOST and @ref * LEVEL_VOLUME. */ HostOnlyDirective(const char *name_, int min_ = 0, int max_ = INT_MAX, unsigned acceptable_levels_ = LEVEL_HOST, unsigned new_level_ = 0): ConfDirective(name_, min_, max_, acceptable_levels_, new_level_) {} void check(const ConfContext &cc) const override; }; /** @brief Base class for directives that can only appear in a volume context */ class VolumeOnlyDirective: public ConfDirective { public: /** @brief Constructor * * @param name_ Name of directive * @param min_ Minimum number of arguments * @param max_ Maximum number of arguments */ VolumeOnlyDirective(const char *name_, int min_ = 0, int max_ = INT_MAX): ConfDirective(name_, min_, max_, LEVEL_VOLUME) {} void check(const ConfContext &cc) const override; }; /** @brief Base class for color-setting directives */ class ColorDirective: public ConfDirective { public: /** @brief Constructor * @param name Name of directive */ ColorDirective(const char *name): ConfDirective(name, 1, 4) {} void check(const ConfContext &cc) const override; void set(ConfContext &cc) const override; /** @brief Set a color * @param cc Configuration context * @param c Color to set */ virtual void set(ConfContext &cc, const Color &c) const = 0; private: /** @brief Parse and set an RGB color representation * @param cc Configuration context * @param n Index for first element */ void set_rgb(ConfContext &cc, size_t n) const; /** @brief Parse and set an HSV color representation * @param cc Configuration context * @param n Index for first element */ void set_hsv(ConfContext &cc, size_t n) const; /** @brief Parse and set a packed integer color representation * @param cc Configuration context * @param n Index for first element * @param radix Radix or 0 to pick as per strtol(3) */ void set_packed(ConfContext &cc, size_t n, int radix = 0) const; }; #endif /* CONFDIRECTIVE_H */ rsbackup-10.0/src/Database.cc000066400000000000000000000143601440730431700160550ustar00rootroot00000000000000// Copyright © 2014, 2015, 2016 Richard Kettlewell. // // 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 . #include #include "Command.h" #include "Database.h" #include "Errors.h" #include "Utils.h" #include Database::Database(const std::string &path, bool rw) { int rc = sqlite3_open_v2(path.c_str(), &db, rw ? SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE : SQLITE_OPEN_READONLY, nullptr); if(rc != SQLITE_OK) { try { error("sqlite3_open_v2 " + path, rc); } catch(std::exception &) { sqlite3_close_v2(db); db = nullptr; throw; } } } void Database::error(sqlite3 *db, const std::string &description, int rc) { if(db) if(rc == SQLITE_BUSY) throw DatabaseBusy(rc, description + ": " + sqlite3_errmsg(db)); else throw DatabaseError(rc, description + ": " + sqlite3_errmsg(db)); else throw DatabaseError(rc, description + ": error " + sqlite3_errstr(rc)); } bool Database::hasTable(const std::string &name) { return Statement(*this, "SELECT name FROM sqlite_master" " WHERE type = 'table' AND name = ?", SQL_STRING, &name, SQL_END) .next(); } void Database::execute(const char *cmd) { Statement(*this, cmd, SQL_END).next(); } void Database::begin() { execute("BEGIN"); } void Database::commit(bool commitAnyway) { assert(globalCommand.act || commitAnyway); // safety check for -n execute("COMMIT"); } void Database::rollback() { execute("ROLLBACK"); } Database::~Database() { int rc = sqlite3_close_v2(db); db = nullptr; if(rc != SQLITE_OK) fatal("sqlite3_close: error: %s", sqlite3_errstr(rc)); } // Database::Statement -------------------------------------------------------- Database::Statement::Statement(Database &d, const char *cmd, ...): Statement(d) { va_list ap; va_start(ap, cmd); try { vprepare(cmd, ap); } catch(std::runtime_error &e) { va_end(ap); throw; } va_end(ap); } void Database::Statement::prepare(const char *cmd, ...) { va_list ap; va_start(ap, cmd); try { vprepare(cmd, ap); } catch(std::runtime_error &e) { va_end(ap); throw; } va_end(ap); } void Database::Statement::vprepare(const char *cmd, va_list ap) { if(stmt) throw std::logic_error("Database::Statement::vprepare: already prepared"); const char *tail; D("vprepare: %s", cmd); int rc = sqlite3_prepare_v2(db, cmd, -1, &stmt, &tail); if(rc != SQLITE_OK) error(std::string("sqlite3_prepare_v2: ") + cmd, rc); if(tail && *tail) throw std::logic_error( std::string("Database::Statement::vprepare: trailing junk: \"") + tail + "\""); try { param = 1; vbind(ap); } catch(std::runtime_error &e) { sqlite3_finalize(stmt); stmt = nullptr; throw; } } void Database::Statement::vbind(va_list ap) { int t, i, rc; sqlite3_int64 i64; const char *cs; const std::string *s; if(param <= 0) throw std::logic_error("Database::Statement::vbind: invalid 'param' value"); while((t = va_arg(ap, int)) != SQL_END) { switch(t) { case SQL_INT: i = va_arg(ap, int); D("vbind %d: %d", param, i); rc = sqlite3_bind_int(stmt, param, i); if(rc != SQLITE_OK) error("sqlite3_bind_int", rc); break; case SQL_INT64: i64 = va_arg(ap, sqlite3_int64); D("vbind %d: %lld", param, (long long)i64); rc = sqlite3_bind_int64(stmt, param, i64); if(rc != SQLITE_OK) error("sqlite3_bind_int64", rc); break; case SQL_STRING: s = va_arg(ap, const std::string *); D("vbind %d: %.*s", param, (int)s->size(), s->data()); rc = sqlite3_bind_text(stmt, param, s->data(), s->size(), SQLITE_TRANSIENT); if(rc != SQLITE_OK) error("sqlite3_bind_text", rc); break; case SQL_CSTRING: cs = va_arg(ap, const char *); D("vbind %d: %s", param, cs); rc = sqlite3_bind_text(stmt, param, cs, -1, SQLITE_TRANSIENT); if(rc != SQLITE_OK) error("sqlite3_bind_text", rc); break; case SQL_BLOB: s = va_arg(ap, const std::string *); D("vbind %d: <%zu bytes>", param, s->size()); rc = sqlite3_bind_blob(stmt, param, s->data(), s->size(), SQLITE_TRANSIENT); if(rc != SQLITE_OK) error("sqlite3_bind_text", rc); break; default: throw std::logic_error( "Database::Statement::vbind: unknown parameter type"); } ++param; } } bool Database::Statement::next() { D("next"); switch(int rc = sqlite3_step(stmt)) { case SQLITE_ROW: return true; case SQLITE_DONE: return false; case SQLITE_OK: throw std::logic_error( "Database::Statement::next: sqlite3_step returned SQLITE_OK"); default: error("sqlite3_step", rc); } } int Database::Statement::get_int(int col) { int n = sqlite3_column_int(stmt, col); D("get_int %5d: %d", col, n); return n; } sqlite_int64 Database::Statement::get_int64(int col) { sqlite_int64 n = sqlite3_column_int64(stmt, col); D("get_int64 %3d: %lld", col, (long long)n); return n; } std::string Database::Statement::get_string(int col) { const unsigned char *t = sqlite3_column_text(stmt, col); int nt = sqlite3_column_bytes(stmt, col); D("get_string %2d: %.*s", col, nt, t); return std::string((const char *)t, nt); } std::string Database::Statement::get_blob(int col) { const void *t = sqlite3_column_blob(stmt, col); int nt = sqlite3_column_bytes(stmt, col); D("get_blob %4d: <%d bytes>", col, nt); return std::string((const char *)t, nt); } Database::Statement::~Statement() { sqlite3_finalize(stmt); } rsbackup-10.0/src/Database.h000066400000000000000000000152431440730431700157200ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2014, 2015 Richard Kettlewell. // // 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 . #ifndef DATABASE_H #define DATABASE_H #include #include /** @file Database.h * @brief %Database support */ /** @brief End of binding information */ #define SQL_END 0 /** @brief Bind an @c int */ #define SQL_INT 1 /** @brief Bind an @c int64_t */ #define SQL_INT64 2 /** @brief Bind a @c const @c std::string @c * as text */ #define SQL_STRING 3 /** @brief Bind a @c const @c std::string @c * as a blob */ #define SQL_BLOB 4 /** @brief Bind a @c const @c char @c * as text */ #define SQL_CSTRING 5 /** @brief A database handle */ class Database { public: /** @brief A SQL statement */ class Statement { public: /** @brief Create a statement * @param d Database * @throw DatabaseError if an error occurs */ inline Statement(Database &d): db(d.db) {} /** @brief Create a statement, prepare it with a command and bind data it * @param d Database * @param cmd Command * @param ... Binding information * @throw DatabaseError if an error occurs * * Binding information is a series of type values followed by parameter * values and terminated by @ref SQL_END. The type values are: * * - @ref SQL_INT, followed by an @c int parameter value. * - @ref SQL_INT64, followed by an @c int64_t parameter value. * - @ref SQL_STRING, followed by a @c const @c std::string @c * parameter * value. * - @ref SQL_CSTRING, followed by a @c const @c char @c * parameter value. */ Statement(Database &d, const char *cmd, ...); Statement(const Statement &) = delete; Statement &operator=(const Statement &) = delete; /** @brief Prepare a statement with a command and bind data to it * @param cmd Command * @param ... Binding information * @throw DatabaseError if an error occurs * * Binding information is a series of type values followed by parameter * values and terminated by @ref SQL_END. The type values are: * * - @ref SQL_INT, followed by an @c int parameter value. * - @ref SQL_INT64, followed by an @c int64_t parameter value. * - @ref SQL_STRING, followed by a @c const @c std::string @c * parameter * value. * - @ref SQL_CSTRING, followed by a @c const @c char @c * parameter value. */ void prepare(const char *cmd, ...); /** @brief Fetch the next row * @return @c true if a row is available, otherwise @c false * @throw DatabaseError if an error occurs * @throw DatabaseBusy if the database is busy */ bool next(); /** @brief Get an @c int value * @param col Column number from 0 * @return Integer value */ int get_int(int col); /** @brief Get an @c int64 value * @param col Column number from 0 * @return Integer value */ sqlite_int64 get_int64(int col); /** @brief Get a @c std::string value as text * @param col Column number from 0 * @return String value */ std::string get_string(int col); /** @brief Get a @c std::string value as a blob * @param col Column number from 0 * @return String value */ std::string get_blob(int col); /** @brief Destroy a statement */ ~Statement(); private: /** @brief Prepare and bind a statment * @param cmd Command * @param ap Binding information * @throw DatabaseError if an error occurs */ void vprepare(const char *cmd, va_list ap); /** @brief Bind to a statement * @param ap Binding information * @throw DatabaseError if an error occurs * * Depends on @ref param being initialized so only callable from or after * @ref vprepare and its callers. */ void vbind(va_list ap); /** @brief Raise an error * @param description Context for error * @param rc Error code * @throw DatabaseError * @throw DatabaseBusy */ inline void error [[noreturn]] (const std::string &description, int rc) { Database::error(db, description, rc); } /** @brief Underlying statement handle */ sqlite3_stmt *stmt = nullptr; /** @brief Underlying database handle */ sqlite3 *db = nullptr; /** @brief Next parameter index * Only meaningful after @ref vprepare (and its callers) */ int param = 0; }; /** @brief Create a database object * @param path Path to database * @param rw Read-write mode * @throw DatabaseError if an error occurs * * In read-write mode, the database is created if it does not exist. */ Database(const std::string &path, bool rw = true); Database(const Database &) = delete; Database &operator=(const Database &) = delete; /** @brief Test whether a table exists * @param name Table name * @return @c true if table exists, @c false otherwise * @throw DatabaseError if an error occurs * @throw DatabaseBusy if the database is busy */ bool hasTable(const std::string &name); /** @brief Execute a simple command * @param cmd Command to execute */ void execute(const char *cmd); /** @brief Begin a transaction */ void begin(); /** @brief Commit a transaction */ void commit(bool commitAnyway = false); /** @brief Abandon a transaction */ void rollback(); /** @brief Destructor */ ~Database(); private: /** @brief Underlying database handle */ sqlite3 *db = nullptr; /** @brief Raise an error * @param description Context for error * @param rc Error code * @throw DatabaseError * @throw DatabaseBusy */ inline void error [[noreturn]] (const std::string &description, int rc) { error(db, description, rc); } /** @brief Raise an error * @param db Database handle * @param description Context for error * @param rc Error code * @throw DatabaseError * @throw DatabaseBusy * * Throws a @ref DatabaseError, unless @p rc is @c SQLITE_BUSY, in which case * @ref DatabaseBusy is thrown instead. */ static void error [[noreturn]] (sqlite3 *db, const std::string &description, int rc); friend class Database::Statement; }; #endif /* DATABASE_H */ rsbackup-10.0/src/Date.cc000066400000000000000000000114041440730431700152220ustar00rootroot00000000000000// Copyright © 2011, 2015 Richard Kettlewell. // // 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 . #include #include "Date.h" #include "Errors.h" #include #include #include #include #include // Cumulative day numbers at start of each month // (for a non-leap-year) const int Date::mday[] = { 0, 0, // January 31, 31 + 28, 31 + 28 + 31, 31 + 28 + 31 + 30, 31 + 28 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31, }; Date::Date(const std::string &dateString) { *this = dateString; } Date &Date::operator=(const std::string &dateString) { long bits[3]; const char *s = dateString.c_str(); char *e; for(int n = 0; n < 3; ++n) { if(n) { if(*s != '-') throw InvalidDate("invalid date string '" + dateString + "'"); ++s; } if(!isdigit(*s)) throw InvalidDate("invalid date string '" + dateString + "'"); errno = 0; bits[n] = strtol(s, &e, 10); if(errno) throw InvalidDate("invalid date string '" + dateString + "' - " + strerror(errno)); s = e; } if(*s) throw InvalidDate("invalid date string '" + dateString + "'"); if(bits[0] < 1 || bits[0] > INT_MAX) throw InvalidDate("invalid date string '" + dateString + "' - year too small"); y = bits[0]; if(bits[1] < 1 || bits[1] > 12) throw InvalidDate("invalid date string '" + dateString + "' - month out of range"); m = bits[1]; if(bits[2] < 1 || bits[2] > monthLength(y, m)) throw InvalidDate("invalid date string '" + dateString + "' - day out of range"); d = bits[2]; return *this; } Date::Date(time_t when) { struct tm result; if(!localtime_r(&when, &result)) { std::stringstream ss; ss << "invalid time_t: " << when << ": " << strerror(errno); throw InvalidDate(ss.str()); } y = result.tm_year + 1900; m = result.tm_mon + 1; d = result.tm_mday; } Date &Date::operator++() { if(d < monthLength(y, m)) ++d; else { d = 1; if(++m > 12) { m -= 12; y += 1; } } return *this; } Date &Date::addMonth() { m += 1; if(m > 12) { m -= 12; y += 1; } d = std::min(d, monthLength(y, m)); return *this; } std::string Date::toString() const { char buffer[64]; snprintf(buffer, sizeof buffer, "%04d-%02d-%02d", y, m, d); return buffer; } std::string Date::format(const char *fmt) const { char buffer[256]; struct tm t; time_t when = toTime(); if(!localtime_r(&when, &t)) { std::stringstream ss; ss << "invalid time_t: " << when << ": " << strerror(errno); throw InvalidDate(ss.str()); } strftime(buffer, sizeof buffer, fmt, &t); return buffer; } int Date::toNumber() const { int dayno = 365 * y + y / 4 - y / 100 + y / 400; dayno += mday[m] + (m > 2 && isLeapYear() ? 1 : 0); dayno += d - 1; return dayno; } time_t Date::toTime() const { struct tm t; memset(&t, 0, sizeof t); t.tm_year = y - 1900; t.tm_mon = m - 1; t.tm_mday = d; t.tm_isdst = -1; time_t r = mktime(&t); if(r == -1) throw SystemError("mktime failed"); // not documented as setting errno return r; } bool Date::operator<(const Date &that) const { int delta; if((delta = y - that.y)) return delta < 0; if((delta = m - that.m)) return delta < 0; if((delta = d - that.d)) return delta < 0; return false; } int Date::operator-(const Date &that) const { return toNumber() - that.toNumber(); } Date Date::today() { return Date(now()); } time_t Date::now() { // Allow overriding of time from environment for testing const char *override_time = getenv("RSBACKUP_TIME"); if(override_time && *override_time) return (time_t)strtoll(override_time, nullptr, 0); return time(nullptr); } int Date::monthLength(int y, int m) { int len = mday[m + 1] - mday[m]; if(m == 2 && isLeapYear(y)) ++len; return len; } rsbackup-10.0/src/Date.h000066400000000000000000000121631440730431700150670ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2014 Richard Kettlewell. // // 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 . #ifndef DATE_H #define DATE_H /** @file Date.h * @brief %Date manipulation */ #include #include /** @brief A (proleptic) Gregorian date * * Don't try years before 1CE. */ class Date { public: /** @brief Constructor */ Date(): Date(0, 1, 1) {} /** @brief Constructor * @param dateString Date in YYYY-MM-DD format */ Date(const std::string &dateString); /** @brief Constructor * @param y_ Year * @param m_ Month from 1 * @param d_ Day from 1 */ Date(int y_, int m_, int d_): y(y_), m(m_), d(d_) {} /** @brief Constructor * @param when Moment in time */ Date(time_t when); /** @brief Assignment operator * @param dateString Date in YYYY-MM-DD format */ Date &operator=(const std::string &dateString); /** @brief Different between two dates in days * @param that Other date */ int operator-(const Date &that) const; /** @brief Comparison operator * @param that Other date * @return true if this is less than that */ bool operator<(const Date &that) const; /** @brief Comparison operator * @param that Other date * @return true if this is equal to that */ bool operator==(const Date &that) const { return y == that.y && m == that.m && d == that.d; } /** @brief Comparison operator * @param that Other date * @return true if this is greater than that */ bool operator>(const Date &that) const { return that < *this; } /** @brief Comparison operator * @param that Other date * @return true if this is less or equal to that */ bool operator<=(const Date &that) const { return !(*this > that); } /** @brief Comparison operator * @param that Other date * @return true if this is greater than or equal to that */ bool operator>=(const Date &that) const { return !(*this < that); } /** @brief Comparison operator * @param that Other date * @return true if this is not equal to that */ bool operator!=(const Date &that) const { return !(*this == that); } /** @brief Increment date * @return Next day */ Date &operator++(); /** @brief Advance by 1 month * @return @c *this * * If the day ends up outside the month it is clipped to the last day. */ Date &addMonth(); /** @brief Convert to string * @return Date in "YYYY-MM-DD" format */ std::string toString() const; // YYYY-MM-DD /** @brief Convert to a string using a strftime format * @param fmt Format string for @c strftime * @return Date in target format */ std::string format(const char *fmt) const; /** @brief Convert to day number * @return Day number */ int toNumber() const; /** @brief Convert to a @c time_t * @return @c time_t value of date */ time_t toTime() const; /** @brief Today * @return Today's date * * Overridden by @c RSBACKUP_TIME. */ static Date today(); /** @brief Now * @return The current time * * Overridden by @c RSBACKUP_TIME. */ static time_t now(); /** @brief Calculate the length of a month in days * @param y Year * @param m Month (1-12) */ static int monthLength(int y, int m); /** @brief Year * * This is the Gregorian year; unlike the C library it is not offset by * 1900. */ int y; /** @brief Month * * 1 = January. */ int m; /** @brief Day of month * * Starts from 1. */ int d; private: /** @brief Test for a leap year * @return @c true iff @ref y is a leap year */ inline bool isLeapYear() const { return isLeapYear(y); } /** @brief Test for a leap year * @param y Year * @return @c true iff @ref y is a leap year */ static inline bool isLeapYear(int y) { return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); } /** @brief Day number at start of month * * The first of January is day 0. * * The values in this array are index by the month number start from 1, and * are cumulative. So @c mday[1] is 0, @c mday[2] is 31, @c mday[3] is * 31+28, etc. @c mday[0] doesn't mean anything. * * This array doesn't take leap years into account so its callers must * correct for that. */ static const int mday[]; }; /** @brief Write a date string to a stream * @param os Output stream * @param d Date * @return @p os * * Formats @p d as if by @ref Date::toString() and writes it to @p os. */ inline std::ostream &operator<<(std::ostream &os, Date &d) { return os << d.toString(); } #endif /* DATE_H */ rsbackup-10.0/src/Defaults.h000066400000000000000000000077221440730431700157660ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014, 2015, 2018, 2020 Richard Kettlewell. // // 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 . #ifndef DEFAULTS_H #define DEFAULTS_H /** @file Defaults.h * @brief Default values, etc */ #if HAVE_PATHS_H #include #endif /** @cond fixup */ #ifndef _PATH_DEVNULL #define _PATH_DEVNULL "/dev/null" #endif #ifndef _PATH_SENDMAIL #define _PATH_SENDMAIL "/usr/sbin/sendmail" #endif /** @endcond */ /* Default values for various things */ /** @brief Path to configuration file */ #define DEFAULT_CONFIG "/etc/rsbackup/config" /** @brief Default maximum age */ #define DEFAULT_MAX_AGE (3 * 86400) /** @brief Default minimum backups */ #define DEFAULT_MIN_BACKUPS "1" /** @brief Default pruning age */ #define DEFAULT_PRUNE_AGE "366d" /** @brief Default pruning age */ #define DEFAULT_DECAY_START "1d" /** @brief Default decay window size */ #define DEFAULT_DECAY_WINDOW "1d" /** @brief Default decay scale */ #define DEFAULT_DECAY_SCALE "2" /** @brief Default backup policy */ #define DEFAULT_BACKUP_POLICY "daily" /** @brief Default pruning policy */ #define DEFAULT_PRUNE_POLICY "age" /** @brief Default age for pruning logs in report */ #define DEFAULT_PRUNE_REPORT_AGE 3 /** @brief Default maximum disk usage */ #define DEFAULT_MAX_USAGE 80 /** @brief Default maximum inode usage */ #define DEFAULT_MAX_FILE_USAGE 80 /** @brief Default log directory */ #define DEFAULT_LOGS "/var/log/backup" /** @brief Default database filename */ #define DEFAULT_DATABASE "backups.db" /** @brief Default SSH timeout */ #define DEFAULT_SSH_TIMEOUT 60 /** @brief Default pruning timeout */ #define DEFAULT_PRUNE_TIMEOUT 0 /** @brief Default period to keep pruning logs */ #define DEFAULT_KEEP_PRUNE_LOGS (31 * 86400) /** @brief Default path to email injector */ #define DEFAULT_SENDMAIL _PATH_SENDMAIL /** @brief Default rm */ #define DEFAULT_RM "rm" /** @brief Default warning mask */ #define DEFAULT_WARNING_MASK \ (WARNING_DEPRECATED | WARNING_PARTIAL | WARNING_ERRORLOGS | WARNING_DATABASE) /* Document rendering */ /** @brief "Good" color in HTML report */ #define COLOR_GOOD 0xE0FFE0 /** @brief "Bad" color in HTML report */ #define COLOR_BAD 0xFF4040 /** @brief Default color-picking strategy */ #define DEFAULT_COLOR_STRATEGY "equidistant-value" /** @brief Default text width */ #define DEFAULT_TEXT_WIDTH 80 /* Valid names */ /** @brief Lower case letters */ #define LOWER "abcdefghijklmnopqrstuvwxyz" /** @brief Upper case letters */ #define UPPER "ABCDEFGHIJKLMNOPQRSTUVWXYZ" /** @brief Letters */ #define ALPHA LOWER UPPER /** @brief Digits */ #define DIGIT "0123456789" /** @brief Valid characters in a device name */ #define DEVICE_VALID ALPHA DIGIT "_." /** @brief Valid characters in a host name */ #define HOST_VALID ALPHA DIGIT ".-" /** @brief Valid characters in a volume name */ #define VOLUME_VALID ALPHA DIGIT "_." /** @brief Valid characters in a policy parameter name */ #define POLICY_PARAMETER_VALID ALPHA DIGIT "-" /** @brief Path separator */ #define PATH_SEP "/" /** @brief MIME boundary string */ #define MIME_BOUNDARY "a911ebf382e50dffdf966c4acf269d36e48824bb" /** @brief MIME boundary suffix */ #define MIME1 "-7bd98a1b" /** @brief MIME boundary suffix */ #define MIME2 "-0e6c69ac" /** @brief Domain name for content IDs */ #define CID_DOMAIN "rsbackup.greenend.org.uk" #endif /* DEFAULTS_H */ rsbackup-10.0/src/Device.cc000066400000000000000000000016361440730431700155520ustar00rootroot00000000000000// Copyright © 2011, 2014, 2015 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Device.h" bool Device::valid(const std::string &name) { return name.size() > 0 && name.at(0) != '-' && name.find_first_not_of(DEVICE_VALID) == std::string::npos; } rsbackup-10.0/src/Device.h000066400000000000000000000027371440730431700154170ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014-2016 Richard Kettlewell. // // 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 . #ifndef DEVICE_H #define DEVICE_H /** @file Device.h * @brief Configuration and state of a device */ #include #include class Store; /** @brief Represents a backup device */ class Device { public: /** @brief Constructor * @param name_ Name of device */ Device(const std::string &name_): name(name_) {} /** @brief Name of device */ std::string name; /** @brief Store for this device, or null pointer * * Set by Store::identify(). */ Store *store = nullptr; /** @brief Validity test for device names * @param n Name of device * @return true if @p n is a valid device name, else false */ static bool valid(const std::string &n); /** @brief Lock for access to this device */ std::mutex lock; }; #endif /* DEVICE_H */ rsbackup-10.0/src/DeviceAccess.cc000066400000000000000000000037101440730431700166670ustar00rootroot00000000000000// Copyright © 2012, 2015-17, 2019 Richard Kettlewell. // // 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 . #include #include "DeviceAccess.h" #include "Conf.h" #include "Subprocess.h" #include "Errors.h" #include "Command.h" #include "IO.h" #include "Utils.h" static bool devicesReady; static std::vector filesToClose; static void runDeviceAccessHook(const std::vector &cmd, const std::string &name) { if(cmd.size() == 0) return; Subprocess sp("device-access", cmd); sp.setenv("RSBACKUP_HOOK", name); sp.setenv("RSBACKUP_ACT", globalCommand.act ? "true" : "false"); std::string devices; for(auto &d: globalConfig.devices) { if(devices.size() != 0) devices += " "; devices += d.first; } sp.setenv("RSBACKUP_DEVICES", devices); sp.reporting(globalWarningMask & WARNING_VERBOSE, false); sp.runAndWait(Subprocess::THROW_ON_ERROR | Subprocess::THROW_ON_CRASH | Subprocess::THROW_ON_SIGPIPE); } void preDeviceAccess() { if(!devicesReady) { runDeviceAccessHook(globalConfig.preDevice, "pre-device-hook"); devicesReady = true; } } void closeOnUnmount(IO *f) { filesToClose.push_back(f); } void postDeviceAccess() { if(devicesReady) { deleteAll(filesToClose); runDeviceAccessHook(globalConfig.postDevice, "post-device-hook"); devicesReady = false; } } rsbackup-10.0/src/DeviceAccess.h000066400000000000000000000020501440730431700165250ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2012 Richard Kettlewell. // // 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 . #ifndef DEVICEACCESS_H #define DEVICEACCESS_H /** @file DeviceAccess.h * @brief %Device access functions */ class IO; /** @brief Run pre-device-hook */ void preDeviceAccess(); /** @brief Close a file before running the post-device-hook */ void closeOnUnmount(IO *f); /** @brief Run post-device-hook */ void postDeviceAccess(); #endif /* DEVICEACCESS_H */ rsbackup-10.0/src/Directory.cc000066400000000000000000000036501440730431700163150ustar00rootroot00000000000000// Copyright © 2011, 2013 Richard Kettlewell. // // 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 . #include #include "IO.h" #include "Errors.h" #include #include Directory::~Directory() { if(dp) closedir(dp); } void Directory::open(const std::string &path_) { if(dp) throw std::logic_error("Directory::open on open directory"); if(!(dp = opendir(path_.c_str()))) throw IOError("opening " + path_, errno); path = path_; } void Directory::close() { if(!dp) throw std::logic_error("Directory::close on closed directory"); closedir(dp); dp = nullptr; path.clear(); } bool Directory::get(std::string &name) const { if(!dp) throw std::logic_error("Directory::get on closed directory"); errno = 0; struct dirent *de = readdir(dp); if(de) { name = de->d_name; return true; } else { if(errno) throw IOError("reading " + path, errno); return false; } } void Directory::get(std::vector &files) const { std::string f; files.clear(); while(get(f)) files.push_back(f); // Sort files by name so that warnings come out in a predictable order. std::sort(files.begin(), files.end()); } void Directory::getFiles(const std::string &path, std::vector &files) { Directory d; d.open(path); d.get(files); } rsbackup-10.0/src/Document.cc000066400000000000000000000060261440730431700161270ustar00rootroot00000000000000// Copyright © 2011, 2012, 2015, 2018 Richard Kettlewell. // // 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 . #include #include #include #include #include #include "Document.h" #include "Utils.h" // LinearContainer ------------------------------------------------------------ Document::LinearContainer::~LinearContainer() { deleteAll(nodes); } // String --------------------------------------------------------------------- Document::String::String(int n) { char buffer[64]; snprintf(buffer, sizeof buffer, "%d", n); text = buffer; } // Table ---------------------------------------------------------------------- Document::Table::~Table() { // Defer deletion because we'll keep looking through Cell pointers std::vector cells; for(auto it: cellMap) { int x = it.first.first, y = it.first.second; Cell *cell = it.second; if(cell->x == x && cell->y == y) cells.push_back(cell); } deleteAll(cells); cellMap.clear(); } Document::Cell *Document::Table::addCell(Cell *cell) { addCell(cell, x, y); while(findOverlappingCell(x, y)) ++x; return cell; } void Document::Table::newRow() { x = 0; ++y; while(findOverlappingCell(x, y)) ++x; } Document::Cell *Document::Table::addCell(Cell *cell, int x, int y) { cell->x = x; cell->y = y; // Update table occupancy for(int xpos = x; xpos < x + cell->w; xpos++) for(int ypos = y; ypos < y + cell->h; ypos++) cellMap[std::pair(xpos, ypos)] = cell; // Update table size tracking width = std::max(width, cell->x + cell->w); height = std::max(height, cell->y + cell->h); return cell; } const Document::Cell *Document::Table::findOverlappingCell(int x, int y) const { auto it = cellMap.find(std::pair(x, y)); if(it != cellMap.end()) return it->second; else return nullptr; } const Document::Cell *Document::Table::findRootedCell(int x, int y) const { const Cell *cell = findOverlappingCell(x, y); if(cell && cell->x == x && cell->y == y) return cell; else return nullptr; } // Image std::string Document::Image::ident() const { static boost::uuids::string_generator sg; static boost::uuids::uuid ns = sg("1a90a5fb-9558-44d0-a9a9-9955c0ed359f"); static boost::uuids::name_generator cg(ns); boost::uuids::uuid u = cg(content.data(), content.size()); return boost::uuids::to_string(u) + "@" CID_DOMAIN; } rsbackup-10.0/src/Document.h000066400000000000000000000372121440730431700157720ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2015, 2018 Richard Kettlewell. // // 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 . #ifndef DOCUMENT_H #define DOCUMENT_H /** @file Document.h * @brief %Document construction and rendering */ #include #include #include #include "Defaults.h" class RenderDocumentContext; /** @brief Structured document class * * Can be rendered to text or HTML. */ class Document { public: /** @brief Base class for document node types */ struct Node { /** @brief Destructor */ virtual ~Node() = default; Node() = default; Node(const Node &) = delete; Node &operator=(const Node &) = delete; /** @brief Style name */ std::string style; /** @brief Foreground color or -1 */ int fgcolor = -1; /** @brief Background color or -1 */ int bgcolor = -1; /** @brief Render as HTML * @param os Output * @param rc Rendering context */ virtual void renderHtml(std::ostream &os, RenderDocumentContext *rc) const = 0; /** @brief Render as text * @param os Output * @param rc Rendering context */ virtual void renderText(std::ostream &os, RenderDocumentContext *rc) const = 0; /** @brief Render an open tag * @param os Output * @param name Element name * @param ... Attribute name/value pairs * * Attribute names and values are const char * and are terminated by a null * pointer of the same type. */ void renderHtmlOpenTag(std::ostream &os, const char *name, ...) const; /** @brief Render a close tag * @param os Output * @param name Element name * @param newline Add a trailing newline */ void renderHtmlCloseTag(std::ostream &os, const char *name, bool newline = true) const; }; /** @brief Intermediate base for leaf node types */ struct Leaf: public Node {}; /** @brief An unadorned string */ struct String: public Leaf { /** @brief Text of string */ std::string text; /** @brief Constructor */ String() = default; /** @brief Constructor * @param s Initial text */ String(const std::string &s): text(s) {} /** @brief Constructor * @param n Integer to convert to string */ String(int n); /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; /** @brief Base class for ordered containers */ struct LinearContainer: public Node { /** @brief Destructor */ ~LinearContainer() override; /** @brief List of nodes in container */ std::vector nodes; /** @brief Append a node * @param node Note to append * @return @p node */ Node *append(Node *node) { nodes.push_back(node); return node; } /** @brief Append a string node * @param text Text for new string node * @return New string node */ String *append(const std::string &text) { String *s = new String(text); nodes.push_back(s); return s; } /** @brief Append a list of string nodes * @param text Strings to append * @param insertNewlines If true append @c \\n to each */ void append(const std::vector &text, bool insertNewlines = true) { for(size_t n = 0; n < text.size(); ++n) { append(text[n]); if(insertNewlines) append("\n"); } } /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtmlContents(std::ostream &os, RenderDocumentContext *rc) const; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderTextContents(std::ostream &os, RenderDocumentContext *rc) const; /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; /** @brief A paragraph */ struct Paragraph: public LinearContainer { /** @brief Constructor */ Paragraph() = default; /** @brief Constructor * @param node Initial node */ Paragraph(Node *node) { append(node); } /** @brief Constructor * @param s Initial text */ Paragraph(const std::string &s) { append(s); } /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; /** @brief A verbatim section */ struct Verbatim: public LinearContainer { /** @brief Constructor */ Verbatim() = default; /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; /** @brief Possible types of list */ enum ListType { UnorderedList, OrderedList }; /** @brief A list */ struct List: public LinearContainer { /** @brief Constructor * @param type_ List type */ List(ListType type_ = UnorderedList): type(type_) {} /** @brief Constructor * @param s Text for initial list entry */ Node *entry(const std::string &s) { return append(new ListEntry(s)); } /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief List type */ ListType type; }; /** @brief One element in a list */ struct ListEntry: public LinearContainer { /** @brief Constructor * @param node Initial node */ ListEntry(Node *node) { append(node); } /** @brief Constructor * @param s Initial text */ ListEntry(const std::string &s) { append(s); } /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; /** @brief A heading * * Level 1 is the highest-level (biggest) heading, level 6 the lowest * (smallest). Don't use levels outside this range. */ struct Heading: public LinearContainer { /** @brief Level of heading, 1-6 */ int level; /** @brief Constructor * @param level_ Level of heading */ Heading(int level_ = 1): level(level_) {} /** @brief Constructor * @param node Initial node * @param level_ Level of heading */ Heading(Node *node, int level_ = 1): Heading(level_) { append(node); } /** @brief Constructor * @param text Initial text * @param level_ Level of heading */ Heading(const std::string &text, int level_ = 1): Heading(level_) { append(text); } /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; class Table; /** @brief A cell in a table. * * The origin (x=y=0) is at the top left. Don't add overlapping cells to a * single table. */ class Cell: public LinearContainer { public: /** @brief Constructor * @param w_ Cell width * @param h_ Cell height */ Cell(int w_ = 1, int h_ = 1, bool heading_ = false): heading(heading_), w(w_), h(h_) {} /** @brief Constructor * @param s Initial contents * @param w_ Cell width * @param h_ Cell height */ Cell(const std::string &s, int w_ = 1, int h_ = 1, bool heading_ = false): Cell(w_, h_, heading_) { append(new String(s)); } /** @brief Constructor * @param n Initial contents * @param w_ Cell width * @param h_ Cell height */ Cell(Node *n, int w_ = 1, int h_ = 1, bool heading_ = false): Cell(w_, h_, heading_) { append(n); } /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; bool getHeader() const { return heading; } int getX() const { return x; } int getY() const { return y; } int getWidth() const { return w; } int getHeight() const { return h; } private: /** @brief True if this is a table heading */ bool heading; /** @brief X position in table */ int x = 0; /** @brief Y position in table */ int y = 0; /** @brief Width in columns */ int w; /** @brief Height in rows */ int h; friend class Table; }; /** @brief A table. * * The origin (x=y=0) is at the top left. Don't add overlapping cells to a * single table. */ class Table: public Node { public: /** @brief Destructor */ ~Table() override; /** @brief Width of table (columns) */ int getWidth() const { return width; } /** @brief Height of table (rows) */ int getHeight() const { return height; } /** @brief Add a cell at the cursor * @param cell Cell to add * @return @p cell */ Cell *addCell(Cell *cell); /** @brief Start a new row */ void newRow(); /** @brief Add a cell at a particular position * @param cell Cell to add * @param x X position * @param y Y position * @return @p cell */ Cell *addCell(Cell *cell, int x, int y); /** @brief Find the cell at a position * @param x X position * @param y Y position * @return @p cell or null pointer * * Note that this returns both cells located at x,y and also * multi-column/row cells that extend to x,y. */ const Cell *findOverlappingCell(int x, int y) const; /** @brief Find the cell rooted at a position * @param x X position * @param y Y position * @return @p cell or null pointer * * Note that this returns pm;u cells located at x,y, not also * multi-column/row cells that extend to x,y. */ const Cell *findRootedCell(int x, int y) const; /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; private: /** @brief Cursor X position */ int x = 0; /** @brief Cursor Y position */ int y = 0; /** @brief Current table width */ int width = 0; /** @brief Current table height */ int height = 0; /** @brief Map of x, y coordinate pairs to cells */ std::map, Cell *> cellMap; }; /** @brief An image */ struct Image: public Node { /** @brief Constructor * @param type Content type * @param content Raw image data */ Image(const std::string &type, const std::string &content): type(type), content(content) {} /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief MIME type of image */ std::string type; /** @brief Content of image */ std::string content; /** @brief Unique ident of image */ std::string ident() const; }; /** @brief The root container for the document */ struct RootContainer: public LinearContainer { /** @brief Render as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const override; /** @brief Render as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const override; }; /** @brief The content of the document */ RootContainer content; /** @brief The title of the document */ std::string title; /** @brief The stylesheet, for HTML output */ std::string htmlStyleSheet; /** @brief Append something to the document */ Node *append(Node *node) { return content.append(node); } /** @brief Append a heading */ Heading *heading(const std::string &text, int level = 1) { Heading *h = new Heading(new String(text), level); content.append(h); return h; } /** @brief Append a paragraph */ Paragraph *para(const std::string &text) { Paragraph *p = new Paragraph(new String(text)); content.append(p); return p; } /** @brief Append a verbatim section */ Verbatim *verbatim() { Verbatim *v = new Verbatim(); content.append(v); return v; } /** @brief Render the document as HTML * @param os Output * @param rc Rendering context */ void renderHtml(std::ostream &os, RenderDocumentContext *rc) const; /** @brief Render the document as text * @param os Output * @param rc Rendering context */ void renderText(std::ostream &os, RenderDocumentContext *rc) const; /** @brief HTML quoting */ static void quoteHtml(std::ostream &os, const std::string &s); /** @brief Word-wrap text */ static void wordWrapText(std::ostream &os, const std::string &s, size_t width, size_t indent = 0, bool indentFirst = true); }; /** @brief Container for rendering */ class RenderDocumentContext { public: /** @brief Accumulated images */ std::vector images; /** @brief Page width */ int width = DEFAULT_TEXT_WIDTH; }; #endif /* DOCUMENT_H */ rsbackup-10.0/src/Email.cc000066400000000000000000000034061440730431700153770ustar00rootroot00000000000000// Copyright © 2011, 2012, 2015-16, 2019 Richard Kettlewell. // // 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 . #include #include "Email.h" #include "Conf.h" #include "Errors.h" #include "IO.h" #include "Utils.h" void Email::send() const { if(to.size() == 0) throw std::logic_error("no recipients for email"); std::vector command = { globalConfig.sendmail, "-t", // recipients from header "-oee", // request bounce xor error "-oi", // de-magic '.' "-odb" // background delivery }; IO mail; mail.popen(command, WriteToPipe, !!(globalWarningMask & WARNING_VERBOSE)); if(from.size()) mail.writef("From: %s\n", from.c_str()); mail.writef("To: "); for(size_t n = 0; n < to.size(); ++n) { if(n) mail.writef(", "); mail.writef("%s", to[n].c_str()); } mail.writef("\n"); mail.writef("Subject: %s\n", subject.c_str()); mail.writef("MIME-Version: 1.0\n"); mail.writef("User-Agent: rsbackup/" VERSION "\n"); mail.writef("Content-Type: %s\n", type.c_str()); mail.writef("\n"); mail.write(content); if(content.size() && content[content.size() - 1] != '\n') mail.write("\n"); mail.close(); } rsbackup-10.0/src/Email.h000066400000000000000000000037401440730431700152420ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2014, 2015 Richard Kettlewell. // // 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 . #ifndef EMAIL_H #define EMAIL_H /** @file Email.h * @brief Constructing and sending email */ #include #include /** @brief An email message */ class Email { public: /** @brief Add a recipient * @param address Destination address */ void addTo(const std::string &address) { to.push_back(address); } /** @brief Set sender * @param address Sender address */ void setFrom(const std::string &address) { from = address; } /** @brief Set subject * @param text Subject */ void setSubject(const std::string &text) { subject = text; } /** @brief Set content type * @param type_ Content type * * The default is @c text/plain (with no indication of charset). */ void setType(const std::string &type_) { type = type_; } /** @brief Set content * @param msg Content */ void setContent(const std::string &msg) { content = msg; } /** @brief Send message */ void send() const; private: /** @brief Sender address */ std::string from; /** @brief Subject */ std::string subject; /** @brief Recipients */ std::vector to; /** @brief MIME Content type */ std::string type = "text/plain"; /** @brief Contents of email */ std::string content; }; #endif /* EMAIL_H */ rsbackup-10.0/src/Errors.cc000066400000000000000000000035631440730431700156300ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014 Richard Kettlewell. // // 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 . #include #include "Errors.h" #include #include #include #include #if HAVE_EXECINFO_H #include #endif std::string SubprocessFailed::format(const std::string &name, int wstat) { if(WIFSIGNALED(wstat)) { int sig = WTERMSIG(wstat); return (name + ": " + strsignal(sig) + (WCOREDUMP(wstat) ? " (core dumped)" : "")); } else if(WIFSTOPPED(wstat)) { int sig = WSTOPSIG(wstat); return (name + ": " + strsignal(sig)); } else if(WIFEXITED(wstat)) { char buffer[64]; int rc = WEXITSTATUS(wstat); snprintf(buffer, sizeof buffer, "%d", rc); return (name + ": exited with status " + buffer); } else { char buffer[64]; snprintf(buffer, sizeof buffer, "%#x", wstat); return (name + ": exited with wait status " + buffer); } } Error::Error(const std::string &msg): std::runtime_error(msg) { #if HAVE_EXECINFO_H stacksize = backtrace(stack, sizeof stack / sizeof *stack); #endif } void Error::trace(FILE *fp) { #if HAVE_EXECINFO_H char **names = backtrace_symbols(stack, stacksize); for(int i = 0; i < stacksize; ++i) fprintf(fp, "%s\n", names[i]); free(names); #endif } rsbackup-10.0/src/Errors.h000066400000000000000000000127701440730431700154720ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014 Richard Kettlewell. // // 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 . #ifndef ERRORS_H #define ERRORS_H /** @file Errors.h * @brief Exceptions */ #include #include #include #ifndef STACK_MAX /** @brief Maximum backtrace size */ #define STACK_MAX 128 #endif /** @brief Base class for all errors */ class Error: public std::runtime_error { public: /** @brief Constructor * @param msg Error message */ Error(const std::string &msg); /** @brief Display stack trace * @param fp Destination for stack trace */ void trace(FILE *fp); private: #if HAVE_EXECINFO_H void *stack[STACK_MAX]; int stacksize; #endif }; /** @brief System-level error */ class SystemError: public Error { public: /** @brief Constructor * @param msg Error message */ SystemError(const std::string &msg): Error(msg), errno_value(0) {} /** @brief Constructor * @param msg Error message * @param error @c errno value */ SystemError(const std::string &msg, int error): Error(msg + ": " + strerror(error)), errno_value(error) {} /** @brief @c errno value */ int errno_value; }; /** @brief An I/O error */ class IOError: public SystemError { public: /** @brief Constructor * @param msg Error message */ IOError(const std::string &msg): SystemError(msg) {} /** @brief Constructor * @param msg Error message * @param error @c errno value */ IOError(const std::string &msg, int error): SystemError(msg, error) {} }; /** @brief A syntax error * * Does not include path/line information. */ class SyntaxError: public Error { public: /** @brief Constructor * @param msg Error message */ SyntaxError(const std::string &msg): Error(msg) {} }; /** @brief Represents some problem with a config file * * Usually equivalent to a SyntaxError but with path/line information included * in the message. */ class ConfigError: public Error { public: /** @brief Constructor * @param msg Error message */ ConfigError(const std::string &msg): Error(msg) {} }; /** @brief Represents some problem with a store * * BadStore errors are reported but are not fatal. */ class BadStore: public Error { public: /** @brief Constructor * @param msg Error message */ BadStore(const std::string &msg): Error(msg) {} }; /** @brief Represents the problem that a store is not mounted * * UnavailableStore errors are 'normal' and are only displayed if * --warn-store is enabled. */ class UnavailableStore: public Error { public: /** @brief Constructor * @param msg Error message */ UnavailableStore(const std::string &msg): Error(msg) {} }; /** @brief Represents some problem with a store * * FatalStoreErrors abort the whole process. */ class FatalStoreError: public Error { public: /** @brief Constructor * @param msg Error message */ FatalStoreError(const std::string &msg): Error(msg) {} }; /** @brief Represents a problem with the command line */ class CommandError: public Error { public: /** @brief Constructor * @param msg Error message */ CommandError(const std::string &msg): Error(msg) {} }; /** @brief Represents some problem with a date string */ class InvalidDate: public Error { public: /** @brief Constructor * @param msg Error message */ InvalidDate(const std::string &msg): Error(msg) {} }; /** @brief Represents some problem with a regexp */ class InvalidRegexp: public SystemError { public: /** @brief Constructor * @param msg Error message */ InvalidRegexp(const std::string &msg): SystemError(msg) {} }; /** @brief Represents some problem with @ref PruneExec output */ class InvalidPruneList: public SystemError { public: /** @brief Constructor * @param msg Error message */ InvalidPruneList(const std::string &msg): SystemError(msg) {} }; /** @brief Represents failure of a subprocess */ class SubprocessFailed: public Error { public: /** @brief Constructor * @param name Subprocess name * @param wstat Wait status */ SubprocessFailed(const std::string &name, int wstat): Error(format(name, wstat)) {} /** @brief Format the error message * @param name Subprocess name * @param wstat Wait status * @return Error string */ static std::string format(const std::string &name, int wstat); }; /** @brief Represents an error from the database subsystem */ class DatabaseError: public Error { public: /** @brief @c SQLITE_... error code */ int status; /** @brief Constructor * @param rc @c SQLITE_... error code * @param msg Error message */ DatabaseError(int rc, const std::string &msg): Error(msg), status(rc) {} }; /** @brief Represents a 'busy' error from the database subsystem */ class DatabaseBusy: public DatabaseError { public: /** @brief Constructor * @param rc @c SQLITE_... error code * @param msg Error message */ DatabaseBusy(int rc, const std::string &msg): DatabaseError(rc, msg) {} }; #endif /* ERRORS_H */ rsbackup-10.0/src/EventLoop.cc000066400000000000000000000113171440730431700162630ustar00rootroot00000000000000// Copyright © 2015, 2016, 2020 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "EventLoop.h" #include "Errors.h" #include #include #include #include #include #include #include #include void Reactor::onReadable(EventLoop *, int, const void *, size_t) { throw std::logic_error("Reactor::onReadable"); } void Reactor::onReadError(EventLoop *, int, int) { throw std::logic_error("Reactor::onReadError"); } void Reactor::onWritable(EventLoop *, int) { throw std::logic_error("Reactor::onWritable"); } void Reactor::onTimeout(EventLoop *, const struct timespec &) { throw std::logic_error("Reactor::onTimeout"); } void Reactor::onWait(EventLoop *, pid_t, int, const struct rusage &) { throw std::logic_error("Reactor::onWait"); } EventLoop::EventLoop() {} EventLoop::~EventLoop() {} void EventLoop::whenReadable(int fd, Reactor *r) { readers[fd] = r; reconf = true; } void EventLoop::cancelRead(int fd) { readers.erase(fd); reconf = true; } void EventLoop::whenWritable(int fd, Reactor *r) { writers[fd] = r; reconf = true; } void EventLoop::cancelWrite(int fd) { writers.erase(fd); reconf = true; } void EventLoop::whenTimeout(const struct timespec &t, Reactor *r) { timeouts.insert({t, r}); reconf = true; } void EventLoop::whenWaited(pid_t pid, Reactor *r) { waiters[pid] = r; reconf = true; } void EventLoop::wait(bool wait_for_timeouts) { while(readers.size() > 0 || writers.size() > 0 || waiters.size() > 0 || (wait_for_timeouts && timeouts.size() > 0)) { fd_set rfds, wfds; struct timespec ts, *tsp; int maxfd = -1, n; if(timeouts.size() > 0) { auto it = timeouts.begin(); struct timespec now, first = it->first; getMonotonicTime(now); if(now >= first) { Reactor *r = it->second; timeouts.erase(it); r->onTimeout(this, now); continue; } ts = first - now; if(ts.tv_sec >= 10) ts.tv_sec = 10; tsp = &ts; } else tsp = nullptr; // Ideally we would wait for SIGCHLD. But this is a threaded program now, // and distribution child termination notifications to the right thread is // too hard. So we just poll. if(waiters.size() > 0) { if(tsp == nullptr || ts.tv_sec > 0 || ts.tv_nsec > 100000000) { ts.tv_sec = 0; ts.tv_nsec = 100000000; tsp = &ts; } } FD_ZERO(&rfds); FD_ZERO(&wfds); for(auto &r: readers) { int fd = r.first; FD_SET(fd, &rfds); maxfd = std::max(maxfd, fd); } for(auto &w: writers) { int fd = w.first; FD_SET(fd, &wfds); maxfd = std::max(maxfd, fd); } n = pselect(maxfd + 1, &rfds, &wfds, nullptr, tsp, nullptr); if(n < 0) { if(errno != EINTR) throw IOError("pselect", errno); } else if(n > 0) { reconf = false; for(auto &r: readers) { int fd = r.first; if(FD_ISSET(fd, &rfds)) { char buffer[4096]; ssize_t nbytes = read(fd, buffer, sizeof buffer); if(nbytes < 0) { if(errno == EINTR || errno == EAGAIN) continue; r.second->onReadError(this, fd, errno); } else r.second->onReadable(this, fd, buffer, nbytes); if(reconf) break; } } for(auto &w: writers) { int fd = w.first; if(FD_ISSET(fd, &wfds)) { w.second->onWritable(this, fd); if(reconf) break; } } } for(auto wi = waiters.begin(); wi != waiters.end();) { struct rusage ru; int status, pid = wait4(wi->first, &status, WNOHANG, &ru); if(pid < 0) { if(errno != EINTR) throw SystemError("wait4", errno); } if(pid) { Reactor *r = wi->second; wi = waiters.erase(wi); r->onWait(this, pid, status, ru); } else ++wi; } } } void EventLoop::terminateSubprocesses() { for(auto it = waiters.begin(); it != waiters.end(); ++it) kill(it->first, SIGTERM); } rsbackup-10.0/src/EventLoop.h000066400000000000000000000175761440730431700161420ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2015, 2016, 2020 Richard Kettlewell. // // 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 . #ifndef EVENTLOOP_H #define EVENTLOOP_H /** @file EventLoop.h * @brief Asynchronous operations * * To manage asynchronous operations: * - create a single @ref EventLoop object * - create file descriptors and subprocesses * - create @ref Reactor objects to handle I/O, timeouts and subprocesses * - attach the reactor objects to the event loop * - use @ref EventLoop::wait to start processing events * * Note that the event loop should be created before any subprocesses are * created; otherwise the signal handling will not work properly. */ #include class EventLoop; class Reactor; /** @brief An object that reacts to asynchronous events * * Reactors must be attached to an @ref EventLoop to be used, using one of the * following methods: * - @ref EventLoop::whenReadable * - @ref EventLoop::whenWritable * - @ref EventLoop::whenTimeout * - @ref EventLoop::whenWaited * * Normally you would implement at least one of the @c on... methods. Those * that are not implemented will raise @c std::logic_error. However they will * not be called unless attached to an event loop. */ class Reactor { public: /** @brief Destructor */ virtual ~Reactor() = default; /** @brief Called when a file descriptor is readable * @param e Calling event loop * @param fd File descriptor * @param ptr Bytes read from @p fd * @param n Number of bytes at @p ptr * * This will be called when bytes are read from @p fd, or when it reaches end * of file, if @ref EventLoop::whenReadable was used to attach this reactor * to an event loop. * * @p n will be 0 at EOF. (Currently) the implementation must cancel reads * (using @ref EventLoop::cancelRead) even at EOF, or the event loop will * just call it again. */ virtual void onReadable(EventLoop *e, int fd, const void *ptr, size_t n); /** @brief Called when a file descriptor cannot be read due to error * @param e Calling event loop * @param fd File descriptor * @param errno_value @c errno value * * This will be called when an error occurs while reading @p fd, if @ref * EventLoop::whenReadable was used to attach this reactor to an event loop. * * (Currently) the implementation must cancel reads (using @ref * EventLoop::cancelRead) even on error, or the event loop will just call it * again. */ virtual void onReadError(EventLoop *e, int fd, int errno_value); /** @brief Called when a file descriptor is writable * @param e Calling event loop * @param fd File descriptor * * This will be called when @p fd is writable, if @ref * EventLoop::whenWritable was used to attach this reactor to an event loop. * * The implementation is responsible for performing the write and for * cancelling further writes (using @ref EventLoop::cancelWrite) if there is * nothing more to write or on error. */ virtual void onWritable(EventLoop *e, int fd); /** @brief Called when a timeout occurs * @param e Calling event loop * @param now (Monotonic) timestamp (see @ref getMonotonicTime) * * This will be called when a time limit is reached, if @ref * EventLoop::whenTimeout was used to attach this reactor to an event loop. * * @p now may be after the time limit, but never before. */ virtual void onTimeout(EventLoop *e, const struct timespec &now); /** @brief Called when a subprocess terminates * @param e Calling event loop * @param pid Subprocess * @param status Wait status * @param ru Resource usage * * This will be called when a subprocess terminates, if @ref * EventLoop::whenWaited was used to attach this reactor to an event loop. * * (In contrast to the read/write methods) the event loop stops waiting for * the process before this call is made. */ virtual void onWait(EventLoop *e, pid_t pid, int status, const struct rusage &ru); }; /** @brief An event loop supporting asynchronous I/O * * Event loops allow multiple file descriptors and subprocesses to be managed * together. All I/O, subprocess and timeout events are reflected in calls to * methods of the @ref Reactor class. * * (Currently) only one event loop object may exist at a time. This limitation * is due to the signal handling strategy. */ class EventLoop: private Reactor { public: /** @brief Construct an event loop */ EventLoop(); EventLoop(const EventLoop &) = delete; EventLoop &operator=(const EventLoop &) = delete; /** @brief Destroy an event loop */ ~EventLoop() override; /** @brief Notify reactor when a file descriptor is readable * @param fd File descriptor to monitor * @param r Reactor to notify * * The reactor is notified by calling @ref Reactor::onReadable and @ref * Reactor::onReadError. */ void whenReadable(int fd, Reactor *r); /** @brief Stop monitoring a file descriptor for readability * @param fd File descriptor to stop monitoring */ void cancelRead(int fd); /** @brief Notify reactor when a file descriptor is writable * @param fd File descriptor to monitor * @param r Reactor to notify * * The reactor is notified by calling @ref Reactor::onWritable. */ void whenWritable(int fd, Reactor *r); /** @brief Stop monitoring a file descriptor for writability * @param fd File descriptor to stop monitoring */ void cancelWrite(int fd); /** @brief Notify a reactor at a future time * @param t (Monotonic) timestamp to wait for (see @ref getMonotonicTime) * @param r Reactor to notify * * The reactor is notified by calling @ref Reactor::onTimeout. */ void whenTimeout(const struct timespec &t, Reactor *r); /** @brief Notify a reactor when a subprocess terminates * @param pid Subprocess * @param r Reactor to notify * * The reactor is notified by calling @ref Reactor::onWait. * * Note that the event loop should be created before any subprocesses are * created; otherwise the signal handling will not work properly. */ void whenWaited(pid_t pid, Reactor *r); /** @brief Stop monitoring a subprocess * @param pid Subprocess to stop monitoring */ void cancelWait(pid_t pid); /** @brief Wait until there is nothing left to wait for * @param wait_for_timeouts Whether to wait for timeouts * * This does not return until all readers and writers have been cancelled and * all subprocesses have been waited for. If @p wait_for_timeouts is @c true * it will also delay returning until all timeouts have expired. */ void wait(bool wait_for_timeouts = false); /** @brief Send all current subprocesses SIGTERM */ void terminateSubprocesses(); private: /** @brief File descriptors monitored for reading */ std::map readers; /** @brief File descriptors monitored for writing */ std::map writers; /** @brief Timeouts */ std::multimap timeouts; /** @brief Subprocesses */ std::map waiters; /** @brief Set if reactors change * * This variable allows the event loop to detect that one of the * EventLoop::readers, EventLoop::writers, EventLoop::timeouts or * EventLoop::waiters has changed, and therefore to stop relying on * iterators that refer to them. */ bool reconf; }; #endif /* EVENTLOOP_H */ rsbackup-10.0/src/FileLock.cc000066400000000000000000000034041440730431700160360ustar00rootroot00000000000000// Copyright © 2011, 2012, 2015 Richard Kettlewell. // // 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 . #include #include "FileLock.h" #include "Errors.h" #include #include #include FileLock::FileLock(const std::string &path_): path(path_) {} FileLock::~FileLock() { if(fd >= 0) close(fd); } void FileLock::ensureOpen() { if(fd >= 0) return; if((fd = open(path.c_str(), O_WRONLY | O_CREAT, 0666)) < 0) throw IOError("opening " + path, errno); int flags; if((flags = fcntl(fd, F_GETFL)) < 0) throw IOError("fcntl F_GETFL " + path, errno); if(fcntl(fd, F_SETFL, flags | FD_CLOEXEC) < 0) throw IOError("fcntl F_SETFL " + path, errno); } bool FileLock::acquire(bool wait) { ensureOpen(); if(held) return true; if(flock(fd, wait ? LOCK_EX : LOCK_EX | LOCK_NB) < 0) { if(errno == EWOULDBLOCK && !wait) return false; throw IOError("acquiring lock on " + path, errno); } held = true; return true; } void FileLock::release() { if(!held) throw std::logic_error("release without acquire"); if(flock(fd, LOCK_UN) < 0) throw IOError("releasing lock on " + path, errno); held = false; } rsbackup-10.0/src/FileLock.h000066400000000000000000000035201440730431700156770ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2014, 2015 Richard Kettlewell. // // 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 . #ifndef FILELOCK_H #define FILELOCK_H /** @file FileLock.h * @brief File locking */ #include /** @brief Hold an flock() lock on a file */ class FileLock { public: /** @brief Constructor * @param path File to lock * * The constructor does not acquire the lock. */ FileLock(const std::string &path); FileLock(const FileLock &) = delete; FileLock &operator=(const FileLock &) = delete; /** @brief Destructor * * Releases the lock if is held. */ ~FileLock(); /** @brief Acquire the lock * @param wait If true, block until lock can be acquire; else give up * @return true if the lock was acquired * * Returns immediately if the lock is already held. Note that this is not a * recursive lock! */ bool acquire(bool wait = true); /** @brief Release the lock * * The lock must be held. */ void release(); private: /** @brief Path to lockfile */ std::string path; /** @brief Open lockfile, or -1 */ int fd = -1; /** @brief @c true when lock is held */ bool held = false; /** @brief @c Ensure @p fd is open */ void ensureOpen(); }; #endif /* FILELOCK_H */ rsbackup-10.0/src/HTML.cc000066400000000000000000000164771440730431700151300ustar00rootroot00000000000000// Copyright © 2011, 2014, 2015, 2018 Richard Kettlewell. // // 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 . #include #include "Document.h" #include "Utils.h" #include "Errors.h" #include #include #include #include // HTML support --------------------------------------------------------------- void Document::quoteHtml(std::ostream &os, const std::string &s) { // We need the string in UTF-32 in order to quote it correctly std::u32string u; toUnicode(u, s); // SGML-quote anything that might be interpreted as a delimiter, and anything // outside of ASCII for(auto w: u) { switch(w) { default: if(w >= 127) { case '&': case '<': case '"': case '\'': os << "&#" << w << ";"; break; } else os << (char)w; } } } void Document::Node::renderHtmlOpenTag(std::ostream &os, const char *name, ...) const { va_list ap; os << '<' << name; if(style.size()) os << " class=" << style; char buffer[64]; if(fgcolor != -1) { snprintf(buffer, sizeof buffer, "#%06x", fgcolor); os << " color=\"" << buffer << "\""; } if(bgcolor != -1) { snprintf(buffer, sizeof buffer, "#%06x", bgcolor); os << " bgcolor=\"" << buffer << "\""; } va_start(ap, name); const char *attributeName, *attributeValue; while((attributeName = va_arg(ap, const char *))) { attributeValue = va_arg(ap, const char *); os << " " << attributeName << "=\""; quoteHtml(os, attributeValue); os << "\""; } os << '>'; va_end(ap); } void Document::Node::renderHtmlCloseTag(std::ostream &os, const char *name, bool newline) const { os << ""; if(newline) os << '\n'; } void Document::LinearContainer::renderHtmlContents( std::ostream &os, RenderDocumentContext *rc) const { for(Node *node: nodes) node->renderHtml(os, rc); } void Document::String::renderHtml(std::ostream &os, RenderDocumentContext *) const { Document::quoteHtml(os, text); } void Document::LinearContainer::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { renderHtmlOpenTag(os, "div", (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, "div"); } void Document::Paragraph::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { renderHtmlOpenTag(os, "p", (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, "p"); } void Document::Verbatim::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { renderHtmlOpenTag(os, "pre", (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, "pre"); } void Document::List::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { switch(type) { case OrderedList: renderHtmlOpenTag(os, "ol", (char *)nullptr); break; case UnorderedList: renderHtmlOpenTag(os, "ul", (char *)nullptr); break; } renderHtmlContents(os, rc); switch(type) { case OrderedList: renderHtmlCloseTag(os, "ol"); break; case UnorderedList: renderHtmlCloseTag(os, "ul"); break; } } void Document::ListEntry::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { renderHtmlOpenTag(os, "li", (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, "li"); } void Document::Heading::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { if(level > 6) throw std::runtime_error("heading level too high"); char tag[64]; snprintf(tag, sizeof tag, "h%d", level); renderHtmlOpenTag(os, tag, (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, tag); } void Document::Cell::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { const char *const tag = heading ? "th" : "td"; char ws[64], hs[64]; snprintf(ws, sizeof ws, "%d", w); snprintf(hs, sizeof hs, "%d", h); if(w > 1 && h > 1) renderHtmlOpenTag(os, tag, "colspan", ws, "rowspan", hs, (char *)nullptr); else if(w > 1) renderHtmlOpenTag(os, tag, "colspan", ws, (char *)nullptr); else if(h > 1) renderHtmlOpenTag(os, tag, "rowspan", hs, (char *)nullptr); else renderHtmlOpenTag(os, tag, (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, tag); } void Document::Table::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { renderHtmlOpenTag(os, "table", (char *)nullptr); for(int row = 0; row < height; ++row) { renderHtmlOpenTag(os, "tr", (char *)nullptr); bool heading = false; // Detect if this is a heading row for(int col = 0; col < width;) { int skip = 0; // How many columns to skip // Find the cell at this position and render it const Cell *cell = findRootedCell(col, row); if(cell) { heading |= cell->getHeader(); cell->renderHtml(os, rc); skip = cell->getWidth(); } else { // No cell was at this position, either because it's // completely empty or because it's eclipsed by a // multi-column or multi-row cell. if(!findOverlappingCell(col, row)) { // Inherit heading status from cells to the left const char *tag = heading ? "th" : "td"; renderHtmlOpenTag(os, tag, (char *)nullptr); renderHtmlCloseTag(os, tag); } skip = 1; } col += skip; } renderHtmlCloseTag(os, "tr"); } renderHtmlCloseTag(os, "table"); } void Document::Image::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { std::string url; if(rc) { rc->images.push_back(this); url = "cid:" + ident(); } else { std::stringstream ss; ss << "data:" << type << ";base64,"; write_base64(ss, content); url = ss.str(); } renderHtmlOpenTag(os, "p", (char *)nullptr); renderHtmlOpenTag(os, "img", "src", url.c_str(), (char *)nullptr); renderHtmlCloseTag(os, "p"); } void Document::RootContainer::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { renderHtmlOpenTag(os, "body", (char *)nullptr); renderHtmlContents(os, rc); renderHtmlCloseTag(os, "body"); } void Document::renderHtml(std::ostream &os, RenderDocumentContext *rc) const { os << "\n"; os << "\n"; os << ""; quoteHtml(os, title); os << "\n"; if(htmlStyleSheet.size()) { os << "\n"; } os << "\n"; content.renderHtml(os, rc); os << "\n"; } rsbackup-10.0/src/HistoryGraph.cc000066400000000000000000000311131440730431700167670ustar00rootroot00000000000000// Copyright © 2015-16, 2019 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "HistoryGraph.h" #include "Errors.h" #include "Utils.h" #include #include #include HostLabels::HostLabels(Render::Context &ctx): Render::Grid(ctx) { unsigned row = 0; if(globalConfig.hosts.size() == 0) throw std::runtime_error("no hosts found in configuration"); for(auto host_iterator: globalConfig.hosts) { Host *host = host_iterator.second; if(!host->selected()) continue; if(host->volumes.size() == 0) printf("%s has no volumes!?!\n", host_iterator.first.c_str()); auto t = new Render::Text(ctx, host_iterator.first, globalConfig.colorGraphForeground, globalConfig.hostNameFont); cleanup(t); add(t, 0, row); for(auto volume_iterator: host->volumes) { Volume *volume = volume_iterator.second; if(!volume->selected()) continue; ++row; } } set_padding(globalConfig.horizontalPadding, globalConfig.verticalPadding); } VolumeLabels::VolumeLabels(Render::Context &ctx): Render::Grid(ctx) { unsigned row = 0; for(auto host_iterator: globalConfig.hosts) { Host *host = host_iterator.second; if(!host->selected()) continue; for(auto volume_iterator: host->volumes) { Volume *volume = volume_iterator.second; if(!volume->selected()) continue; auto t = new Render::Text(ctx, volume_iterator.first, globalConfig.colorGraphForeground, globalConfig.volumeNameFont); cleanup(t); add(t, 0, row); ++row; } } set_padding(globalConfig.horizontalPadding, globalConfig.verticalPadding); } DeviceKey::DeviceKey(Render::Context &ctx): Render::Grid(ctx) { unsigned row = 0; for(auto device_iterator: globalConfig.devices) { const auto &device = device_iterator.first; device_rows[device] = row; auto t = new Render::Text(ctx, device, globalConfig.colorGraphForeground, globalConfig.deviceNameFont); cleanup(t); add(t, 0, row); auto r = new Render::Rectangle(ctx, globalConfig.backupIndicatorKeyWidth, indicator_height, device_color(row)); rectangles.push_back(r); cleanup(r); add(r, 1, row, -1, 0); ++row; } set_padding(globalConfig.horizontalPadding, globalConfig.verticalPadding); } void DeviceKey::set_indicator_height(double h) { indicator_height = h; for(auto r: rectangles) r->set_size(-1, indicator_height); } const Color DeviceKey::device_color(unsigned row) const { /* TODO char di[64]; snprintf(di, sizeof di, "device%u", row); if(context.colors.find(di) != context.colors.end()) return context.colors[di]; */ return globalConfig.deviceColorStrategy->get(row, globalConfig.devices.size()); } HistoryGraphContent::HistoryGraphContent(Render::Context &ctx, const DeviceKey &device_key): Render::Widget(ctx), earliest(INT_MAX, 1, 0), latest(0), device_key(device_key), rows(0) { for(auto host_iterator: globalConfig.hosts) { Host *host = host_iterator.second; if(!host->selected()) continue; for(auto volume_iterator: host->volumes) { Volume *volume = volume_iterator.second; if(!volume->selected()) continue; for(auto backup: volume->backups) { if(backup->getStatus() == COMPLETE) { earliest = std::min(earliest, Date(backup->time)); latest = std::max(latest, Date(backup->time)); } } ++rows; } } if(rows == 0) throw CommandError("no volumes selected"); } void HistoryGraphContent::set_extent() { assert(row_height > 0); auto columns = latest.toNumber() - earliest.toNumber() + 1; height = (rows ? row_height * rows + globalConfig.verticalPadding * (rows - 1) : 0); width = latest >= earliest ? globalConfig.backupIndicatorWidth * columns : 0; } void HistoryGraphContent::render_vertical_guides() { set_source_color(globalConfig.colorMonthGuide); Date d = earliest; while(d <= latest) { d.d = 1; d.addMonth(); Date next = d; next.addMonth(); double x = (d - earliest) * globalConfig.backupIndicatorWidth; double w = (next - d) * globalConfig.backupIndicatorWidth; w = std::min(w, width - x); context.cairo->rectangle(x, 0, w, height); d.addMonth(); } context.cairo->fill(); } void HistoryGraphContent::render_horizontal_guides() { unsigned row = 0; for(auto host_iterator: globalConfig.hosts) { Host *host = host_iterator.second; if(!host->selected()) continue; set_source_color(globalConfig.colorHostGuide); for(auto volume_iterator: host->volumes) { Volume *volume = volume_iterator.second; if(!volume->selected()) continue; context.cairo->rectangle( 0, row * (row_height + globalConfig.verticalPadding), width, 1); context.cairo->fill(); set_source_color(globalConfig.colorVolumeGuide); ++row; } } set_source_color(globalConfig.colorVolumeGuide); context.cairo->rectangle(0, row * (row_height + globalConfig.verticalPadding) - globalConfig.verticalPadding, width, 1); context.cairo->fill(); } void HistoryGraphContent::render_data() { double y = 0; double base = floor((row_height + globalConfig.verticalPadding - 1 - (indicator_height * globalConfig.devices.size())) / 2) + 1; for(auto host_iterator: globalConfig.hosts) { Host *host = host_iterator.second; if(!host->selected()) continue; for(auto volume_iterator: host->volumes) { Volume *volume = volume_iterator.second; if(!volume->selected()) continue; for(auto backup: volume->backups) { if(backup->getStatus() == COMPLETE) { double x = (Date(backup->time) - earliest) * globalConfig.backupIndicatorWidth; auto device_row = device_key.device_row(backup); double offset = base + device_row * indicator_height; set_source_color(device_key.device_color(backup)); context.cairo->rectangle(x, y + offset, globalConfig.backupIndicatorWidth, indicator_height); context.cairo->fill(); } } y += row_height + globalConfig.verticalPadding; } } } void HistoryGraphContent::render() { render_vertical_guides(); render_horizontal_guides(); render_data(); } TimeLabels::TimeLabels(Render::Context &ctx, HistoryGraphContent &content): Render::Container(ctx), content(content) {} void TimeLabels::set_extent() { if(width < 0) { Date d = content.earliest; int year = -1; double limit = 0; while(d <= content.latest) { Date next = d; next.d = 1; next.addMonth(); double xnext = (next - content.earliest) * globalConfig.backupIndicatorWidth; auto t = new Render::Text(context, "", globalConfig.colorGraphForeground, globalConfig.timeLabelFont); cleanup(t); // Try increasingly compact formats until one fits static const char *const formats[] = { "%B %Y", "%b %Y", "%b %y", "%B", "%b", }; static const unsigned nformats = sizeof formats / sizeof *formats; unsigned format; double x; for(format = (d.y != year ? 0 : 3); format < nformats; ++format) { t->set_text(d.format(formats[format])); t->set_extent(); // At the right hand edge, push back so it fits x = std::min((d - content.earliest) * globalConfig.backupIndicatorWidth, content.width - t->width); // If it fits, use it if(x >= limit && x + t->width < xnext) break; } if(format < nformats) { add(t, x, 0); limit = x + t->width; year = d.y; } d = next; } Container::set_extent(); } } HistoryGraph::HistoryGraph(Render::Context &ctx): Render::Grid(ctx), host_labels(ctx), volume_labels(ctx), device_key(ctx), content(ctx, device_key), time_labels(ctx, content) { set_padding(globalConfig.horizontalPadding, globalConfig.verticalPadding); } void HistoryGraph::set_extent() { host_labels.set_extent(); volume_labels.set_extent(); device_key.set_extent(); // time_labels.set_extent(); double row_height = std::max( {host_labels.get_maximum_height(), volume_labels.get_maximum_height(), (globalConfig.backupIndicatorHeight * globalConfig.devices.size())}); double indicator_height = floor(row_height / globalConfig.devices.size()); device_key.set_indicator_height(indicator_height); content.set_indicator_height(indicator_height); host_labels.set_minimum(0, row_height); volume_labels.set_minimum(0, row_height); content.set_row_height(row_height); content.set_extent(); Grid::set_extent(); } void HistoryGraph::addPart(const std::string &partspec) { static std::regex partspec_regex( "^([^:]+):([0-9]+),([0-9]+)(?::([LCR])([TCB]))?$"); std::smatch mr; if(!std::regex_match(partspec, mr, partspec_regex)) throw SyntaxError("invalid graph component specification '" + partspec + "'"); std::string part = mr[1]; unsigned column = parseInteger(mr[2], 0, std::numeric_limits::max()); unsigned row = parseInteger(mr[3], 0, std::numeric_limits::max()); int hj, vj; switch(mr[4].length() ? *mr[4].first : 'L') { case 'L': hj = -1; break; case 'C': hj = 0; break; case 'R': hj = 1; break; default: throw std::logic_error("HistoryGraph::addPart hj"); } switch(mr[5].length() ? *mr[5].first : 'T') { case 'T': vj = -1; break; case 'C': vj = 0; break; case 'B': vj = 1; break; default: throw std::logic_error("HistoryGraph::addPart vj"); } Widget *w; if(part == "host-labels") w = &host_labels; else if(part == "volume-labels") w = &volume_labels; else if(part == "content") w = &content; else if(part == "time-labels") w = &time_labels; else if(part == "device-key") w = &device_key; else throw SyntaxError("unrecognized graph component '" + part + "'"); add(w, column, row, hj, vj); } void HistoryGraph::addParts(const std::vector &partspecs) { for(auto &partspec: partspecs) addPart(partspec); } void HistoryGraph::render() { set_source_color(globalConfig.colorGraphBackground); context.cairo->rectangle(0, 0, width, height); context.cairo->fill(); Grid::render(); } void HistoryGraph::adjustConfig() { if(content.width == 0) return; // Figure out how big a content panel we can get away with double maxContentWidth = globalConfig.graphTargetWidth - (width - content.width); maxContentWidth = floor(maxContentWidth); auto columns = content.latest.toNumber() - content.earliest.toNumber() + 1; // Work out how big an indicator we can get away with double maxIndicatorWidth = maxContentWidth / columns; // Constrain to integral widths. This is a bug, but currently nonintegral // widths render badly, so a necessary one. if(maxIndicatorWidth >= 1) maxIndicatorWidth = floor(maxIndicatorWidth); if(width < globalConfig.graphTargetWidth) { // We can make the indicators bigger // Pick the biggest globalConfig.backupIndicatorWidth = std::max(globalConfig.backupIndicatorWidth, maxIndicatorWidth); content.changed(); return; } if(globalConfig.graphTargetWidth > 0 && width > globalConfig.graphTargetWidth) { // We have exceeded the target width if(maxContentWidth <= 0) { // User has asked for the impossible warning(WARNING_ALWAYS, "graph-target-width is much too small"); return; // Oh well } globalConfig.backupIndicatorWidth = maxIndicatorWidth; content.changed(); return; } } rsbackup-10.0/src/HistoryGraph.h000066400000000000000000000121041440730431700166300ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef HISTORYGRAPH_H #define HISTORYGRAPH_H /** @file HistoryGraph.h * @brief Visualization of backup history */ #include "Render.h" #include "Conf.h" /** @brief Host name labels */ class HostLabels: public Render::Grid { public: /** @brief Constructor * @param ctx Rendering context */ HostLabels(Render::Context &ctx); }; /** @brief Volume name labels */ class VolumeLabels: public Render::Grid { public: /** @brief Constructor * @param ctx Rendering context */ VolumeLabels(Render::Context &ctx); }; /** @brief Key showing mapping of device names to colors */ class DeviceKey: public Render::Grid { public: /** @brief Constructor * @param ctx Rendering context */ DeviceKey(Render::Context &ctx); /** @brief Return the device row number for a backup * @param backup Backup * @return Device row number */ unsigned device_row(const Backup *backup) const { return device_rows.find(backup->deviceName)->second; } /** @brief Return the color for a device by number * @param row Device row number * @return Color */ const Color device_color(unsigned row) const; /** @brief Return the color for a backup * @param backup Backup * @return Color */ const Color device_color(const Backup *backup) const { return device_color(device_row(backup)); } /** @brief Set the device indicator height * @param h Height */ void set_indicator_height(double h); private: /** @brief Mapping of device names to device rows */ std::map device_rows; /** @brief Child rectangles */ std::list rectangles; /** @brief Height of device indicator rectangle */ double indicator_height = 1; }; /** @brief Visualization of backup history */ class HistoryGraphContent: public Render::Widget { public: /** @brief Constructor * @param ctx Rendering context * @param device_key Corresponding @ref DeviceKey structure */ HistoryGraphContent(Render::Context &ctx, const DeviceKey &device_key); /** @brief Set the rot height * @param h Row height */ void set_row_height(double h) { row_height = h; changed(); } /** @brief Render the vertical guides */ void render_vertical_guides(); /** @brief Render the horizontal guides */ void render_horizontal_guides(); /** @brief Render the data */ void render_data(); void set_extent() override; void render() override; /** @brief Earliest date of any backup */ Date earliest; /** @brief Latest date of any backup */ Date latest; /** @brief Set the device indicator height * @param h New height */ void set_indicator_height(double h) { indicator_height = h; } private: /** @brief Height of a single row * * Set by @ref set_extent. */ double row_height = 0; /** @brief Corresponding @ref DeviceKey object */ const DeviceKey &device_key; /** @brief Number of rows */ unsigned rows; /** @brief Height of device indicator rectangle */ double indicator_height = 1; }; /** @brief Time-axis labels */ class TimeLabels: public Render::Container { public: /** @brief Constructor * @param ctx Rendering context * @param content Corresponding @ref HistoryGraphContent object */ TimeLabels(Render::Context &ctx, HistoryGraphContent &content); void set_extent() override; private: /** @brief Corresponding @ref HistoryGraphContent object */ HistoryGraphContent &content; }; /** @brief Complete graph showing backup history */ class HistoryGraph: public Render::Grid { public: /** @brief Constructor * @param ctx Rendering context */ HistoryGraph(Render::Context &ctx); /** @brief Add a component of the graph * @param partspec Component name and position */ void addPart(const std::string &partspec); /** @brief Add graph components * @param partspecs List of component names and positions */ void addParts(const std::vector &partspecs); /** @brief Adjust configuration to match targets */ void adjustConfig(); /** @brief Host name labels */ HostLabels host_labels; /** @brief Volume name labels */ VolumeLabels volume_labels; /** @brief Key showing mapping of device names to colors */ DeviceKey device_key; /** @brief Visualization of backup history */ HistoryGraphContent content; /** @brief Time-axis labels */ TimeLabels time_labels; void set_extent() override; void render() override; }; #endif /* HISTORYGRAPH_H */ rsbackup-10.0/src/Host.cc000066400000000000000000000111021440730431700152550ustar00rootroot00000000000000// Copyright © 2011, 2014-17 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Subprocess.h" #include #include #include Host::~Host() { for(auto &v: volumes) delete v.second; } void Host::select(bool sense) { for(auto &v: volumes) v.second->select(sense); } bool Host::selected() const { for(auto &v: volumes) if(v.second->selected()) return true; return false; } bool Host::valid(const std::string &name) { return name.size() > 0 && name.at(0) != '-' && name.find_first_not_of(HOST_VALID) == std::string::npos; } void Host::addVolume(Volume *v) { volumes[v->name] = v; } Volume *Host::findVolume(const std::string &volumeName) const { auto it = volumes.find(volumeName); return it != volumes.end() ? it->second : nullptr; } std::string Host::userAndHost() const { return (user.size() ? user + "@" + hostname : hostname); } std::string Host::sshPrefix() const { std::string s = userAndHost(); return s == "localhost" ? "" : s + ":"; } bool Host::available() const { // localhost is always available if(hostname == "localhost") return true; if(hostCheck.at(0) == "always-up") return true; if(hostCheck.at(0) == "ssh") return invoke(nullptr, "true", (const char *)nullptr) == 0; if(hostCheck.at(0) == "command") { std::vector args(hostCheck.begin() + 1, hostCheck.end()); args.push_back(hostname); Subprocess sp(args); return sp.runAndWait(Subprocess::THROW_ON_CRASH | Subprocess::THROW_ON_SIGPIPE) == 0; } // Configuration parser should stop us getting here throw std::logic_error("invalid host-check for " + name); } void Host::write(std::ostream &os, int step, bool verbose) const { describe_type *d = verbose ? describe : nodescribe; os << indent(step) << "host " << quote(name) << '\n'; step += 4; ConfBase::write(os, step, verbose); writeHostCheck(os, step, verbose); d(os, "", step); d(os, "# Concurrency group", step); d(os, "# group NAME", step); if(group != name) os << indent(step) << "group " << quote(group) << '\n'; d(os, "", step); d(os, "# Hostname for SSH", step); d(os, "# hostname NAME", step); os << indent(step) << "hostname " << quote(hostname) << '\n'; d(os, "", step); d(os, "# Username for SSH; default is not to supply a username", step); d(os, "# user NAME", step); if(user.size()) os << indent(step) << "user " << quote(user) << '\n'; d(os, "", step); d(os, "# Glob pattern for devices this host will be backed up to", step); d(os, "# devices PATTERN", step); if(devicePattern.size()) os << indent(step) << "devices " << quote(devicePattern) << '\n'; d(os, "", step); d(os, "# Priority for this host (higher priority = backed up earlier)", step); d(os, "# priority INTEGER", step); os << indent(step) << "priority " << priority << '\n'; for(auto &v: volumes) { os << '\n'; v.second->write(os, step, verbose); } } int Host::invoke(std::string *capture, const char *cmd, ...) const { std::vector args; const char *arg; va_list ap; if(hostname != "localhost") { args.push_back("ssh"); if(sshTimeout > 0) { char buffer[64]; snprintf(buffer, sizeof buffer, "%d", sshTimeout); args.push_back(std::string("-oConnectTimeout=") + buffer); } args.push_back(userAndHost()); } args.push_back(cmd); va_start(ap, cmd); while((arg = va_arg(ap, const char *))) args.push_back(arg); va_end(ap); Subprocess sp(args); if(capture) { sp.capture(1, capture); return sp.runAndWait(Subprocess::THROW_ON_ERROR | Subprocess::THROW_ON_CRASH); } else { sp.nullChildFD(1); sp.nullChildFD(2); return sp.runAndWait(Subprocess::THROW_ON_CRASH); } } ConfBase *Host::getParent() const { return parent; } std::string Host::what() const { return "host"; } rsbackup-10.0/src/Host.h000066400000000000000000000070131440730431700151250ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014-2016 Richard Kettlewell. // // 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 . #ifndef HOST_H #define HOST_H /** @file Host.h * @brief Configuration and state of a host */ #include "ConfBase.h" /** @brief Type of map from volume names to volumes * @see Host::volumes */ typedef std::map volumes_type; /** @brief Represents a host */ class Host: public ConfBase { public: /** @brief Constructor * @param parent_ Parent configuration * @param name_ Name of host */ Host(Conf *parent_, const std::string &name_): ConfBase(static_cast(parent_)), parent(parent_), name(name_), group(name_), hostname(name_) { parent->addHost(this); } /** @brief Destructor */ virtual ~Host(); /** @brief Parent configuration */ Conf *parent; /** @brief Name of host */ std::string name; /** @brief Host group name */ std::string group; /** @brief Volumes for this host */ volumes_type volumes; /** @brief Remote username */ std::string user; /** @brief Remote hostname */ std::string hostname; /** @brief Priority of this host */ int priority = 0; /** @brief Unrecognized volume names found in logs * * Maps volume names to device names. * * Set by Conf::readState(). */ std::set> unknownVolumes; /** @brief Test whether host is selected * @return True if any volume for this host is selected */ bool selected() const; /** @brief (De-)select all volumes * @param sense True to select all volumes, false to deselect */ void select(bool sense); /** @brief Add a volume * @param v Pointer to new volume * * The volume name must not be in use. */ void addVolume(Volume *v); /** @brief Find a volume by name * @param volumeName Name of volume to find * @return Volume or null pointer */ Volume *findVolume(const std::string &volumeName) const; /** @brief SSH user+host string * @return String to pass to SSH client */ std::string userAndHost() const; /** @brief SSH prefix * @return Either "" or "user@host:" */ std::string sshPrefix() const; /** @brief Test if host available * @return true if host is available */ bool available() const; /** @brief Test whether a host name is valid * @param n Host name * @return true if @p n is a valid host name */ static bool valid(const std::string &n); /** @brief Invoke a command on the host and return its exit status * @param capture Where to put capture stdout, or null pointer * @param cmd Command to invoke * @param ... Arguments to command, terminatd by a null pointer * @return Exit status */ int invoke(std::string *capture, const char *cmd, ...) const; ConfBase *getParent() const override; std::string what() const override; void write(std::ostream &os, int step, bool verbose) const override; }; #endif /* HOST_H */ rsbackup-10.0/src/IO.cc000066400000000000000000000070061440730431700146570ustar00rootroot00000000000000// Copyright © 2011-17 Richard Kettlewell. // // 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 . #include #include "IO.h" #include "Errors.h" #include "Subprocess.h" #include #include #include #include #include #include IO::~IO() { if(closeFile && fp) fclose(fp); if(subprocess) delete subprocess; } void IO::open(const std::string &path_, const std::string &mode) { if(!(fp = fopen(path_.c_str(), mode.c_str()))) throw IOError("opening " + path_, errno); path = path_; closeFile = true; } void IO::popen(const std::vector &command, PipeDirection d, bool verbose) { subprocess = new Subprocess(command); int p[2]; if(pipe(p) < 0) throw IOError("creating pipe for " + command[0], errno); switch(d) { case ReadFromPipe: subprocess->addChildFD(1, p[1], p[0]); break; case WriteToPipe: subprocess->addChildFD(0, p[0], p[1]); break; } subprocess->reporting(verbose, false); subprocess->run(); switch(d) { case ReadFromPipe: path = "pipe from " + command[0]; fp = fdopen(p[0], "r"); break; case WriteToPipe: path = "pipe to " + command[0]; fp = fdopen(p[1], "w"); break; } if(!fp) throw IOError("fdopen", errno); closeFile = true; } int IO::close(unsigned waitBehaviour) { FILE *fpSave = fp; fp = nullptr; if(fclose(fpSave) < 0) { if(abortOnError) abort(); throw IOError("closing " + path); } return subprocess ? subprocess->wait(waitBehaviour) : 0; } bool IO::readline(std::string &line) { int c; line.clear(); while((c = getc(fp)) != EOF && c != '\n') line += c; if(ferror(fp)) readError(); return line.size() || !feof(fp); } void IO::readlines(std::vector &lines) { std::string line; lines.clear(); while(readline(line)) lines.push_back(line); } void IO::readall(std::string &file) { int c; file.clear(); while((c = getc(fp)) != EOF) file += c; if(ferror(fp)) readError(); } void IO::write(const std::string &s) { fwrite(s.data(), 1, s.size(), fp); if(ferror(fp)) writeError(); } int IO::writef(const char *format, ...) { va_list ap; int rc; va_start(ap, format); rc = vfprintf(fp, format, ap); va_end(ap); if(rc < 0) writeError(); return rc; } int IO::vwritef(const char *format, va_list ap) { int rc = vfprintf(fp, format, ap); if(rc < 0) writeError(); return rc; } void IO::flush() { if(fflush(fp) < 0) writeError(); } int IO::width() const { int fd = fileno(fp); if(!isatty(fd)) return 0; struct winsize ws; if(ioctl(fd, TIOCGWINSZ, &ws) < 0) return 0; return ws.ws_col; } void IO::readError() { if(abortOnError) abort(); throw IOError("reading " + path, errno); } void IO::writeError() { if(abortOnError) abort(); throw IOError("writing " + path, errno); } IO IO::out(stdout, "stdout"); IO IO::err(stderr, "stderr", true); rsbackup-10.0/src/IO.h000066400000000000000000000151161440730431700145220ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014, 2015, 2017 Richard Kettlewell. // // 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 . #ifndef IO_H #define IO_H /** @file IO.h * @brief I/O support */ #include #include #include #include #include #include #include "Subprocess.h" /** @brief Possible directions of pipe * * Both are given from the parent's point of view. */ enum PipeDirection { /** @brief Parent reads, child writes */ ReadFromPipe, /** @brief Parent writes, child reads */ WriteToPipe, }; class Subprocess; /** @brief RAII-friendly I/O class * * Members will throw IOError if anything goes wrong. */ class IO { public: /** @brief Constructor */ IO() = default; IO(const IO &) = delete; IO &operator=(const IO &) = delete; /** @brief Constructor * @param fp_ Underlying stream * @param path_ Filename for error messages */ IO(FILE *fp_, const std::string &path_): IO(fp_, path_, false) {} /** @brief Destructor * * Closes underlying stream, discarding any errors, if it is still open. */ ~IO(); /** @brief Open a file * @param path File to open * @param mode Open mode as per @c fopen() */ void open(const std::string &path, const std::string &mode); /** @brief Open a pipe to/from a subprocess * @param command Command to execute in subprocess * @param d Pipe direction * @param verbose If true, report command and environment to stderr */ void popen(const std::vector &command, PipeDirection d, bool verbose); /** @brief Close file * @param waitBehaviour How to check exit status * @return Wait status for a subprocess, or 0 * * If the file is a pipe then @ref Subprocess::wait error checking * applies. @p waitBehaviour may contain the following bits: * - @ref Subprocess::THROW_ON_ERROR> - throw if the process terminates * normally with nonzero status. * - @ref Subprocess::THROW_ON_CRASH> - throw if the process terminates due to * a signal other than SIGPIPE. * - @ref Subprocess::THROW_ON_SIGPIPE> - throw if the process terminates due * to SIGPIPE. * * If nothing is thrown then the wait status is returned. * * If the file is not a pipe then the return value is 0. */ int close(unsigned waitBehaviour = Subprocess::THROW_ON_ERROR | Subprocess::THROW_ON_CRASH); /** @brief Read one line * @param line Where to put line * @return true on success, false at eof * * Strips the newline. */ bool readline(std::string &line); /** @brief Read all lines * @param lines Where to put lines * * Strips the newlines. */ void readlines(std::vector &lines); /** @brief Read whole file * @param file Where to put contents */ void readall(std::string &file); /** @brief Write a string * @param s String to write */ void write(const std::string &s); /** @brief Write a formatted string * @param format Format string as per @c printf() * @param ... Arguments * @return Number of bytes written */ int writef(const char *format, ...); /** @brief Write a formatted string * @param format Format string as per @c printf() * @param ap Arguments * @return Number of bytes written */ int vwritef(const char *format, va_list ap); /** @brief Flush any buffered writes */ void flush(); /** @brief Return recommended width * @return Recommend text width, or 0 if unknown */ int width() const; /** @brief Standard output */ static IO out; /** @brief Standard error * * If writes to this stream fail, the process will be aborted. */ static IO err; private: /** @brief Constructor with special error handling * @param fp_ Underlying stream * @param path_ Filename for error messages * @param abortOnError_ If @c true, write errors will abort process * * This is used for @ref err. */ IO(FILE *fp_, const std::string &path_, bool abortOnError_): fp(fp_), path(path_), abortOnError(abortOnError_) {} /** @brief Underlying stdio stream */ FILE *fp = nullptr; /** @brief Path to open file */ std::string path; /** @brief Subprocess handler or null pointer * * Used by @ref popen(). */ Subprocess *subprocess = nullptr; /** @brief If @c true, close @ref fp in destructor */ bool closeFile = false; /** @brief If @c true, abort on error */ bool abortOnError = false; /** @brief Called when a read error occurs */ void readError(); /** @brief Called when a write error occurs */ void writeError(); }; /** @brief RAII-friendly directory reader * * Members will throw IOError if anything goes wrong. */ class Directory { public: /** @brief Destructor * * Closes the directory if it is open. */ ~Directory(); Directory() = default; Directory(const Directory &) = delete; Directory &operator=(const Directory &) = delete; /** @brief Open a directory * @param path Directory to open * @throws IOError if @p path cannot be opened as a directory * * The directory must not already be open. */ void open(const std::string &path); /** @brief Close a directory * * Does nothing if the directory is not open. */ void close(); /** @brief Get one filename * @param name Where to put filename * @return true if a filename returned, false at end. * * Only the basename is supplied. * * The directory must be open. */ bool get(std::string &name) const; /** @brief Get all filenames * @param files Where to put filenames * * The directory must be open. */ void get(std::vector &files) const; /** @brief Get all filenames from a directory * @param path Directory to read * @param files Where to put filenames */ static void getFiles(const std::string &path, std::vector &files); private: /** @brief Underlying directory handle */ DIR *dp = nullptr; /** @brief Path to directory */ std::string path; }; #endif /* IO_H */ rsbackup-10.0/src/Indent.cc000066400000000000000000000026521440730431700155730ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2017 Richard Kettlewell. // // 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 . #include #include "Indent.h" void Indent::clear() { stack.clear(); stack.push_back(0); new_level = 0; } unsigned Indent::check(unsigned acceptable_levels, size_t indent) { unsigned level; // See if the line matches anything in the stack for(size_t i = 0; i < stack.size(); ++i) { if(stack[i] == indent) { // If does; revert to that position if(i + 1 != stack.size()) stack.erase(stack.begin() + i + 1, stack.end()); new_level = 0; level = 1 << i; return level & acceptable_levels; } } if(new_level && indent > stack.back()) { stack.push_back(indent); level = new_level; new_level = 0; return level & acceptable_levels; } // Nothing found return 0; } rsbackup-10.0/src/Indent.h000066400000000000000000000050301440730431700154260ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2017 Richard Kettlewell. // // 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 . #ifndef INDENT_H #define INDENT_H /** @file Indent.h * @brief File indentation tracking */ #include #include /** @brief Indentation tracker * * The @e indent @e depth of a line is the number of spaces at the start. (The * caller will convert tabs in the usual way but that is out of scope for this * class.) * * The @e level of a line is an integer with a single bit set representing its * logical depth. Within a single block at a given level, the indent depth must * be consistent. * * The caller is expected to figure out the acceptable levels of a line based * on its content (and perhaps the current level), and call @ref Indent::check * to identify which actual level to select. * * For a line that introduces a new, more deeply-indented, section, it should * also call @ref Indent::introduce, to indicate the new level. This must be * greater than the level returned by @ref Indent::check. */ class Indent { public: /** @brief Constructor */ Indent() { clear(); } /** @brief Clear state */ void clear(); /** @brief Check whether a line is acceptable * @param acceptable_levels Bitmap of acceptable levels for line * @param indent Actual indent depth * @return Actual level or 0 on error * * This should be called for each nonempty line. */ unsigned check(unsigned acceptable_levels, size_t indent); /** @brief Introduce a new section * @param new_level Single-bit bitmap of new level * * This should be called for lines that introduce a new section. The * expected indentation will be taken from the next line passed to @ref * check(). */ void introduce(unsigned new_level) { this->new_level = new_level; } private: /** @brief Indent level stack */ std::vector stack; /** @brief Level for next line */ unsigned new_level; }; #endif /* INDENT_H */ rsbackup-10.0/src/Latest.cc000066400000000000000000000041651440730431700156070ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Backup.h" #include "Command.h" #include "Conf.h" #include "Store.h" #include "Utils.h" #include "Volume.h" #include "Device.h" #include "IO.h" void findLatest() { // We will accumulate paths and release output only in non-error cases. std::vector paths; // Make sure all state is available globalConfig.readState(); globalConfig.identifyDevices(Store::Enabled); for(auto &s: globalCommand.selections) { const Volume *volume = globalConfig.findVolume(s.host, s.volume); if(!volume) { error("unrecognized volume %s:%s", s.host.c_str(), s.volume.c_str()); continue; } const Backup *backup = nullptr; for(auto &b: volume->backups) { // Only want complete backups if(b->getStatus() != COMPLETE) continue; // Want the latest backup if(backup != nullptr && backup->time > b->time) continue; // Only want available backups const Device *device = b->getDevice(); if(device == nullptr) continue; const Store *store = device->store; if(store == nullptr) continue; backup = b; } if(!backup) { error("no backup found for %s:%s", s.host.c_str(), s.volume.c_str()); continue; } paths.push_back(backup->backupPath()); } if(globalErrors) return; for(const auto &path: paths) IO::out.writef("%s%c", path.c_str(), globalCommand.eol); IO::out.flush(); } rsbackup-10.0/src/MakeBackup.cc000066400000000000000000000546121440730431700163600ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include #include #include #include #include #include #include #include #include #include #include #include "rsbackup.h" #include "Conf.h" #include "Device.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Store.h" #include "Command.h" #include "IO.h" #include "Subprocess.h" #include "Errors.h" #include "Utils.h" #include "Database.h" #include "BulkRemove.h" /** @brief rsync exit status indicating a file vanished during backup */ const int RERR_VANISHED = 24; /** @brief Subprocess subclass for interpreting @c rsync exit status * * Exit status @c RERR_VANISHED from @c rsync indicates (as far as I can tell) * that a file that @c rsync expected to exist (e.g. because it appeared in a * directory listing) did not exist when it attempted to access it. The source * code treats it as a warning rather than an error, presumably on the grounds * that the file could have disappeared a fraction of a second earlier and the * resulting copy would be no different, and we follow the same approach. */ class RsyncSubprocess: public Subprocess { public: /** @brief Constructor * @param name Action name */ RsyncSubprocess(const std::string &name): Subprocess(name) {} bool getActionStatus() const { int rc = getStatus(); if(WIFEXITED(rc) && WEXITSTATUS(rc) == RERR_VANISHED) return true; // just a warning return rc == 0; } }; /** @brief State for a single backup attempt */ class MakeBackup { public: /** @brief Volume to back up */ Volume *volume; /** @brief Target device */ Device *device; /** @brief Host containing @ref volume */ Host *host; /** @brief Start time of backup */ time_t startTime; /** @brief Today's date */ Date today; /** @brief ID of new backup */ std::string id; /** @brief Volume path on device */ const std::string volumePath; /** @brief Backup path on device */ const std::string backupPath; /** @brief .incomplete file on device */ const std::string incompletePath; /** @brief .nolink file on device */ const std::string noLinkPath; /** @brief Current work */ const char *what = "pending"; /** @brief Log output */ std::string log; /** @brief Constructor */ MakeBackup(Volume *volume_, Device *device_); /** @brief Find backups to link against. */ void getOldBackups(std::vector &oldBackups); /** @brief Set up logfile IO for a subprocess * @param sp Subprocess * @param outputToo Log stdout as well as just stderr */ void subprocessIO(Subprocess &sp, bool outputToo = true); /** @brief Run rsync to make the backup * @return Wait status */ int rsyncBackup(const std::string &sourcePath); /** @brief Perform a backup */ void performBackup(const std::string &sourcePath); /** @brief Return the ID for a new backup */ static std::string backupID() { time_t now = Date::now(); // overridden by ${RSBACKUP_TIME} struct tm t; if(!gmtime_r(&now, &t)) throw SystemError("gmtime_r", errno); char buffer[64]; snprintf(buffer, sizeof buffer, "%04d-%02d-%02dT%02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); return buffer; } }; MakeBackup::MakeBackup(Volume *volume_, Device *device_): volume(volume_), device(device_), host(volume->parent), startTime(Date::now()), today(Date::today()), id(backupID()), volumePath(device->store->path + PATH_SEP + host->name + PATH_SEP + volume->name), backupPath(volumePath + PATH_SEP + id), incompletePath(backupPath + ".incomplete"), noLinkPath(volumePath + ".nolink") {} // Find backups to link to. void MakeBackup::getOldBackups(std::vector &oldBackups) { // Start with the most recent backup and work back for(const Backup *backup: boost::adaptors::reverse(volume->backups)) { // Consider only backups on the right device if(device->name == backup->deviceName) { if(backup->getStatus() == COMPLETE) { // Always link against the most recent complete backup oldBackups.push_back(backup); // Once we have a complete backup, stop searching. break; } else { // If the most recent backup is incomplete, link against that. if(oldBackups.size() == 0) oldBackups.push_back(backup); } } } } /** @brief Set up the common environment for a subprocess * @param sp Subprocess */ void setEnvironment(Volume *volume, Subprocess &sp) { Host *host = volume->parent; sp.setenv("RSBACKUP_HOST", host->name); sp.setenv("RSBACKUP_GROUP", host->group); sp.setenv("RSBACKUP_SSH_HOSTNAME", host->hostname); sp.setenv("RSBACKUP_SSH_USERNAME", host->user); sp.setenv("RSBACKUP_SSH_TARGET", host->userAndHost()); sp.setenv("RSBACKUP_VOLUME", volume->name); sp.setenv("RSBACKUP_VOLUME_PATH", volume->path); sp.setenv("RSBACKUP_ACT", globalCommand.act ? "true" : "false"); } void MakeBackup::subprocessIO(Subprocess &sp, bool outputToo) { sp.capture(2, &log, outputToo ? 1 : -1); } /** @brief Run the pre-volume hook if there is one * @param volume Volume to be backed up * @param sourcePath Hook output (new location of source volume) * @param hookLog Hook error output * @return Wait status */ int preBackup(Volume *volume, std::string &sourcePath, std::string &hookLog) { if(volume->preVolume.size()) { EventLoop e; ActionList al(&e); std::string output; Subprocess sp("pre-volume-hook/" + volume->parent->name + "/" + volume->name, volume->preVolume); sp.capture(1, &output); sp.setenv("RSBACKUP_HOOK", "pre-volume-hook"); setEnvironment(volume, sp); sp.setTimeout(volume->hookTimeout); sp.reporting(globalWarningMask & WARNING_VERBOSE, false); sp.capture(2, &hookLog, false); al.add(&sp); al.go(); // Hook output replaces the source path if(output.size()) { if(output[output.size() - 1] == '\n') output.erase(output.size() - 1); sourcePath = output; } return sp.getStatus(); } return 0; } /** @brief Action performed before each backup * * Creates the backup directories and @c .incomplete file. */ class BeforeBackup: public Action { public: /** @brief Constructor * @param mb Parent @ref MakeBackup object */ BeforeBackup(MakeBackup *mb): Action("before-backup/" + mb->volume->parent->name + "/" + mb->volume->name + "/" + mb->device->name), mb(mb) {} void go(EventLoop *, ActionList *al) override { try { mb->what = "creating volume directory"; boost::filesystem::create_directories(mb->volumePath); // Create the .incomplete flag file mb->what = "creating .incomplete file"; IO ifile; ifile.open(mb->incompletePath, "w"); ifile.close(); // Create backup directory mb->what = "creating backup directory"; boost::filesystem::create_directories(mb->backupPath); } catch(std::runtime_error &e) { // TODO refactor mb->log += "ERROR: "; mb->log += e.what(); mb->log += "\n"; al->completed(this, false); return; } al->completed(this, true); } /** @brief Parent @ref MakeBackup object */ MakeBackup *mb; }; int MakeBackup::rsyncBackup(const std::string &sourcePath) { int rc; try { EventLoop e; ActionList al(&e); BeforeBackup before_backup(this); al.add(&before_backup); // Synthesize command what = "constructing command"; std::vector cmd; cmd.push_back(host->rsyncCommand); cmd.insert(cmd.end(), volume->rsyncBaseOptions.begin(), volume->rsyncBaseOptions.end()); cmd.insert(cmd.end(), volume->rsyncExtraOptions.begin(), volume->rsyncExtraOptions.end()); if(!volume->traverse) cmd.push_back("--one-file-system"); // don't cross mount points if(volume->rsyncRemote.size()) { cmd.push_back("--rsync-path"); cmd.push_back(volume->rsyncRemote); } // Exclusions for(auto &exclusion: volume->exclude) cmd.push_back("--exclude=" + exclusion); // Use old backups if(volume->rsyncLinkDest) { // As a hack to deal with untrusted existing backups (e.g. following a // fsck), if the .nolink exists then we suppress all link targets. // The file is deleted when a backup succeeds. bool suppressLinkDest = false; struct stat sb; if(stat(noLinkPath.c_str(), &sb) == 0) suppressLinkDest = true; std::vector oldBackups; getOldBackups(oldBackups); if(oldBackups.size() > 0 && suppressLinkDest) { warning(WARNING_ALWAYS, "suppressing %zu --link-dest candidates due because %s exists", oldBackups.size(), noLinkPath.c_str()); } else { for(auto oldBackup: oldBackups) { cmd.push_back("--link-dest=" + oldBackup->backupPath()); } } } // Timeout if(volume->rsyncIOTimeout) { char buffer[128]; snprintf(buffer, sizeof buffer, "--timeout=%d", volume->rsyncIOTimeout); cmd.push_back(buffer); } // Source cmd.push_back(host->sshPrefix() + sourcePath + "/."); // Destination cmd.push_back(backupPath + "/."); // Set up subprocess RsyncSubprocess sp("backup/" + volume->parent->name + "/" + volume->name + "/" + device->name); sp.setCommand(cmd); setEnvironment(volume, sp); sp.reporting(globalWarningMask & WARNING_VERBOSE, !globalCommand.act); sp.after(before_backup.get_name(), ACTION_SUCCEEDED); if(!globalCommand.act) return 0; subprocessIO(sp, true); sp.setTimeout(volume->backupJobTimeout); // Make the backup, with the global lock released al.add(&sp); { release_guard globalRelease(globalLock); al.go(); } rc = sp.getStatus(); what = "rsync"; // Suppress exit status 24 "Partial transfer due to vanished source files" if(WIFEXITED(rc) && WEXITSTATUS(rc) == RERR_VANISHED) { warning(WARNING_PARTIAL, "partial transfer backing up %s:%s to %s", host->name.c_str(), volume->name.c_str(), device->name.c_str()); rc = 0; } // Clean up when finished if(rc == 0) { if(remove(incompletePath.c_str()) < 0) { throw SystemError("removing " + incompletePath, errno); } if(remove(noLinkPath.c_str()) < 0 && errno != ENOENT) { throw SystemError("removing " + noLinkPath, errno); } } } catch(std::runtime_error &e) { // Try to handle any other errors the same way as rsync failures. If we // can't even write to the logfile we error out. log += "ERROR: "; log += e.what(); log += "\n"; // This is a bit misleading (it's not really a wait status) but it will // do for now. rc = 255; } return rc; } /** @brief Run the post-volume hook if there is one */ void postBackup(Volume *volume, std::string &hookLog) { if(volume->postVolume.size()) { EventLoop e; ActionList al(&e); Subprocess sp("post-volume-hook/" + volume->parent->name + "/" + volume->name, volume->postVolume); sp.setenv("RSBACKUP_HOOK", "post-volume-hook"); setEnvironment(volume, sp); sp.setTimeout(volume->hookTimeout); sp.reporting(globalWarningMask & WARNING_VERBOSE, false); sp.capture(2, &hookLog, 1); al.add(&sp); al.go(); } } void MakeBackup::performBackup(const std::string &sourcePath) { // Put together the backup record Backup *outcome = new Backup(); outcome->time = startTime; outcome->id = id; outcome->deviceName = device->name; outcome->volume = volume; outcome->setStatus(UNDERWAY); if(globalCommand.act) { // Record in the database that the backup is underway // If this fails then the backup just fails. globalConfig.getdb().begin(); outcome->insert(globalConfig.getdb(), true /*replace*/); globalConfig.getdb().commit(); } // Make the backup int rc = rsyncBackup(sourcePath); if(!globalCommand.act) { // In dry-run mode, we're done for now delete outcome; return; } // Update the backup record outcome->waitStatus = rc; outcome->contents = log; if(outcome->contents.size() && outcome->contents[outcome->contents.size() - 1] != '\n') outcome->contents += '\n'; // if(outcome->waitStatus) { // Backup failed ++globalErrors; if(globalWarningMask & (WARNING_VERBOSE | WARNING_ERRORLOGS)) { warning(WARNING_VERBOSE | WARNING_ERRORLOGS, "backup of %s:%s to %s: %s", host->name.c_str(), volume->name.c_str(), device->name.c_str(), SubprocessFailed::format(what, outcome->waitStatus).c_str()); IO::err.write(outcome->contents); IO::err.writef("\n"); } outcome->setStatus(FAILED); } else outcome->setStatus(COMPLETE); // Attach the backup to the volume outcome->volume->addBackup(outcome); // Store the result in the database // We really care about 'busy' errors - the backup has been made, we must // record this fact. for(;;) { int retries = 0; try { globalConfig.getdb().begin(); outcome->update(globalConfig.getdb()); globalConfig.getdb().commit(); break; } catch(DatabaseBusy &) { // Log a message every second or so if(!(retries++ & 1023)) warning(WARNING_DATABASE, "backup of %s:%s to %s: retrying database update", host->name.c_str(), volume->name.c_str(), device->name.c_str()); // Wait a millisecond and try again usleep(1000); continue; } } } // Backup VOLUME onto DEVICE. // // device->store is assumed to be set. // // The group lock is assumed to be held on entry, and stays held. // The global lock is assumed to be held on entry. From time to // time it will be transiently released while waiting for resource // availability or (further down the call tree) during command execution. // The device lock is assumed to be held on entry, and stays held. static void backupVolumeToDevice(Volume *volume, const std::string &sourcePath, Device *device) { Host *host = volume->parent; if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: backup %s:%s to %s\n", host->name.c_str(), volume->name.c_str(), device->name.c_str()); MakeBackup mb(volume, device); mb.performBackup(sourcePath); } // Backup VOLUME onto DEVICE, if possible. // // The group lock is assumed to be held on entry, and stays held. // The global lock is assumed to be held on entry. From time to // time it will be transiently released while waiting for resource // availability or (further down the call tree) during command execution. // The device lock is assumed to be held on entry, and stays held. static void maybeBackupVolumeToDevice(Volume *volume, const std::string &sourcePath, Device *device) { Host *host = volume->parent; char buffer[1024]; BackupRequirement br = volume->needsBackup(device); if(br == AlreadyBackedUp && globalCommand.force) { IO::out.writef("INFO: %s:%s is already backed up on %s, but backing up " "anyway because --force\n", host->name.c_str(), volume->name.c_str(), device->name.c_str()); br = BackupRequired; } switch(br) { case BackupRequired: globalConfig.identifyDevices(Store::Enabled); if(device->store && device->store->state == Store::Enabled) backupVolumeToDevice(volume, sourcePath, device); else if(globalWarningMask & WARNING_STORE) { globalConfig.identifyDevices(Store::Disabled); if(device->store) switch(device->store->state) { case Store::Disabled: warning( WARNING_STORE, "cannot backup %s:%s to %s - device suppressed due to --store", host->name.c_str(), volume->name.c_str(), device->name.c_str()); break; default: snprintf(buffer, sizeof buffer, "device %s store %s unexpected state %d", device->name.c_str(), device->store->path.c_str(), device->store->state); throw FatalStoreError(buffer); } else warning(WARNING_STORE, "cannot backup %s:%s to %s - device not available", host->name.c_str(), volume->name.c_str(), device->name.c_str()); } break; case AlreadyBackedUp: if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: %s:%s is already backed up on %s\n", host->name.c_str(), volume->name.c_str(), device->name.c_str()); break; case NotAvailable: if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: %s:%s is not available\n", host->name.c_str(), volume->name.c_str()); break; case NotThisDevice: if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: %s:%s excluded from %s by device pattern\n", host->name.c_str(), volume->name.c_str(), device->name.c_str()); break; } } // Backup VOLUME on all devices. // // The group lock is assumed to be held on entry, and stays held. // The global lock is assumed to be held on entry. From time to // time it will be transiently released while waiting for resource // availability or (further down the call tree) during command execution. static void backupVolumeToAllDevices(Volume *volume) { Host *host = volume->parent; // Build a list of devices std::set devices; for(auto &d: globalConfig.devices) { devices.insert(d.second); } bool ran_pre_volume_hook = false; bool attempted_backups = false; std::string sourcePath = volume->path; while(devices.size() > 0) { bool worked = false; // Look for a device we can lock for(auto device: devices) { if(device->lock.try_lock()) { std::lock_guard guard(device->lock, std::adopt_lock); if(!ran_pre_volume_hook) { // Run the pre-volume-hook. Note that this happens with the device // locked. This isn't ideal but nor is the alternative, of running // the hook long before any device is available. std::string hookLog; int hookrc = preBackup(volume, sourcePath, hookLog); if(!(WIFEXITED(hookrc) && WEXITSTATUS(hookrc) == 0)) { // The hook failed. if(WIFEXITED(hookrc) && WEXITSTATUS(hookrc) == EX_TEMPFAIL) { // It's a harmless 'temporary' failure if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: %s:%s is temporarily unavailable due to " "pre-volume-hook\n", host->name.c_str(), volume->name.c_str()); } else { // It's a hard failure. ++globalErrors; if(hookLog.size() > 0 && hookLog.back() != '\n') hookLog += "\n"; IO::err.writef("ERROR: %s:%s pre-volume-hook failed:\n%s", host->name.c_str(), volume->name.c_str(), hookLog.c_str()); } return; // stop now } ran_pre_volume_hook = true; } maybeBackupVolumeToDevice(volume, sourcePath, device); attempted_backups = true; devices.erase(device); worked = true; break; } } // If we didn't find a suitable volume wait a bit and try again if(!worked) { release_guard globalRelease(globalLock); usleep(100 * 000 /*µs*/); } } if(attempted_backups) { // We only run the post-volume-hook if we made some backups. std::string hookLog; postBackup(volume, hookLog); if(hookLog.size() && (globalWarningMask & WARNING_VERBOSE)) { IO::out.writef("ERROR: %s:%s post-volume-hook output:\n%s\n", host->name.c_str(), volume->name.c_str(), hookLog.c_str()); } } } // Backup HOST static void backupHost(Host *host, std::mutex *lock) { // Do a quick check for unavailable hosts bool available = host->available(); // Serialize host groups std::lock_guard groupGuard(*lock); std::lock_guard globalGuard(globalLock); if(!available) { warning(WARNING_UNREACHABLE, "cannot backup %s - not reachable", host->name.c_str()); return; } for(auto &v: host->volumes) { Volume *volume = v.second; if(volume->selected()) backupVolumeToAllDevices(volume); } } static bool order_host(const Host *a, const Host *b) { if(a->priority > b->priority) return true; if(a->priority < b->priority) return false; return a->name < b->name; } // Backup everything void makeBackups() { // Load up log files globalConfig.readState(); std::vector hosts; for(auto &h: globalConfig.hosts) { Host *host = h.second; if(host->selected()) hosts.push_back(host); } std::sort(hosts.begin(), hosts.end(), order_host); // Create concurrency group locks std::map locks; for(Host *h: hosts) { if(locks.find(h->group) == locks.end()) locks[h->group] = new std::mutex(); } // Initiate backups in threads std::map threads; for(Host *h: hosts) { threads[h->name] = new std::thread(backupHost, h, locks[h->group]); } { // Release the global lock while we wait for the threads release_guard globalRelease(globalLock); // Wait for the threads for(auto it: threads) { it.second->join(); } } // Clean up the locks and threads for(auto it: threads) { delete it.second; } for(auto it: locks) { delete it.second; } } rsbackup-10.0/src/Makefile.am000066400000000000000000000131441440730431700160750ustar00rootroot00000000000000# Copyright © Richard Kettlewell. # # 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 . noinst_LIBRARIES=librsbackup.a bin_PROGRAMS=rsbackup rsbackup-graph noinst_PROGRAMS=test-date test-io test-directory test-subprocess \ test-unicode test-timespec test-command test-select \ test-confbase test-check test-device test-host test-volume \ test-progress test-database test-tolines test-globfiles \ test-lock test-split test-parseinteger test-prunedecay \ test-eventloop test-color test-base64 test-indent test-action \ test-parsetimeinterval test-namelt test-parsefloat dist_noinst_SCRIPTS=check-source TAG:=$(shell git describe --tags --dirty) AM_CXXFLAGS=$(SQLITE3_CFLAGS) $(CAIROMM_CFLAGS) $(PANGOMM_CFLAGS) $(FINAL_CXXFLAGS) -DTAG=\"${TAG}\" librsbackup_a_SOURCES=Backup.cc BulkRemove.cc Check.cc Command.cc \ Conf.cc \ Date.cc DeviceAccess.cc Device.cc Directory.cc Document.cc Email.cc \ error.cc Errors.cc FileLock.cc Host.cc HTML.cc IO.cc MakeBackup.cc \ Progress.cc Prune.h Prune.cc PrunePolicy.h PrunePolicy.cc Report.cc \ RetireDevices.cc RetireVolumes.cc Store.cc stylesheet.cc \ Subprocess.cc Text.cc Unicode.cc Volume.cc Command.h Conf.h Date.h \ Defaults.h DeviceAccess.h Document.h Email.h Errors.h FileLock.h IO.h \ rsbackup.h Store.h Subprocess.h Utils.h ConfBase.cc \ toLines.cc globFiles.cc Database.h Database.cc Report.h \ parseInteger.cc split.cc EventLoop.cc EventLoop.h nonblock.cc \ Action.cc Action.h BulkRemove.h Selection.h Selection.cc Color.h \ Color.cc parseFloat.cc Render.h Render.cc HistoryGraph.h \ HistoryGraph.cc ColorStrategy.cc ConfDirective.h ConfDirective.cc \ base64.cc substitute.cc timestamp.cc debug.cc ConfBase.h Volume.h \ Host.h Backup.h Device.h Indent.h Indent.cc CheckBackups.cc \ BackupPolicy.h BackupPolicy.cc parseTimeInterval.cc namelt.cc \ CompressTable.h Latest.cc POLICIES=PrunePolicyAge.cc PrunePolicyNever.cc PrunePolicyExec.cc \ PrunePolicyDecay.cc \ BackupPolicyDaily.cc BackupPolicyAlways.cc BackupPolicyInterval.cc rsbackup_SOURCES=rsbackup.cc ${POLICIES} rsbackup_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) rsbackup_graph_SOURCES=rsbackup-graph.cc ${POLICIES} rsbackup_graph_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) $(CAIROMM_LIBS) $(PANGOMM_LIBS) test_date_SOURCES=test-date.cc test_date_LDADD=librsbackup.a test_color_SOURCES=test-color.cc test_color_LDADD=librsbackup.a test_subprocess_SOURCES=test-subprocess.cc Subprocess.cc Errors.cc IO.cc \ EventLoop.cc nonblock.cc Action.cc timestamp.cc debug.cc test_unicode_SOURCES=test-unicode.cc test_unicode_LDADD=librsbackup.a test_io_SOURCES=test-io.cc test_io_LDADD=librsbackup.a test_directory_SOURCES=test-directory.cc test_directory_LDADD=librsbackup.a test_timespec_SOURCES=test-timespec.cc test_timespec_LDADD=librsbackup.a test_command_SOURCES=test-command.cc test_command_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) test_select_SOURCES=test-select.cc test_select_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) test_confbase_SOURCES=test-confbase.cc test_confbase_LDADD=librsbackup.a test_check_SOURCES=test-check.cc test_check_LDADD=librsbackup.a $(LIBPTHREAD) $(SQLITE3_LIBS) $(BOOST_LIBS) test_device_SOURCES=test-device.cc test_device_LDADD=librsbackup.a test_host_SOURCES=test-host.cc test_host_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) test_volume_SOURCES=test-volume.cc test_volume_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) test_progress_SOURCES=test-progress.cc test_progress_LDADD=librsbackup.a test_database_SOURCES=test-database.cc test_database_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) test_tolines_SOURCES=test-tolines.cc test_tolines_LDADD=librsbackup.a test_globfiles_SOURCES=test-globfiles.cc test_globfiles_LDADD=librsbackup.a test_lock_SOURCES=test-lock.cc test_lock_LDADD=librsbackup.a test_split_SOURCES=test-split.cc test_split_LDADD=librsbackup.a test_parseinteger_SOURCES=test-parseinteger.cc test_parseinteger_LDADD=librsbackup.a test_parsefloat_SOURCES=test-parsefloat.cc test_parsefloat_LDADD=librsbackup.a test_parsetimeinterval_SOURCES=test-parsetimeinterval.cc test_parsetimeinterval_LDADD=librsbackup.a test_namelt_SOURCES=test-namelt.cc namelt.cc test_prunedecay_SOURCES=test-prunedecay.cc PrunePolicyDecay.cc test_prunedecay_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) test_eventloop_SOURCES=test-eventloop.cc EventLoop.cc test_eventloop_LDADD=librsbackup.a test_base64_SOURCES=test-base64.cc test_base64_LDADD=librsbackup.a test_indent_SOURCES=test-indent.cc test_indent_LDADD=librsbackup.a test_action_SOURCES=test-action.cc test_action_LDADD=librsbackup.a $(SQLITE3_LIBS) $(BOOST_LIBS) TESTS=test-date test-io test-directory test-subprocess test-unicode \ test-timespec test-command test-select test-confbase \ test-check test-device test-host test-volume test-progress test-database \ test-tolines test-globfiles test-lock test-split test-parseinteger \ test-prunedecay test-eventloop test-color test-base64 test-indent \ test-action test-parsetimeinterval test-namelt check-source stylesheet.cc: ${top_srcdir}/doc/rsbackup.css ${top_srcdir}/scripts/txt2src stylesheet < $^ > $@ rsbackup-10.0/src/Progress.cc000066400000000000000000000022751440730431700161570ustar00rootroot00000000000000// Copyright © 2011 Richard Kettlewell. // // 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 . #include #include "IO.h" #include void progressBar(IO &stream, const char *prompt, size_t done, size_t total) { const int width = 79; if(total == 0) { stream.writef("\r%*s\r", width, " "); } else { std::string s; int bar = width - (3 + strlen(prompt)); s.append("\r"); s.append(prompt); s.append(" ["); s.append(done * bar / total, '='); s.append(bar - (done * bar / total), ' '); s.append("]\r"); stream.writef("%s", s.c_str()); } stream.flush(); } rsbackup-10.0/src/Prune.cc000066400000000000000000000307101440730431700154370ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Device.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Command.h" #include "Errors.h" #include "IO.h" #include "Utils.h" #include "Store.h" #include "Database.h" #include "Prune.h" #include "PrunePolicy.h" #include "BulkRemove.h" #include #include #include #include #include #include #include #include class RemovableBackup; /** @brief Action class to clean up after a backup has been removed */ class RemovedBackup: public Action { public: /** @brief Constructor */ RemovedBackup(const std::string &name, RemovableBackup *rb): Action(name), parent(rb) {} /** @brief Initiate cleanup */ void go(EventLoop *e, ActionList *al) override; private: /** @brief Owning @ref RemovableBackup * * This forces @ref RemovableBackup to be noncopyable. */ RemovableBackup *parent = nullptr; }; /** @brief A removable backup */ class RemovableBackup: boost::noncopyable { public: /** @brief Constructor * @param b Backup to remove */ RemovableBackup(Backup *b): backup(b), bulkRemover("remove/" + b->volume->parent->name + "/" + b->volume->name + "/" + b->deviceName + "/" + b->id), removedBackup("removed/" + b->volume->parent->name + "/" + b->volume->name + "/" + b->deviceName + "/" + b->id, this) { // Cleanup happens after removal (unconditionally) removedBackup.after(bulkRemover.get_name(), 0); } /** @brief Initialize the child objects */ void initialize(ActionList &al) { bulkRemover.initialize(backup->backupPath()); bulkRemover.uses(backup->deviceName); removedBackup.uses(backup->deviceName); al.add(&bulkRemover); al.add(&removedBackup); } /** @brief Called when a removal has completed (not necessarily successfully) * @param timedOut Indicates the whole pruning job timed out */ void completed(bool timedOut) { // Only run once if(alreadyComplete) return; alreadyComplete = true; // Log what happened and (on success) remove .incomplete const std::string backupPath = backup->backupPath(); int status = bulkRemover.getStatus(); switch(status) { default: // Log failed prunes // If we timed out then a SIGTERM is expected, so hide that behind // WARNING_VERBOSE. Any other failure is an error. if(timedOut && WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) warning(WARNING_VERBOSE, "failed to remove %s: %s", backupPath.c_str(), SubprocessFailed::format(globalConfig.rm, status).c_str()); else error("failed to remove %s: %s", backupPath.c_str(), SubprocessFailed::format(globalConfig.rm, status).c_str()); break; case -1: // Never ran // This happens if we timed out or errored before getting to // this backup. warning(WARNING_VERBOSE, "failed to remove %s: cancelled", backupPath.c_str()); break; case 0: // Succeeded const std::string incompletePath = backupPath + ".incomplete"; // Remove the 'incomplete' marker. if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: removing %s\n", incompletePath.c_str()); if(unlink(incompletePath.c_str()) < 0 && errno != ENOENT) { error("removing %s: %s", incompletePath.c_str(), strerror(errno)); ++globalErrors; } } if(status == 0) { backup->setStatus(PRUNED); // When the state is PRUNING, the pruned date indicates // when it was decided to prune the backup; when the state // is PRUNED, it indicates when pruning completed. backup->pruned = Date::now(); backup->update(globalConfig.getdb()); // Update the database for(;;) { int retries = 0; try { globalConfig.getdb().begin(); globalConfig.getdb().commit(); } catch(DatabaseBusy &) { // Keep trying, database should be in sync with reality // Log a message every second or so if(!(retries++ & 1023)) warning(WARNING_DATABASE, "pruning: retrying database update"); // Wait a millisecond and try again usleep(1000); continue; } break; // success } // Update internal state if(status == 0) backup->volume->removeBackup(backup); } } /** @brief The backup to remove */ Backup *backup = nullptr; /** @brief The bulk remove instance for this backup */ BulkRemove bulkRemover; /** @brief The cleanup instance for this backup */ RemovedBackup removedBackup; /** @brief Completion status */ bool alreadyComplete = false; }; void RemovedBackup::go(EventLoop *, ActionList *al) { // Hand everything off to the parent RemovableBackup parent->completed(false); // Report back that we finished immediately al->completed(this, true); } static void findObsoleteBackups(std::vector &obsoleteBackups); static void markObsoleteBackups(std::vector obsoleteBackups); static void findRemovableBackups(std::vector obsoleteBackups, std::vector &removableBackups); void backupPrunable(std::vector &onDevice, std::map &prune, int total) { if(onDevice.size() == 0) return; const Volume *volume = onDevice.at(0)->volume; const PrunePolicy *policy = PrunePolicy::find(volume->prunePolicy); policy->prunable(onDevice, prune, total); } PrunePolicy::policies_type *PrunePolicy::policies; // Remove old and incomplete backups void pruneBackups() { // Make sure all state is available globalConfig.readState(); // An _obsolete_ backup is a backup which exists on any device which is now // due for removal. This includes devices which aren't currently available. std::vector obsoleteBackups; findObsoleteBackups(obsoleteBackups); // Return straight away if there's nothing to do if(obsoleteBackups.size() == 0) return; // We set all obsolete backups to PRUNING state even if they're on currently // unavailable devices. Note that this means that pruning policies are // implemented even for these devices. if(globalCommand.act) markObsoleteBackups(obsoleteBackups); // Identify devices globalConfig.identifyDevices(Store::Enabled); // A _removable_ backup is an obsolete backup which is on an available device // and can therefore actually be removed. std::vector removableBackups; findRemovableBackups(obsoleteBackups, removableBackups); if(!globalCommand.act) assert(removableBackups.size() == 0); EventLoop e; ActionList al(&e); // Initialize the bulk remove operations for(auto &removable: removableBackups) { removable->initialize(al); } // Give up if it takes too long if(globalConfig.pruneTimeout > 0) { struct timespec limit; getMonotonicTime(limit); limit.tv_sec += globalConfig.pruneTimeout; al.setLimit(limit); } // Perform the deletions al.go(); if(globalCommand.act) { // If we timed out then complete any uncompleted removal attempts if(al.timeLimitExceeded()) { for(auto &removable: removableBackups) { removable->completed(true); } } } deleteAll(removableBackups); } // Get a list of all the backups to prune. This means backups for // which pruning has already started, and backups selected by the // pruning policy for their volume. It includes backups that are // on unavailable devices. static void findObsoleteBackups(std::vector &obsoleteBackups) { for(auto &h: globalConfig.hosts) { const Host *host = h.second; if(!host->selected()) continue; for(auto &v: host->volumes) { Volume *volume = v.second; if(!volume->selected()) continue; // For each device, the complete backups on that device std::map> onDevices; // Total backups of this volume int total = 0; for(Backup *backup: volume->backups) { switch(backup->getStatus()) { case UNKNOWN: case UNDERWAY: case FAILED: if(globalCommand.pruneIncomplete) { // Prune incomplete backups. Anything that failed is counted as // incomplete (a succesful retry will overwrite the log entry). backup->contents = std::string("status=") + backup_status_names[backup->getStatus()]; obsoleteBackups.push_back(backup); } break; case PRUNING: // Both commands continue pruning anything that has started being // pruned. log should already be set. obsoleteBackups.push_back(backup); break; case PRUNED: break; case COMPLETE: if(globalCommand.prune) { onDevices[backup->deviceName].push_back(backup); ++total; } break; } } // Find complete backups that are now prunable for(auto &od: onDevices) { std::vector &onDevice = od.second; std::map prune; backupPrunable(onDevice, prune, total); for(auto &p: prune) { Backup *backup = p.first; backup->contents = p.second; obsoleteBackups.push_back(backup); --total; } } } } } // Update the database to mark all obsolete backups for pruning. static void markObsoleteBackups(std::vector obsoleteBackups) { globalConfig.getdb().begin(); for(Backup *b: obsoleteBackups) { if(b->getStatus() != PRUNING) { b->setStatus(PRUNING); b->pruned = Date::now(); b->update(globalConfig.getdb()); } } globalConfig.getdb().commit(); // We don't catch DatabaseBusy here; the prune just fails. } // Winnow down the obsolete backups to those we can actually remove, // i.e. those on an available device. We also create .incomplete // files here, to signal to the operator that these backups are // not suitable for restoration. static void findRemovableBackups(std::vector obsoleteBackups, std::vector &removableBackups) { for(auto backup: obsoleteBackups) { Device *device = globalConfig.findDevice(backup->deviceName); Store *store = device->store; // Can't delete backups from unavailable stores if(!store || store->state != Store::Enabled) continue; std::string backupPath = backup->backupPath(); std::string incompletePath = backupPath + ".incomplete"; try { // Schedule removal of the backup if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: pruning %s because: %s\n", backupPath.c_str(), backup->contents.c_str()); if(globalCommand.act) { // Create the .incomplete flag file so that the operator knows this // backup is now partial IO ifile; ifile.open(incompletePath, "w"); ifile.close(); // Queue up a bulk remove operation removableBackups.push_back(new RemovableBackup(backup)); } } catch(std::runtime_error &exception) { // Log anything that goes wrong error("failed to remove %s: %s\n", backupPath.c_str(), exception.what()); ++globalErrors; } } } // Remove old prune logfiles void prunePruneLogs() { if(globalCommand.act) // Delete status=PRUNED records that are too old Database::Statement(globalConfig.getdb(), "DELETE FROM backup" " WHERE status=?" " AND pruned < ?", SQL_INT, PRUNED, SQL_INT64, (int64_t)(Date::now() - globalConfig.keepPruneLogs), SQL_END) .next(); } rsbackup-10.0/src/Prune.h000066400000000000000000000030161440730431700153000ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef PRUNE_H #define PRUNE_H /** @file Prune.h * @brief Definitions used by the pruning logic */ #include #include #include class Backup; /** @brief Identify prunable backups * @param onDevice Number of backups of same volume on same device * @param prune Map of backups to prune to reason strings * @param total Number of backups anywhere */ void backupPrunable(std::vector &onDevice, std::map &prune, int total); /** @brief Identify the bucket for a backup * @param w Decay window * @param s Decay scale * @param a Age of backup * @return Bucket number from 0 * * See decay.pdf for * more information. */ int prune_decay_bucket(double w, double s, int a); #endif /* PRUNE_H */ rsbackup-10.0/src/PrunePolicy.cc000066400000000000000000000040101440730431700166110ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014-2016, 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Volume.h" #include "Errors.h" #include "PrunePolicy.h" #include PrunePolicy::PrunePolicy(const std::string &name) { if(!policies) policies = new policies_type(); (*policies)[name] = this; } const std::string &PrunePolicy::get(const Volume *volume, const std::string &name) const { auto it = volume->pruneParameters.find(name); if(it != volume->pruneParameters.end()) return it->second; else throw ConfigError("missing pruning parameter '" + name + "'"); } const std::string &PrunePolicy::get(const Volume *volume, const std::string &name, const std::string &def) const { auto it = volume->pruneParameters.find(name); if(it != volume->pruneParameters.end()) return it->second; else return def; } const PrunePolicy *PrunePolicy::find(const std::string &name) { assert(policies != nullptr); // policies not statically initialized auto it = policies->find(name); if(it == policies->end()) throw ConfigError("unrecognized pruning policy '" + name + "'"); return it->second; } void validatePrunePolicy(const Volume *volume) { const PrunePolicy *policy = PrunePolicy::find(volume->prunePolicy); policy->validate(volume); } rsbackup-10.0/src/PrunePolicy.h000066400000000000000000000053061440730431700164640ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef PRUNEPOLICY_H #define PRUNEPOLICY_H /** @file PrunePolicy.h * @brief Definitions used by the pruning policies */ #include #include #include class Backup; class Volume; /** @brief Base class for pruning policies */ class PrunePolicy { public: /** @brief Constructor * * Policies are automatically registered upon construction. */ PrunePolicy(const std::string &name); /** @brief Validate a pruning policy * @param volume Volume to validate */ virtual void validate(const Volume *volume) const = 0; /** @brief Get a parameter value * @param volume Volume to validate * @param name Name of parameter */ const std::string &get(const Volume *volume, const std::string &name) const; /** @brief Get a parameter value * @param volume Volume to validate * @param name Name of parameter * @param def Default value */ const std::string &get(const Volume *volume, const std::string &name, const std::string &def) const; /** @brief Identify prunable backups * @param onDevice Surviving backups of same volume on same device * @param total Number of backups anywhere * @param prune Map of backups to prune to reason strings * * @p total does not include backups on other devices that have "only just" * been selected for pruning. */ virtual void prunable(std::vector &onDevice, std::map &prune, int total) const = 0; /** @brief Find a prune policy by name * @param name Name of policy * @return Prune policy */ static const PrunePolicy *find(const std::string &name); private: /** @brief Type for @ref policies */ typedef std::map policies_type; /** @brief Map of policy names to implementations */ static policies_type *policies; }; /** @brief Validate a pruning policy * @param volume Volume to validate */ void validatePrunePolicy(const Volume *volume); #endif // PRUNEPOLICY_H rsbackup-10.0/src/PrunePolicyAge.cc000066400000000000000000000043341440730431700172370ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "PrunePolicy.h" #include "Utils.h" #include "Errors.h" #include /** @brief The @c age pruning policy */ class PruneAge: public PrunePolicy { public: PruneAge(): PrunePolicy("age") {} void validate(const Volume *volume) const override { if(parseTimeInterval(get(volume, "prune-age", DEFAULT_PRUNE_AGE)) < 86400) throw SyntaxError("prune-age is too small"); parseInteger(get(volume, "min-backups", DEFAULT_MIN_BACKUPS), 1, std::numeric_limits::max()); } void prunable(std::vector &onDevice, std::map &prune, int) const override { const Volume *volume = onDevice.at(0)->volume; int pruneAge = parseTimeInterval(get(volume, "prune-age", DEFAULT_PRUNE_AGE)) / 86400; int minBackups = parseInteger(get(volume, "min-backups", DEFAULT_MIN_BACKUPS), 1, std::numeric_limits::max()); size_t left = onDevice.size(); for(Backup *backup: onDevice) { int age = Date::today() - Date(backup->time); // Keep backups that are young enough if(age <= pruneAge) continue; // Keep backups that are on underpopulated devices if(left <= static_cast(minBackups)) continue; std::ostringstream ss; ss << "age " << age << " > " << pruneAge << " and remaining " << onDevice.size() << " > " << minBackups; prune[backup] = ss.str(); --left; } } } prune_age; rsbackup-10.0/src/PrunePolicyDecay.cc000066400000000000000000000101341440730431700175630ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "PrunePolicy.h" #include "Utils.h" #include "Errors.h" #include // See also https://www.greenend.org.uk/rjk/rsbackup/decay.pdf int prune_decay_bucket(double w, double s, int a) { return ceil(logbase((s - 1) * a / w + 1, s)) - 1; } /** @brief The @c decay pruning policy */ class PruneDecay: public PrunePolicy { public: PruneDecay(): PrunePolicy("decay") {} void validate(const Volume *volume) const override { if(parseTimeInterval(get(volume, "decay-start", DEFAULT_DECAY_START)) < 1) throw SyntaxError("decay-start too small"); if(parseTimeInterval(get(volume, "decay-window", DEFAULT_DECAY_WINDOW)) < 1) throw SyntaxError("decay-window too small"); parseFloat(get(volume, "decay-scale", DEFAULT_DECAY_SCALE), 1, std::numeric_limits::max(), ExclusiveLimit); if(parseTimeInterval(get(volume, "decay-limit", DEFAULT_PRUNE_AGE)) < 1) throw SyntaxError("decay-limit too small"); } void prunable(std::vector &onDevice, std::map &prune, int) const override { const Volume *volume = onDevice.at(0)->volume; int decayStart = parseTimeInterval(get(volume, "decay-start", DEFAULT_DECAY_START)) / 86400; int decayWindow = parseTimeInterval(get(volume, "decay-window", DEFAULT_DECAY_WINDOW)) / 86400; double decayScale = parseFloat(get(volume, "decay-scale", DEFAULT_DECAY_SCALE), 1, std::numeric_limits::max(), ExclusiveLimit); int decayLimit = parseTimeInterval(get(volume, "decay-limit", DEFAULT_PRUNE_AGE)) / 86400; if(onDevice.size() == 1) return; // Map of bucket numbers to oldest backup in the bucket. These will be // preserved. std::map oldest; for(Backup *backup: onDevice) { int age = Date::today() - Date(backup->time); // Keep backups that are young enough int a = age - decayStart; if(a <= 0) continue; // Prune backups that are much too old if(age > decayLimit) { std::ostringstream ss; ss << "age " << age << " > " << decayLimit << " and other backups exist"; prune[backup] = ss.str(); continue; } // Assign backups to buckets int bucket = prune_decay_bucket(decayWindow, decayScale, a); // Track the oldest backup in this bucket auto bucket_iterator = oldest.find(bucket); if(bucket_iterator == oldest.end() || backup->time < bucket_iterator->second->time) oldest[bucket] = backup; } // Now that we know what the oldest backup in each bucket is, we can prune // the rest. for(Backup *backup: onDevice) { int age = Date::today() - Date(backup->time); // Keep backups that are young enough int a = age - decayStart; if(a <= 0 || age > decayLimit) continue; int bucket = prune_decay_bucket(decayWindow, decayScale, a); auto bucket_iterator = oldest.find(bucket); assert(bucket_iterator != oldest.end()); const Backup *oldest_in_this_bucket = bucket_iterator->second; if(backup != oldest_in_this_bucket) { std::ostringstream ss; ss << "age " << age << " > " << decayStart << " and oldest in bucket " << bucket; prune[backup] = ss.str(); } } } } prune_decay; rsbackup-10.0/src/PrunePolicyExec.cc000066400000000000000000000065311440730431700174300ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "PrunePolicy.h" #include "Subprocess.h" #include "Utils.h" #include "Errors.h" #include #include #include /** @brief Pruning policy that executes a program */ class PruneExec: public PrunePolicy { public: PruneExec(): PrunePolicy("exec") {} void validate(const Volume *volume) const override { const std::string &path = get(volume, "path"); if(access(path.c_str(), X_OK) < 0) throw ConfigError("cannot execute pruning policy " + volume->prunePolicy); for(auto &p: volume->pruneParameters) for(auto ch: p.first) if(ch != '_' && !isalnum(ch)) throw ConfigError("invalid pruning parameter '" + p.first + "' for executable policies"); } void prunable(std::vector &onDevice, std::map &prune, int total) const override { char buffer[64]; const Volume *volume = onDevice.at(0)->volume; std::vector command = {get(volume, "path")}; Subprocess sp(command); for(auto &p: volume->pruneParameters) sp.setenv("PRUNE_" + p.first, p.second); std::stringstream ss; for(size_t i = 0; i < onDevice.size(); ++i) { if(i) ss << ' '; ss << onDevice[i]->time; } sp.setenv("PRUNE_ONDEVICE", ss.str()); snprintf(buffer, sizeof buffer, "%d", total); sp.setenv("PRUNE_TOTAL", buffer); sp.setenv("PRUNE_HOST", volume->parent->name); sp.setenv("PRUNE_VOLUME", volume->name); sp.setenv("PRUNE_DEVICE", onDevice.at(0)->deviceName); std::string reasons; sp.capture(1, &reasons); sp.runAndWait(); size_t pos = 0; while(pos < reasons.size()) { size_t newline = reasons.find('\n', pos); if(newline == std::string::npos) throw InvalidPruneList("missing newline"); size_t colon = reasons.find(':', pos); if(colon > newline) throw InvalidPruneList("no colon found"); std::string timestr(reasons, pos, colon - pos); std::string reason(reasons, colon + 1, newline - (colon + 1)); time_t pruneTime = parseInteger(timestr, 0, std::numeric_limits::max()); bool found = false; for(Backup *backup: onDevice) { if(backup->time == pruneTime) { if(contains(prune, backup)) throw InvalidPruneList("duplicate entry in prune list"); prune[backup] = reason; found = true; } } if(!found) throw InvalidPruneList("nonexistent entry in prune list"); pos = newline + 1; } } } prune_exec; rsbackup-10.0/src/PrunePolicyNever.cc000066400000000000000000000020551440730431700176200ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "PrunePolicy.h" /** @brief The @c never pruning policy */ class PruneNever: public PrunePolicy { public: PruneNever(): PrunePolicy("never") {} void validate(const Volume *) const override {} void prunable(std::vector &, std::map &, int) const override {} } prune_never; rsbackup-10.0/src/Render.cc000066400000000000000000000121461440730431700155700ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Render.h" #include "Utils.h" #include // Widget Render::Widget::~Widget() { deleteAll(cleanup_list); } void Render::Widget::changed() { if(width >= 0) { width = height = -1; if(parent) parent->changed(); } } void Render::Widget::cleanup(Widget *w) { cleanup_list.push_back(w); } // Container void Render::Container::add(Widget *widget, double x, double y) { children.push_back(Child(widget, x, y)); widget->parent = this; changed(); } void Render::Container::set_extent() { width = 0; height = 0; for(const auto &child: children) { if(child.widget->width < 0) child.widget->set_extent(); width = std::max(child.x + child.widget->width, width); height = std::max(child.y + child.widget->height, height); } width = ceil(width); height = ceil(height); } void Render::Container::render() { Render::Guard g(this); for(const auto &child: children) { context.cairo->translate(child.x, child.y); child.widget->render(); context.cairo->translate(-child.x, -child.y); } } // Grid void Render::Grid::add(Widget *widget, unsigned column, unsigned row, int hj, int vj) { children.push_back(GridChild(widget, column, row, hj, vj)); widget->parent = this; changed(); } void Render::Grid::set_padding(double xp, double yp) { xpadding = xp; ypadding = yp; changed(); } void Render::Grid::set_minimum(double w, double h) { force_width = w; force_height = h; changed(); } void Render::Grid::set_extent() { column_widths.clear(); row_heights.clear(); for(const auto &child: children) { if(child.widget->width < 0) child.widget->set_extent(); if(child.column >= column_widths.size()) column_widths.resize(child.column + 1); column_widths[child.column] = std::max(child.widget->width, column_widths[child.column]); if(child.row >= row_heights.size()) row_heights.resize(child.row + 1); row_heights[child.row] = std::max(child.widget->height, row_heights[child.row]); } for(auto &column_width: column_widths) column_width = std::max(column_width, force_width); width = xpadding * (column_widths.size() - 1); for(auto column_width: column_widths) width += column_width; for(auto &row_height: row_heights) row_height = std::max(row_height, force_height); height = ypadding * (row_heights.size() - 1); for(auto row_height: row_heights) height += row_height; } void Render::Grid::render() { Render::Guard g(this); for(const auto &child: children) { double child_x = 0; for(unsigned column = 0; column < child.column; ++column) child_x += column_widths[column] + xpadding; double child_y = 0; for(unsigned row = 0; row < child.row; ++row) child_y += row_heights[row] + ypadding; child_x = justify(child_x, column_widths[child.column], child.widget->width, child.hj); child_y = justify(child_y, row_heights[child.row], child.widget->height, child.vj); context.cairo->translate(child_x, child_y); child.widget->render(); context.cairo->translate(-child_x, -child_y); } } double Render::Grid::justify(double x, double cell_width, double child_width, int justification) { return x + floor((cell_width - child_width) * (justification + 1) / 2.0); } // Colored void Render::Colored::render() { set_source_color(color); } // Text Render::Text::Text(Context &context, const std::string &t, const Color &c, const std::string &f): Colored(context, c), text(t), font(f) {} void Render::Text::set_text(const std::string &t) { text = t; changed(); } void Render::Text::set_font(const std::string &f) { font = Pango::FontDescription(f); changed(); } void Render::Text::set_extent() { if(!layout) layout = Pango::Layout::create(context.cairo); layout->set_text(text); layout->set_font_description(font); Pango::Rectangle ink, logical; layout->get_pixel_extents(ink, logical); width = ceil(logical.get_width()); height = ceil(logical.get_height()); } void Render::Text::render() { Colored::render(); context.cairo->move_to(0, 0); layout->show_in_cairo_context(context.cairo); } // Rectangle void Render::Rectangle::set_extent() {} void Render::Rectangle::render() { Colored::render(); context.cairo->rectangle(0, 0, width, height); context.cairo->fill(); } void Render::Rectangle::changed() {} rsbackup-10.0/src/Render.h000066400000000000000000000207561440730431700154400ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef RENDER_H #define RENDER_H /** @file Render.h * @brief Graphical rendering */ #include #include #include "Color.h" namespace Render { /** @brief Rendering context */ struct Context { /** @brief Cairo context */ Cairo::RefPtr cairo; }; /** @brief Base class for widgets */ class Widget { public: /** @brief Constructor * @param ctx Rendering context */ Widget(Context &ctx): context(ctx) {} /** @brief Destructor */ virtual ~Widget(); /** @brief Width * * Set by @ref Widget::set_extent */ double width = -1; /** @brief Height * * Set by @ref Widget::set_extent. */ double height = -1; /** @brief Parent widget */ Widget *parent = nullptr; /** @brief Set @ref width and @ref height * * If there are child widgets, the first step should be to recursively * calculate their extents. This may be skipped for children with * non-negative @ref width fields. */ virtual void set_extent() = 0; /** @brief Render this widget */ virtual void render() = 0; /** @brief Clean up some other widget when this one is destroyed */ void cleanup(Widget *w); /** @brief Called when the widget's extent potentially changes * * Invalidates @ref Widget::width and @ref Widget::height, and then * recurses into its parent (if there is one). */ virtual void changed(); protected: /** @brief Context for drawing */ Context &context; /** @brief Set the drawing color * @param color Color to draw in */ inline void set_source_color(const Color &color) { context.cairo->set_source_rgb(color.red, color.green, color.blue); } /** @brief List of widgets to clean up on destruction */ std::vector cleanup_list; friend class Guard; }; /** @brief Container that places widgets at arbitrary locations */ class Container: public Widget { public: /** @brief Constructor * @param ctx Rendering context */ Container(Context &ctx): Widget(ctx) {} /** @brief Add a child widget * @param widget Pointer to child widget * @param x X coordinate of top left of widget * @param y Y coordinate of top left of widget */ void add(Widget *widget, double x, double y); void set_extent() override; void render() override; private: /** @brief Annotated child of a grid */ struct Child { /** @brief Child widget */ Widget *widget; /** @brief X coordinate */ double x; /** @brief Y coordinate */ double y; /** @brief Constructor */ Child(Widget *w, double x, double y): widget(w), x(x), y(y) {} }; /** @brief Child widgets in order of addition */ std::vector children; }; /** @brief Container that arranges widgets in a grid */ class Grid: public Widget { public: /** @brief Constructor * @param ctx Rendering context */ Grid(Context &ctx): Widget(ctx) {} /** @brief Add a child widget * @param widget Pointer to child widget * @param column Column number from 0 * @param row Row number from 0 * @param hj Horizontal justification * @param vj Vertical justification * * Justification values are: * - -1 for left justified * - 0 for centered * - 1 for right justified */ void add(Widget *widget, unsigned column, unsigned row, int hj = -1, int vj = -1); /** @brief Set inter-child padding * @param xp Horizontal padding * @param yp Vertical padding */ void set_padding(double xp, double yp); /** @brief Set minimum child sizes * @param w Minimum width * @param h Minimum height */ void set_minimum(double w, double h); /** @brief Get the maximum width of any column * @return Maximum column width * * @ref Grid::set_extent must have been called. */ double get_maximum_width() const { return *std::max_element(column_widths.begin(), column_widths.end()); } /** @brief Get the maximum height of any row * @return Maximum row height * * @ref Grid::set_extent must have been called. */ double get_maximum_height() const { return *std::max_element(row_heights.begin(), row_heights.end()); } void set_extent() override; void render() override; private: /** @brief Annotated child of a grid */ struct GridChild { /** @brief Child widget */ Widget *widget; /** @brief Column number from 0 */ unsigned column; /** @brief row Row number from 0 */ unsigned row; /** @brief Horizontal justification */ int hj; /** @brief Vertical justification */ int vj; /** @brief Constructor */ GridChild(Widget *w, int c, int r, int h, int v): widget(w), column(c), row(r), hj(h), vj(v) {} }; /** @brief Horizontal padding */ double xpadding = 0; /** @brief Vertical padding */ double ypadding = 0; /** @brief Minimum column width */ double force_width = 0; /** @brief Minimum row height */ double force_height = 0; /** @brief Child widgets in order of addition */ std::vector children; /** @brief Column widths (only after Grid::set_extent) */ std::vector column_widths; /** @brief Column heights (only after Grid::set_extent) */ std::vector row_heights; /** @brief Justify coordinate * @param x Coordinate of container * @param cell_width Size of container * @param child_width Width of contained element * @param justification Justification mode * @return Justified coordinate */ static double justify(double x, double cell_width, double child_width, int justification); }; /** @brief Colored widget */ class Colored: public Widget { public: /** @brief Constructor * @param ctx Rendering context * @param c Color */ Colored(Context &ctx, const Color &c = {0, 0, 0}): Widget(ctx), color(c) {} /** @brief Set color * @param c Color */ void set_color(const Color &c) { color = c; } /** @brief Set the color in the rendering context */ void render() override; private: /** @brief Color */ Color color; }; /** @brief Text widget */ class Text: public Colored { public: /** @brief Constructor * @param ctx Rendering context * @param t Text to display * @param c Color for text * @param f Pango font description */ Text(Context &ctx, const std::string &t = "", const Color &c = {0, 0, 0}, const std::string &f = ""); /** @brief Set text * @param t Text to display */ void set_text(const std::string &t); /** @brief Set text * @param f Pango font description */ void set_font(const std::string &f); void set_extent() override; void render() override; private: /** @brief Text to render */ std::string text; /** @brief Font */ Pango::FontDescription font; /** @brief Pango layout for text */ Glib::RefPtr layout; }; /** @brief Filled rectangular widget */ class Rectangle: public Colored { public: /** @brief Constructor * @param ctx Rendering context * @param w Width * @param h Height * @param c Color */ Rectangle(Context &ctx, double w, double h, const Color &c = {0, 0, 0}): Colored(ctx, c) { width = w; height = h; } /** @brief Set the rectangle size * @param w New width or non-positive to not set * @param h New height or non-positive to not set */ void set_size(double w, double h) { if(w > 0) width = w; if(h > 0) height = h; } void set_extent() override; void render() override; protected: void changed() override; }; /** @brief Guard class for rendering contexts */ class Guard { public: /** @brief Constructor * @param w Any widget with the right rendering context */ Guard(Widget *w): context(w->context) { context.cairo->save(); } /** @brief Destructor */ ~Guard() { context.cairo->restore(); } private: /** @brief Rendering context */ Context &context; }; }; // namespace Render #endif /* RENDER_H */ rsbackup-10.0/src/Report.cc000066400000000000000000000372721440730431700156330ustar00rootroot00000000000000// Copyright © 2011-2015, 2018, 2019 Richard Kettlewell. // // 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 . #include #include #include #include #include #include #include "rsbackup.h" #include "Document.h" #include "Conf.h" #include "Device.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Command.h" #include "IO.h" #include "Database.h" #include "Report.h" #include "Utils.h" #include "Subprocess.h" #include "Errors.h" #include "CompressTable.h" // Split up a color into RGB components void Report::unpackColor(unsigned color, int rgb[3]) { rgb[0] = (color >> 16) & 255; rgb[1] = (color >> 8) & 255; rgb[2] = color & 255; } // Pack a color from RGB components unsigned Report::packColor(const int rgb[3]) { return rgb[0] * 65536 + rgb[1] * 256 + rgb[2]; } // Pick a color as a (clamped) linear combination of two endpoints unsigned Report::pickColor(unsigned zero, unsigned one, double param) { int zeroRgb[3], oneRgb[3], resultRgb[3]; unpackColor(zero, zeroRgb); unpackColor(one, oneRgb); if(param < 0) param = 0; if(param > 1) param = 1; resultRgb[0] = zeroRgb[0] * (1 - param) + oneRgb[0] * param; resultRgb[1] = zeroRgb[1] * (1 - param) + oneRgb[1] * param; resultRgb[2] = zeroRgb[2] * (1 - param) + oneRgb[2] * param; return packColor(resultRgb); } void Report::compute() { backups_missing = 0; backups_partial = 0; backups_out_of_date = 0; backups_failed = 0; devices_unknown = globalConfig.unknownDevices.size(); hosts_unknown = globalConfig.unknownHosts.size(); volumes_unknown = 0; for(auto &h: globalConfig.hosts) { const Host *host = h.second; volumes_unknown += host->unknownVolumes.size(); for(auto &v: host->volumes) { const Volume *volume = v.second; bool out_of_date = true; size_t devices_used = 0; for(auto &d: globalConfig.devices) { const Device *device = d.second; auto perDevice = volume->findDevice(device->name); if(perDevice && perDevice->count) { // At least one successful backup exists... int newestAge = Date::today() - perDevice->newest; if(newestAge <= volume->maxAge / 86400) out_of_date = false; // ...and it's recent enough ++devices_used; } // Look for the most recent attempt at this device const Backup *most_recent_backup = nullptr; for(const Backup *b: boost::adaptors::reverse(volume->backups)) { if(b->getStatus() == COMPLETE && b->deviceName == device->name) { most_recent_backup = b; break; } } if(most_recent_backup && most_recent_backup->getStatus() != COMPLETE) ++backups_failed; // most recent backup failed } if(devices_used < globalConfig.devices.size()) { // some device lacks a backup if(devices_used == 0) ++backups_missing; // no device has a backup else ++backups_partial; // only some devices have a backup } else if(out_of_date) ++backups_out_of_date; // no device has a recent enough backup } } } // Generate the list of warnings void Report::warnings() { char buffer[1024]; Document::List *l = new Document::List(); for(auto &d: globalConfig.unknownDevices) l->entry("Unknown device " + d); for(auto &h: globalConfig.unknownHosts) l->entry("Unknown host " + h.first + " on device " + h.second); for(auto &h: globalConfig.hosts) { const std::string &hostName = h.first; Host *host = h.second; for(auto &v: host->unknownVolumes) l->entry("Unknown volume " + hostName + ":" + v.first + " on device " + v.second); } if(backups_missing) { snprintf(buffer, sizeof buffer, "WARNING: %d volumes have no backups.", backups_missing); l->entry(buffer); } if(backups_partial) { snprintf(buffer, sizeof buffer, "WARNING: %d volumes are not fully backed up.", backups_partial); l->entry(buffer); } if(backups_out_of_date) { snprintf(buffer, sizeof buffer, "WARNING: %d volumes are out of date.", backups_out_of_date); l->entry(buffer); } if(backups_failed) { snprintf(buffer, sizeof buffer, "WARNING: %d volumes failed latest backup.", backups_failed); l->entry(buffer); } d.append(l); } int Report::warningCount() const { int warnings = globalConfig.unknownDevices.size() + globalConfig.unknownHosts.size(); for(auto &h: globalConfig.hosts) warnings += h.second->unknownVolumes.size(); if(backups_missing) ++warnings; if(backups_partial) ++warnings; if(backups_out_of_date) ++warnings; if(backups_failed) ++warnings; return warnings; } // Generate the summary table void Report::summary() { Document::Table *t = new Document::Table(); t->addCell(new Document::Cell("Host", 1, 3, true)); t->addCell(new Document::Cell("Volume", 1, 3, true)); t->addCell(new Document::Cell("Oldest", 1, 3, true)); t->addCell(new Document::Cell("Total", 1, 3, true)); t->addCell( new Document::Cell("Devices", 3 * globalConfig.devices.size(), 1, true)); t->newRow(); for(auto &d: globalConfig.devices) t->addCell(new Document::Cell(d.second->name, 3, 1, true)); t->newRow(); for(auto attribute((unused)) & d: globalConfig.devices) { t->addCell(new Document::Cell("Newest", 1, 1, true)); t->addCell(new Document::Cell("Count", 1, 1, true)); t->addCell(new Document::Cell("Size", 1, 1, true)); } t->newRow(); for(auto &h: globalConfig.hosts) { const Host *host = h.second; t->addCell(new Document::Cell(host->name, 1, host->volumes.size()))->style = "host"; for(auto &v: host->volumes) { const Volume *volume = v.second; // See if every device has a backup bool missingDevice = false; for(const auto &d: globalConfig.devices) { const Device *device = d.second; if(!contains(volume->perDevice, device->name)) missingDevice = true; } t->addCell(new Document::Cell(volume->name))->style = "volume"; t->addCell(new Document::Cell( volume->oldest != 0 ? Date(volume->oldest).toString() : "none")); t->addCell(new Document::Cell(new Document::String(volume->completed))) ->style = missingDevice ? "bad" : "good"; // Add columns for each device for(const auto &d: globalConfig.devices) { const Device *device = d.second; auto perDevice = volume->findDevice(device->name); int perDeviceCount = perDevice ? perDevice->count : 0; if(perDeviceCount) { // At least one successful backups Document::Cell *c = t->addCell( new Document::Cell(Date(perDevice->newest).toString())); int newestAge = Date::today() - perDevice->newest; if(newestAge <= volume->maxAge / 86400) { double param = (pow(2, (double)newestAge / (volume->maxAge / 86400)) - 1) / 2.0; c->bgcolor = pickColor(globalConfig.colorGood, globalConfig.colorBad, param); } else { c->style = "bad"; } } else { // No succesful backups! t->addCell(new Document::Cell("none"))->style = "bad"; } t->addCell(new Document::Cell(new Document::String(perDeviceCount))) ->style = perDeviceCount ? "good" : "bad"; // Log the size std::stringstream size; if(perDevice && perDevice->size >= 0) { if(perDevice->size < 1024) size << perDevice->size; else if(perDevice->size < (1LL << 20)) size << (perDevice->size >> 10) << "K"; else if(perDevice->size < (1LL << 30)) size << (perDevice->size >> 20) << "M"; else if(perDevice->size < (1LL << 40)) size << (perDevice->size >> 30) << "G"; else size << (perDevice->size >> 40) << "T"; } t->addCell(new Document::Cell(new Document::String(size.str()))); } t->newRow(); } } d.append(t); } // Return true if this is a suitable log for the report bool Report::suitableLog(const Volume *volume, const Backup *backup) { // Empty logs are never shown. if(!backup->contents.size()) return false; switch(globalCommand.logVerbosity) { case Command::All: // Show everything return true; case Command::Errors: // Show all error logs return backup->getStatus() != COMPLETE; case Command::Recent: // Show the most recent error log for the device return backup == volume->mostRecentFailedBackup(backup->getDevice()); case Command::Latest: // Show the most recent logfile for the device return backup == volume->mostRecentBackup(backup->getDevice()); case Command::Failed: // Show the most recent logfile for the device, if it is failed/underway switch(backup->getStatus()) { case UNKNOWN: case UNDERWAY: case FAILED: return backup == volume->mostRecentBackup(backup->getDevice()); default: return false; } default: throw std::logic_error("unknown log verbosity"); } } // Generate the report of backup logfiles for a volume void Report::logs(const Volume *volume) { Document::LinearContainer *lc = nullptr; const Host *host = volume->parent; // Backups for a volume are ordered primarily by date and secondarily by // device. The most recent backups are the most interesting so they are // displayed in reverse. std::set devicesSeen; for(const Backup *backup: boost::adaptors::reverse(volume->backups)) { // Only include logs of failed backups if(suitableLog(volume, backup)) { if(!lc) { d.heading("Host " + host->name + " volume " + volume->name + " (" + volume->path + ")", 3); lc = new Document::LinearContainer(); lc->style = "volume"; d.append(lc); } Document::Heading *heading = new Document::Heading( Date(backup->time).toString() + " device " + backup->deviceName + " volume " + backup->volume->parent->name + ":" + backup->volume->name, 4); if(!contains(devicesSeen, backup->deviceName)) heading->style = "recent"; lc->append(heading); Document::Verbatim *v = new Document::Verbatim(); v->style = "log"; v->append(backup->contents); lc->append(v); } devicesSeen.insert(backup->deviceName); } } // Generate the report of backup logfiles for everything void Report::logs() { // Sort by host/volume first, then date, device *last* for(auto &h: globalConfig.hosts) { const Host *host = h.second; for(auto &v: host->volumes) { const Volume *volume = v.second; logs(volume); } } } // Generate the report of pruning logfiles void Report::pruneLogs(const std::string &interval) { int ndays = DEFAULT_PRUNE_REPORT_AGE; if(interval.size()) ndays = parseTimeInterval(interval) / 86400; Document::Table *t = new Document::Table(); t->addCell(new Document::Cell("Created", 1, 1, true)); t->addCell(new Document::Cell("Pruned", 1, 1, true)); t->addCell(new Document::Cell("Host", 1, 1, true)); t->addCell(new Document::Cell("Volume", 1, 1, true)); t->addCell(new Document::Cell("Device", 1, 1, true)); t->addCell(new Document::Cell("Reason", 1, 1, true)); t->newRow(); const int64_t cutoff = Date::now() - 86400 * ndays; Database::Statement stmt(globalConfig.getdb(), "SELECT host,volume,device,time,pruned,log" " FROM backup" " WHERE (status=? OR status=?) AND pruned >= ?" " ORDER BY pruned DESC", SQL_INT, PRUNING, SQL_INT, PRUNED, SQL_INT64, cutoff, SQL_END); Table st; while(stmt.next()) { Backup backup; char timestr[64]; std::string hostName = stmt.get_string(0); std::string volumeName = stmt.get_string(1); std::string deviceName = stmt.get_string(2); time_t when = stmt.get_int64(3); time_t pruned = stmt.get_int64(4); std::string reason = stmt.get_blob(5); std::vector row; strftime(timestr, sizeof timestr, "%Y-%m-%d", localtime(&when)); row.push_back(timestr); strftime(timestr, sizeof timestr, "%Y-%m-%d", localtime(&pruned)); row.push_back(timestr); row.push_back(hostName); row.push_back(volumeName); row.push_back(deviceName); row.push_back(reason); st.push_back(row); } st.compress(); for(auto &row: st.rows) { for(auto &cell: row.cells) { std::string values; for(auto &value: cell) { if(values.size() > 0) values += ","; values += value; } t->addCell(new Document::Cell(values)); } t->newRow(); } d.append(t); } void Report::historyGraph() { std::string history_png; std::string rg = Subprocess::pathSearch("rsbackup-graph"); if(rg.size() == 0) return; std::vector cmd = { "rsbackup-graph", "-c", globalConfigPath, "-D", globalDatabase, "-o-", }; if(globalDebug) cmd.push_back("-d"); Subprocess sp(cmd); sp.capture(1, &history_png); sp.runAndWait(); if(history_png.size()) { Document::Image *image = new Document::Image("image/png", history_png); image->style = "history"; d.append(image); } } void Report::section(const std::string &n) { std::string name = n, value, condition; size_t colon = name.find("?"); if(colon != std::string::npos) { condition.assign(name, colon + 1, std::string::npos); name.erase(colon); if(condition == "warnings") { if(!warningCount()) return; } else throw SyntaxError("unrecognized report condition '" + condition + "'"); // Update Conf.cc and rsbackup.5 if any new conditionss } colon = name.find(":"); if(colon != std::string::npos) { value = substitute(name, colon + 1, std::string::npos); name.erase(colon); } if(name == "warnings") warnings(); else if(name == "summary") summary(); else if(name == "logs") logs(); else if(name == "prune-logs") pruneLogs(value); else if(name == "history-graph") historyGraph(); else if(name == "h1") d.heading(value, 1); else if(name == "h2") d.heading(value, 2); else if(name == "h3") d.heading(value, 3); else if(name == "p") d.para(value); else if(name == "title") d.title = value; else throw SyntaxError("unrecognized report name '" + name + "'"); // Update Conf.cc and rsbackup.5 if any new names added } // Generate the full report void Report::generate() { if(::setenv("RSBACKUP_DATE", Date::today().toString().c_str(), 1 /*overwrite*/)) throw SystemError("setenv", errno); const char *override_time = getenv("RSBACKUP_TIME"); if(override_time && *override_time) { if(::setenv("RSBACKUP_CTIME", "", 1 /*overwrite*/)) throw SystemError("setenv", errno); } else { time_t now; time(&now); if(::setenv("RSBACKUP_CTIME", ctime(&now), 1 /*overwrite*/)) throw SystemError("setenv", errno); } compute(); for(const auto &s: globalConfig.report) section(s); } rsbackup-10.0/src/Report.h000066400000000000000000000057611440730431700154730ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef REPORT_H #define REPORT_H /** @file Report.h * @brief %Report generation */ #include "Document.h" class Volume; class Backup; /** @brief Generator for current state */ class Report { /** @brief Destination for report */ Document &d; public: /** @brief Constructor * @param d_ Destination for report */ Report(Document &d_): d(d_) {} /** @brief Generate the report and set counters */ void generate(); /** @brief Number of volumes with no backups at all */ int backups_missing = 0; /** @brief Number of volumes missing a backup on at least one device */ int backups_partial = 0; /** @brief Number of volumes with no backup within max-age */ int backups_out_of_date = 0; /** @brief Number of volume/device pairs where most recent backup failed */ int backups_failed = 0; /** @brief Number of unknown devices */ int devices_unknown = 0; /** @brief Number of unknown hosts */ int hosts_unknown = 0; /** @brief Number of unknown volumes */ int volumes_unknown = 0; private: /** @brief Split up a color into RGB components */ static void unpackColor(unsigned color, int rgb[3]); /** @brief Pack a color from RGB components */ static unsigned packColor(const int rgb[3]); /** @brief Pick a color as a (clamped) linear combination of two endpoints */ static unsigned pickColor(unsigned zero, unsigned one, double param); /** @brief Compute counters */ void compute(); /** @brief Generate the list of warnings */ void warnings(); /** @brief Calculate how many warnings there are */ int warningCount() const; /** @brief Generate the summary table and set counters */ void summary(); /** @brief Return @c true if this is a suitable log for the report */ bool suitableLog(const Volume *volume, const Backup *backup); /** @brief Generate the report of backup logs for a volume */ void logs(const Volume *volume); /** @brief Generate the report of backup logs for everything */ void logs(); /** @brief Generate the report of pruning logfiles */ void pruneLogs(const std::string &interval); /** @brief Generate backup history graphic */ void historyGraph(); /** @brief Generate a named report section */ void section(const std::string &name); /** @brief Current backup history */ std::string history_png; }; #endif /* REPORT_H */ rsbackup-10.0/src/RetireDevices.cc000066400000000000000000000031421440730431700171020ustar00rootroot00000000000000// Copyright © 2011, 2014, 2015, 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Command.h" #include "Utils.h" #include "IO.h" #include "Database.h" #include #include static void retireDevice(const std::string &deviceName) { // If the device is still configured, check whether it should really be // retired Device *device = globalConfig.findDevice(deviceName); if(device) { if(!check("Really retire device '%s'?", deviceName.c_str())) return; } // Remove all the log records for this device. if(globalCommand.act) { globalConfig.getdb().begin(); Database::Statement(globalConfig.getdb(), "DELETE FROM backup" " WHERE device=?", SQL_STRING, &deviceName, SQL_END) .next(); globalConfig.getdb().commit(); } } void retireDevices() { for(std::string &d: globalCommand.devices) retireDevice(d); } rsbackup-10.0/src/RetireVolumes.cc000066400000000000000000000166541440730431700171660ustar00rootroot00000000000000// Copyright © 2011, 2013-2017, 2019 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Conf.h" #include "Device.h" #include "Store.h" #include "Command.h" #include "Utils.h" #include "Errors.h" #include "IO.h" #include "Database.h" #include "BulkRemove.h" #include "Backup.h" #include #include static void removeDirectory(const std::string &path) { if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: removing %s\n", path.c_str()); if(globalCommand.act && rmdir(path.c_str()) < 0 && errno != ENOENT) { error("removing %s: %s", path.c_str(), strerror(errno)); } } /** @brief One retirable backup */ struct Retirable { /** @brief Host name */ std::string hostName; /** @brief Volume name */ std::string volumeName; /** @brief Pointer to containing device */ Device *device; /** @brief Backup ID */ std::string id; /** @brief Bulk removal */ BulkRemove *b = nullptr; /** @brief Constructor * @param h Host name * @param v Volume name * @param d Pointer to containing device * @param i Backup ID */ inline Retirable(const std::string &h, const std::string &v, Device *d, std::string i): hostName(h), volumeName(v), device(d), id(i) {} /** @brief Destructor */ ~Retirable() { delete b; } /** @brief Schedule retire of this backup */ void scheduleRetire(ActionList &al) { assert(!b); const std::string backupPath = (device->store->path + PATH_SEP + hostName + PATH_SEP + volumeName + PATH_SEP + id); if(globalWarningMask & WARNING_VERBOSE) IO::out.writef("INFO: removing %s\n", backupPath.c_str()); if(globalCommand.act) { b = new BulkRemove("remove/" + hostName + "/" + volumeName + "/" + device->name + "/" + id, backupPath); b->uses(device->name); al.add(b); } } /** @brief Clean up after retire of this backup */ void retired() { const std::string backupPath = (device->store->path + PATH_SEP + hostName + PATH_SEP + volumeName + PATH_SEP + id); if(globalCommand.act) { if(b->getStatus()) { error("removing %s: %s", backupPath.c_str(), SubprocessFailed::format("rm", b->getStatus()).c_str()); return; } // Remove incomplete indicator std::string incompletePath = backupPath + ".incomplete"; if(unlink(incompletePath.c_str()) < 0 && errno != ENOENT) { error("removing %s: %s", incompletePath.c_str(), strerror(errno)); return; } forget(); } } /** @brief Remove backup record from the database */ void forget() { Database::Statement(globalConfig.getdb(), "DELETE FROM backup" " WHERE host=? AND volume=? AND device=? AND id=?", SQL_STRING, &hostName, SQL_STRING, &volumeName, SQL_STRING, &device->name, SQL_STRING, &id, SQL_END) .next(); } }; // Schedule the retire of one volume or host static void identifyVolumes(std::vector &retire, std::set &volume_directories, std::set &host_directories, const std::string &hostName, const std::string &volumeName) { // Verify action with user if(volumeName == "*") { if(globalConfig.findHost(hostName)) warning(WARNING_UNKNOWN, "host %s is still in configuration", hostName.c_str()); if(globalCommand.act && !check("Really delete backups for host '%s'?", hostName.c_str())) return; } else { if(globalConfig.findVolume(hostName, volumeName)) warning(WARNING_UNKNOWN, "volume %s:%s is still in configuration", hostName.c_str(), volumeName.c_str()); if(globalCommand.act && !check("Really delete backups for volume '%s:%s'?", hostName.c_str(), volumeName.c_str())) return; } // Find all the backups to retire { Database::Statement stmt(globalConfig.getdb()); if(volumeName == "*") stmt.prepare("SELECT volume,device,id FROM backup" " WHERE host=? AND status!=?", SQL_STRING, &hostName, SQL_INT, PRUNED, SQL_END); else stmt.prepare("SELECT volume,device,id FROM backup" " WHERE host=? AND volume=? AND status!=?", SQL_STRING, &hostName, SQL_STRING, &volumeName, SQL_INT, PRUNED, SQL_END); while(stmt.next()) { std::string deviceName = stmt.get_string(1); globalConfig.identifyDevices(Store::Enabled); Device *device = globalConfig.findDevice(deviceName); if(!device) { // User should use --retire-device instead error("backup on unknown device %s (use --retire-device)", deviceName.c_str()); continue; } if(!device->store) { error("backup on unavailable device %s", deviceName.c_str()); continue; } if(device->store->state != Store::Enabled) { error("backup on disabled device %s", deviceName.c_str()); continue; } retire.push_back( Retirable(hostName, stmt.get_string(0), device, stmt.get_string(2))); // Zap all retired volume directories volume_directories.insert(device->store->path + PATH_SEP + hostName + PATH_SEP + stmt.get_string(0)); // ...and host directories if the whole host is being retired. if(volumeName == "*") host_directories.insert(device->store->path + PATH_SEP + hostName); } } } void retireVolumes(bool remove) { // Sanity-check command for(auto &selection: globalCommand.selections) if(selection.sense == false) throw CommandError("cannot use negative selections with --retire"); // Identify backups to retire and directories to remove std::vector retire; std::set volume_directories, host_directories; for(auto &selection: globalCommand.selections) { if(selection.host == "*") throw CommandError("cannot retire all hosts"); identifyVolumes(retire, volume_directories, host_directories, selection.host, selection.volume); } if(remove) { // Schedule removal EventLoop e; ActionList al(&e); for(Retirable &r: retire) r.scheduleRetire(al); // Perform removal al.go(); // Clean up .incomplete files and db after removal for(Retirable &r: retire) r.retired(); // Clean up redundant directories for(auto &d: volume_directories) removeDirectory(d); for(auto &d: host_directories) removeDirectory(d); } else { if(globalCommand.act) for(Retirable &r: retire) r.forget(); } } rsbackup-10.0/src/Selection.cc000066400000000000000000000042561440730431700163010ustar00rootroot00000000000000// Copyright © 2015, 2016 Richard Kettlewell. // // 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 . #include #include "Selection.h" #include "Errors.h" #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Host.h" Selection::Selection(const std::string &host_, const std::string &volume_, bool sense_): sense(sense_), host(host_), volume(volume_) { if(!Host::valid(host) && host != "*") throw CommandError("invalid host: '" + host + "'"); if(!Volume::valid(volume) && volume != "*") throw CommandError("invalid volume: '" + volume + "'"); if(host == "*" && volume != "*") throw CommandError("invalid host: '" + host + "'"); } void VolumeSelections::add(const std::string &selection) { // Establish the sense of this entry bool sense; if(selection.size() == 0) throw CommandError("invalid selection"); size_t pos = 0; switch(selection.at(pos)) { case '-': case '!': sense = false; ++pos; break; default: sense = true; break; } size_t colon = selection.find(':', pos); if(colon != std::string::npos) { // A host:volume pair selections.push_back(Selection(selection.substr(pos, colon - pos), selection.substr(colon + 1), sense)); } else { // Just a host selections.push_back(Selection(selection.substr(pos), "*", sense)); } } void VolumeSelections::select(Conf &config) const { if(selections.size() == 0) config.selectVolume("*", "*", true); for(auto &selection: selections) config.selectVolume(selection.host, selection.volume, selection.sense); } rsbackup-10.0/src/Selection.h000066400000000000000000000057421440730431700161440ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2015 Richard Kettlewell. // // 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 . #ifndef SELECTION_H #define SELECTION_H /** @file Selection.h * @brief %Selection of volumes to operate on */ #include #include class Conf; /** @brief Represents a single selection * * A selection is a volume or collection of volumes and a positive or * negative sense (represented by @c true and @c false respectively). The * list of selections in @ref Command::selections determines which volumes an * operation applies to. */ struct Selection { /** @brief Construct a selection * @param host_ Host or "*" for all hosts * @param volume_ Volume or "*" for all hosts * @param sense_ @c true for "+ and @c false for "-" * * A @p host_ of "*" but a @p volume_ not equal to "*" does not make sense * and will fail in @ref Conf::selectVolume. */ Selection(const std::string &host_, const std::string &volume_, bool sense_ = true); /** @brief Sense of selection * * @c true for "+" and @c false for "-" */ bool sense; /** @brief Host name or "*" * * "*" means all hosts. */ std::string host; /** @brief Volume name or "*" * * "*" means all volumes. */ std::string volume; }; /** @brief Represents a list of selections */ class VolumeSelections { public: /** @brief Add a selection to the list * @param selection Selection string from caller */ void add(const std::string &selection); /** @brief Select volumes * * Invokes Conf::selectVolumes() according to the selected volumes. * If no voumes were selected, selects everyting. */ void select(Conf &config) const; /** @brief Return the number of selections */ size_t size() const { return selections.size(); } /** @brief Return the nth selection * @param n Index into selections array */ const Selection &operator[](size_t n) const { return selections.at(n); } /** @brief Return an iterator pointing to the first selection * @return Iterator */ std::vector::const_iterator begin() const { return selections.begin(); } /** @brief Return an iterator pointing after the last selection * @return Iterator */ std::vector::const_iterator end() const { return selections.end(); } private: /** @brief Selections */ std::vector selections; }; #endif /* SELECTION_H */ rsbackup-10.0/src/Store.cc000066400000000000000000000063031440730431700154430ustar00rootroot00000000000000// Copyright © 2011-13, 2015-19 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Store.h" #include "Device.h" #include "Errors.h" #include "IO.h" #include "Utils.h" #include "DeviceAccess.h" #include #include #include // Identify the device on this store, if any void Store::identify() { IO *f = nullptr; try { struct stat sb; if(device) return; // already identified if(stat(path.c_str(), &sb) < 0) throw BadStore("store '" + path + "' does not exist"); if(mounted) { const std::string parent_path = path + "/.."; struct stat parent_sb; if(stat(parent_path.c_str(), &parent_sb) < 0) throw FatalStoreError("cannot stat '" + parent_path + "'"); if(sb.st_dev == parent_sb.st_dev) throw UnavailableStore("store '" + path + "' is not mounted"); } // Make sure backup devices are mounted preDeviceAccess(); // Read the device name f = new IO(); f->open(path + PATH_SEP + "device-id", "r"); std::string deviceName; if(!f->readline(deviceName)) throw BadStore("store '" + path + "' has a malformed device-id"); // See if it exists auto devices_iterator = globalConfig.devices.find(deviceName); if(devices_iterator == globalConfig.devices.end()) throw BadStore("store '" + path + "' has unknown device-id '" + deviceName + "'"); Device *foundDevice = devices_iterator->second; // Duplicates are bad, sufficiently so that we don't treat it as // just an unsuitable store; something is seriously wrong and it // needs immediate attention. if(foundDevice->store) throw FatalStoreError("store '" + path + "' has duplicate device-id '" + deviceName + "', also found on store '" + foundDevice->store->path + "'"); if(!globalConfig.publicStores) { // Verify permissions if(sb.st_uid) throw BadStore("store '" + path + "' not owned by root"); if(sb.st_mode & 077) throw BadStore("store '" + path + "' is not private"); } device = foundDevice; device->store = this; // On success, leave a file open on the store to stop it being unmounted // while we it's a potential destination for backups; but close it before // unmounting. closeOnUnmount(f); } catch(IOError &e) { // Re-throw with the appropriate error type if(e.errno_value == ENOENT) throw UnavailableStore(e.what()); else throw BadStore(e.what()); } catch(...) { delete f; throw; } } rsbackup-10.0/src/Store.h000066400000000000000000000035101440730431700153020ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014, 2015, 2018 Richard Kettlewell. // // 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 . #ifndef STORE_H #define STORE_H /** @file Store.h * @brief %Store (mount point) support */ #include class Device; /** @brief Represents a store * * A store is a path at which a backup device may be mounted. */ class Store { public: /** @brief Constructor * @param path_ Location of store * @param mounted_ True if path must be a mount point */ Store(const std::string &path_, bool mounted_): path(path_), mounted(mounted_) {} /** @brief Possible states */ enum State { /** @brief A disabled store */ Disabled = 1, /** @brief An enabled store */ Enabled = 2, }; /** @brief Location of store */ std::string path; /** @brief True if path must be a mount point */ bool mounted; /** @param Device mounted at this store * * Set to null pointer before checking, or if no device is mounted here */ Device *device = nullptr; /** @brief State of this store */ State state = Enabled; /** @brief Identify the device mounted here * @throw BadStore * @throw FatalStoreError * @throw UnavailableStore */ void identify(); }; #endif /* STORE_H */ rsbackup-10.0/src/Subprocess.cc000066400000000000000000000175661440730431700165140ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014-2017, 2020 Richard Kettlewell. // // 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 . #include #include "Subprocess.h" #include "Errors.h" #include "Defaults.h" #include "IO.h" #include "Utils.h" #include #include #include #include #include #include #include #include Subprocess::Subprocess(const std::string &name, const std::vector &cmd_): Action(name), cmd(cmd_) {} Subprocess::Subprocess(const std::string &name): Action(name) {} Subprocess::~Subprocess() { if(pid >= 0) { kill(pid, SIGKILL); try { if(eventloop) wait(0); } catch(...) { } } // Clean up any capture FDs that didn't reach EOF for(const auto &c: captures) { close(c.first); } delete eventloop; } void Subprocess::setCommand(const std::vector &cmd_) { cmd = cmd_; } void Subprocess::addChildFD(int childFD, int pipeFD, int closeFD, int otherChildFD) { fds.push_back(ChildFD(childFD, pipeFD, closeFD, otherChildFD)); } void Subprocess::nullChildFD(int childFD) { fds.push_back(ChildFD(childFD, -1, -1, -1)); } void Subprocess::capture(int childFD, std::string *s, int otherChildFD) { int p[2]; if(pipe(p) < 0) throw IOError("creating pipe", errno); addChildFD(childFD, p[1], p[0], otherChildFD); captures[p[0]] = s; } pid_t Subprocess::run() { assert(!eventloop); eventloop = new EventLoop(); return launch(eventloop); } pid_t Subprocess::launch(EventLoop *e) { assert(e); // EventLoop must already exist if(pid >= 0) throw std::logic_error("Subprocess::launch but already running"); // Report if necessary if(reportNeeded) report(); // Convert the command if(cmd.size() == 0) throw std::logic_error("Subprocess::launch with no command"); std::vector args; for(auto &arg: cmd) args.push_back(arg.c_str()); args.push_back(nullptr); // Start the subprocess switch(pid = fork()) { case -1: throw SystemError("creating subprocess for " + cmd[0], errno); case 0: { int nullfd = -1; // Dup file descriptors into place for(size_t n = 0; n < fds.size(); ++n) { const ChildFD &cfd = fds[n]; if(cfd.pipe >= 0) { if(dup2(cfd.pipe, cfd.child) < 0) { perror("dup2"); _exit(-1); } } else { if(nullfd < 0 && (nullfd = open(_PATH_DEVNULL, O_RDWR)) < 0) { perror("/dev/null"); _exit(-1); } if(dup2(nullfd, cfd.child) < 0) { perror("dup2"); _exit(-1); } } if(cfd.childOther >= 0 && dup2(cfd.child, cfd.childOther) < 0) { perror("dup2"); _exit(-1); } } // Close leftovers for(size_t n = 0; n < fds.size(); ++n) { const ChildFD &cfd = fds[n]; if(cfd.pipe >= 0) { if(close(cfd.pipe) < 0) { perror("close"); _exit(-1); } for(size_t m = n + 1; m < fds.size(); ++m) if(fds[m].pipe == cfd.pipe) fds[m].pipe = -1; } if(cfd.close >= 0) if(close(cfd.close) < 0) { perror("close"); _exit(-1); } } if(nullfd >= 0 && close(nullfd) < 0) { perror("close"); _exit(-1); } for(auto &e: env) { const std::string &name = e.first, &value = e.second; if(::setenv(name.c_str(), value.c_str(), 1 /*overwrite*/)) { perror("setenv"); _exit(-1); } } // Execute the command execvp(args[0], (char **)&args[0]); perror(args[0]); _exit(-1); } } // Close file descriptors used by the child for(size_t n = 0; n < fds.size(); ++n) { const ChildFD &cfd = fds[n]; if(cfd.pipe >= 0) { if(close(cfd.pipe) < 0) throw IOError("closing FD for " + cmd[0], errno); for(size_t m = n + 1; m < fds.size(); ++m) if(fds[m].pipe == cfd.pipe) fds[m].pipe = -1; } } return pid; } void Subprocess::onReadable(EventLoop *e, int fd, const void *ptr, size_t n) { if(n) captures[fd]->append((char *)ptr, n); else { e->cancelRead(fd); close(fd); captures.erase(fd); } } void Subprocess::onReadError(EventLoop *, int, int errno_value) { throw IOError("reading pipe", errno_value); } void Subprocess::onTimeout(EventLoop *, const struct timespec &) { warning(WARNING_ALWAYS, "%s exceeded timeout of %d seconds", cmd[0].c_str(), timeout); if(pid > 0) kill(pid, SIGKILL); } void Subprocess::onWait(EventLoop *, pid_t, int status, const struct rusage &) { this->status = status; this->pid = -1; if(actionlist) actionlist->completed(this, getActionStatus()); } bool Subprocess::getActionStatus() const { return status == 0; } void Subprocess::setup(EventLoop *e) { if(pid < 0) throw std::logic_error("Subprocess::setup but not running"); for(auto &c: captures) e->whenReadable(c.first, static_cast(this)); if(timeout > 0) { struct timespec timeLimit; getMonotonicTime(timeLimit); if(timeLimit.tv_sec <= std::numeric_limits::max() - timeout) timeLimit.tv_sec += timeout; else timeLimit.tv_sec = std::numeric_limits::max(); e->whenTimeout(timeLimit, this); } e->whenWaited(pid, this); } int Subprocess::wait(unsigned waitBehavior) { assert(eventloop); setup(eventloop); eventloop->wait(); delete eventloop; eventloop = nullptr; pid = -1; if(waitBehavior & THROW_ON_ERROR) { if(WIFEXITED(status) && WEXITSTATUS(status)) throw SubprocessFailed(cmd[0], status); } if(waitBehavior & THROW_ON_CRASH) { if(WIFSIGNALED(status) && WTERMSIG(status) != SIGPIPE) throw SubprocessFailed(cmd[0], status); } if(waitBehavior & THROW_ON_SIGPIPE) { if(WIFSIGNALED(status) && WTERMSIG(status) == SIGPIPE) throw SubprocessFailed(cmd[0], status); } return status; } void Subprocess::go(EventLoop *e, ActionList *al) { actionlist = al; launch(e); setup(e); } void Subprocess::report() { if(reported) return; if(env.size()) { IO::out.writef("> # environment for next command\n"); for(auto &e: env) IO::out.writef("> %s=%s\n", e.first.c_str(), e.second.c_str()); } std::string command; for(size_t i = 0; i < cmd.size(); ++i) { if(i) command += ' '; command += cmd[i]; } IO::out.writef("> %s\n", command.c_str()); reported = true; } void Subprocess::reporting(bool reportCommand, bool reportNow) { reportNeeded = reportCommand; if(reportNeeded && reportNow) report(); } std::string Subprocess::pathSearch(const std::string &name) { if(name.find('/') != std::string::npos) return name; const char *pathc = getenv("PATH"); char buffer[1024]; if(!pathc) { // Follow documented execvp behavior size_t rc = confstr(_CS_PATH, buffer, sizeof buffer); if(rc == 0) throw SystemError("confstr", errno); if(rc > sizeof buffer) throw SystemError("confstr: not enough space"); pathc = buffer; } std::string path = pathc; size_t pos = 0; while(pos < path.size()) { size_t colon = path.find(':', pos); std::string e(path, pos, colon - pos); std::string p(e + "/" + name); if(access(p.c_str(), X_OK) == 0) return p; if(!(pos = colon + 1)) break; } return ""; } rsbackup-10.0/src/Subprocess.h000066400000000000000000000237451440730431700163520ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2014-2017 Richard Kettlewell. // // 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 . #ifndef SUBPROCESS_H #define SUBPROCESS_H /** @file Subprocess.h * @brief Subprocess/command execution support */ #include #include #include #include #include "EventLoop.h" #include "Action.h" /** @brief Subprocess execution * * This class supports three modes of operation. * * 1. Standalone synchronous execution. The caller sets up the subprocess and * then invokes @ref Subprocess::runAndWait. An internal @ref EventLoop is * used. In this case it is possible to capture output to strings with @ref * Subprocess::capture but not to manage pipes directly since the caller has no * opportunity to read or write them. Only one subprocess may exist at a time. * * 2. Standalone asynchronous execution. The caller sets up the subprocess and * then invokes @ref Subprocess::run. Later the caller invokes @ref * Subprocess::wait. As above, an internal @ref EventLoop is used. In this * case it is possible to manage pipes directly with @ref * Subprocess::addChildFD. Captures will work with small outputs but since the * event loop only has an opportunity to read or write the capture pipes when * @c wait() is called, they do not work in general. Only one subprocess may * exist at a time. * * 3. Concurrent execution as part of an @ref ActionList. The caller sets up * many subprocesses and adds them to the action list with @ref * ActionList::add, before starting them and waiting for them with @ref * ActionList::go. When the action list is complete, wait statuses may be * retrieved with @ref Subprocess::getStatus. String captures will work and if * the caller registers reactors with the event loop then pipes can also be * managed directly. */ class Subprocess: private Reactor, public Action { public: /** @brief Possible wait behaviors */ enum WaitBehavior { /** @brief Throw if the process terminates normally with nonzero status */ THROW_ON_ERROR = 1, /** @brief Throw if the process terminates due to a signal other than SIGPIPE */ THROW_ON_CRASH = 2, /** @brief Throw if the process terminates due to SIGPIPE */ THROW_ON_SIGPIPE = 4, }; /** @brief Constructor */ Subprocess(): Subprocess("") {} /** @brief Constructor * @param name Action name */ Subprocess(const std::string &name); /** @brief Constructor * @param cmd Command that will be executed */ Subprocess(const std::vector &cmd): Subprocess("", cmd) {} /** @brief Constructor * @param name Action name * @param cmd Command that will be executed */ Subprocess(const std::string &name, const std::vector &cmd); /** @brief Destructor */ ~Subprocess() override; /** @brief Set the command to execute * @param cmd Command that will be executed */ void setCommand(const std::vector &cmd); /** @brief Get the command to execute * @return Command that will be executed */ const std::vector &getCommand() const { return cmd; } /** @brief Add a pipe * @param childFD Child file descriptor to redirect * @param pipeFD Child end of pipe * @param closeFD Parent end of pipe * @param otherChildFD Another child file descriptor to redirect * * In the child dup @p pipeFD onto @p childFD and close @p closeFD. * * @p pipeFD is automatically closed in the parent, but only after all * redirections have taken place, so it may be used more than once. */ void addChildFD(int childFD, int pipeFD, int closeFD = -1, int otherChildFD = -1); /** @brief Null out a file descriptor * @param childFD Child file descriptor * * In the child dup @c /dev/null onto @p childFD. */ void nullChildFD(int childFD); /** @brief Capture output from the child * @param childFD Child file descriptor to capture * @param s Where to put result * @param otherChildFD Another child file descriptor to capture * * The capture is performed in wait(); */ void capture(int childFD, std::string *s, int otherChildFD = -1); /** @brief Set an environment variable in the child * @param name Environment variable name * @param value Environment variable value */ void setenv(const std::string &name, const std::string &value) { env[name] = value; } /** @brief Set the child timeout * @param seconds Number of seconds after which to give up and kill child */ void setTimeout(int seconds) { timeout = seconds; } /** @brief Start subprocess * @return Process ID */ pid_t run(); /** @brief Configure reporting * @param reportCommand The command must be logged * @param reportNow Log command immediately * * If @p reportCommand is @c true then the command will be reported, either * now or just before execution. * * If @p reportNow is @c true then if the command is to be reported, it will * happen immediately. * * A command is never reported more than once. */ void reporting(bool reportCommand, bool reportNow); /** @brief Wait for the subprocess. * @param waitBehaviour How to check exit status * @return Wait status * * @p waitBehaviour may contain the following bits: * - @ref THROW_ON_ERROR - throw if the process terminates normally with * nonzero status. * - @ref THROW_ON_CRASH - throw if the process terminates due to a signal * other than SIGPIPE. * - @ref THROW_ON_SIGPIPE - throw if the process terminates due to SIGPIPE. */ int wait(unsigned waitBehaviour = THROW_ON_ERROR | THROW_ON_CRASH); /** @brief Run and then wait * @param waitBehaviour How to check exit status * @return Wait status * * @p waitBehaviour may contain the following bits: * - @ref THROW_ON_ERROR - throw if the process terminates normally with * nonzero status. * - @ref THROW_ON_CRASH - throw if the process terminates due to a signal * other than SIGPIPE. * - @ref THROW_ON_SIGPIPE - throw if the process terminates due to SIGPIPE. */ int runAndWait(unsigned waitBehaviour = THROW_ON_ERROR | THROW_ON_CRASH) { run(); return wait(waitBehaviour); } /** @brief Return the wait status * @return Wait status * * Meaningless until the process has terminated. */ int getStatus() const { return status; } /** @brief Get the status to report to @ref ActionList::completed */ virtual bool getActionStatus() const; /** @brief Find @p name on the path * @param name Program to execute * @return Full path to @p name, or "" if not found */ static std::string pathSearch(const std::string &name); void go(EventLoop *e, ActionList *al) override; private: /** @brief Process ID of child * Set to -1 before there is a child. */ pid_t pid = -1; /** @brief A rule for file descriptor changes in the child process */ struct ChildFD { /** @brief Child file descriptor to redirect */ int child; /** @brief Child's pipe endpoint, or -1 * * If this is -1 then the child's file descriptor is redirected to @c * /dev/null */ int pipe; /** @brief Child file descriptor to close or -1 * * If this is not -1 then this file descriptor will be closed in the child. * This is used for the parent's pipe endpoints. */ int close; /** @brief Other child file descriptor to redirect or -1 */ int childOther; /** @brief Construct a ChildFD * @param child_ Child file descriptor to redirect * @param pipe_ Child's pipe endpoint, or -1 * @param close_ File descriptor to close in child, or -1 * @param childOther_ Another child file descriptor to redirect, or -1 */ ChildFD(int child_, int pipe_, int close_, int childOther_): child(child_), pipe(pipe_), close(close_), childOther(childOther_) {} }; /** @brief Rules for file descriptor changes in the child process */ std::vector fds; /** @brief Command to execute in the child */ std::vector cmd; /** @brief Environment variables to set in the child */ std::map env; /** @brief Outputs to capture from the child * * Keys are file descriptors to read from, values are pointers to the strings * used to accumulate the output. */ std::map captures; /** @brief Launch subprocess * @param e Event loop * @return Process ID */ pid_t launch(EventLoop *e); /** @brief Setup event loop integration * @param e Event loop */ void setup(EventLoop *e); /** @brief Report what will or would be run */ void report(); /** @brief Timeout after which child is killed, in seconds * 0 means no timeout: the child may run indefinitely. */ int timeout = 0; void onReadable(EventLoop *e, int fd, const void *ptr, size_t n) override; void onReadError(EventLoop *e, int fd, int errno_value) override; void onTimeout(EventLoop *e, const struct timespec &now) override; void onWait(EventLoop *e, pid_t pid, int status, const struct rusage &ru) override; /** @brief Wait status */ int status = -1; /** @brief Containing action list */ ActionList *actionlist = nullptr; /** @brief Private event loop */ EventLoop *eventloop = nullptr; /** @brief True if the command has been logged */ bool reported = false; /** @brief True if the command needs to be logged */ bool reportNeeded = false; }; #endif /* SUBPROCESS_H */ rsbackup-10.0/src/Text.cc000066400000000000000000000167651440730431700153100ustar00rootroot00000000000000// Copyright © 2011, 2015 Richard Kettlewell. // // 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 . #include #include "Document.h" #include "Utils.h" #include "Errors.h" #include #include #include #include void Document::LinearContainer::renderTextContents( std::ostream &os, RenderDocumentContext *rc) const { for(auto &node: nodes) node->renderText(os, rc); } void Document::String::renderText(std::ostream &os, RenderDocumentContext *) const { os << text; } void Document::LinearContainer::renderText(std::ostream &os, RenderDocumentContext *rc) const { renderTextContents(os, rc); } void Document::wordWrapText(std::ostream &os, const std::string &s, size_t width, size_t indent, bool indentFirst) { size_t x = 0; std::string::size_type pos = 0; bool first = true; while(pos < s.size()) { // Skip whitespace if(isspace(s[pos])) { ++pos; continue; } // Find the length of this word std::string::size_type len = 0; while(pos + len < s.size() && !isspace(s[pos + len])) ++len; if(x) { if(x + len + 1 <= width) { os << ' '; ++x; } else { os << '\n'; x = 0; } } else { if(indentFirst || !first) for(size_t i = 0; i < indent; ++i) os << ' '; } os.write(s.data() + pos, len); x += len; pos += len; } if(x) os << '\n'; } void Document::Paragraph::renderText(std::ostream &os, RenderDocumentContext *rc) const { // Convert to a single string std::stringstream ss; renderTextContents(ss, rc); wordWrapText(os, ss.str(), rc ? rc->width : DEFAULT_TEXT_WIDTH); os << '\n'; } void Document::Verbatim::renderText(std::ostream &os, RenderDocumentContext *rc) const { renderTextContents(os, rc); os << '\n'; } void Document::List::renderText(std::ostream &os, RenderDocumentContext *rc) const { const size_t width = rc ? rc->width : DEFAULT_TEXT_WIDTH; for(size_t n = 0; n < nodes.size(); ++n) { char prefix[64]; std::stringstream ss; nodes[n]->renderText(ss, rc); switch(type) { case OrderedList: snprintf(prefix, sizeof prefix, " %zu. ", n + 1); os << prefix; break; case UnorderedList: strcpy(prefix, " * "); break; } os << prefix; wordWrapText(os, ss.str(), width - strlen(prefix), strlen(prefix), false); } os << '\n'; } void Document::ListEntry::renderText(std::ostream &os, RenderDocumentContext *rc) const { renderTextContents(os, rc); } void Document::Heading::renderText(std::ostream &os, RenderDocumentContext *rc) const { if(level > 6) throw std::runtime_error("heading level too high"); switch(level) { case 1: os << "==== "; renderTextContents(os, rc); os << " ===="; os << '\n'; break; case 2: os << "=== "; renderTextContents(os, rc); os << " ==="; os << '\n'; break; case 3: os << "== "; renderTextContents(os, rc); os << " =="; os << '\n'; break; case 4: case 5: case 6: os << "* "; renderTextContents(os, rc); os << '\n'; break; } os << '\n'; } void Document::Cell::renderText(std::ostream &os, RenderDocumentContext *rc) const { renderTextContents(os, rc); } void Document::Table::renderText(std::ostream &os, RenderDocumentContext *rc) const { // First pass: compute column widths based on single-column cells std::vector columnWidths; for(int xpos = 0; xpos < width; ++xpos) { size_t columnWidth = 0; for(int ypos = 0; ypos < height; ++ypos) { const Cell *cell = findOverlappingCell(xpos, ypos); // We only consider single-width cells if(cell && cell->getWidth() == 1) { std::stringstream ss; cell->renderText(ss, rc); size_t w = ss.str().size(); if(w > columnWidth) columnWidth = w; } } columnWidths.push_back(columnWidth); } // Second pass: add extra space to accomodate multiple-column cells for(int xpos = 0; xpos < width; ++xpos) { for(int ypos = 0; ypos < height; ++ypos) { const Cell *cell = findOverlappingCell(xpos, ypos); if(cell && cell->getX() == xpos && cell->getWidth() > 1) { std::stringstream ss; cell->renderText(ss, rc); // Determine the space available size_t availableWidth = 3 * (cell->getWidth() - 1); for(int i = 0; i < cell->getWidth(); ++i) availableWidth += columnWidths[xpos + i]; if(ss.str().size() <= availableWidth) continue; // If there's a shortage of space, bump up the first column columnWidths[xpos] += ss.str().size() - availableWidth; } } } // We lay out as | | | ... | // Third pass: render the table for(int ypos = 0; ypos < height; ++ypos) { for(int xpos = 0; xpos < width; ++xpos) { const Cell *cell = findOverlappingCell(xpos, ypos); if(cell) { if(cell->getY() == ypos) { if(cell->getX() == xpos) { // First row/column of the cell // Determine the space available size_t availableWidth = 3 * (cell->getWidth() - 1); for(int i = 0; i < cell->getWidth(); ++i) availableWidth += columnWidths[xpos + i]; if(xpos) os << ' '; os << "| "; std::stringstream ss; cell->renderText(ss, rc); size_t left = availableWidth - ss.str().size(); if(cell->getHeader()) { for(size_t i = 0; i < left / 2; ++i) os << ' '; left -= left / 2; os << ss.str(); } else os << ss.str(); for(size_t i = 0; i < left; ++i) os << ' '; } else { // 2nd or subsequent column of the cell } } else { // 2nd or subsequent row of the cell if(xpos) os << ' '; os << "| "; for(size_t i = 0; i < columnWidths[xpos]; ++i) os << ' '; } } else { // Unfilled cell if(xpos) os << ' '; os << "| "; for(size_t i = 0; i < columnWidths[xpos]; ++i) os << ' '; } } os << '|'; os << '\n'; } os << '\n'; } void Document::Image::renderText(std::ostream &, RenderDocumentContext *) const {} void Document::RootContainer::renderText(std::ostream &os, RenderDocumentContext *rc) const { renderTextContents(os, rc); } void Document::renderText(std::ostream &os, RenderDocumentContext *rc) const { content.renderText(os, rc); } rsbackup-10.0/src/Unicode.cc000066400000000000000000000033021440730431700157310ustar00rootroot00000000000000// Copyright © 2011, 2012 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "Errors.h" #include #include #include #include // We need to specify the endianness explicitly or iconv() assumes we want a // BOM. #ifdef WORDS_BIGENDIAN #define ENCODING "UTF-32BE" #else #define ENCODING "UTF-32LE" #endif void toUnicode(std::u32string &u, const std::string &mbs) { static iconv_t cd; if(cd == nullptr) { char *mbsEncoding = nl_langinfo(CODESET); cd = iconv_open(ENCODING, mbsEncoding); if(!cd) throw std::runtime_error(std::string("iconv_open: ") + strerror(errno)); } u.clear(); char32_t buffer[1024]; char *inptr = (char *)mbs.data(); size_t inleft = mbs.size(); while(inleft > 0) { char *outptr = (char *)buffer; size_t outleft = sizeof buffer; size_t n = iconv(cd, ICONV_FIXUP & inptr, &inleft, &outptr, &outleft); if(n == (size_t)-1 && errno != E2BIG) throw SystemError(std::string("iconv: "), errno); u.append(buffer, (char32_t *)outptr - buffer); } } rsbackup-10.0/src/Utils.h000066400000000000000000000267471440730431700153270ustar00rootroot00000000000000//-*-C++-*- // Copyright © 2011, 2012, 2014-2019 Richard Kettlewell. // // 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 . #ifndef UTILS_H #define UTILS_H /** @file Utils.h * @brief Miscellaneous utilities */ #include #include #include #include #include #include #include #include class IO; // forward declaration /** @brief Display a prompt and retrieve a yes/no reply * @param format Format string as per @c printf() * @param ... Arguments * @return True if the user said yes * * Overridden by @c --force, which means "always yes". */ bool check(const char *format, ...); /** @brief Convert to Unicode * @param u Where to put Unicode string * @param mbs Multibyte string */ void toUnicode(std::u32string &u, const std::string &mbs); /** @brief Display a progress bar * @param stream Output stream * @param prompt Prompt string * @param done Work done * @param total Total work * * If @p total is 0 then the progress bar is erased. */ void progressBar(IO &stream, const char *prompt, size_t done, size_t total); /** @brief Break up a string into lines * @param lines Where to put lines * @param s Input string * @return Number of lines * * Lines are terminated by @c \\n. The members of @p lines do not include the * newline. */ size_t toLines(std::vector &lines, const std::string &s); /** @brief Expand a filename glob pattern * @param files List of filenames * @param pattern Pattern * @param flags Flags as per glob(3) */ void globFiles(std::vector &files, const std::string &pattern, int flags); /** @brief Parse an integer * @param s Representation of integer * @param min Minimum acceptable value * @param max Maximum acceptable value * @param radix Base, or 0 to follow C conventions * @return Integer value * @throws SyntaxError if the @p s doesn't represent an integer * @throws SyntaxError if the integer value is out of range */ long long parseInteger(const std::string &s, long long min = std::numeric_limits::min(), long long max = std::numeric_limits::max(), int radix = 0); enum LimitType { InclusiveLimit, ExclusiveLimit, }; /** @brief Parse a float * @param s Representation of float * @param min Minimum acceptable value * @param max Maximum acceptable value * @param limitType Whether bounds are inclusive or exclusive * @return Floating point value * @throws SyntaxError if the @p s doesn't represent a number * @throws SyntaxError if the value is out of range */ double parseFloat(const std::string &s, double min = -std::numeric_limits::max(), double max = std::numeric_limits::max(), LimitType limitType = InclusiveLimit); /** @brief Parse a time interval * @param s Representation of time interval * @param max Maximum acceptable value * @return Number of seconds * @throws SyntaxError if the @p s doesn't represent a time interval * @throws SyntaxError if the value is out of range */ long long parseTimeInterval(std::string s, long long max = std::numeric_limits::max()); /** @brief Format a time interval * @param n Number of seconds * @return Representation of time interval */ std::string formatTimeInterval(long long n); /** @brief Split and parse a list represented as a string * @param bits Destination for components of the string * @param line String to parse * @param indent Where to store indent value, or @c nullptr * @throws SyntaxError if @p line is malformed. * * Each component can be quoted or unquoted. * * Unquoted components are delimited by whitespace and cannot contain double * quotes or backslashes. * * Quoted components are delimited by double quotes. Within the quotes * backslash can be used to escape the next character. * * The hash character can appear inside quotes or noninitially in an * unquoted component, but otherwise introduces a comment which extends to * the end of * @p line. * * If @p indent is not null, the indent level for the line is stored at @p * indent. */ void split(std::vector &bits, const std::string &line, size_t *indent = nullptr); /** @brief Display an error message * @param fmt Format string, as printf() * @param ... Arguments to format string * * Writes an error message to standard error. * * Increments @ref globalErrors. */ void error(const char *fmt, ...); /** @brief Display an error message and terminate * @param fmt Format string, as printf() * @param ... Arguments to format string * * Writes an error message to standard error and terminates the program * by calling @c exit(). */ void fatal [[noreturn]] (const char *fmt, ...); /** @brief Display a warning message * @param type Warning type * @param fmt Format string, as printf() * @param ... Arguments to format string * * Writes an warning message to standard error. */ void warning(unsigned type, const char *fmt, ...); /** @brief Always issue this warning */ #define WARNING_ALWAYS 0 /** @brief Display warnings for unknown devices, hosts and volumes */ #define WARNING_UNKNOWN 0x00000001 /** @brief Display warnings for unsuitable stores and unavailable devices */ #define WARNING_STORE 0x00000002 /** @brief Display warnings for deprecated configuration syntax */ #define WARNING_DEPRECATED 0x00000004 /** @brief Display rsync partial transfer warnings */ #define WARNING_PARTIAL 0x00000008 /** @brief Display verbose messages */ #define WARNING_VERBOSE 0x00000010 /** @brief Display error logs */ #define WARNING_ERRORLOGS 0x00000020 /** @brief Display warnings for database activity */ #define WARNING_DATABASE 0x00000040 /** @brief Display warnings for unreachable hosts */ #define WARNING_UNREACHABLE 0x00000080 /** @brief Current warning mask */ extern unsigned globalWarningMask; /** @brief Enable warning(s) * @param bits Warning bit(s) to enable */ inline void enable_warning(unsigned bits) { globalWarningMask |= bits; } /** @brief Disable warning(s) * @param bits Warning bit(s) to disable */ inline void disable_warning(unsigned bits) { globalWarningMask &= ~bits; } /** @brief Compare timespec values */ inline int compare_timespec(const struct timespec &a, const struct timespec &b) { if(a.tv_sec < b.tv_sec) return -1; if(a.tv_sec > b.tv_sec) return 1; if(a.tv_nsec < b.tv_nsec) return -1; if(a.tv_nsec > b.tv_nsec) return 1; return 0; } /** @brief Compare timespec values */ inline bool operator>=(const struct timespec &a, const struct timespec &b) { return compare_timespec(a, b) >= 0; } /** @brief Compare timespec values */ inline bool operator==(const struct timespec &a, const struct timespec &b) { return compare_timespec(a, b) == 0; } /** @brief Compare timespec values */ inline bool operator<(const struct timespec &a, const struct timespec &b) { return compare_timespec(a, b) < 0; } /** @brief Subtract timespec values */ inline struct timespec operator-(const struct timespec &a, const struct timespec &b) { struct timespec r; r.tv_sec = a.tv_sec - b.tv_sec; r.tv_nsec = a.tv_nsec - b.tv_nsec; if(r.tv_nsec < 0) { r.tv_nsec += 1000000000; r.tv_sec -= 1; } return r; } /** @brief Log to arbitrary base * @param x Argument to logarithm * @param b Base for logarithm * @return \f$log_b(x)\f$ */ inline double logbase(double x, double b) { /* Why log2 instead of log? Because FreeBSD's log() implementation is * inaccurate. */ return log2(x) / log2(b); } /** @brief Make a file descriptor nonblocking * @param fd File descriptor */ void nonblock(int fd); /** @brief Delete all the children of a container * @param container Container * * Uses @c delete to destroy all the elements of @p container, and then empties * it. */ template void deleteAll(C &container) { for(auto element: container) delete element; container.clear(); } /** @brief Test whether a container contains a particular element * @param container container to search * @param element element to test for * @return @c true if @p element can be found in @p container */ template bool contains(const C &container, const E &element) { return container.find(element) != container.end(); } /** @brief Scoped release of a mutex * * The mutex is released on construction and automatically re-acquired on * destruction. */ template class release_guard { public: /** @brief Constructor taking the mutex to release * @param m mutex to release */ explicit release_guard(M &m): mutex(m) { mutex.unlock(); } ~release_guard() { mutex.lock(); } private: /** @brief Mutex released by this instance */ M &mutex; }; /** @brief RFC4648 base64 alphabet for @ref write_base64() */ extern const char rfc4648_base64[]; /** @brief Convert a string to base64 * @param os Output stream * @param s String to convert * @param alphabet Digits and padding (must be 65 bytes long) * @return @p os */ std::ostream &write_base64(std::ostream &os, const std::string &s, const char *alphabet = rfc4648_base64); /** @brief Expand environment variable references * @param s Input string * @param pos Start position within @p s * @param n Number of characters to read within @p s * @return @p s with environment variables expanded * * Within @p s: * * 1. The @c \ character quotes the following character. The exception is that * if it is at the end of the string, it is not replaced. * * 2. The sequence @c ${NAME} is replaced (nonrecursively) with the value * of the environment variable @c NAME, if it is set. */ std::string substitute(const std::string &s, std::string::size_type pos = 0, std::string::size_type n = std::string::npos); /** @brief Get a timestamp for the current time * @param now Where to store timestamp * * If possible the monotonic clock is used. Otherwise the real time clock is * used. */ void getMonotonicTime(struct timespec &now); /** @brief Issue debug output * * Affects the @ref D macro and @ref write_debug(). * * The default is @c false. */ extern bool globalDebug; /** @brief Write a debug message to standard error * * The arguments are the same as @c printf(). A newline is added to the output * (so debug messages should not end with a newline). * * Only displays the a message if @ref globalDebug is set. */ int write_debug(const char *path, long line, const char *msg, ...); /** @brief Write a debug message to standard error * * The arguments are the same as @c printf(). A newline is added to the output * (so debug messages should not end with a newline). * * Only displays the a message if @ref globalDebug is set. */ #define D(...) \ (void)(globalDebug && write_debug(__FILE__, __LINE__, __VA_ARGS__)) #endif /* UTILS_H */ rsbackup-10.0/src/Volume.cc000066400000000000000000000164071440730431700156240ustar00rootroot00000000000000// Copyright © 2011-2016 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Device.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include "Subprocess.h" #include "Utils.h" #include "Store.h" #include "BackupPolicy.h" #include #include #include Volume::Volume(Host *parent_, const std::string &name_, const std::string &path_): ConfBase(static_cast(parent_)), parent(parent_), name(name_), path(path_) { parent->addVolume(this); } Volume::~Volume() { deleteAll(backups); } void Volume::select(bool sense) { isSelected = sense; } bool Volume::valid(const std::string &name) { return name.size() > 0 && name.at(0) != '-' && name.find_first_not_of(VOLUME_VALID) == std::string::npos; } void Volume::calculate() { completed = 0; for(auto &pd: perDevice) pd.second.count = 0; for(const Backup *backup: backups) { // Only count complete backups which aren't going to be pruned if(backup->getStatus() == COMPLETE) { // Global figures ++completed; if(completed == 1 || backup->time < oldest) oldest = backup->time; if(completed == 1 || backup->time > newest) newest = backup->time; // Per-device figures Volume::PerDevice &pd = perDevice[backup->deviceName]; ++pd.count; if(pd.count == 1 || backup->time < pd.oldest) pd.oldest = backup->time; if(pd.count == 1 || backup->time > pd.newest) { pd.newest = backup->time; pd.size = backup->getSize(); } } } for(auto it = perDevice.begin(); it != perDevice.end();) { auto jt = it; ++it; Volume::PerDevice &pd = jt->second; if(!pd.count) perDevice.erase(jt); } } bool Volume::addBackup(Backup *backup) { bool inserted = backups.insert(backup).second; if(inserted) calculate(); return inserted; } bool Volume::removeBackup(const Backup *backup) { for(auto it = backups.begin(); it != backups.end(); ++it) { Backup *s = *it; if(s == backup) { backups.erase(it); delete s; // Recalculate totals calculate(); return true; } } return false; } const Backup *Volume::mostRecentBackup(const Device *device) const { const Backup *result = nullptr; for(const Backup *backup: backups) { // Note that if we ask for 'any device', i.e. device=nullptr, // we can get backups on devices not mentioned in the config file. if(!device || backup->getDevice() == device) { if(!result || *result < *backup) result = backup; } } return result; } const Backup *Volume::mostRecentFailedBackup(const Device *device) const { const Backup *result = nullptr; for(const Backup *backup: backups) { if(!device || backup->getDevice() == device) { switch(backup->getStatus()) { case COMPLETE: case PRUNING: case PRUNED: break; case UNKNOWN: case FAILED: case UNDERWAY: if(!result || *result < *backup) result = backup; break; } } } return result; } bool Volume::available() const { if(checkMounted) { std::string os, stats; std::string parent_directory = path + "/.."; const char *option; // Guess which version of stat to use based on uname. if(parent->invoke(&os, "uname", "-s", (const char *)nullptr) != 0) return false; if(os == "Darwin" || (os.size() >= 3 && os.compare(os.size() - 3, 3, "BSD") == 0)) { option = "-f"; } else { // For everything else assume coreutils stat(1) option = "-c"; } // Get the device numbers for path and its parent if(parent->invoke(&stats, "stat", option, "%d", path.c_str(), parent_directory.c_str(), (const char *)nullptr)) return false; // Split output into lines std::vector lines; toLines(lines, stats); // If stats is malformed, or if device numbers match (implying path is not // a mount point), volume is not available. if(lines.size() != 2 || lines[0].size() == 0 || lines[1].size() == 0 || lines[0] == lines[1]) return false; } if(checkFile.size()) { std::string file = (checkFile[0] == '/' ? checkFile : path + "/" + checkFile); if(parent->invoke(nullptr, "test", "-e", file.c_str(), (const char *)nullptr) != 0) return false; } return true; } BackupRequirement Volume::needsBackup(Device *device) { switch(fnmatch(devicePattern.c_str(), device->name.c_str(), FNM_NOESCAPE)) { case 0: break; case FNM_NOMATCH: return NotThisDevice; default: warning(WARNING_ALWAYS, "invalid device pattern '%s'", devicePattern.c_str()); /* fail safe - make the backup */ break; } const BackupPolicy *policy = BackupPolicy::find(backupPolicy); if(!policy->backup(this, device)) return AlreadyBackedUp; if(!available()) return NotAvailable; return BackupRequired; } void Volume::write(std::ostream &os, int step, bool verbose) const { describe_type *d = verbose ? describe : nodescribe; os << indent(step) << "volume " << quote(name) << ' ' << quote(path) << '\n'; step += 4; ConfBase::write(os, step, verbose); d(os, "", step); d(os, "# Glob pattern for devices this host will be backed up to", step); d(os, "# devices PATTERN", step); if(devicePattern.size()) os << indent(step) << "devices " << quote(devicePattern) << '\n'; d(os, "", step); d(os, "# Paths to exclude from backup", step); d(os, "# Patterns are glob patterns, starting at the root of the volume as '/'.", step); d(os, "# '*' matches multiple characters but not '/'", step); d(os, "# '**' matches multiple characters including '/'", step); d(os, "# Consult rsync manual for full pattern syntax", step); d(os, "# exclude PATTERN", step); for(const std::string &exclusion: exclude) os << indent(step) << "exclude " << quote(exclusion) << '\n'; d(os, "", step); d(os, "# Back up across mount points", step); d(os, "# traverse true|false", step); os << indent(step) << "traverse " << (traverse ? "true" : "false") << '\n'; d(os, "", step); d(os, "# Check that a named file exists before performing backup", step); d(os, "# check-file PATH", step); if(checkFile.size()) os << indent(step) << "check-file " << quote(checkFile) << '\n'; d(os, "", step); d(os, "# Check that volume is a mount point before performing backup", step); d(os, "# check-mounted true|false", step); os << indent(step) << "check-mounted " << (checkMounted ? "true" : "false") << '\n'; } ConfBase *Volume::getParent() const { return parent; } std::string Volume::what() const { return "volume"; } rsbackup-10.0/src/Volume.h000066400000000000000000000127141440730431700154630ustar00rootroot00000000000000// -*-C++-*- // Copyright © 2011, 2012, 2014-2016 Richard Kettlewell. // // 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 . #ifndef VOLUME_H #define VOLUME_H /** @file Volume.h * @brief Configuration and state of a volume */ #include #include "ConfBase.h" #include "Date.h" #include "Backup.h" class Host; class Device; /** @brief Type of an ordered set of backups * @see Volume::backups */ typedef std::set backups_type; /** @brief Possible states of a volume */ enum BackupRequirement { /** @brief Volume already backed up */ AlreadyBackedUp, /** @brief Device not usable */ NotThisDevice, /** @brief Volume not usable */ NotAvailable, /** @brief Volume can and should be backed up */ BackupRequired }; /** @brief Represents a single volume (usually, filesystem) to back up */ class Volume: public ConfBase { public: /** @brief Construct a volume * @param parent_ Host containing volume * @param name_ Volume name * @param path_ Path to volume */ Volume(Host *parent_, const std::string &name_, const std::string &path_); /** @brief Destructor */ ~Volume(); /** @brief Host containing volume */ Host *parent; /** @brief Volume name */ std::string name; /** @brief Path to volume */ std::string path; /** @brief List of exclusion patterns for this volume */ std::vector exclude; /** @brief Traverse mount points if true */ bool traverse = false; /** @brief File to check before backing up */ std::string checkFile; /** @brief Check that root path is a mount point before backing up */ bool checkMounted = false; /** @brief Return true if volume is selected */ bool selected() const { return isSelected; } /** @brief (De-)select volume * @param sense true to select, false to de-select */ void select(bool sense); /** @brief Check whether a proposed volume name is valid * @param n Proposed volume name * @return true if valid, else false */ static bool valid(const std::string &n); /** @brief Test if volume available * @return true if volume is available */ bool available() const; /** @brief Known backups of this volume */ backups_type backups; /** @brief Per-device information about this volume */ struct PerDevice { /** @brief Number of backups of volume on device */ int count = 0; /** @brief Oldest backup of volume on device */ time_t oldest = 0; /** @brief Newest backup of volume on device */ time_t newest = 0; /** @brief Size of newest backup on device, or -1 if unknown */ long long size; }; /** @brief Number of completed backups */ int completed = 0; /** @brief Time of oldest backup (on any device) */ time_t oldest = 0; /** @brief Time of newest backup (on any device) */ time_t newest = 0; /** @brief Type for @ref perDevice */ typedef std::map perdevice_type; /** @brief Map of device names to per-device information */ perdevice_type perDevice; /** @brief Find the per-device information for @p device * @param device Device name * @return Per-device information or @c nullptr */ const PerDevice *findDevice(const std::string &device) const { auto it = perDevice.find(device); return it != perDevice.end() ? &it->second : nullptr; } /** @brief Add a backup * @return @c true if the backup was inserted, @c false if already present */ bool addBackup(Backup *backup); /** @brief Remove a backup */ bool removeBackup(const Backup *backup); /** @brief Find the most recent backup (of any status) * @param device If not null pointer, only consider backups from this device * @return Most recent backup or null pointer */ const Backup *mostRecentBackup(const Device *device = nullptr) const; /** @brief Find the most recent failedbackup * @param device If not null pointer, only consider backups from this device * @return Most recent failed backup or null pointer */ const Backup *mostRecentFailedBackup(const Device *device = nullptr) const; /** @brief Identify whether this volume needs backing up on a particular * device * @param device Target device * @return Volume state */ BackupRequirement needsBackup(Device *device); ConfBase *getParent() const override; std::string what() const override; void write(std::ostream &os, int step, bool verbose) const override; private: /** @brief Set to @c true if this volume is selected */ bool isSelected = false; /** @brief Recalculate statistics * * After calling this method the following members will accurately reflect * the contents of the @ref backups container: * - @ref completed * - @ref oldest * - @ref newest * - @ref perDevice * * @ref perDevice will not contain any entries with @ref PerDevice::count * equal to 0. */ void calculate(); friend void Backup::setStatus(int); }; #endif /* VOLUME_H */ rsbackup-10.0/src/base64.cc000066400000000000000000000037021440730431700154330ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include #include "Utils.h" std::ostream &write_base64(std::ostream &os, const std::string &s, const char *alphabet) { int line_length = 76; int offset = 0; size_t pos = 0, limit = s.size(); while(pos < limit) { unsigned b; int bit, bitlimit; switch(limit - pos) { default: b = static_cast(s.at(pos++)); b = 256 * b + static_cast(s.at(pos++)); b = 256 * b + static_cast(s.at(pos++)); bitlimit = 0; break; case 2: b = static_cast(s.at(pos++)); b = 256 * b + static_cast(s.at(pos++)); b = 256 * b; bitlimit = 8; break; case 1: b = static_cast(s.at(pos++)); b = 256 * b; b = 256 * b; bitlimit = 16; break; } bit = 3 * 6; while(bit + 6 > bitlimit) { os.put(alphabet[(b >> bit) & 0x3F]); bit -= 6; ++offset; } while(bit >= 0) { os.put(alphabet[64]); bit -= 6; ++offset; } if(offset >= line_length) { os.put('\n'); offset = 0; } } return os; } const char rfc4648_base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; rsbackup-10.0/src/check-source.in000077500000000000000000000021631440730431700167460ustar00rootroot00000000000000#! /bin/sh # # Copyright (C) 2019 Richard Kettlewell # # 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 . # set -e cd ${srcdir} status=0 CLANG_FORMAT=@CLANG_FORMAT@ if ${CLANG_FORMAT} --version >/dev/null 2>&1; then : else echo >&2 ${CLANG_FORMAT} failed, skipping check-source test exit 77 fi formatted=$(mktemp -d) trap "rm -rf ${formatted}" EXIT for path in *.h *.cc; do ${CLANG_FORMAT} ${path} > ${formatted}/${path} if diff -u ${path} ${formatted}/${path}; then : else status=1 fi rm -f ${formatted}/${path} done exit $status rsbackup-10.0/src/debug.cc000066400000000000000000000020151440730431700154310ustar00rootroot00000000000000// Copyright © 2016, 2019 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include bool globalDebug; int write_debug(const char *path, long line, const char *msg, ...) { if(globalDebug) { va_list ap; va_start(ap, msg); fprintf(stderr, "%s:%ld: ", path, line); vfprintf(stderr, msg, ap); fputc('\n', stderr); va_end(ap); } return 0; } rsbackup-10.0/src/error.cc000066400000000000000000000027611440730431700155040ustar00rootroot00000000000000// Copyright © 2014, 2016, 2019 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "Defaults.h" #include "IO.h" #include int globalErrors; unsigned globalWarningMask = DEFAULT_WARNING_MASK; static void error_generic(const char *tag, const char *fmt, va_list ap) { IO::err.writef("%s: ", tag); IO::err.vwritef(fmt, ap); IO::err.write("\n"); } void fatal(const char *fmt, ...) { va_list ap; va_start(ap, fmt); error_generic("ERROR", fmt, ap); va_end(ap); exit(1); } void error(const char *fmt, ...) { va_list ap; va_start(ap, fmt); error_generic("ERROR", fmt, ap); va_end(ap); ++globalErrors; } void warning(unsigned warning_type, const char *fmt, ...) { if(!warning_type || (warning_type & globalWarningMask)) { va_list ap; va_start(ap, fmt); error_generic("WARNING", fmt, ap); va_end(ap); } } rsbackup-10.0/src/globFiles.cc000066400000000000000000000034311440730431700162540ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Utils.h" #include "Errors.h" #include #include extern "C" { static int globFilesError(const char *epath, int errno_value); }; static std::string globErrorPath; static int globErrno; void globFiles(std::vector &files, const std::string &pattern, int flags) { glob_t g; memset(&g, 0, sizeof g); try { switch(glob(pattern.c_str(), flags, globFilesError, &g)) { case GLOB_NOSPACE: throw SystemError("glob: out of memory"); case GLOB_ABORTED: throw SystemError(globErrorPath, globErrno); default: throw SystemError("glob: unrecognized return value"); case GLOB_NOMATCH: case 0: break; } files.clear(); for(size_t n = 0; n < g.gl_pathc; ++n) files.push_back(g.gl_pathv[n]); } catch(std::runtime_error &) { globfree(&g); throw; } globfree(&g); } static int globFilesError(const char *epath, int errno_value) { // Not re-entrant (stupid API design). Fortunately this isn't a threaded // program... globErrorPath = epath; globErrno = errno_value; return -1; } rsbackup-10.0/src/namelt.cc000066400000000000000000000037411440730431700156320ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include #include "Conf.h" bool namelt(const std::string &a, const std::string &b) { size_t apos = 0, bpos = 0; const size_t asize = a.size(), bsize = b.size(); while(apos < asize && bpos < bsize) { int ach = a[apos], bch = b[bpos]; if(isdigit(ach) && isdigit(bch)) { // Skip leading 0s while(apos < asize && a[apos] == '0') apos++; while(bpos < bsize && b[bpos] == '0') bpos++; size_t astart = apos, bstart = bpos; // Find the ends while(apos < asize && isdigit(a[apos])) apos++; while(bpos < bsize && isdigit(b[bpos])) bpos++; // If the numbers are different lengths, longest wins size_t alen = apos - astart, blen = bpos - bstart; if(alen < blen) return true; if(alen > blen) return false; // The numbers are equal lengths, so lexicograph order matches numeric // order int cmp = a.compare(astart, alen, b, bstart, blen); if(cmp < 0) return true; if(cmp > 0) return false; } else if(isdigit(ach)) { return true; } else if(isdigit(bch)) { return false; } else { if(ach < bch) return true; if(ach > bch) return false; apos++; bpos++; } } return a < b; } rsbackup-10.0/src/nonblock.cc000066400000000000000000000017561440730431700161630ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Errors.h" #include "Utils.h" #include #include void nonblock(int fd) { int ret = fcntl(fd, F_GETFL); if(ret < 0) throw SystemError("fcntl", errno); if(fcntl(fd, F_SETFL, ret | O_NONBLOCK) < 0) throw SystemError("fcntl", errno); } rsbackup-10.0/src/parseFloat.cc000066400000000000000000000032731440730431700164520ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Errors.h" #include "Utils.h" #include #include double parseFloat(const std::string &s, double min, double max, LimitType limitType) { // strtod is a bit loose and accepts e.g. initial whitespace, which we don't // want. if(s.size() > 0 && !((s.at(0) >= '0' && s.at(0) <= '9') || s.at(0) == '-')) throw SyntaxError("invalid integer '" + s + "'"); errno = 0; const char *sc = s.c_str(); char *e; double n = strtod(sc, &e); if(errno) throw SyntaxError("invalid number '" + s + "': " + strerror(errno)); if(*e || e == sc) throw SyntaxError("invalid number '" + s + "'"); switch(limitType) { case InclusiveLimit: if(!(n >= min && n <= max)) throw SyntaxError("number '" + s + "' out of range"); break; case ExclusiveLimit: if(!(n > min && n < max)) throw SyntaxError("number '" + s + "' out of range"); break; default: throw std::logic_error("unrecognized LimitType"); } return n; } rsbackup-10.0/src/parseInteger.cc000066400000000000000000000030721440730431700167770ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Errors.h" #include "Utils.h" #include #include // Convert a string into an integer, throwing a SyntaxError if it is malformed // or outside [min,max]. long long parseInteger(const std::string &s, long long min, long long max, int radix) { // strtoll is a bit loose and accepts e.g. initial whitespace, which we don't // want. if(s.size() > 0 && !((s.at(0) >= '0' && s.at(0) <= '9') || s.at(0) == '-')) throw SyntaxError("invalid integer '" + s + "'"); errno = 0; const char *sc = s.c_str(); char *e; long long n = strtoll(sc, &e, radix); if(errno) throw SyntaxError("invalid integer '" + s + "': " + strerror(errno)); if(*e || e == sc) throw SyntaxError("invalid integer '" + s + "'"); if(n > max || n < min) throw SyntaxError("integer '" + s + "' out of range"); return n; } rsbackup-10.0/src/parseTimeInterval.cc000066400000000000000000000040131440730431700200010ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014, 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Errors.h" #include "Utils.h" #include #include #include /** @brief A unit of time */ struct time_unit { /** @brief Character representing time unit */ int ch; /** @brief Number of seconds in time unit */ int seconds; }; static const struct time_unit time_units[] = { {'d', 86400}, {'h', 3600}, {'m', 60}, {'s', 1}, }; long long parseTimeInterval(std::string s, long long max) { int unit = 0; if(s.size() > 0) { char ch = s.at(s.size() - 1); if(isalpha(ch)) { ch = tolower(ch); unit = -1; for(auto &tu: time_units) { if(ch == tu.ch) { unit = tu.seconds; break; } } if(unit < 0) throw SyntaxError("unrecognized time unit"); s.pop_back(); } } if(unit == 0) throw SyntaxError("time interval must have a unit"); long long n = parseInteger(s, 0); if(n > max / unit) throw SyntaxError("time interval too large to represent"); return n * unit; } std::string formatTimeInterval(long long n) { char buffer[64]; char ch = 0; for(auto &tu: time_units) { if(n % tu.seconds == 0) { n /= tu.seconds; ch = tu.ch; break; } } assert(ch); snprintf(buffer, sizeof buffer, "%lld%c", n, ch); return buffer; } rsbackup-10.0/src/rsbackup-graph.cc000066400000000000000000000122611440730431700172600ustar00rootroot00000000000000// Copyright © 2015-17, 2019 Richard Kettlewell. // // 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 . #include #include #include "Conf.h" #include "Backup.h" #include "Command.h" #include "Selection.h" #include "IO.h" #include "HistoryGraph.h" #include "Errors.h" #include "Utils.h" #include #include static const struct option options[] = { {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"config", required_argument, nullptr, 'c'}, {"debug", no_argument, nullptr, 'd'}, {"database", required_argument, nullptr, 'D'}, {"output", required_argument, nullptr, 'o'}, {"fonts", no_argument, nullptr, 'F'}, {nullptr, 0, nullptr, 0}}; static const char *graphHelpString() { return "Usage:\n" " rsbackup-graph [OPTIONS] [--] [[-]HOST...] [[-]HOST:VOLUME...]\n" "\n" "Options:\n" " --config, -c PATH Set config file (default: " "/etc/rsbackup/config)\n" " --debug, -d Debug output\n" " --database, -D PATH Override database path\n" " --output, -o PATH Output filename\n" " --fonts, -F List supported fonts\n" " --help, -h Display usage message\n" " --version, -V Display version number\n" "\n"; } [[noreturn]] static void help() { IO::out.writef(graphHelpString()); IO::out.close(); exit(0); } [[noreturn]] static void version() { IO::out.writef("%s\n", VERSION); IO::out.close(); exit(0); } static Cairo::ErrorStatus stdout_write_func(const unsigned char *data, unsigned int length) { fwrite(data, 1, length, stdout); if(ferror(stdout)) throw SystemError("writing to stdout", errno); return CAIRO_STATUS_SUCCESS; } static void listFonts() { auto pfm = pango_cairo_font_map_get_default(); PangoFontFamily **families; int n_families; pango_font_map_list_families(pfm, &families, &n_families); std::vector names; for(int n = 0; n < n_families; n++) names.push_back(pango_font_family_get_name(families[n])); std::sort(names.begin(), names.end()); for(auto name: names) IO::out.writef("%s\n", name.c_str()); } int main(int argc, char **argv) { try { int n; const char *output = "rsbackup.png"; VolumeSelections selections; // Override debug if(getenv("RSBACKUP_DEBUG")) globalDebug = true; // Hack to avoid graph generation causing a database upgrade globalCommand.act = false; // Parse options optind = 1; while((n = getopt_long(argc, (char *const *)argv, "+hVdc:D:o:F", options, nullptr)) >= 0) { switch(n) { case 'h': help(); case 'V': version(); case 'c': globalConfigPath = optarg; break; case 'd': globalDebug = true; break; case 'D': globalDatabase = optarg; break; case 'o': output = optarg; break; case 'F': listFonts(); exit(0); default: exit(1); } } if(optind < argc) for(n = optind; n < argc; ++n) selections.add(argv[n]); globalConfig.read(); globalConfig.validate(); globalConfig.readState(); selections.select(globalConfig); // Eliminates segfault with "Failed to wrap object of type // 'PangoLayout'. Hint: this error is commonly caused by failing to call a // library init() function.". // // How you're supposed to know about this I've not discovered. Pango::init(); // Rendering context Render::Context context; // Use a throwaway graph and surface to work out size Cairo::RefPtr surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1); context.cairo = Cairo::Context::create(surface); HistoryGraph graph0(context); graph0.addParts(globalConfig.graphLayout); graph0.set_extent(); graph0.adjustConfig(); // Create the real graph HistoryGraph graph(context); graph.addParts(globalConfig.graphLayout); graph.set_extent(); // Create the real surface surface = Cairo::ImageSurface::create( Cairo::FORMAT_ARGB32, ceil(graph.width), ceil(graph.height)); context.cairo = Cairo::Context::create(surface); graph.render(); if(std::string(output) == "-") surface->write_to_png_stream(&stdout_write_func); else surface->write_to_png(output); return 0; } catch(Error &e) { error("%s", e.what()); if(globalDebug) e.trace(stderr); return 1; } catch(std::runtime_error &e) { error("%s", e.what()); return 1; } } rsbackup-10.0/src/rsbackup.cc000066400000000000000000000226661440730431700161730ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Command.h" #include "Conf.h" #include "Store.h" #include "Errors.h" #include "Document.h" #include "Email.h" #include "IO.h" #include "FileLock.h" #include "Subprocess.h" #include "DeviceAccess.h" #include "Utils.h" #include "Report.h" #include #include #include #include #include std::mutex globalLock; static void commandLineStores(const std::vector &stores, bool mounted); int main(int argc, char **argv) { // The global lock is normally held by the main thread globalLock.lock(); try { if(setlocale(LC_CTYPE, "") == nullptr) throw std::runtime_error(std::string("setlocale: ") + strerror(errno)); // Parse command line globalCommand.parse(argc, argv); // Read configuration globalConfig.read(); // Validate configuration globalConfig.validate(); // Dump configuration if(globalCommand.dumpConfig) { globalConfig.write(std::cout, 0, !!(globalWarningMask & WARNING_VERBOSE)); exit(0); } // Override stores if(globalCommand.stores.size() != 0 || globalCommand.unmountedStores.size() != 0) { for(auto &s: globalConfig.stores) s.second->state = Store::Disabled; commandLineStores(globalCommand.stores, true); commandLineStores(globalCommand.unmountedStores, false); } // Take the lock, if one is defined. FileLock lockFile(globalConfig.lock); if((globalCommand.backup || globalCommand.prune || globalCommand.pruneIncomplete || globalCommand.retireDevice || globalCommand.retire) && globalConfig.lock.size()) { D("attempting to acquire lockfile %s", globalConfig.lock.c_str()); if(!lockFile.acquire(globalCommand.wait)) { // Failing to acquire the lock is not really an error if --wait was not // requested. warning(WARNING_VERBOSE, "cannot acquire lockfile %s", globalConfig.lock.c_str()); exit(0); } } // Select volumes if(globalCommand.backup || globalCommand.prune || globalCommand.pruneIncomplete) globalCommand.selections.select(globalConfig); // Execute commands if(globalCommand.backup) makeBackups(); if(globalCommand.retire) retireVolumes(!globalCommand.forgetOnly); if(globalCommand.retireDevice) retireDevices(); if(globalCommand.prune || globalCommand.pruneIncomplete) pruneBackups(); if(globalCommand.prune) prunePruneLogs(); if(globalCommand.checkUnexpected) checkUnexpected(); if(globalCommand.latest) findLatest(); // Run post-device hook postDeviceAccess(); // Generate report if(globalCommand.html || globalCommand.text || globalCommand.email) { globalConfig.readState(); Document d; if(globalConfig.stylesheet.size()) { IO ssf; ssf.open(globalConfig.stylesheet, "r"); ssf.readall(d.htmlStyleSheet); } else d.htmlStyleSheet = stylesheet; // Include user colors in the stylesheet std::stringstream ss; ss << "td.bad { background-color: #" << globalConfig.colorBad << " }\n"; ss << "td.good { background-color: #" << globalConfig.colorGood << " }\n"; ss << "span.bad { color: #" << globalConfig.colorBad << " }\n"; d.htmlStyleSheet += ss.str(); Report report(d); report.generate(); if(globalCommand.html) { std::stringstream htmlStream; RenderDocumentContext htmlRenderDocumentContext; d.renderHtml(htmlStream, nullptr); if(*globalCommand.html == "-") { IO::out.write(htmlStream.str()); } else { IO f; f.open(*globalCommand.html, "w"); f.write(htmlStream.str()); f.close(); } } if(globalCommand.text) { IO *f; if(*globalCommand.text == "-") { f = &IO::out; } else { f = new IO(); f->open(*globalCommand.text, "w"); } std::stringstream textStream; RenderDocumentContext textRenderDocumentContext; int w = f->width(); if(w) textRenderDocumentContext.width = w; d.renderText(textStream, &textRenderDocumentContext); f->write(textStream.str()); if(f != &IO::out) f->close(); } if(globalCommand.email) { std::stringstream htmlStream, textStream; RenderDocumentContext htmlRenderDocumentContext; RenderDocumentContext textRenderDocumentContext; d.renderHtml(htmlStream, &htmlRenderDocumentContext); d.renderText(textStream, &textRenderDocumentContext); Email e; e.addTo(*globalCommand.email); std::stringstream subject; subject << d.title; if(report.backups_missing) subject << " missing:" << report.backups_missing; if(report.backups_partial) subject << " partial:" << report.backups_partial; if(report.backups_out_of_date) subject << " stale:" << report.backups_out_of_date; if(report.backups_failed) subject << " failed:" << report.backups_failed; if(report.devices_unknown || report.hosts_unknown || report.volumes_unknown) subject << " unknown:" << (report.devices_unknown + report.hosts_unknown + report.volumes_unknown); e.setSubject(subject.str()); e.setType("multipart/related; boundary=" MIME_BOUNDARY MIME1); std::stringstream body; body << "--" MIME_BOUNDARY MIME1 "\n"; body << "Content-Type: multipart/alternative; boundary=" MIME_BOUNDARY MIME2 "\n"; body << "\n"; body << "--" MIME_BOUNDARY MIME2 "\n"; body << "Content-Type: text/plain\n"; body << "\n"; body << textStream.str(); body << "\n"; body << "--" MIME_BOUNDARY MIME2 "\n"; body << "Content-Type: text/html\n"; body << "\n"; body << htmlStream.str(); body << "\n"; body << "--" MIME_BOUNDARY MIME2 "--\n"; body << "\n"; for(auto image: htmlRenderDocumentContext.images) { body << "--" MIME_BOUNDARY MIME1 "\n"; body << "Content-ID: <" << image->ident() << ">\n"; body << "Content-Type: " << image->type << "\n"; body << "Content-Transfer-Encoding: base64\n"; body << "\n"; std::stringstream ss; write_base64(ss, image->content); body << ss.str(); body << "\n"; } body << "--" MIME_BOUNDARY MIME1 "--\n"; e.setContent(body.str()); e.send(); } } if(globalErrors) warning(WARNING_VERBOSE, "%d errors detected", globalErrors); IO::out.close(); } catch(Error &e) { error("%s", e.what()); if(globalDebug) e.trace(stderr); } catch(std::regex_error &e) { switch(e.code()) { case std::regex_constants::error_collate: error("std::regex_constants::error_collate"); break; case std::regex_constants::error_ctype: error("std::regex_constants::error_ctype"); break; case std::regex_constants::error_escape: error("std::regex_constants::error_escape"); break; case std::regex_constants::error_backref: error("std::regex_constants::error_backref"); break; case std::regex_constants::error_brack: error("std::regex_constants::error_brack"); break; case std::regex_constants::error_paren: error("std::regex_constants::error_paren"); break; case std::regex_constants::error_brace: error("std::regex_constants::error_brace"); break; case std::regex_constants::error_badbrace: error("std::regex_constants::error_badbrace"); break; case std::regex_constants::error_range: error("std::regex_constants::error_range"); break; case std::regex_constants::error_space: error("std::regex_constants::error_space"); break; case std::regex_constants::error_badrepeat: error("std::regex_constants::error_badrepeat"); break; case std::regex_constants::error_complexity: error("std::regex_constants::error_complexity"); break; case std::regex_constants::error_stack: error("std::regex_constants::error_stack"); break; default: error("regex error code %d", e.code()); break; } } catch(std::runtime_error &e) { error("%s", e.what()); } exit(!!globalErrors); } static void commandLineStores(const std::vector &stores, bool mounted) { for(auto &s: stores) { auto it = globalConfig.stores.find(s); if(it == globalConfig.stores.end()) globalConfig.stores[s] = new Store(s, mounted); else it->second->state = Store::Enabled; } } rsbackup-10.0/src/rsbackup.h000066400000000000000000000026611440730431700160260ustar00rootroot00000000000000//-*-C++-*- // Copyright © Richard Kettlewell. // // 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 . #ifndef RSBACKUP_H #define RSBACKUP_H /** @file rsbackup.h * @brief Common definitions */ #include #include #include class Document; /** @brief Make backups */ void makeBackups(); /** @brief Retire volumes */ void retireVolumes(bool remove); /** @brief Retire devices */ void retireDevices(); /** @brief Prune backups */ void pruneBackups(); /** @brief Prune redundant logs */ void prunePruneLogs(); /** @brief Check backups for unexpected files */ void checkUnexpected(); /** @brief Find latest backups */ void findLatest(); /** @brief HTML stylesheet */ extern char stylesheet[]; /** @brief Error count */ extern int globalErrors; /** @brief Global state lock */ extern std::mutex globalLock; #endif /* RSBACKUP_H */ rsbackup-10.0/src/split.cc000066400000000000000000000044561440730431700155110ustar00rootroot00000000000000// Copyright © 2011, 2012, 2014, 2015, 2017 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Errors.h" #include "Utils.h" // Split a string into words. Words are separated by whitespace but can also // be quoted; inside quotes a backslash escapes any character. A '#' where the // start of a word would be introduces a comment which consumes the rest of the // line. void split(std::vector &bits, const std::string &line, size_t *indent) { bits.clear(); std::string::size_type pos = 0; std::string s; char c; if(indent) { size_t i = 0; while(pos < line.size() && ((c = line.at(pos)) == ' ' || c == '\t')) { if(c == ' ') ++i; else i = (i + 8) & ~static_cast(7); ++pos; } *indent = i; } while(pos < line.size()) { c = line.at(pos); switch(c) { case ' ': case '\t': case '\r': case '\f': ++pos; break; case '#': return; case '"': s.clear(); ++pos; while(pos < line.size() && line.at(pos) != '"') { if(line.at(pos) == '\\') { ++pos; if(pos >= line.size()) throw SyntaxError("unterminated string"); } s += line.at(pos); ++pos; } if(pos >= line.size()) throw SyntaxError("unterminated string"); ++pos; bits.push_back(s); break; case '\\': throw SyntaxError("unquoted backslash"); default: s.clear(); while(pos < line.size() && !isspace(line.at(pos)) && line.at(pos) != '"' && line.at(pos) != '\\') { s += line.at(pos); ++pos; } bits.push_back(s); break; } } } rsbackup-10.0/src/stylesheet.cc000066400000000000000000000047221440730431700165430ustar00rootroot00000000000000char stylesheet[] = "/* Copyright © 2011 Richard Kettlewell.\n" " *\n" " * This program is free software: you can redistribute it and/or modify\n" " * it under the terms of the GNU General Public License as published by\n" " * the Free Software Foundation, either version 3 of the License, or\n" " * (at your option) any later version.\n" " *\n" " * This program is distributed in the hope that it will be useful,\n" " * but WITHOUT ANY WARRANTY; without even the implied warranty of\n" " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" " * GNU General Public License for more details.\n" " *\n" " * You should have received a copy of the GNU General Public License\n" " * along with this program. If not, see http://www.gnu.org/licenses/.\n" " */\n" "body {\n" " color: black;\n" " background-color: white\n" "}\n" "\n" "a:link, a:visited, a:active {\n" " color: blue;\n" " text-decoration: underline\n" "}\n" "\n" "h1 {\n" " background-color: #e0ffe0;\n" " padding: 0.2em\n" "}\n" "\n" "h2 {\n" " background-color: #e0e0e0;\n" " padding: 0.2em\n" "}\n" "\n" "h3 {\n" " text-decoration: underline\n" "}\n" "\n" "a.h3 {\n" " margin-left: 1em\n" "}\n" "\n" "h1,h2,h3,h4 {\n" " font-family: sans-serif\n" "}\n" "\n" "table {\n" " border-collapse: collapse\n" "}\n" "\n" "th {\n" " background-color: #e0e0e0;\n" " border-left: 1px solid #e0e0e0;\n" " border-top: 1px solid #e0e0e0;\n" " border-right: 1px solid #e0e0e0\n" "}\n" "\n" "td {\n" " border: 1px solid black;\n" " vertical-align: top;\n" " padding-left: 4px;\n" " padding-right: 4px\n" "}\n" "\n" "td.bad {\n" " background-color: #ff4040;\n" " color: #ffffff\n" "}\n" "\n" "td.good {\n" " background-color: #e0ffe0;\n" " color: #000000\n" "}\n" "\n" "span.bad {\n" " color: #ff4040\n" "}\n" "\n" "pre.log {\n" " background-color: #f0f0f0\n" "}\n" "\n" ".example {\n" " border-left: 2px solid black;\n" " padding-left: 2px\n" "}\n" "\n" "div.volume {\n" " margin-left: 1em\n" "}\n" "\n" ".recent {\n" " color: #ff4040\n" "}\n" "\n" "img.history {\n" " border: 1px solid black;\n" " padding: 2px\n" "}\n"; rsbackup-10.0/src/substitute.cc000066400000000000000000000030661440730431700165650ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include std::string substitute(const std::string &s, std::string::size_type pos, std::string::size_type n) { std::string r; pos = std::min(s.size(), pos); n = std::min(n, s.size() - pos); std::string::size_type l = pos + n; while(pos < l) { char c = s.at(pos++); switch(c) { default: r += c; break; case '\\': if(pos >= l) r += c; else r += s.at(pos++); break; case '$': if(pos < l && s.at(pos) == '{') { std::string::size_type e = s.find('}', pos); if(e < l) { std::string name(s, pos + 1, e - (pos + 1)); const char *value = getenv(name.c_str()); if(value) { r += value; pos = e + 1; break; } } } r += c; break; } } return r; } rsbackup-10.0/src/test-action.cc000066400000000000000000000172371440730431700166110ustar00rootroot00000000000000// Copyright © 2016, 2020 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "EventLoop.h" #include "Action.h" #include "Subprocess.h" #include #include #include static int action_number; class SimpleAction: public Action { public: SimpleAction(const std::string &n): Action(n) {} void go(EventLoop *, ActionList *al) override { acted = ++action_number; al->completed(this, true); } int acted = 0; }; static void test_action_simple() { SimpleAction a1("a1"), a2("a2"), a3("a3"); EventLoop e; ActionList al(&e); al.add(&a1); al.add(&a2); al.add(&a3); al.go(); assert(a1.acted); assert(a2.acted); assert(a3.acted); } class SlowAction: public Action, public Reactor { public: SlowAction(const std::string &n, bool outcome = true): Action(n), outcome(outcome) {} void check() { for(auto a: require_not_acting) assert(!a->acting); for(auto a: require_complete) assert(a->acted); for(auto a: require_not_complete) assert(!a->acted); } void go(EventLoop *e, ActionList *al) override { check(); acting = true; this->al = al; struct timespec now; getMonotonicTime(now); now.tv_nsec += 10 * 10000000; now.tv_sec += now.tv_nsec / 1000000000; now.tv_sec %= 1000000000; e->whenTimeout(now, this); } void onTimeout(EventLoop *, const struct timespec &) override { check(); acting = false; acted = ++action_number; al->completed(this, outcome); } bool acting = false; int acted = 0; bool outcome; ActionList *al; std::vector require_not_acting; std::vector require_complete; std::vector require_not_complete; }; static void test_action_resources() { SlowAction a1("a1"), a2("a2"), a3("a3"); EventLoop e; ActionList al(&e); al.add(&a1); a1.uses("r1"); a1.require_not_acting.push_back(&a2); al.add(&a2); a2.uses("r1"); a2.require_not_acting.push_back(&a1); al.add(&a3); a3.uses("r2"); al.go(true); assert(a1.acted); assert(a2.acted); assert(a3.acted); assert(!a1.acting); assert(!a2.acting); assert(!a3.acting); } static void test_action_dependencies() { SlowAction a1("a1"), a2("a2"), a3("a3"); EventLoop e; ActionList al(&e); al.add(&a1); a1.after("a2", 0); a1.require_not_acting.push_back(&a2); a1.require_not_acting.push_back(&a3); a1.require_complete.push_back(&a2); a1.require_complete.push_back(&a3); al.add(&a2); a2.after("a3", 0); a2.require_not_acting.push_back(&a1); a2.require_not_acting.push_back(&a3); a2.require_complete.push_back(&a3); a2.require_not_complete.push_back(&a1); al.add(&a3); a3.require_not_acting.push_back(&a1); a3.require_not_acting.push_back(&a2); a2.require_not_complete.push_back(&a1); a2.require_not_complete.push_back(&a2); al.go(true); assert(a1.acted); assert(a2.acted); assert(a3.acted); assert(!a1.acting); assert(!a2.acting); assert(!a3.acting); } static void test_action_status() { SlowAction a1("a1"), a2("a2", false), a3("a3", false); EventLoop e; ActionList al(&e); al.add(&a1); a1.after("a2", ACTION_SUCCEEDED); a1.require_not_acting.push_back(&a2); a1.require_not_acting.push_back(&a3); a1.require_complete.push_back(&a2); a1.require_complete.push_back(&a3); al.add(&a2); a2.after("a3", 0); a2.require_not_acting.push_back(&a1); a2.require_not_acting.push_back(&a3); a2.require_complete.push_back(&a3); a2.require_not_complete.push_back(&a1); al.add(&a3); a3.require_not_acting.push_back(&a1); a3.require_not_acting.push_back(&a2); a2.require_not_complete.push_back(&a1); a2.require_not_complete.push_back(&a2); al.go(true); assert(!a1.acted); assert(a2.acted); assert(a3.acted); assert(!a1.acting); assert(!a2.acting); assert(!a3.acting); } static void test_action_glob() { SlowAction m1("middle/1"), m2("middle/2"); SlowAction s("start"); SlowAction l("last"); EventLoop e; ActionList al(&e); al.add(&s); s.require_not_acting.push_back(&m1); s.require_not_acting.push_back(&m2); s.require_not_acting.push_back(&l); s.require_not_complete.push_back(&m1); s.require_not_complete.push_back(&m2); s.require_not_complete.push_back(&l); al.add(&m1); m1.after("start", ACTION_SUCCEEDED); m1.require_complete.push_back(&s); m1.require_not_acting.push_back(&l); m1.require_not_complete.push_back(&l); al.add(&m2); m2.after("start", ACTION_SUCCEEDED); m2.require_complete.push_back(&s); m2.require_not_acting.push_back(&l); m2.require_not_complete.push_back(&l); al.add(&l); l.after("middle/*", ACTION_SUCCEEDED | ACTION_GLOB); l.require_complete.push_back(&s); l.require_complete.push_back(&m1); l.require_complete.push_back(&m2); al.go(true); assert(s.acted); assert(m1.acted); assert(m2.acted); assert(l.acted); } static void test_action_glob_status() { SlowAction m1("middle/1"), m2("middle/2", false); SlowAction s("start", false); SlowAction l("last"); EventLoop e; ActionList al(&e); al.add(&s); s.require_not_acting.push_back(&m1); s.require_not_acting.push_back(&m2); s.require_not_acting.push_back(&l); s.require_not_complete.push_back(&m1); s.require_not_complete.push_back(&m2); s.require_not_complete.push_back(&l); al.add(&m1); m1.after("start", 0); m1.require_complete.push_back(&s); m1.require_not_acting.push_back(&l); m1.require_not_complete.push_back(&l); al.add(&m2); m2.after("start", 0); m2.require_complete.push_back(&s); m2.require_not_acting.push_back(&l); m2.require_not_complete.push_back(&l); al.add(&l); l.after("middle/*", ACTION_SUCCEEDED | ACTION_GLOB); l.require_complete.push_back(&s); l.require_complete.push_back(&m1); l.require_complete.push_back(&m2); al.go(true); assert(s.acted); assert(m1.acted); assert(m2.acted); assert(!l.acted); } static void test_action_priority() { EventLoop e; ActionList al(&e); SimpleAction a("a"), b("b"), c("c"), d("d"); a.set_priority(1); b.set_priority(2); c.set_priority(3); d.set_priority(4); al.add(&b); al.add(&c); al.add(&d); al.add(&a); action_number = 0; al.go(true); assert(a.acted == 4); assert(b.acted == 3); assert(c.acted == 2); assert(d.acted == 1); } static void test_action_timelimit(void) { EventLoop e; ActionList al(&e); struct timespec started, finished, limit; getMonotonicTime(started); limit = started; limit.tv_nsec += 1000000 * 100; al.setLimit(limit); Subprocess sleep("sleep", {"sh", "-c", "sleep 120"}); al.add(&sleep); SimpleAction a("a"); a.after("sleep", 0); al.add(&a); al.go(); getMonotonicTime(finished); assert(finished.tv_sec - started.tv_sec <= 1); int status = sleep.getStatus(); assert(WIFSIGNALED(status)); assert(WTERMSIG(status) == SIGTERM); assert(a.acted == 0); } int main() { // debug = true; test_action_simple(); test_action_resources(); test_action_dependencies(); test_action_status(); test_action_glob(); test_action_glob_status(); test_action_priority(); test_action_timelimit(); return 0; } rsbackup-10.0/src/test-base64.cc000066400000000000000000000022521440730431700164070ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include #include "Utils.h" static std::string base64(const std::string &s) { std::stringstream ss; write_base64(ss, s); std::cerr << "[" << s << "] -> [" << ss.str() << "]\n"; return ss.str(); } int main() { assert(base64("") == ""); assert(base64("f") == "Zg=="); assert(base64("fo") == "Zm8="); assert(base64("foo") == "Zm9v"); assert(base64("foob") == "Zm9vYg=="); assert(base64("fooba") == "Zm9vYmE="); assert(base64("foobar") == "Zm9vYmFy"); } rsbackup-10.0/src/test-check.cc000066400000000000000000000075301440730431700164040ustar00rootroot00000000000000// Copyright © 2014-15, 2019 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "Errors.h" #include "Command.h" #include #include #include #include static FILE *input; static int output; static bool result; static void *background(void *) { result = check("%s", "spong"); return nullptr; } /* We can't use stdio here because reading from an unbuffered file causes a * flush of other files, which (at least in Glibc) implies taking a lock on * other files. Since the background thread is blocked in a read, holding a * lock while it does so, a deadlock results. * * References: * - C99 7.19.3#3 * - http://austingroupbugs.net/view.php?id=689 * - _IO_flush_all_linebuffered in Glibc */ static int fd_getc(int fd) { unsigned char buffer[1]; if(read(fd, buffer, 1) < 0) return EOF; else return buffer[0]; } static char *fd_fgets(char buffer[], size_t bufsize, int fd) { size_t index = 0; int ch; assert(bufsize > 0); while(index + 1 < bufsize && (ch = fd_getc(fd)) >= 0) { buffer[index++] = ch; if(ch == '\n') break; } buffer[index] = 0; return ch < 0 ? nullptr : buffer; } static void test(const char *typed, const char *typed2, bool expect) { pthread_t tid; char buffer[1024]; assert(pthread_create(&tid, nullptr, background, nullptr) == 0); assert(fd_fgets(buffer, sizeof buffer, output)); assert(std::string(buffer) == "spong\n"); assert(fd_getc(output) == 'y'); assert(fd_getc(output) == 'e'); assert(fd_getc(output) == 's'); assert(fd_getc(output) == '/'); assert(fd_getc(output) == 'n'); assert(fd_getc(output) == 'o'); assert(fd_getc(output) == '>'); assert(fd_getc(output) == ' '); assert(fprintf(input, "%s\n", typed) >= 0); assert(fflush(input) >= 0); if(typed2) { assert(fd_fgets(buffer, sizeof buffer, output)); assert(std::string(buffer) == "Please answer 'yes' or 'no'.\n"); assert(fd_fgets(buffer, sizeof buffer, output)); assert(std::string(buffer) == "spong\n"); assert(fd_getc(output) == 'y'); assert(fd_getc(output) == 'e'); assert(fd_getc(output) == 's'); assert(fd_getc(output) == '/'); assert(fd_getc(output) == 'n'); assert(fd_getc(output) == 'o'); assert(fd_getc(output) == '>'); assert(fd_getc(output) == ' '); assert(fprintf(input, "%s\n", typed2) >= 0); assert(fflush(input) >= 0); } assert(pthread_join(tid, nullptr) == 0); assert(result == expect); } static void test_force(void) { pthread_t tid; globalCommand.force = true; assert(pthread_create(&tid, nullptr, background, nullptr) == 0); assert(pthread_join(tid, nullptr) == 0); assert(result == true); globalCommand.force = false; } int main() { int i[2], o[2]; assert(pipe(i) >= 0); assert(pipe(o) >= 0); assert(dup2(i[0], 0) >= 0); assert(close(i[0]) >= 0); assert(dup2(o[1], 1) >= 0); assert(close(o[1]) >= 0); assert((input = fdopen(i[1], "w"))); output = o[0]; test("yes", nullptr, true); test("no", nullptr, false); test("", "yes", true); test("whatever", "yes", true); test_force(); assert(fclose(input) >= 0); try { check("%s", "spong"); assert(!"unexpectedly succeeded"); } catch(IOError &e) { } return 0; } rsbackup-10.0/src/test-color.cc000066400000000000000000000044371440730431700164500ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Color.h" #include #include #include int main() { for(unsigned n = 0; n < 256; ++n) { Color c(n); unsigned u = c; printf("%s:%d: c=(%g,%g,%g) n=%u u=%u\n", __FILE__, __LINE__, c.red, c.green, c.blue, n, u); assert(u == n); c = Color(n << 8); u = c >> 8; printf("%s:%d: c=(%g,%g,%g) n=%u u=%u\n", __FILE__, __LINE__, c.red, c.green, c.blue, n, u); assert(u == n); c = Color(n << 16); u = c >> 16; printf("%s:%d: c=(%g,%g,%g) n=%u u=%u\n", __FILE__, __LINE__, c.red, c.green, c.blue, n, u); assert(u == n); } assert(Color::HSV(0, 1, 1) == Color(0xFF0000)); assert(Color::HSV(60, 1, 1) == Color(0xFFFF00)); assert(Color::HSV(120, 1, 1) == Color(0x00FF00)); assert(Color::HSV(180, 1, 1) == Color(0x00FFFF)); assert(Color::HSV(240, 1, 1) == Color(0x0000FF)); assert(Color::HSV(300, 1, 1) == Color(0xFF00FF)); assert(Color::HSV(30, 1, 1) == Color(0xFF7F00)); assert(Color::HSV(90, 1, 1) == Color(0x7FFF00)); assert(Color::HSV(150, 1, 1) == Color(0x00FF7F)); assert(Color::HSV(210, 1, 1) == Color(0x007FFF)); assert(Color::HSV(270, 1, 1) == Color(0x7F00FF)); assert(Color::HSV(330, 1, 1) == Color(0xFF007F)); { Color c = 0xE0FFE0; unsigned u = static_cast(c); printf("%06x %06x\n", u, static_cast(c)); assert(u == 0xE0FFE0); std::stringstream ss; printf("c=(%g,%g,%g)=(%A,%A,%A)\n", c.red, c.green, c.blue, c.red, c.green, c.blue); ss << c; puts(ss.str().c_str()); assert(ss.str() == "e0ffe0"); } return 0; } rsbackup-10.0/src/test-command.cc000066400000000000000000000160161440730431700167440ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Command.h" #include "Errors.h" #include "Conf.h" #include #include static void test_action_backup(void) { static const char *argv[] = {"rsbackup", "--backup", nullptr}; Command c; assert(c.backup == false); c.parse(2, argv); assert(c.backup == true); } static void test_action_html(void) { static const char *argv[] = {"rsbackup", "--html", "PATH", nullptr}; Command c; assert(c.html == nullptr); c.parse(3, argv); assert(c.html != nullptr); assert(*c.html == "PATH"); } static void test_action_text(void) { static const char *argv[] = {"rsbackup", "--text", "PATH", nullptr}; Command c; assert(c.text == nullptr); c.parse(3, argv); assert(c.text != nullptr); assert(*c.text == "PATH"); } static void test_action_email(void) { static const char *argv[] = {"rsbackup", "--email", "user@domain", nullptr}; Command c; assert(c.email == nullptr); c.parse(3, argv); assert(c.email != nullptr); assert(*c.email == "user@domain"); } static void test_action_prune(void) { static const char *argv[] = {"rsbackup", "--prune", nullptr}; Command c; assert(c.prune == false); c.parse(2, argv); assert(c.prune == true); } static void test_action_prune_incomplete(void) { static const char *argv[] = {"rsbackup", "--prune-incomplete", nullptr}; Command c; assert(c.pruneIncomplete == false); c.parse(2, argv); assert(c.pruneIncomplete == true); } static void test_action_retire(void) { static const char *argv[] = {"rsbackup", "--retire", "VOLUME", nullptr}; Command c; assert(c.retire == false); c.parse(3, argv); assert(c.retire == true); assert(c.selections.size() == 1); Command d; assert(d.retire == false); try { d.parse(2, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } } static void test_action_retire_device(void) { static const char *argv[] = {"rsbackup", "--retire-device", "DEVICE", nullptr}; Command c; assert(c.retireDevice == false); c.parse(3, argv); assert(c.retireDevice == true); assert(c.devices.size() == 1); assert(c.devices.at(0) == "DEVICE"); Command d; assert(d.retireDevice == false); try { d.parse(2, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } } static void test_action_dump_config(void) { static const char *argv[] = {"rsbackup", "--dump-config", "JUNK", nullptr}; Command c; assert(c.dumpConfig == false); c.parse(2, argv); assert(c.dumpConfig == true); Command d; assert(d.dumpConfig == false); try { d.parse(3, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } } static void test_action_none(void) { static const char *argv[] = {"rsbackup", nullptr}; Command c; try { c.parse(1, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } } static void test_action_incompatible(void) { try { static const char *argv[] = {"rsbackup", "--retire", "--retire-device", "XYZ", nullptr}; Command c; c.parse(4, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { assert(std::string(e.what()).find("cannot be used together") != std::string::npos); } try { static const char *argv[] = {"rsbackup", "--retire", "--backup", "XYZ", nullptr}; Command c; c.parse(4, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { assert(std::string(e.what()).find("cannot be used together") != std::string::npos); } try { static const char *argv[] = {"rsbackup", "--retire-device", "--backup", "XYZ", nullptr}; Command c; c.parse(4, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { assert(std::string(e.what()).find("cannot be used together") != std::string::npos); } try { static const char *argv[] = {"rsbackup", "--dump-config", "--backup", "XYZ", nullptr}; Command c; c.parse(4, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { assert(std::string(e.what()).find("cannot be used with any other action") != std::string::npos); } } static void test_selection(void) { { static const char *argv[] = {"rsbackup", "--backup", "A", "-A:B", "!C", nullptr}; Command c; c.parse(5, argv); assert(c.backup == true); assert(c.selections.size() == 3); assert(c.selections[0].sense == true); assert(c.selections[0].host == "A"); assert(c.selections[0].volume == "*"); assert(c.selections[1].sense == false); assert(c.selections[1].host == "A"); assert(c.selections[1].volume == "B"); assert(c.selections[2].sense == false); assert(c.selections[2].host == "C"); assert(c.selections[2].volume == "*"); } try { static const char *argv[] = {"rsbackup", "--backup", "~A", nullptr}; Command c; c.parse(3, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } try { static const char *argv[] = {"rsbackup", "--backup", "A:~", nullptr}; Command c; c.parse(3, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } try { static const char *argv[] = {"rsbackup", "--backup", "A:B:C", nullptr}; Command c; c.parse(3, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } try { static const char *argv[] = {"rsbackup", "--backup", "*:C", nullptr}; Command c; c.parse(3, argv); assert(!"unexpectedly succeeded"); } catch(CommandError &e) { } } int main() { int errors = 0; const std::string h = Command::helpString(); for(size_t n = 0; Command::options[n].name; ++n) { std::string full = "--" + std::string(Command::options[n].name); if(h.find(full + ",") == std::string::npos && h.find(full + " ") == std::string::npos) { fprintf(stderr, "ERROR: help for option %s not found\n", full.c_str()); ++errors; } } test_action_backup(); test_action_html(); test_action_text(); test_action_email(); test_action_prune(); test_action_prune_incomplete(); test_action_retire(); test_action_retire_device(); test_action_dump_config(); test_action_none(); test_action_incompatible(); test_selection(); return !!errors; } rsbackup-10.0/src/test-confbase.cc000066400000000000000000000027411440730431700171060ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Errors.h" #include "ConfBase.h" #include void test_quote() { assert(ConfBase::quote("") == "\"\""); assert(ConfBase::quote("x") == "x"); assert(ConfBase::quote("#") == "\"#\""); assert(ConfBase::quote(" ") == "\" \""); assert(ConfBase::quote("x y") == "\"x y\""); assert(ConfBase::quote("\\") == "\"\\\\\""); assert(ConfBase::quote("\"") == "\"\\\"\""); } void test_quote_vector() { std::vector vs = { "", "x", "#", " ", "x y", "\\", "\"", }; std::string s = ConfBase::quote(vs); assert(s == "\"\" x \"#\" \" \" \"x y\" \"\\\\\" \"\\\"\""); } void test_indent() { assert(ConfBase::indent(0) == ""); assert(ConfBase::indent(3) == " "); } int main(void) { test_indent(); test_quote(); test_quote_vector(); return 0; } rsbackup-10.0/src/test-database.cc000066400000000000000000000041761440730431700170760ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "Database.h" #include #include #define DBPATH "test.db" static void test_create() { Database d(DBPATH); assert(!d.hasTable("t")); d.execute("CREATE TABLE t (i INT PRIMARY KEY, s TEXT)"); assert(d.hasTable("t")); } static void test_populate() { Database d(DBPATH); d.begin(); Database::Statement(d, "INSERT INTO t (i, s) VALUES (?, ?)", SQL_INT, 0, SQL_CSTRING, "zero", SQL_END) .next(); Database::Statement(d, "INSERT INTO t (i, s) VALUES (?, ?)", SQL_INT64, (sqlite_int64)1, SQL_CSTRING, "one", SQL_END) .next(); d.commit(); } static void test_retrieve() { Database d(DBPATH); { Database::Statement s(d); s.prepare("SELECT s FROM t WHERE i = ?", SQL_INT, 0, SQL_END); assert(s.next()); std::string str = s.get_string(0); assert(str == "zero"); assert(!s.next()); } { Database::Statement s(d); s.prepare("SELECT i FROM t WHERE s = ?", SQL_CSTRING, "one", SQL_END); assert(s.next()); int n = s.get_int(0); assert(n == 1); assert(!s.next()); } { Database::Statement s(d); s.prepare("SELECT i FROM t WHERE s = ?", SQL_CSTRING, "zero", SQL_END); assert(s.next()); sqlite_int64 n = s.get_int64(0); assert(n == 0); assert(!s.next()); } } int main() { unlink(DBPATH); test_create(); test_populate(); test_retrieve(); unlink(DBPATH); return 0; } rsbackup-10.0/src/test-date.cc000066400000000000000000000116461440730431700162470ustar00rootroot00000000000000// Copyright © 2012 Richard Kettlewell. // // 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 . #include #include "Date.h" #include "Errors.h" #include #include #include #define assert_throws(expr, except) \ do { \ try { \ (expr); \ assert(!"unexpected succeeded"); \ } catch(except & e) { \ } \ } while(0) int main() { Date d("1997-03-02"); assert(d.toString() == "1997-03-02"); assert(d.toNumber() == (1997 * 365 + (1997 / 4) - (1997 / 100) + (1997 / 400) + 31 + 28 + 2 - 1)); Date dd(d.toTime()); assert(d.toString() == dd.toString()); std::stringstream s; s << d; assert(s.str() == "1997-03-02"); Date e("1998-03-02"); assert(e.toString() == "1998-03-02"); assert(e.toNumber() == (1998 * 365 + (1997 / 4) - (1997 / 100) + (1997 / 400) + 31 + 28 + 2 - 1)); assert(e - d == 365); Date ee(e.toTime()); assert(e.toString() == ee.toString()); Date f; assert(f.toString() == "0000-01-01"); Date t = Date::today(); printf("today = %s = %d\n", t.toString().c_str(), t.toNumber()); Date tt(t.toTime()); assert(t.toString() == tt.toString()); assert(Date("1997-03-01") < Date("1997-03-02")); assert(Date("1997-03-02") < Date("1997-04-01")); assert(Date("1997-03-02") < Date("1998-01-01")); assert(Date::monthLength(1999, 1) == 31); assert(Date::monthLength(1999, 2) == 28); assert(Date::monthLength(2000, 2) == 29); assert(Date::monthLength(2004, 2) == 29); assert(Date::monthLength(2100, 2) == 28); assert_throws(Date(""), InvalidDate); assert_throws(Date("whatever"), InvalidDate); assert_throws(Date("2012"), InvalidDate); assert_throws(Date("2012-03"), InvalidDate); assert_throws(Date("2012--03-01"), InvalidDate); assert_throws(Date("2012-03-04-05"), InvalidDate); assert_throws(Date("2012/03/04"), InvalidDate); assert_throws(Date("0-01-01"), InvalidDate); assert_throws(Date("2012-00-01"), InvalidDate); assert_throws(Date("2012-13-01"), InvalidDate); assert_throws(Date("2012-01-00"), InvalidDate); assert_throws(Date("2012-01-32"), InvalidDate); assert_throws(Date("2011-02-29"), InvalidDate); assert_throws(Date("0x100-02-29"), InvalidDate); assert_throws(Date("2147483648-02-01"), InvalidDate); assert_throws(Date("9223372036854775808-02-21"), InvalidDate); static const struct { const char *before, *after; } day_increment_tests[] = { {"1995-01-01", "1995-01-02"}, {"1995-01-31", "1995-02-01"}, {"1995-02-28", "1995-03-01"}, {"1995-12-31", "1996-01-01"}, {"1996-02-28", "1996-02-29"}, {"1996-02-29", "1996-03-01"}, }; for(auto &t: day_increment_tests) { d = t.before; assert(d.toString() == t.before); ++d; printf("%s:%d: %s %s %s\n", __FILE__, __LINE__, t.before, t.after, d.toString().c_str()); assert(d.toString() == t.after); } static const struct { const char *before, *after; } month_increment_tests[] = { {"1995-01-01", "1995-02-01"}, {"1995-01-31", "1995-02-28"}, {"1995-02-28", "1995-03-28"}, {"1995-12-31", "1996-01-31"}, {"1996-01-28", "1996-02-28"}, {"1996-01-29", "1996-02-29"}, {"1996-01-30", "1996-02-29"}, {"1996-02-28", "1996-03-28"}, {"1996-02-29", "1996-03-29"}, }; for(auto &t: month_increment_tests) { d = t.before; assert(d.toString() == t.before); d.addMonth(); printf("%s:%d: %s %s %s\n", __FILE__, __LINE__, t.before, t.after, d.toString().c_str()); assert(d.toString() == t.after); } static const struct { const std::string a, b; int delta; } subtract_tests[] = { {"2015-12-01", "2015-11-01", 30}, {"2016-02-01", "2016-01-01", 31}, }; for(auto &t: subtract_tests) { Date a = t.a; assert(a.toString() == t.a); Date b = t.b; assert(b.toString() == t.b); int delta = a - b; printf("%s-%s=%d\n", a.toString().c_str(), b.toString().c_str(), delta); assert(delta == t.delta); } return 0; } rsbackup-10.0/src/test-device.cc000066400000000000000000000021221440730431700165560ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Device.h" #include #include int main() { assert(!Device::valid("")); assert(Device::valid( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.")); assert(!Device::valid("/")); assert(!Device::valid("~")); assert(!Device::valid("\x80")); assert(!Device::valid(" ")); assert(!Device::valid("\x1F")); return 0; } rsbackup-10.0/src/test-directory.cc000066400000000000000000000031611440730431700173270ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "IO.h" #include "Errors.h" #include #include int main() { { Directory d; std::vector files; d.open("."); d.get(files); for(size_t n = 0; n + 1 < files.size(); ++n) assert(files[n] < files[n + 1]); d.close(); } { std::vector files; Directory::getFiles(".", files); for(size_t n = 0; n + 1 < files.size(); ++n) assert(files[n] < files[n + 1]); } { Directory d; std::vector files; d.open("."); std::string name; while(d.get(name)) files.push_back(name); } try { Directory d; d.open("/dev/null"); assert(!"unexpectedly succeeded"); } catch(IOError &e) { assert(e.errno_value == ENOTDIR); } try { Directory d; d.open("does not exist"); assert(!"unexpectedly succeeded"); } catch(IOError &e) { assert(e.errno_value == ENOENT); } return 0; } rsbackup-10.0/src/test-eventloop.cc000066400000000000000000000040471440730431700173420ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "EventLoop.h" #include "Errors.h" #include #include #include class TestReactor: public Reactor { public: void onReadable(EventLoop *e, int fd, const void *, size_t) override { assert(close(fd) == 0); e->cancelRead(fd); ++read_calls; } void onWritable(EventLoop *e, int fd) override { if(wrote_bytes < writeme.size()) { size_t remain = writeme.size() - wrote_bytes; ssize_t n = ::write(fd, &writeme[wrote_bytes], remain); assert(n >= 0); assert(static_cast(n) <= remain); wrote_bytes += n; } else { assert(close(fd) == 0); e->cancelWrite(fd); } } int read_calls = 0; std::string writeme; size_t wrote_bytes = 0; }; static void test_read_closed() { int p[2]; assert(pipe(p) == 0); assert(close(p[1]) == 0); EventLoop e; TestReactor tr; e.whenReadable(p[0], &tr); e.wait(); assert(tr.read_calls == 1); } static void test_write() { int p[2]; assert(pipe(p) == 0); EventLoop e; TestReactor tr; tr.writeme = "test data"; e.whenWritable(p[1], &tr); e.wait(); char buffer[4096]; ssize_t n = ::read(p[0], buffer, sizeof buffer); assert(static_cast(n) == tr.writeme.size()); assert(std::string(buffer, n) == tr.writeme); } int main() { test_read_closed(); test_write(); return 0; } rsbackup-10.0/src/test-globfiles.cc000066400000000000000000000037501440730431700172750ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include #include #include #include #include #include void create(const char *path) { int fd = open(path, O_WRONLY | O_CREAT, 0666); assert(fd >= 0); close(fd); } int main() { const char *tmpdir; char *dir; std::vector files; tmpdir = getenv("TMPDIR"); if(!tmpdir) tmpdir = "/tmp"; assert(asprintf(&dir, "%s/XXXXXX", tmpdir) > 0); assert(mkdtemp(dir)); assert(chdir(dir) >= 0); globFiles(files, "nothing", 0); assert(files.size() == 0); files.push_back("spong"); globFiles(files, "nothing", 0); assert(files.size() == 0); globFiles(files, "*", 0); assert(files.size() == 0); globFiles(files, "*", GLOB_NOCHECK); assert(files.size() == 1); assert(files[0] == "*"); create("a"); globFiles(files, "*", 0); assert(files.size() == 1); assert(files[0] == "a"); globFiles(files, "b", 0); assert(files.size() == 0); create("b"); globFiles(files, "*", 0); assert(files.size() == 2); assert(files[0] == "a"); assert(files[1] == "b"); globFiles(files, "b", 0); assert(files.size() == 1); assert(files[0] == "b"); int r = system(("rm -rf " + (std::string)dir).c_str()); (void)r; // Work around GCC/Glibc stupidity free(dir); return 0; } rsbackup-10.0/src/test-host.cc000066400000000000000000000023031440730431700162750ustar00rootroot00000000000000// Copyright © 2014-15 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Host.h" #include #include int main() { assert(!Host::valid("")); assert(Host::valid( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.")); assert(!Host::valid("_")); assert(!Host::valid("/")); assert(!Host::valid("~")); assert(!Host::valid("\x80")); assert(!Host::valid(" ")); assert(!Host::valid("\x1F")); assert(!Host::valid("-whatever")); assert(Host::valid("what-are-the-civilian-applications")); return 0; } rsbackup-10.0/src/test-indent.cc000066400000000000000000000040061440730431700166030ustar00rootroot00000000000000// Copyright © 2017 Richard Kettlewell. // // 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 . #include #include "Indent.h" #include "Utils.h" // Flat config should work OK static void indent_flat() { Indent i; unsigned level; level = i.check(1, 0); assert(level == 1); level = i.check(1, 0); assert(level == 1); level = i.check(3, 0); assert(level == 1); } // A slightly fiddlier config static void indent_fiddly() { Indent i; unsigned level; level = i.check(1, 0); assert(level == 1); i.introduce(2); level = i.check(7, 8); assert(level == 2); level = i.check(7, 8); assert(level == 2); i.introduce(4); level = i.check(7, 15); assert(level == 4); level = i.check(7, 8); assert(level == 2); level = i.check(7, 0); assert(level == 1); i.introduce(2); level = i.check(7, 8); assert(level == 2); } // Short indent forbidden static void indent_short() { Indent i; unsigned level; level = i.check(1, 0); assert(level == 1); i.introduce(2); level = i.check(7, 8); assert(level == 2); level = i.check(7, 6); assert(level == 0); } // Long indent forbidden static void indent_long() { Indent i; unsigned level; level = i.check(1, 0); assert(level == 1); i.introduce(2); level = i.check(3, 8); assert(level == 2); level = i.check(3, 12); assert(level == 0); } int main() { indent_flat(); indent_fiddly(); indent_short(); indent_long(); return 0; } rsbackup-10.0/src/test-io.cc000066400000000000000000000034151440730431700157340ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "IO.h" #include #include static void test_io_write(void) { IO f; f.open("test-io-tmp", "w"); f.write("line 1\n"); f.writef("line %d\n", 2); f.close(); } static void test_io_readline(void) { IO f; f.open("test-io-tmp", "r"); std::string l; bool rc; rc = f.readline(l); assert(rc); assert(l == "line 1"); rc = f.readline(l); assert(rc); assert(l == "line 2"); rc = f.readline(l); assert(!rc); f.close(); } static void test_io_readlines(void) { IO f; f.open("test-io-tmp", "r"); std::vector ls; f.readlines(ls); assert(ls.size() == 2); assert(ls[0] == "line 1"); assert(ls[1] == "line 2"); f.close(); } static void test_io_capture(void) { IO f; std::vector command = {"echo", "spong"}; std::vector ls; f.popen(command, ReadFromPipe, false); f.readlines(ls); f.close(); assert(ls.size() == 1); assert(ls[0] == "spong"); } int main() { test_io_write(); test_io_readline(); test_io_readlines(); test_io_capture(); remove("test-io-tmp"); return 0; } rsbackup-10.0/src/test-lock.cc000066400000000000000000000026321440730431700162550ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "FileLock.h" #include #include #include #include #include int main(void) { int p[2]; char buf[1]; int rc; char path[] = "lock.XXXXXX"; assert(mkstemp(path)); assert(pipe(p) >= 0); FileLock a(path); assert(a.acquire()); switch(pid_t child = fork()) { case -1: abort(); case 0: { FileLock b(path); assert(!b.acquire(false)); assert(write(p[1], "", 1) == 1); assert(b.acquire(true)); _exit(0); } default: while((rc = read(p[0], buf, 1)) < 0 && errno == EINTR) ; assert(rc == 1); a.release(); int w; assert(wait(&w) == child); assert(w == 0); break; } unlink(path); return 0; } rsbackup-10.0/src/test-namelt.cc000066400000000000000000000023701440730431700166040ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include int main() { assert(!namelt("", "")); assert(!namelt("aaaa", "aaaa")); assert(namelt("aaa", "aaaa")); assert(!namelt("aaaa", "aaa")); assert(namelt("aaaa", "bbbb")); assert(!namelt("bbbb", "aaaa")); assert(namelt("", "bbbb")); assert(!namelt("bbbb", "")); assert(namelt("1", "2")); assert(namelt("01", "2")); assert(namelt("1", "02")); assert(namelt("x1", "x2")); assert(namelt("x01", "x2")); assert(namelt("x1b", "x2a")); assert(!namelt("x1", "1")); assert(namelt("1", "x1")); return 0; } rsbackup-10.0/src/test-parsefloat.cc000066400000000000000000000044301440730431700174630ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "Errors.h" #include "Utils.h" int main(void) { // In range assert(parseFloat("0", 0, 0, InclusiveLimit) == 0); assert(parseFloat("0", 0, 0, InclusiveLimit) == 0); assert(parseFloat("0.25", 0, INT_MAX, InclusiveLimit) == 0.25); assert(parseFloat("-100", INT_MIN, INT_MAX, InclusiveLimit) == -100); assert(parseFloat("-0.25", INT_MIN, INT_MAX, InclusiveLimit) == -0.25); assert(parseFloat("100", 0, 100, InclusiveLimit) == 100); assert(parseFloat("-100", -100, 100, InclusiveLimit) == -100); assert(parseFloat("1e40", 0, HUGE_VAL, InclusiveLimit) == 1.0e40); // Essentially test that compiler and C library are reasonably consistent assert(parseFloat("0.1", 0, INT_MAX, InclusiveLimit) == 0.1); assert(parseFloat("0.01", 0, INT_MAX, InclusiveLimit) == 0.01); assert(parseFloat("0.001", 0, INT_MAX, InclusiveLimit) == 0.001); // Out of range try { parseFloat("10", 0, 9, InclusiveLimit); assert(0); } catch(SyntaxError &) { } try { parseFloat("10", 0, 10, ExclusiveLimit); assert(0); } catch(SyntaxError &) { } try { parseFloat("-10", -9, 0, InclusiveLimit); assert(0); } catch(SyntaxError &) { } try { parseFloat("1-0", -10, 0, ExclusiveLimit); assert(0); } catch(SyntaxError &) { } // Parse error try { parseFloat("junk", 0, 1000, InclusiveLimit); assert(0); } catch(SyntaxError &) { } try { parseFloat("100 ", 0, 1000, InclusiveLimit); assert(0); } catch(SyntaxError &) { } try { parseFloat(" 100", 0, 1000, InclusiveLimit); assert(0); } catch(SyntaxError &) { } return 0; } rsbackup-10.0/src/test-parseinteger.cc000066400000000000000000000043551440730431700200210ustar00rootroot00000000000000// Copyright © Richard Kettlewell. // // 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 . #include #include "Errors.h" #include "Utils.h" int main(void) { assert(parseInteger("0", 0, 0, 0) == 0); assert(parseInteger("0", 0, 0, 8) == 0); assert(parseInteger("0", 0, 0, 10) == 0); assert(parseInteger("0", 0, 0, 16) == 0); assert(parseInteger("100", 0, INT_MAX, 0) == 100); assert(parseInteger("100", 0, INT_MAX, 8) == 64); assert(parseInteger("0100", 0, INT_MAX, 0) == 64); assert(parseInteger("100", 0, INT_MAX, 10) == 100); assert(parseInteger("100", 0, INT_MAX, 16) == 256); assert(parseInteger("0x100", 0, INT_MAX, 0) == 256); assert(parseInteger("-100", INT_MIN, INT_MAX, 0) == -100); assert(parseInteger("-100", INT_MIN, INT_MAX, 8) == -64); assert(parseInteger("-0100", INT_MIN, INT_MAX, 0) == -64); assert(parseInteger("-100", INT_MIN, INT_MAX, 10) == -100); assert(parseInteger("-100", INT_MIN, INT_MAX, 16) == -256); assert(parseInteger("-0x100", INT_MIN, INT_MAX, 0) == -256); try { parseInteger("10", 0, 9, 0); assert(0); } catch(SyntaxError &) { } try { parseInteger("-10", 0, 9, 0); assert(0); } catch(SyntaxError &) { } try { parseInteger("0x10", 0, 1000, 10); assert(0); } catch(SyntaxError &) { } try { parseInteger("junk", 0, 1000, 0); assert(0); } catch(SyntaxError &) { } try { parseInteger("100 ", 0, 1000, 0); assert(0); } catch(SyntaxError &) { } try { parseInteger(" 100", 0, 1000, 0); assert(0); } catch(SyntaxError &) { } try { parseInteger("2.2", 0, 1000, 0); assert(0); } catch(SyntaxError &) { } return 0; } rsbackup-10.0/src/test-parsetimeinterval.cc000066400000000000000000000031561440730431700210650ustar00rootroot00000000000000// Copyright © 2020 Richard Kettlewell. // // 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 . #include #include "Errors.h" #include "Utils.h" int main(void) { assert(parseTimeInterval("0s") == 0); assert(parseTimeInterval("60s") == 60); assert(parseTimeInterval("10m") == 600); assert(parseTimeInterval("10d") == 86400 * 10); try { parseTimeInterval("10w"); assert(0); } catch(SyntaxError &) { } try { parseTimeInterval("10ss"); assert(0); } catch(SyntaxError &) { } try { parseTimeInterval(""); assert(0); } catch(SyntaxError &) { } try { parseTimeInterval("3600s", 1000); assert(0); } catch(SyntaxError &) { } try { parseTimeInterval("3600"); assert(0); } catch(SyntaxError &) { } assert(formatTimeInterval(0) == "0d"); assert(formatTimeInterval(1) == "1s"); assert(formatTimeInterval(60) == "1m"); assert(formatTimeInterval(120) == "2m"); assert(formatTimeInterval(3600) == "1h"); assert(formatTimeInterval(86400) == "1d"); return 0; } rsbackup-10.0/src/test-progress.cc000066400000000000000000000035631440730431700171750ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "IO.h" #include #include #include #include #include #define WIDTH 79 static std::string getBytes(int fd) { std::string s; char buffer[128]; int n; while((n = read(fd, buffer, sizeof buffer)) > 0) s.append(buffer, static_cast(n)); if(n < 0) assert(errno == EAGAIN); return s; } int main() { FILE *ofp; int p[2], r; assert(pipe(p) >= 0); assert((ofp = fdopen(p[1], "w"))); assert((r = fcntl(p[0], F_GETFL)) >= 0); assert(fcntl(p[0], F_SETFL, r | O_NONBLOCK) >= 0); IO o(ofp, "output"); progressBar(o, "spong", 0, 0); std::string s = getBytes(p[0]); assert(s == "\r" + std::string(WIDTH, ' ') + "\r"); progressBar(o, "spong", 0, 10); s = getBytes(p[0]); assert(s == "\rspong [" + std::string(WIDTH - 8, ' ') + "]\r"); progressBar(o, "spong", 1, 10); s = getBytes(p[0]); assert(s == "\rspong [" + std::string((WIDTH - 8) / 10, '=') + std::string((WIDTH - 8) - (WIDTH - 8) / 10, ' ') + "]\r"); progressBar(o, "spong", 10, 10); s = getBytes(p[0]); assert(s == "\rspong [" + std::string(WIDTH - 8, '=') + "]\r"); return 0; } rsbackup-10.0/src/test-prunedecay.cc000066400000000000000000000026601440730431700174650ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Prune.h" #include #include static const int v12[] = {0, 1, 1, 2, 2, 2, 2, 3, -1}; static const int v22[] = {0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, -1}; static const int v13[] = {0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, -1}; static const int v23[] = {0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, -1}; static void check(int w, int s, const int *v) { int a = 1; while(*v >= 0) { int n = prune_decay_bucket(w, s, a); // printf("w=%d s=%d a=%d n=%d expected %d\n", w, s, a, n, *v); assert(n == *v); ++v; ++a; } } int main(void) { check(1, 2, v12); check(2, 2, v22); check(1, 3, v13); check(2, 3, v23); return 0; } rsbackup-10.0/src/test-select.cc000066400000000000000000000060621440730431700166050ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Backup.h" #include "Volume.h" #include "Host.h" #include #define SETUP() \ Conf c; \ auto h1 = new Host(&c, "h1"); \ auto h2 = new Host(&c, "h2"); \ auto h1v1 = new Volume(h1, "v1", "/v1"); \ auto h1v2 = new Volume(h1, "v2", "/v2"); \ auto h2v1 = new Volume(h2, "v1", "/v1"); \ auto h2v2 = new Volume(h2, "v2", "/v2") static void test_initial_state() { SETUP(); assert(!h1->selected()); assert(!h1v1->selected()); assert(!h1v2->selected()); assert(!h2->selected()); assert(!h2v1->selected()); assert(!h2v2->selected()); } static void test_select_all() { SETUP(); c.selectVolume("*", "*", true); assert(h1->selected()); assert(h1v1->selected()); assert(h1v2->selected()); assert(h2->selected()); assert(h2v1->selected()); assert(h2v2->selected()); } static void test_select_host() { SETUP(); c.selectVolume("h1", "*", true); assert(h1->selected()); assert(h1v1->selected()); assert(h1v2->selected()); assert(!h2->selected()); assert(!h2v1->selected()); assert(!h2v2->selected()); } static void test_deselect_host() { SETUP(); c.selectVolume("*", "*", true); c.selectVolume("h2", "*", false); assert(h1->selected()); assert(h1v1->selected()); assert(h1v2->selected()); assert(!h2->selected()); assert(!h2v1->selected()); assert(!h2v2->selected()); } static void test_select_volume() { SETUP(); c.selectVolume("h1", "v1", true); assert(h1->selected()); assert(h1v1->selected()); assert(!h1v2->selected()); assert(!h2->selected()); assert(!h2v1->selected()); assert(!h2v2->selected()); } static void test_deselect_volume() { SETUP(); c.selectVolume("*", "*", true); c.selectVolume("h2", "v1", false); assert(h1->selected()); assert(h1v1->selected()); assert(h1v2->selected()); assert(h2->selected()); assert(!h2v1->selected()); assert(h2v2->selected()); } int main() { test_initial_state(); test_select_all(); test_select_host(); test_deselect_host(); test_select_volume(); test_deselect_volume(); return 0; } rsbackup-10.0/src/test-split.cc000066400000000000000000000063411440730431700164610ustar00rootroot00000000000000// Copyright © 2015, 2017 Richard Kettlewell. // // 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 . #include #include "Errors.h" #include "Utils.h" int main(void) { std::vector bits; bits.push_back("spong"); split(bits, ""); assert(bits.size() == 0); bits.push_back("spong"); split(bits, " "); assert(bits.size() == 0); bits.push_back("spong"); split(bits, "# junk"); assert(bits.size() == 0); bits.push_back("spong"); split(bits, "word"); assert(bits.size() == 1); assert(bits[0] == "word"); bits.push_back("spong"); split(bits, " word "); assert(bits.size() == 1); assert(bits[0] == "word"); bits.push_back("spong"); split(bits, "\tword\r"); assert(bits.size() == 1); assert(bits[0] == "word"); bits.push_back("spong"); split(bits, "word second"); assert(bits.size() == 2); assert(bits[0] == "word"); assert(bits[1] == "second"); bits.push_back("spong"); split(bits, "word second # junk"); assert(bits.size() == 2); assert(bits[0] == "word"); assert(bits[1] == "second"); bits.push_back("spong"); split(bits, "\"quoted\""); assert(bits.size() == 1); assert(bits[0] == "quoted"); bits.push_back("spong"); split(bits, "\"quoted\""); assert(bits.size() == 1); assert(bits[0] == "quoted"); bits.push_back("spong"); split(bits, "\"with spaces\""); assert(bits.size() == 1); assert(bits[0] == "with spaces"); bits.push_back("spong"); split(bits, "\"\\\\\""); assert(bits.size() == 1); assert(bits[0] == "\\"); bits.push_back("spong"); split(bits, "\"\\\"\""); assert(bits.size() == 1); assert(bits[0] == "\""); bits.push_back("spong"); split(bits, "\"\\xxx\""); assert(bits.size() == 1); assert(bits[0] == "xxx"); try { split(bits, "\"unterminated"); assert(0); } catch(SyntaxError &) { } try { split(bits, "\"unterminated\\"); assert(0); } catch(SyntaxError &) { } try { split(bits, "\"unterminated\\\""); assert(0); } catch(SyntaxError &) { } try { split(bits, "\\"); assert(0); } catch(SyntaxError &) { } size_t indent = (size_t)-1; split(bits, "anything", &indent); assert(indent == 0); split(bits, " anything", &indent); assert(indent == 1); split(bits, " anything", &indent); assert(indent == 3); split(bits, "\tanything", &indent); assert(indent == 8); split(bits, " \tanything", &indent); assert(indent == 8); split(bits, "\t anything", &indent); assert(indent == 9); split(bits, "\t \tanything", &indent); assert(indent == 16); split(bits, "", &indent); assert(indent == 0); split(bits, " ", &indent); assert(indent == 3); return 0; } rsbackup-10.0/src/test-subprocess.cc000066400000000000000000000062671440730431700175250ustar00rootroot00000000000000// Copyright © 2012-17 Richard Kettlewell. // // 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 . #include #include "Subprocess.h" #include "Errors.h" #include #include #include #include #include #include #include static const char *warnings[64]; static size_t nwarnings; void warning(unsigned, const char *fmt, ...) { char *w; va_list ap; assert(nwarnings < sizeof warnings / sizeof *warnings); va_start(ap, fmt); assert(vasprintf(&w, fmt, ap) >= 0); va_end(ap); warnings[nwarnings++] = w; } void fatal(const char *, ...) { abort(); } int main() { // Separate capture of stdout and stderr std::vector command = { "sh", "-c", "echo stdout; sleep 1; echo >&2 stderr; sleep 2; echo skipped"}; Subprocess sp(command); std::string stdoutCapture, stderrCapture; sp.capture(1, &stdoutCapture); sp.capture(2, &stderrCapture); sp.setTimeout(2); int rc = sp.runAndWait(0); assert(stdoutCapture == "stdout\n"); assert(stderrCapture == "stderr\n"); assert(WIFSIGNALED(rc)); assert(WTERMSIG(rc) == SIGKILL); assert(nwarnings == 1); assert(!strcmp(warnings[0], "sh exceeded timeout of 2 seconds")); // Capture of both stdout and stderr command = {"sh", "-c", "echo stdout; echo >&2 stderr"}; Subprocess sp2(command); std::string bothCapture; sp2.capture(1, &bothCapture, 2); rc = sp2.runAndWait(0); assert(bothCapture == "stdout\nstderr\n"); assert(WIFEXITED(rc)); assert(WEXITSTATUS(rc) == 0); assert(nwarnings == 1); // NB assumes the 'usual' encoding of exit status, will need to do something // more sophisticated if some useful platform doesn't play along. // // For reference the 'usual' encoding is: // bits 0-6: // The termination/stop signal // 0 = exited // 1-126 = terminated with this signal // 127 = stopped // bits 7 // Set if core dumped // bits 8-15 // If exited: the exit status // If stopped: the stop signal std::string d; d = SubprocessFailed::format("progname", SIGKILL); assert(d == std::string("progname: ") + strsignal(SIGKILL)); d = SubprocessFailed::format("progname", SIGKILL | 0x80); assert(d == std::string("progname: ") + strsignal(SIGKILL) + " (core dumped)"); d = SubprocessFailed::format("progname", 37 << 8); assert(d.find("progname") != std::string::npos); assert(d == "progname: exited with status 37"); d = SubprocessFailed::format("progname", (SIGSTOP << 8) + 0x7F); assert(d == std::string("progname: ") + strsignal(SIGSTOP)); return 0; } rsbackup-10.0/src/test-timespec.cc000066400000000000000000000025741440730431700171430ustar00rootroot00000000000000// Copyright © 2014 Richard Kettlewell. // // 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 . #include #include "Utils.h" int main() { const struct timespec ts[] = { {0, 0}, {0, 500 * 1000000}, {1, 0}, {1, 500 * 1000000}, }; #define NTS (sizeof ts / sizeof *ts) for(size_t i = 0; i < NTS; ++i) { for(size_t j = 0; j < NTS; ++j) { if(i < j) { assert(!(ts[i] >= ts[j])); assert(!(ts[i] == ts[j])); } else if(i == j) { assert(ts[i] >= ts[j]); assert(ts[i] == ts[j]); } else { assert(ts[i] >= ts[j]); assert(!(ts[i] == ts[j])); } assert(ts[i] - ts[i] == ts[0]); } } assert(ts[2] - ts[1] == ts[1]); assert(ts[3] - ts[2] == ts[1]); assert(ts[3] - ts[1] == ts[2]); return 0; } rsbackup-10.0/src/test-tolines.cc000066400000000000000000000022711440730431700170010ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include int main() { std::vector lines; toLines(lines, ""); assert(lines.size() == 0); lines.push_back(""); toLines(lines, ""); assert(lines.size() == 0); toLines(lines, "1"); assert(lines.size() == 1); assert(lines[0] == "1"); toLines(lines, "1\n"); assert(lines.size() == 1); assert(lines[0] == "1"); toLines(lines, "1\n2"); assert(lines.size() == 2); assert(lines[0] == "1"); assert(lines[1] == "2"); return 0; } rsbackup-10.0/src/test-unicode.cc000066400000000000000000000035241440730431700167540ustar00rootroot00000000000000// Copyright © 2012, 2014 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include #include #include static const unsigned char narrow[] = { 0xf0, 0x90, 0x8c, 0xb2, 0xf0, 0x90, 0x8c, 0xbf, 0xf0, 0x90, 0x8d, 0x84, 0xf0, 0x90, 0x8c, 0xb0, 0xf0, 0x90, 0x8c, 0xbd, 0xf0, 0x90, 0x8d, 0x83, 0x20, 0xf0, 0x90, 0x8c, 0xbf, 0xf0, 0x90, 0x8d, 0x83, 0x20, 0xf0, 0x90, 0x8d, 0x84, 0xf0, 0x90, 0x8d, 0x82, 0xf0, 0x90, 0x8c, 0xb9, 0xf0, 0x90, 0x8c, 0xbe, 0xf0, 0x90, 0x8c, 0xbf, 0xf0, 0x90, 0x8d, 0x83, 0}; static const char32_t wide[] = {0x10332, 0x1033f, 0x10344, 0x10330, 0x1033d, 0x10343, 0x20, 0x1033f, 0x10343, 0x20, 0x10344, 0x10342, 0x10339, 0x1033e, 0x1033f, 0x10343, 0}; int main() { std::u32string w; if(!setlocale(LC_CTYPE, "C.UTF-8") && !setlocale(LC_CTYPE, "en_US.UTF-8") && !setlocale(LC_CTYPE, "en_GB.UTF-8")) { fprintf(stderr, "ERROR: cannot find a UTF-8 locale to test in\n"); return 77; } toUnicode(w, "just ascii"); assert(w == U"just ascii"); toUnicode(w, reinterpret_cast(narrow)); assert(w == wide); return 0; } rsbackup-10.0/src/test-volume.cc000066400000000000000000000022201440730431700166250ustar00rootroot00000000000000// Copyright © 2014-15 Richard Kettlewell. // // 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 . #include #include "Conf.h" #include "Backup.h" #include "Volume.h" #include #include int main() { assert(!Volume::valid("")); assert(Volume::valid( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.")); assert(!Volume::valid("/")); assert(!Volume::valid("~")); assert(!Volume::valid("\x80")); assert(!Volume::valid(" ")); assert(!Volume::valid("\x1F")); assert(!Volume::valid("-whatever")); return 0; } rsbackup-10.0/src/timestamp.cc000066400000000000000000000021651440730431700163540ustar00rootroot00000000000000// Copyright © 2015 Richard Kettlewell. // // 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 . #include #include "Utils.h" #include "Errors.h" #include #include #include void getMonotonicTime(struct timespec &now) { #ifdef CLOCK_MONOTONIC if(clock_gettime(CLOCK_MONOTONIC, &now) < 0) throw IOError("clock_gettime", errno); #else struct timeval tv; if(gettimeofday(&tv, nullptr) < 0) throw IOError("gettimeofday", errno); now.tv_sec = tv.tv_sec; now.tv_nsec = tv.tv_sec * 1000; #endif } rsbackup-10.0/src/toLines.cc000066400000000000000000000022111440730431700157560ustar00rootroot00000000000000// Copyright © 2014, 2015 Richard Kettlewell. // // 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 . #include #include "rsbackup.h" #include "Utils.h" size_t toLines(std::vector &lines, const std::string &s) { lines.clear(); size_t pos = 0; const size_t limit = s.size(); while(pos < limit) { size_t nl = s.find('\n', pos); if(nl == std::string::npos) break; lines.push_back(s.substr(pos, nl - pos)); pos = nl + 1; } if(pos < limit) lines.push_back(s.substr(pos, limit - pos)); return lines.size(); } rsbackup-10.0/tests/000077500000000000000000000000001440730431700144115ustar00rootroot00000000000000rsbackup-10.0/tests/Makefile.am000066400000000000000000000075721440730431700164600ustar00rootroot00000000000000# Copyright © 2011-2015,17-20 Richard Kettlewell. # # 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 . TESTS=bashisms backup prune pruneage prunenever pruneexec prunedecay \ retire-device retire-volume retire-forget store \ check-file check-configs check-bad-configs \ check-mounted glob-store style issue37 partial issue43 \ issue55 issue70 issue71 prune-timeout \ concurrency hostgroup backupdaily backupalways backupinterval EXTRA_DIST=${TESTS} setup.sh pruner.sh hook rsync-wrap \ expect/retire-device/create.txt \ expect/retire-device/device2-db.txt \ expect/retire-device/created-db.txt \ expect/retire-device/device2.txt \ expect/retire-device/dryrun.txt \ expect/include.txt \ expect/retire-volume/volume2.html \ expect/retire-volume/dryrun.html \ expect/retire-volume/create.txt \ expect/retire-volume/volume2.txt \ expect/retire-volume/created-db.txt \ expect/retire-volume/dryrun.txt \ expect/retire-volume/all.txt \ expect/retire-volume/all.html \ expect/retire-volume/all-db.txt \ expect/retire-volume/volume2-db.txt \ expect/retire-volume/create.html \ expect/store/duplicate-stderr.txt \ expect/store/duplicate.html \ expect/store/overridden.txt \ expect/store/overridden.html \ expect/store/duplicate.txt \ expect/store/overridden-stderr.txt \ expect/store/notmounted.txt \ expect/prune/createsecond.txt \ expect/prune/null.txt \ expect/prune/dryrun-db.txt \ expect/prune/volume2.txt \ expect/prune/volume1.txt \ expect/prune/third.txt \ expect/prune/dryrun.txt \ expect/prune/null-db.txt \ expect/prune/unselected-db.txt \ expect/prune/created.txt \ expect/prune/volume2-db.txt \ expect/prune/volume1-db.txt \ expect/prune/everything-db.txt \ expect/prune/later-db.txt \ expect/prune/unselected.txt \ expect/prunedecay/prunedecay-db.txt \ expect/pruneexec/pruneexec-db.txt \ expect/prunenever/neverprune-db.txt \ expect/prunetimeout/prunetimeout-db.txt \ expect/check-file/missing.html \ expect/check-file/missing.txt \ expect/style/styled.txt \ expect/style/styled.html \ expect/empty.txt \ expect/glob-store/everything.txt \ expect/glob-store/everything.html \ expect/backup/onehost.html \ expect/backup/dryrun.html \ expect/backup/everything.txt \ expect/backup/onevolume.html \ expect/backup/onevolume.txt \ expect/backup/dryrun.txt \ expect/backup/everything.html \ expect/backup/onehost.txt \ expect/outdent.txt \ expect/pruneparam.txt \ expect/issue43/failhookerr.txt expect/issue43/failhookout.txt expect/issue43/tempfailhook.txt \ configs/pruneparam/config \ configs/empty/config \ configs/include/config \ configs/include/config.d/z configs/include/config.d/backup~ \ configs/include/config.d/empty \ configs/include/config.d/\#recovery\# \ configs/include/config.d/.dotfile configs/include/config.d/a \ configs/outdent/config \ bad-configs/badquotes.config bad-configs/badquotes.errors \ bad-configs/inconsistent-indent.config \ bad-configs/inconsistent-indent.errors \ bad-configs/inconsistent-outdent.config \ bad-configs/inconsistent-outdent.errors \ bad-configs/inconsistent-volume.config \ bad-configs/inconsistent-volume.errors \ bad-configs/indent-global.config \ bad-configs/indent-global.errors \ bad-configs/unrecognized.config \ bad-configs/unrecognized.errors clean-local: rm -rf $(patsubst %,w-%,${TESTS}) rsbackup-10.0/tests/backup000077500000000000000000000142571440730431700156150ustar00rootroot00000000000000#! /usr/bin/env bash # Copyright © Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| --dry-run should do nothing" RUN=dryrun RSBACKUP_TIME=315532800 s ${RSBACKUP} --dry-run --backup --text ${WORKSPACE}/got/dryrun.txt --html ${WORKSPACE}/got/dryrun.html host1:volume1 exists ${WORKSPACE}/dryrun-dev-pre.ran exists ${WORKSPACE}/dryrun-dev-post.ran absent ${WORKSPACE}/dryrun-dev-pre.acted absent ${WORKSPACE}/dryrun-dev-post.acted exists ${WORKSPACE}/dryrun-pre.ran exists ${WORKSPACE}/dryrun-post.ran absent ${WORKSPACE}/dryrun-pre.acted absent ${WORKSPACE}/dryrun-post.acted absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store2/host1 compare ${srcdir:-.}/expect/backup/dryrun.txt ${WORKSPACE}/got/dryrun.txt compare ${srcdir:-.}/expect/backup/dryrun.html ${WORKSPACE}/got/dryrun.html absent ${WORKSPACE}/logs/backups.db # not created with --dry-run echo "| --latest before any backups made should fail" fails ${RSBACKUP} --latest host1:volume1 echo "| Create backup for one volume" RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/onevolume.txt --html ${WORKSPACE}/got/onevolume.html host1:volume1 exists ${WORKSPACE}/volume1-dev-pre.ran exists ${WORKSPACE}/volume1-dev-post.ran exists ${WORKSPACE}/volume1-dev-pre.ran exists ${WORKSPACE}/volume1-dev-post.ran exists ${WORKSPACE}/volume1-pre.acted exists ${WORKSPACE}/volume1-post.acted exists ${WORKSPACE}/volume1-pre.acted exists ${WORKSPACE}/volume1-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store1/host1/volume3 absent ${WORKSPACE}/store2/host1/volume2 absent ${WORKSPACE}/store2/host1/volume3 compare ${srcdir:-.}/expect/backup/onevolume.txt ${WORKSPACE}/got/onevolume.txt compare ${srcdir:-.}/expect/backup/onevolume.html ${WORKSPACE}/got/onevolume.html exists ${WORKSPACE}/logs/backups.db # default database path echo "| --latest for one backup of one volume" cat > ${WORKSPACE}/expect-latest1.txt << EOF ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 EOF s ${RSBACKUP} --latest host1:volume1 > ${WORKSPACE}/got/latest1.txt compare ${WORKSPACE}/expect-latest1.txt ${WORKSPACE}/got/latest1.txt echo "| Create backup for one host" RUN=host1 RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/onehost.txt --html ${WORKSPACE}/got/onehost.html host1 exists ${WORKSPACE}/host1-pre.acted exists ${WORKSPACE}/host1-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00.incomplete compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 absent ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-02T00:00:00 absent ${WORKSPACE}/store2/host1/volume3/1980-01-02T00:00:00.incomplete compare ${srcdir:-.}/expect/backup/onehost.txt ${WORKSPACE}/got/onehost.txt compare ${srcdir:-.}/expect/backup/onehost.html ${WORKSPACE}/got/onehost.html echo "| --latest for one backup of both volumes" cat > ${WORKSPACE}/expect-latest2.txt < ${WORKSPACE}/got/latest2.txt compare ${WORKSPACE}/expect-latest2.txt ${WORKSPACE}/got/latest2.txt echo "| Create backup for everything" RUN=all RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/everything.txt --html ${WORKSPACE}/got/everything.html exists ${WORKSPACE}/all-pre.acted exists ${WORKSPACE}/all-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00.incomplete compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00 absent ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00.incomplete compare ${srcdir:-.}/expect/backup/everything.txt ${WORKSPACE}/got/everything.txt compare ${srcdir:-.}/expect/backup/everything.html ${WORKSPACE}/got/everything.html echo "| Backup should be clean" s ${RSBACKUP} --check-unexpected > ${WORKSPACE}/got/check-clean.txt touch ${WORKSPACE}/expect-clean.txt compare ${WORKSPACE}/expect-clean.txt ${WORKSPACE}/got/check-clean.txt echo "| Dirty backups should be detected" touch ${WORKSPACE}/store1/bogus touch ${WORKSPACE}/store1/host1/bogus touch ${WORKSPACE}/store1/host1/volume1/bogus s ${RSBACKUP} --check-unexpected > ${WORKSPACE}/got/check-dirty.txt cat > ${WORKSPACE}/expect-dirty.txt < ${WORKSPACE}/got/check-dirty.txt echo -ne "${WORKSPACE}/store1/bogus\\0${WORKSPACE}/store1/host1/bogus\\0${WORKSPACE}/store1/host1/volume1/bogus\\0" > ${WORKSPACE}/expect-dirty.txt compare ${WORKSPACE}/expect-dirty.txt ${WORKSPACE}/got/check-dirty.txt cleanup rsbackup-10.0/tests/backupalways000077500000000000000000000021441440730431700170260ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh BACKUP_POLICY=always setup echo "| Create backup" RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup host1:volume1 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 echo "| Another backup on the same day" RUN=volume1 RSBACKUP_TIME=315576000 s ${RSBACKUP} --backup host1:volume1 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T12:00:00 rsbackup-10.0/tests/backupdaily000077500000000000000000000024021440730431700166250ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| Create backup" RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup host1:volume1 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 echo "| Skip backup because same day" RUN=volume1 RSBACKUP_TIME=315576000 s ${RSBACKUP} --backup host1:volume1 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T12:00:00 echo "| Force backup even on same day" RUN=volume1 RSBACKUP_TIME=315576000 s ${RSBACKUP} --force --backup host1:volume1 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T12:00:00 rsbackup-10.0/tests/backupinterval000077500000000000000000000024551440730431700173570ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh BACKUP_POLICY=interval BACKUP_INTERVAL=3600s setup echo "| Create backup" RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup host1:volume1 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 echo "| Another backup in less than an hour" RUN=volume1 RSBACKUP_TIME=315534600 s ${RSBACKUP} --backup host1:volume1 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:30:00 echo "| Another backup after an hour" RUN=volume1 RSBACKUP_TIME=315536400 s ${RSBACKUP} --backup host1:volume1 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T01:00:00 rsbackup-10.0/tests/bad-configs/000077500000000000000000000000001440730431700165655ustar00rootroot00000000000000rsbackup-10.0/tests/bad-configs/badquotes.config000066400000000000000000000000071440730431700217400ustar00rootroot00000000000000"spong rsbackup-10.0/tests/bad-configs/badquotes.errors000066400000000000000000000000571440730431700220140ustar00rootroot00000000000000ERROR: badquotes.config:1: unterminated string rsbackup-10.0/tests/bad-configs/inconsistent-indent.config000066400000000000000000000000651440730431700237540ustar00rootroot00000000000000host alpha ssh-timeout 1s ssh-timeout 2s rsbackup-10.0/tests/bad-configs/inconsistent-indent.errors000066400000000000000000000000761440730431700240250ustar00rootroot00000000000000ERROR: inconsistent-indent.config:3: inconsistent indentation rsbackup-10.0/tests/bad-configs/inconsistent-outdent.config000066400000000000000000000001041440730431700241470ustar00rootroot00000000000000host alpha ssh-timeout 1s volume root / ssh-timeout 2s rsbackup-10.0/tests/bad-configs/inconsistent-outdent.errors000066400000000000000000000000771440730431700242270ustar00rootroot00000000000000ERROR: inconsistent-outdent.config:4: inconsistent indentation rsbackup-10.0/tests/bad-configs/inconsistent-volume.config000066400000000000000000000000621440730431700237770ustar00rootroot00000000000000host alpha volume root / volume home /home rsbackup-10.0/tests/bad-configs/inconsistent-volume.errors000066400000000000000000000000761440730431700240530ustar00rootroot00000000000000ERROR: inconsistent-volume.config:3: inconsistent indentation rsbackup-10.0/tests/bad-configs/indent-global.config000066400000000000000000000000231440730431700224660ustar00rootroot00000000000000 device whatever rsbackup-10.0/tests/bad-configs/indent-global.errors000066400000000000000000000000701440730431700225370ustar00rootroot00000000000000ERROR: indent-global.config:1: inconsistent indentation rsbackup-10.0/tests/bad-configs/unrecognized.config000066400000000000000000000000061440730431700224440ustar00rootroot00000000000000spong rsbackup-10.0/tests/bad-configs/unrecognized.errors000066400000000000000000000000661440730431700225210ustar00rootroot00000000000000ERROR: unrecognized.config:1: unknown command 'spong' rsbackup-10.0/tests/bashisms000077500000000000000000000016671440730431700161620ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2014 Richard Kettlewell. # # 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 . set -e if type checkbashisms >/dev/null 2>&1; then for s in setup.sh hook \ prune retire-device retire-volume store check-file check-configs \ check-mounted glob-store style; do checkbashisms -f -x -p "${srcdir:-.}/$s" done else exit 77 fi rsbackup-10.0/tests/check-bad-configs000077500000000000000000000022561440730431700175730ustar00rootroot00000000000000#! /bin/sh # Copyright © 2017 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh RSBACKUP="${VALGRIND} ${PWD}/../src/rsbackup" rm -rf ${WORKSPACE} mkdir ${WORKSPACE} cd ${srcdir:-.}/bad-configs for config in *.config; do if ${RSBACKUP} --config ${config} ${VERBOSE_OPT} \ --dump-config >${WORKSPACE}/got.txt 2>${WORKSPACE}/errors.txt; then echo >&2 "ERROR: parse of ${srcdir:-.}/bad-configs/${config} unexpectedly succeeded" exit 1 else errors=${config%%.config}.errors compare $errors ${WORKSPACE}/errors.txt fi done rsbackup-10.0/tests/check-configs000077500000000000000000000042161440730431700170450ustar00rootroot00000000000000#! /bin/sh # Copyright © 2014 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh RSBACKUP="${VALGRIND} ${PWD}/../src/rsbackup" rm -rf ${WORKSPACE} mkdir ${WORKSPACE} # in case of accident... if [ ! -f ${srcdir:-.}/configs/include/config.d/backup~ ]; then echo 'host backup' > ${srcdir:-.}/configs/include/config.d/backup~.$$ mv ${srcdir:-.}/configs/include/config.d/backup~.$$ \ ${srcdir:-.}/configs/include/config.d/backup~ fi # Initial consistency check for root in ${srcdir:-.}/configs/*; do exec 3>${WORKSPACE}/got.txt s ${RSBACKUP} --config ${srcdir:-.}/expect/${root##*configs/}.txt ${VERBOSE_OPT} \ --dump-config >&3 compare ${srcdir:-.}/expect/${root##*configs/}.txt ${WORKSPACE}/got.txt rm -f ${WORKSPACE}/got.txt done # Verbose output must be parsable for root in ${srcdir:-.}/configs/*; do exec 3>${WORKSPACE}/got-verbose.txt s ${RSBACKUP} --config ${srcdir:-.}/expect/${root##*configs/}.txt ${VERBOSE_OPT} \ --dump-config --verbose >&3 exec 4>${WORKSPACE}/got-concise.txt s ${RSBACKUP} --config ${WORKSPACE}/got-verbose.txt ${VERBOSE_OPT} \ --dump-config >&4 compare ${srcdir:-.}/expect/${root##*configs/}.txt ${WORKSPACE}/got-concise.txt rm -f ${WORKSPACE}/got.txt done # Check against configs for root in ${srcdir:-.}/configs/*; do exec 3>${WORKSPACE}/got.txt s cd ${root} s ${RSBACKUP} --config config ${VERBOSE_OPT} \ --dump-config >&3 s cd - compare ${srcdir:-.}/expect/${root##*configs/}.txt ${WORKSPACE}/got.txt rm -f ${WORKSPACE}/got.txt done rm -rf ${WORKSPACE} rsbackup-10.0/tests/check-file000077500000000000000000000025311440730431700163320ustar00rootroot00000000000000#! /bin/sh # Copyright © 2012, 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| Backup is skipped if check-file missing" rm -f ${WORKSPACE}/volume1/file1 RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup --verbose --text ${WORKSPACE}/got/missing.txt --html ${WORKSPACE}/got/missing.html absent ${WORKSPACE}/store1/host1/volume1 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00 compare ${srcdir:-.}/expect/check-file/missing.txt ${WORKSPACE}/got/missing.txt compare ${srcdir:-.}/expect/check-file/missing.html ${WORKSPACE}/got/missing.html cleanup rsbackup-10.0/tests/check-mounted000077500000000000000000000042671440730431700170760ustar00rootroot00000000000000#! /usr/bin/env bash # Copyright © 2014, 2015, 2018 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh . ${srcdir:-.}/../scripts/fakeshell.sh setup fake_init sed < ${WORKSPACE}/config > ${WORKSPACE}/config.new \ 's/check-file.*/check-mounted true/' mv ${WORKSPACE}/config.new ${WORKSPACE}/config case $(uname -s) in Darwin | *BSD ) stat_opt=-f ;; * ) stat_opt=-c ;; esac fake_reset fake_cmd --must-run stat "echo 99; echo 99" \ --must-args $stat_opt %d ${WORKSPACE}/store1/host1/volume3 ${WORKSPACE}/store1/host1/volume3/.. echo "| Backup is skipped if check-mounted fails" rm -f ${WORKSPACE}/volume1/file1 RSBACKUP_TIME=315705600 s fake_run ${RSBACKUP} --backup --verbose absent ${WORKSPACE}/store1/host1/volume1 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00 fake_reset fake_cmd --must-run stat "echo 99; echo 66" \ --must-args $stat_opt %d ${WORKSPACE}/store1/host1/volume3 ${WORKSPACE}/store1/host1/volume3/.. echo "| Backup is made if check-mounted succeeds" RUN=all RSBACKUP_TIME=315705600 s fake_run ${RSBACKUP} --backup exists ${WORKSPACE}/all-pre.acted exists ${WORKSPACE}/all-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00 cleanup rsbackup-10.0/tests/concurrency000077500000000000000000000031251440730431700166720ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e srcdir=${srcdir:-.} . ${srcdir}/setup.sh # Use the wrapper RSYNC_COMMAND="${srcdir}/rsync-wrap" setup # Add a second host echo "host host2" >> ${WORKSPACE}/config echo " hostname localhost" >> ${WORKSPACE}/config echo " volume volume1 ${WORKSPACE}/h2v1" >> ${WORKSPACE}/config echo " volume volume2 ${WORKSPACE}/h2v2" >> ${WORKSPACE}/config mkdir ${WORKSPACE}/h2v1 echo h2v1 > ${WORKSPACE}/h2v1/h2v1.txt mkdir ${WORKSPACE}/h2v2 echo h2v2 > ${WORKSPACE}/h2v2/h2v2.txt echo "| Backup hosts concurrently, devices serially" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup while read time action device host volume; do case $action in start ) if [ -e ${WORKSPACE}/${device}.active ]; then echo >&2 "ERROR: device ${device} not serialized" exit 1 else touch ${WORKSPACE}/${device}.active fi ;; stop ) rm -f ${WORKSPACE}/${device}.active ;; esac done < ${WORKSPACE}/wrap.log rsbackup-10.0/tests/configs/000077500000000000000000000000001440730431700160415ustar00rootroot00000000000000rsbackup-10.0/tests/configs/empty/000077500000000000000000000000001440730431700171775ustar00rootroot00000000000000rsbackup-10.0/tests/configs/empty/config000066400000000000000000000000001440730431700203550ustar00rootroot00000000000000rsbackup-10.0/tests/configs/include/000077500000000000000000000000001440730431700174645ustar00rootroot00000000000000rsbackup-10.0/tests/configs/include/config000066400000000000000000000000211440730431700206450ustar00rootroot00000000000000include config.d rsbackup-10.0/tests/configs/include/config.d/000077500000000000000000000000001440730431700211535ustar00rootroot00000000000000rsbackup-10.0/tests/configs/include/config.d/#recovery#000066400000000000000000000000161440730431700230370ustar00rootroot00000000000000host recovery rsbackup-10.0/tests/configs/include/config.d/.dotfile000066400000000000000000000000151440730431700225760ustar00rootroot00000000000000Host dotfile rsbackup-10.0/tests/configs/include/config.d/a000066400000000000000000000000071440730431700213130ustar00rootroot00000000000000host a rsbackup-10.0/tests/configs/include/config.d/backup~000066400000000000000000000000141440730431700225340ustar00rootroot00000000000000host backup rsbackup-10.0/tests/configs/include/config.d/empty000066400000000000000000000000001440730431700222220ustar00rootroot00000000000000rsbackup-10.0/tests/configs/include/config.d/z000066400000000000000000000000071440730431700213440ustar00rootroot00000000000000host z rsbackup-10.0/tests/configs/outdent/000077500000000000000000000000001440730431700175235ustar00rootroot00000000000000rsbackup-10.0/tests/configs/outdent/config000066400000000000000000000002631440730431700207140ustar00rootroot00000000000000backup-job-timeout 7200s ssh-timeout 60s host alpha volume root / max-age 1d priority 123 volume home /home max-age 2d host beta ssh-timeout 120s rsbackup-10.0/tests/configs/pruneparam/000077500000000000000000000000001440730431700202135ustar00rootroot00000000000000rsbackup-10.0/tests/configs/pruneparam/config000066400000000000000000000001771440730431700214100ustar00rootroot00000000000000prune-parameter a b prune-parameter c d prune-parameter e f host spong prune-parameter a bb prune-parameter --remove c rsbackup-10.0/tests/expect/000077500000000000000000000000001440730431700157015ustar00rootroot00000000000000rsbackup-10.0/tests/expect/backup/000077500000000000000000000000001440730431700171465ustar00rootroot00000000000000rsbackup-10.0/tests/expect/backup/dryrun.html000066400000000000000000000061301440730431700213570ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 3 volumes have no backups.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 none 0 none 0 none 0
      volume2 none 0 none 0 none 0
      volume3 none 0 none 0 none 0

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/backup/dryrun.txt000066400000000000000000000013361440730431700212350ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 3 volumes have no backups. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | none | 0 | none | 0 | | none | 0 | | | | volume2 | none | 0 | none | 0 | | none | 0 | | | | volume3 | none | 0 | none | 0 | | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/backup/everything.html000066400000000000000000000063101440730431700222200ustar00rootroot00000000000000 Backup report (1980-01-03)

      Backup report (1980-01-03)

      Warnings

      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 6 1980-01-03 3 4K 1980-01-03 3 4K
      volume2 1980-01-02 4 1980-01-03 2 2M 1980-01-03 2 2M
      volume3 1980-01-02 2 none 0 1980-01-03 2 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/backup/everything.txt000066400000000000000000000014561440730431700221010ustar00rootroot00000000000000==== Backup report (1980-01-03) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 6 | 1980-01-03 | 3 | 4K | 1980-01-03 | 3 | 4K | | | volume2 | 1980-01-02 | 4 | 1980-01-03 | 2 | 2M | 1980-01-03 | 2 | 2M | | | volume3 | 1980-01-02 | 2 | none | 0 | | 1980-01-03 | 2 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/backup/onehost.html000066400000000000000000000063101440730431700215130ustar00rootroot00000000000000 Backup report (1980-01-02)

      Backup report (1980-01-02)

      Warnings

      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 4 1980-01-02 2 4K 1980-01-02 2 4K
      volume2 1980-01-02 2 1980-01-02 1 2M 1980-01-02 1 2M
      volume3 1980-01-02 1 none 0 1980-01-02 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/backup/onehost.txt000066400000000000000000000014561440730431700213740ustar00rootroot00000000000000==== Backup report (1980-01-02) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 4 | 1980-01-02 | 2 | 4K | 1980-01-02 | 2 | 4K | | | volume2 | 1980-01-02 | 2 | 1980-01-02 | 1 | 2M | 1980-01-02 | 1 | 2M | | | volume3 | 1980-01-02 | 1 | none | 0 | | 1980-01-02 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/backup/onevolume.html000066400000000000000000000062011440730431700220440ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 2 volumes have no backups.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 2 1980-01-01 1 4K 1980-01-01 1 4K
      volume2 none 0 none 0 none 0
      volume3 none 0 none 0 none 0

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/backup/onevolume.txt000066400000000000000000000014461440730431700217250ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 2 volumes have no backups. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume2 | none | 0 | none | 0 | | none | 0 | | | | volume3 | none | 0 | none | 0 | | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/check-file/000077500000000000000000000000001440730431700176735ustar00rootroot00000000000000rsbackup-10.0/tests/expect/check-file/missing.html000066400000000000000000000063141440730431700222360ustar00rootroot00000000000000 Backup report (1980-01-03)

      Backup report (1980-01-03)

      Warnings

      • WARNING: 1 volumes have no backups.
      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 none 0 none 0 none 0
      volume2 1980-01-03 2 1980-01-03 1 2M 1980-01-03 1 2M
      volume3 1980-01-03 1 none 0 1980-01-03 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/check-file/missing.txt000066400000000000000000000015251440730431700221100ustar00rootroot00000000000000==== Backup report (1980-01-03) ==== === Warnings === * WARNING: 1 volumes have no backups. * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | none | 0 | none | 0 | | none | 0 | | | | volume2 | 1980-01-03 | 2 | 1980-01-03 | 1 | 2M | 1980-01-03 | 1 | 2M | | | volume3 | 1980-01-03 | 1 | none | 0 | | 1980-01-03 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/empty.txt000066400000000000000000000022331440730431700176000ustar00rootroot00000000000000max-age 3d backup-policy daily prune-policy age ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh public false logs /var/log/backup keep-prune-logs 31d prune-timeout 0d color-good 0xe0ffe0 color-bad 0xff4040 sendmail /usr/sbin/sendmail report "title:Backup report (${RSBACKUP_DATE})" report + "h1:Backup report (${RSBACKUP_DATE})" h2:Warnings?warnings warnings report + h2:Summary summary history-graph h2:Logfiles logs "h3:Pruning logs" report + prune-logs "p:Generated ${RSBACKUP_CTIME}" color-graph-background 0xffffff color-graph-foreground 0x000000 color-month-guide 0xf7f7f7 color-host-guide 0xdfdfdf color-volume-guide 0xefefef device-color-strategy equidistant-value 120 0.75 horizontal-padding 8 vertical-padding 2 backup-indicator-width 4 backup-indicator-height 2 graph-target-width 0 backup-indicator-key-width 16 host-name-font Normal volume-name-font Normal device-name-font Normal time-label-font Normal graph-layout host-labels:0,0 volume-labels:1,0 content:2,0 time-labels:2,1 graph-layout + device-key:2,3:RC rsbackup-10.0/tests/expect/glob-store/000077500000000000000000000000001440730431700177565ustar00rootroot00000000000000rsbackup-10.0/tests/expect/glob-store/everything.html000066400000000000000000000063101440730431700230300ustar00rootroot00000000000000 Backup report (1980-01-03)

      Backup report (1980-01-03)

      Warnings

      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-03 2 1980-01-03 1 4K 1980-01-03 1 4K
      volume2 1980-01-03 2 1980-01-03 1 2M 1980-01-03 1 2M
      volume3 1980-01-03 1 none 0 1980-01-03 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/glob-store/everything.txt000066400000000000000000000014561440730431700227110ustar00rootroot00000000000000==== Backup report (1980-01-03) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-03 | 2 | 1980-01-03 | 1 | 4K | 1980-01-03 | 1 | 4K | | | volume2 | 1980-01-03 | 2 | 1980-01-03 | 1 | 2M | 1980-01-03 | 1 | 2M | | | volume3 | 1980-01-03 | 1 | none | 0 | | 1980-01-03 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/include.txt000066400000000000000000000034671440730431700200770ustar00rootroot00000000000000max-age 3d backup-policy daily prune-policy age ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh public false logs /var/log/backup keep-prune-logs 31d prune-timeout 0d color-good 0xe0ffe0 color-bad 0xff4040 sendmail /usr/sbin/sendmail report "title:Backup report (${RSBACKUP_DATE})" report + "h1:Backup report (${RSBACKUP_DATE})" h2:Warnings?warnings warnings report + h2:Summary summary history-graph h2:Logfiles logs "h3:Pruning logs" report + prune-logs "p:Generated ${RSBACKUP_CTIME}" color-graph-background 0xffffff color-graph-foreground 0x000000 color-month-guide 0xf7f7f7 color-host-guide 0xdfdfdf color-volume-guide 0xefefef device-color-strategy equidistant-value 120 0.75 horizontal-padding 8 vertical-padding 2 backup-indicator-width 4 backup-indicator-height 2 graph-target-width 0 backup-indicator-key-width 16 host-name-font Normal volume-name-font Normal device-name-font Normal time-label-font Normal graph-layout host-labels:0,0 volume-labels:1,0 content:2,0 time-labels:2,1 graph-layout + device-key:2,3:RC host a max-age 3d backup-policy daily prune-policy age ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh hostname a devices * priority 0 host z max-age 3d backup-policy daily prune-policy age ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh hostname z devices * priority 0 rsbackup-10.0/tests/expect/issue43/000077500000000000000000000000001440730431700172005ustar00rootroot00000000000000rsbackup-10.0/tests/expect/issue43/failhookerr.txt000066400000000000000000000000751440730431700222500ustar00rootroot00000000000000ERROR: host1:volume1 pre-volume-hook failed: failhook-stderr rsbackup-10.0/tests/expect/issue43/failhookout.txt000066400000000000000000000000001440730431700222530ustar00rootroot00000000000000rsbackup-10.0/tests/expect/issue43/tempfailhook.txt000066400000000000000000000000001440730431700224110ustar00rootroot00000000000000rsbackup-10.0/tests/expect/outdent.txt000066400000000000000000000053071440730431700201310ustar00rootroot00000000000000max-age 3d backup-policy daily prune-policy age backup-job-timeout 2h ssh-timeout 2m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh public false logs /var/log/backup keep-prune-logs 31d prune-timeout 0d color-good 0xe0ffe0 color-bad 0xff4040 sendmail /usr/sbin/sendmail report "title:Backup report (${RSBACKUP_DATE})" report + "h1:Backup report (${RSBACKUP_DATE})" h2:Warnings?warnings warnings report + h2:Summary summary history-graph h2:Logfiles logs "h3:Pruning logs" report + prune-logs "p:Generated ${RSBACKUP_CTIME}" color-graph-background 0xffffff color-graph-foreground 0x000000 color-month-guide 0xf7f7f7 color-host-guide 0xdfdfdf color-volume-guide 0xefefef device-color-strategy equidistant-value 120 0.75 horizontal-padding 8 vertical-padding 2 backup-indicator-width 4 backup-indicator-height 2 graph-target-width 0 backup-indicator-key-width 16 host-name-font Normal volume-name-font Normal device-name-font Normal time-label-font Normal graph-layout host-labels:0,0 volume-labels:1,0 content:2,0 time-labels:2,1 graph-layout + device-key:2,3:RC host alpha max-age 3d backup-policy daily prune-policy age backup-job-timeout 2h ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh hostname alpha devices * priority 123 volume home /home max-age 2d backup-policy daily prune-policy age backup-job-timeout 2h ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime devices * traverse false check-mounted false volume root / max-age 1d backup-policy daily prune-policy age backup-job-timeout 2h ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime devices * traverse false check-mounted false host beta max-age 3d backup-policy daily prune-policy age backup-job-timeout 2h ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh hostname beta devices * priority 0 rsbackup-10.0/tests/expect/prune/000077500000000000000000000000001440730431700170325ustar00rootroot00000000000000rsbackup-10.0/tests/expect/prune/created.txt000066400000000000000000000014561440730431700212100ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume2 | 1980-01-01 | 2 | 1980-01-01 | 1 | 2M | 1980-01-01 | 1 | 2M | | | volume3 | 1980-01-01 | 1 | none | 0 | | 1980-01-01 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/prune/createsecond.txt000066400000000000000000000014561440730431700222400ustar00rootroot00000000000000==== Backup report (1980-01-02) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 4 | 1980-01-02 | 2 | 4K | 1980-01-02 | 2 | 4K | | | volume2 | 1980-01-01 | 4 | 1980-01-02 | 2 | 2M | 1980-01-02 | 2 | 2M | | | volume3 | 1980-01-01 | 2 | none | 0 | | 1980-01-02 | 2 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/prune/dryrun-db.txt000066400000000000000000000011041440730431700214750ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|2|315532800|0 host1|volume1|device2|1980-01-01T00:00:00|0|2|315532800|0 host1|volume2|device1|1980-01-01T00:00:00|0|2|315532800|0 host1|volume2|device2|1980-01-01T00:00:00|0|2|315532800|0 host1|volume3|device2|1980-01-01T00:00:00|0|2|315532800|0 host1|volume1|device1|1980-01-02T00:00:00|0|2|315619200|0 host1|volume1|device2|1980-01-02T00:00:00|0|2|315619200|0 host1|volume2|device1|1980-01-02T00:00:00|0|2|315619200|0 host1|volume2|device2|1980-01-02T00:00:00|0|2|315619200|0 host1|volume3|device2|1980-01-02T00:00:00|0|2|315619200|0 rsbackup-10.0/tests/expect/prune/dryrun.txt000066400000000000000000000015251440730431700211210ustar00rootroot00000000000000==== Backup report (1980-02-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. * WARNING: 2 volumes are out of date. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 4 | 1980-01-02 | 2 | 4K | 1980-01-02 | 2 | 4K | | | volume2 | 1980-01-01 | 4 | 1980-01-02 | 2 | 2M | 1980-01-02 | 2 | 2M | | | volume3 | 1980-01-01 | 2 | none | 0 | | 1980-01-02 | 2 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/prune/everything-db.txt000066400000000000000000000004461440730431700223460ustar00rootroot00000000000000host1|volume3|device2|1980-01-01T00:00:00|0|5|315532800|347155200|age 365 > 2 and remaining 3 > 2 host1|volume1|device1|1980-01-02T00:00:00|0|5|315619200|347155200|age 364 > 2 and remaining 2 > 1 host1|volume1|device2|1980-01-02T00:00:00|0|5|315619200|347155200|age 364 > 2 and remaining 2 > 1 rsbackup-10.0/tests/expect/prune/later-db.txt000066400000000000000000000000001440730431700212530ustar00rootroot00000000000000rsbackup-10.0/tests/expect/prune/null-db.txt000066400000000000000000000004421440730431700211300ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|2|315532800|0 host1|volume1|device2|1980-01-01T00:00:00|0|2|315532800|0 host1|volume2|device1|1980-01-01T00:00:00|0|2|315532800|0 host1|volume2|device2|1980-01-01T00:00:00|0|2|315532800|0 host1|volume3|device2|1980-01-01T00:00:00|0|2|315532800|0 rsbackup-10.0/tests/expect/prune/null.txt000066400000000000000000000015251440730431700205500ustar00rootroot00000000000000==== Backup report (1980-02-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. * WARNING: 2 volumes are out of date. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume2 | 1980-01-01 | 2 | 1980-01-01 | 1 | 2M | 1980-01-01 | 1 | 2M | | | volume3 | 1980-01-01 | 1 | none | 0 | | 1980-01-01 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/prune/third.txt000066400000000000000000000016701440730431700207110ustar00rootroot00000000000000==== Backup report (1980-01-03) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-02 | 4 | 1980-01-03 | 2 | 4K | 1980-01-03 | 2 | 4K | | | volume2 | 1980-01-01 | 6 | 1980-01-03 | 3 | 2M | 1980-01-03 | 3 | 2M | | | volume3 | 1980-01-01 | 3 | none | 0 | | 1980-01-03 | 3 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason | | 1980-01-01 | 1980-02-01 | host1 | volume1 | device1,device2 | age 31 > 2 and remaining 2 > 1| Generated rsbackup-10.0/tests/expect/prune/unselected-db.txt000066400000000000000000000003021440730431700223040ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|5|315532800|318211200|age 31 > 2 and remaining 2 > 1 host1|volume1|device2|1980-01-01T00:00:00|0|5|315532800|318211200|age 31 > 2 and remaining 2 > 1 rsbackup-10.0/tests/expect/prune/unselected.txt000066400000000000000000000016701440730431700217320ustar00rootroot00000000000000==== Backup report (1980-01-04) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-02 | 4 | 1980-01-03 | 2 | 4K | 1980-01-03 | 2 | 4K | | | volume2 | 1980-01-01 | 6 | 1980-01-03 | 3 | 2M | 1980-01-03 | 3 | 2M | | | volume3 | 1980-01-01 | 3 | none | 0 | | 1980-01-03 | 3 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason | | 1980-01-01 | 1980-02-01 | host1 | volume1 | device1,device2 | age 31 > 2 and remaining 2 > 1| Generated rsbackup-10.0/tests/expect/prune/volume1-db.txt000066400000000000000000000003021440730431700215410ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|5|315532800|318211200|age 31 > 2 and remaining 2 > 1 host1|volume1|device2|1980-01-01T00:00:00|0|5|315532800|318211200|age 31 > 2 and remaining 2 > 1 rsbackup-10.0/tests/expect/prune/volume1.txt000066400000000000000000000017371440730431700211730ustar00rootroot00000000000000==== Backup report (1980-02-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. * WARNING: 2 volumes are out of date. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-02 | 2 | 1980-01-02 | 1 | 4K | 1980-01-02 | 1 | 4K | | | volume2 | 1980-01-01 | 4 | 1980-01-02 | 2 | 2M | 1980-01-02 | 2 | 2M | | | volume3 | 1980-01-01 | 2 | none | 0 | | 1980-01-02 | 2 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason | | 1980-01-01 | 1980-02-01 | host1 | volume1 | device1,device2 | age 31 > 2 and remaining 2 > 1| Generated rsbackup-10.0/tests/expect/prune/volume2-db.txt000066400000000000000000000006021440730431700215450ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|5|315532800|318211200|age 31 > 2 and remaining 2 > 1 host1|volume1|device2|1980-01-01T00:00:00|0|5|315532800|318211200|age 31 > 2 and remaining 2 > 1 host1|volume2|device1|1980-01-01T00:00:00|0|5|315532800|315792000|age 3 > 2 and remaining 3 > 2 host1|volume2|device2|1980-01-01T00:00:00|0|5|315532800|315792000|age 3 > 2 and remaining 3 > 2 rsbackup-10.0/tests/expect/prune/volume2.txt000066400000000000000000000020301440730431700211570ustar00rootroot00000000000000==== Backup report (1980-01-04) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-02 | 4 | 1980-01-03 | 2 | 4K | 1980-01-03 | 2 | 4K | | | volume2 | 1980-01-02 | 4 | 1980-01-03 | 2 | 2M | 1980-01-03 | 2 | 2M | | | volume3 | 1980-01-01 | 3 | none | 0 | | 1980-01-03 | 3 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason | | 1980-01-01 | 1980-02-01 | host1 | volume1 | device1,device2 | age 31 > 2 and remaining 2 > 1| | 1980-01-01 | 1980-01-04 | host1 | volume2 | device1,device2 | age 3 > 2 and remaining 3 > 2 | Generated rsbackup-10.0/tests/expect/prunedecay/000077500000000000000000000000001440730431700200405ustar00rootroot00000000000000rsbackup-10.0/tests/expect/prunedecay/prunedecay-db.txt000066400000000000000000000006161440730431700233260ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|5|315532800|316310400|age 9 > 8 and other backups exist host1|volume1|device2|1980-01-01T00:00:00|0|5|315532800|316310400|age 9 > 8 and other backups exist host1|volume1|device1|1980-01-06T00:00:00|0|5|315964800|316224000|age 3 > 1 and oldest in bucket 1 host1|volume1|device2|1980-01-06T00:00:00|0|5|315964800|316224000|age 3 > 1 and oldest in bucket 1 rsbackup-10.0/tests/expect/pruneexec/000077500000000000000000000000001440730431700176775ustar00rootroot00000000000000rsbackup-10.0/tests/expect/pruneexec/pruneexec-db.txt000066400000000000000000000005361440730431700230250ustar00rootroot00000000000000host1|volume1|device1|1980-01-02T00:00:00|0|5|315619200|315792000|zap host1|volume1|device2|1980-01-02T00:00:00|0|5|315619200|315792000|zap host1|volume2|device1|1980-01-02T00:00:00|0|5|315619200|315792000|zap host1|volume2|device2|1980-01-02T00:00:00|0|5|315619200|315792000|zap host1|volume3|device2|1980-01-02T00:00:00|0|5|315619200|315792000|zap rsbackup-10.0/tests/expect/prunenever/000077500000000000000000000000001440730431700200725ustar00rootroot00000000000000rsbackup-10.0/tests/expect/prunenever/neverprune-db.txt000066400000000000000000000000001440730431700233750ustar00rootroot00000000000000rsbackup-10.0/tests/expect/pruneparam.txt000066400000000000000000000031751440730431700206220ustar00rootroot00000000000000max-age 3d backup-policy daily prune-policy age prune-parameter a b prune-parameter c d prune-parameter e f ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh public false logs /var/log/backup keep-prune-logs 31d prune-timeout 0d color-good 0xe0ffe0 color-bad 0xff4040 sendmail /usr/sbin/sendmail report "title:Backup report (${RSBACKUP_DATE})" report + "h1:Backup report (${RSBACKUP_DATE})" h2:Warnings?warnings warnings report + h2:Summary summary history-graph h2:Logfiles logs "h3:Pruning logs" report + prune-logs "p:Generated ${RSBACKUP_CTIME}" color-graph-background 0xffffff color-graph-foreground 0x000000 color-month-guide 0xf7f7f7 color-host-guide 0xdfdfdf color-volume-guide 0xefefef device-color-strategy equidistant-value 120 0.75 horizontal-padding 8 vertical-padding 2 backup-indicator-width 4 backup-indicator-height 2 graph-target-width 0 backup-indicator-key-width 16 host-name-font Normal volume-name-font Normal device-name-font Normal time-label-font Normal graph-layout host-labels:0,0 volume-labels:1,0 content:2,0 time-labels:2,1 graph-layout + device-key:2,3:RC host spong max-age 3d backup-policy daily prune-policy age prune-parameter a bb prune-parameter e f prune-parameter --remove c ssh-timeout 1m rsync-command rsync rsync-base-options --archive --sparse --numeric-ids --compress --fuzzy --hard-links --delete --stats rsync-extra-options --xattrs --acls --open-noatime host-check ssh hostname spong devices * priority 0 rsbackup-10.0/tests/expect/prunetimeout/000077500000000000000000000000001440730431700204415ustar00rootroot00000000000000rsbackup-10.0/tests/expect/prunetimeout/prunetimeout-db.txt000066400000000000000000000016361440730431700243330ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|4|315532800|318211200 host1|volume1|device2|1980-01-01T00:00:00|0|4|315532800|318211200 host1|volume2|device1|1980-01-01T00:00:00|0|4|315532800|318211200 host1|volume2|device2|1980-01-01T00:00:00|0|4|315532800|318211200 host1|volume3|device2|1980-01-01T00:00:00|0|4|315532800|318211200 host1|volume1|device1|1980-01-02T00:00:00|0|4|315619200|318211200 host1|volume1|device2|1980-01-02T00:00:00|0|4|315619200|318211200 host1|volume2|device1|1980-01-02T00:00:00|0|2|315619200|0 host1|volume2|device2|1980-01-02T00:00:00|0|2|315619200|0 host1|volume3|device2|1980-01-02T00:00:00|0|2|315619200|0 host1|volume1|device1|1980-01-03T00:00:00|0|2|315705600|0 host1|volume1|device2|1980-01-03T00:00:00|0|2|315705600|0 host1|volume2|device1|1980-01-03T00:00:00|0|2|315705600|0 host1|volume2|device2|1980-01-03T00:00:00|0|2|315705600|0 host1|volume3|device2|1980-01-03T00:00:00|0|2|315705600|0 rsbackup-10.0/tests/expect/retire-device/000077500000000000000000000000001440730431700204305ustar00rootroot00000000000000rsbackup-10.0/tests/expect/retire-device/create.txt000066400000000000000000000014561440730431700224420ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume2 | 1980-01-01 | 2 | 1980-01-01 | 1 | 2M | 1980-01-01 | 1 | 2M | | | volume3 | 1980-01-01 | 1 | none | 0 | | 1980-01-01 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/retire-device/created-db.txt000066400000000000000000000003461440730431700231660ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|2 host1|volume1|device2|1980-01-01T00:00:00|0|2 host1|volume2|device1|1980-01-01T00:00:00|0|2 host1|volume2|device2|1980-01-01T00:00:00|0|2 host1|volume3|device2|1980-01-01T00:00:00|0|2 rsbackup-10.0/tests/expect/retire-device/device2-db.txt000066400000000000000000000001341440730431700230730ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|2 host1|volume2|device1|1980-01-01T00:00:00|0|2 rsbackup-10.0/tests/expect/retire-device/device2.txt000066400000000000000000000011761440730431700225170ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes have no backups. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | | | | | | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 1 | 1980-01-01 | 1 | 4K | | | volume2 | 1980-01-01 | 1 | 1980-01-01 | 1 | 2M | | | volume3 | none | 0 | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/retire-device/dryrun.txt000066400000000000000000000012301440730431700225100ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * Unknown device device2 * WARNING: 1 volumes have no backups. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | | | | | | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 1 | 1980-01-01 | 1 | 4K | | | volume2 | 1980-01-01 | 1 | 1980-01-01 | 1 | 2M | | | volume3 | none | 0 | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/retire-volume/000077500000000000000000000000001440730431700205005ustar00rootroot00000000000000rsbackup-10.0/tests/expect/retire-volume/all-db.txt000066400000000000000000000000001440730431700223620ustar00rootroot00000000000000rsbackup-10.0/tests/expect/retire-volume/all.html000066400000000000000000000056371440730431700221510ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 2 volumes have no backups.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 none 0 none 0 none 0
      volume3 none 0 none 0 none 0

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/retire-volume/all.txt000066400000000000000000000012121440730431700220050ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 2 volumes have no backups. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | none | 0 | none | 0 | | none | 0 | | | | volume3 | none | 0 | none | 0 | | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/retire-volume/create.html000066400000000000000000000063101440730431700226310ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 2 1980-01-01 1 4K 1980-01-01 1 4K
      volume2 1980-01-01 2 1980-01-01 1 2M 1980-01-01 1 2M
      volume3 1980-01-01 1 none 0 1980-01-01 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/retire-volume/create.txt000066400000000000000000000014561440730431700225120ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume2 | 1980-01-01 | 2 | 1980-01-01 | 1 | 2M | 1980-01-01 | 1 | 2M | | | volume3 | 1980-01-01 | 1 | none | 0 | | 1980-01-01 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/retire-volume/created-db.txt000066400000000000000000000003461440730431700232360ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|2 host1|volume1|device2|1980-01-01T00:00:00|0|2 host1|volume2|device1|1980-01-01T00:00:00|0|2 host1|volume2|device2|1980-01-01T00:00:00|0|2 host1|volume3|device2|1980-01-01T00:00:00|0|2 rsbackup-10.0/tests/expect/retire-volume/dryrun.html000066400000000000000000000061261440730431700227160ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • Unknown volume host1:volume2 on device device1
      • Unknown volume host1:volume2 on device device2
      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 2 1980-01-01 1 4K 1980-01-01 1 4K
      volume3 1980-01-01 1 none 0 1980-01-01 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/retire-volume/dryrun.txt000066400000000000000000000014621440730431700225670ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * Unknown volume host1:volume2 on device device1 * Unknown volume host1:volume2 on device device2 * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume3 | 1980-01-01 | 1 | none | 0 | | 1980-01-01 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/retire-volume/volume2-db.txt000066400000000000000000000002121440730431700232100ustar00rootroot00000000000000host1|volume1|device1|1980-01-01T00:00:00|0|2 host1|volume1|device2|1980-01-01T00:00:00|0|2 host1|volume3|device2|1980-01-01T00:00:00|0|2 rsbackup-10.0/tests/expect/retire-volume/volume2.html000066400000000000000000000057461440730431700227730ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 2 1980-01-01 1 4K 1980-01-01 1 4K
      volume3 1980-01-01 1 none 0 1980-01-01 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/retire-volume/volume2.txt000066400000000000000000000013161440730431700226330ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 2 | 1980-01-01 | 1 | 4K | 1980-01-01 | 1 | 4K | | | volume3 | 1980-01-01 | 1 | none | 0 | | 1980-01-01 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/store/000077500000000000000000000000001440730431700170355ustar00rootroot00000000000000rsbackup-10.0/tests/expect/store/duplicate-stderr.txt000066400000000000000000000005431440730431700230530ustar00rootroot00000000000000WARNING: store '/store1' has duplicate device-id 'device1', also found on store '/store3' WARNING: cannot backup host1:volume1 to device2 - device suppressed due to --store WARNING: cannot backup host1:volume2 to device2 - device suppressed due to --store WARNING: cannot backup host1:volume3 to device2 - device suppressed due to --store rsbackup-10.0/tests/expect/store/duplicate.html000066400000000000000000000062731440730431700217050ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 1 volumes have no backups.
      • WARNING: 2 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 1 1980-01-01 1 4K none 0
      volume2 1980-01-01 1 1980-01-01 1 2M none 0
      volume3 none 0 none 0 none 0

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/store/duplicate.txt000066400000000000000000000014751440730431700215570ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes have no backups. * WARNING: 2 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 1 | 1980-01-01 | 1 | 4K | none | 0 | | | | volume2 | 1980-01-01 | 1 | 1980-01-01 | 1 | 2M | none | 0 | | | | volume3 | none | 0 | none | 0 | | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/store/notmounted.txt000066400000000000000000000007261440730431700217770ustar00rootroot00000000000000WARNING: store '/store3' is not mounted ERROR: no backup devices found WARNING: cannot backup host1:volume1 to device1 - device not available WARNING: cannot backup host1:volume1 to device2 - device suppressed due to --store WARNING: cannot backup host1:volume2 to device1 - device not available WARNING: cannot backup host1:volume2 to device2 - device suppressed due to --store WARNING: cannot backup host1:volume3 to device2 - device suppressed due to --store rsbackup-10.0/tests/expect/store/overridden-stderr.txt000066400000000000000000000003711440730431700232410ustar00rootroot00000000000000WARNING: cannot backup host1:volume1 to device2 - device suppressed due to --store WARNING: cannot backup host1:volume2 to device2 - device suppressed due to --store WARNING: cannot backup host1:volume3 to device2 - device suppressed due to --store rsbackup-10.0/tests/expect/store/overridden.html000066400000000000000000000062731440730431700220740ustar00rootroot00000000000000 Backup report (1980-01-01)

      Backup report (1980-01-01)

      Warnings

      • WARNING: 1 volumes have no backups.
      • WARNING: 2 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-01 1 1980-01-01 1 4K none 0
      volume2 1980-01-01 1 1980-01-01 1 2M none 0
      volume3 none 0 none 0 none 0

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/store/overridden.txt000066400000000000000000000014751440730431700217460ustar00rootroot00000000000000==== Backup report (1980-01-01) ==== === Warnings === * WARNING: 1 volumes have no backups. * WARNING: 2 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-01 | 1 | 1980-01-01 | 1 | 4K | none | 0 | | | | volume2 | 1980-01-01 | 1 | 1980-01-01 | 1 | 2M | none | 0 | | | | volume3 | none | 0 | none | 0 | | none | 0 | | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/expect/style/000077500000000000000000000000001440730431700170415ustar00rootroot00000000000000rsbackup-10.0/tests/expect/style/styled.html000066400000000000000000000031141440730431700212320ustar00rootroot00000000000000 Backup report (1980-01-03)

      Backup report (1980-01-03)

      Warnings

      • WARNING: 1 volumes are not fully backed up.

      Summary

      Host Volume Oldest Total Devices
      device1 device2
      Newest Count Size Newest Count Size
      host1 volume1 1980-01-03 2 1980-01-03 1 4K 1980-01-03 1 4K
      volume2 1980-01-03 2 1980-01-03 1 2M 1980-01-03 1 2M
      volume3 1980-01-03 1 none 0 1980-01-03 1 4

      Logfiles

      Pruning logs

      Created Pruned Host Volume Device Reason

      Generated <timestamp>

      rsbackup-10.0/tests/expect/style/styled.txt000066400000000000000000000014561440730431700211140ustar00rootroot00000000000000==== Backup report (1980-01-03) ==== === Warnings === * WARNING: 1 volumes are not fully backed up. === Summary === | Host | Volume | Oldest | Total | Devices | | | | | | device1 | device2 | | | | | | Newest | Count | Size | Newest | Count | Size| | host1 | volume1 | 1980-01-03 | 2 | 1980-01-03 | 1 | 4K | 1980-01-03 | 1 | 4K | | | volume2 | 1980-01-03 | 2 | 1980-01-03 | 1 | 2M | 1980-01-03 | 1 | 2M | | | volume3 | 1980-01-03 | 1 | none | 0 | | 1980-01-03 | 1 | 4 | === Logfiles === == Pruning logs == | Created | Pruned | Host | Volume | Device | Reason| Generated rsbackup-10.0/tests/glob-store000077500000000000000000000032401440730431700164130ustar00rootroot00000000000000#! /bin/sh # Copyright © 2011, 2012, 2014, 2015, 2018 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup sed < ${WORKSPACE}/config > ${WORKSPACE}/config.new \ '/^store /d'; echo "store-pattern --no-mounted ${WORKSPACE}/store*" >>${WORKSPACE}/config.new mv -f ${WORKSPACE}/config.new ${WORKSPACE}/config echo "| Create backup for everything using glob-pattern directive" RUN=all RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/everything.txt --html ${WORKSPACE}/got/everything.html exists ${WORKSPACE}/all-pre.acted exists ${WORKSPACE}/all-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00 compare ${srcdir:-.}/expect/glob-store/everything.txt ${WORKSPACE}/got/everything.txt compare ${srcdir:-.}/expect/glob-store/everything.html ${WORKSPACE}/got/everything.html cleanup rsbackup-10.0/tests/hook000077500000000000000000000043721440730431700153050ustar00rootroot00000000000000#! /bin/sh # Copyright © 2012-14, 2018 Richard Kettlewell. # # 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 . set -e #echo "Running hook..." #env | grep ^RSBACKUP | sort case "$RSBACKUP_HOOK" in pre-volume-hook ) if [ -e ${WORKSPACE}/hookdata ]; then echo >&3 "HOOK ERROR: $RSBACKUP_HOOK: hookdata already exists" exit 1 fi touch ${WORKSPACE}/hookdata what=pre ;; post-volume-hook ) if [ ! -e ${WORKSPACE}/hookdata ]; then echo >&3 "HOOK ERROR: $RSBACKUP_HOOK: hookdata does not exist" exit 1 fi rm -f ${WORKSPACE}/hookdata what=post ;; pre-device-hook ) if [ -e ${WORKSPACE}/devhookdata ]; then echo >&3 "HOOK ERROR: $RSBACKUP_HOOK: devhookdata exists" exit 1 fi touch ${WORKSPACE}/devhookdata what=dev-pre ;; post-device-hook ) if [ ! -e ${WORKSPACE}/devhookdata ]; then echo >&3 "HOOK ERROR: $RSBACKUP_HOOK: devhookdata does not exist" exit 1 fi rm -f ${WORKSPACE}/devhookdata what=dev-post ;; * ) echo >&3 "HOOK ERROR: unknown hook $RSBACKUP_HOOK" exit 1 ;; esac if [ ! -z "${RUN}" ]; then touch ${WORKSPACE}/${RUN}-${what}.ran if ${RSBACKUP_ACT}; then touch ${WORKSPACE}/${RUN}-${what}.acted fi fi case "$RSBACKUP_HOOK" in pre-volume-hook ) case "$PRE_BACKUP_HOOK_STDERR" in *? ) echo "$PRE_BACKUP_HOOK_STDERR" >&2 ;; esac case "$PRE_BACKUP_HOOK_STATUS" in 75 ) rm -f ${WORKSPACE}/hookdata exit $PRE_BACKUP_HOOK_STATUS ;; 0 | "" ) exit 0 ;; * ) exit $PRE_BACKUP_HOOK_STATUS ;; esac ;; esac rsbackup-10.0/tests/hostgroup000077500000000000000000000033071440730431700163740ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e srcdir=${srcdir:-.} . ${srcdir}/setup.sh # Use the wrapper RSYNC_COMMAND="${srcdir}/rsync-wrap" setup # Add a second host echo "host host2" >> ${WORKSPACE}/config echo " group host1" >> ${WORKSPACE}/config echo " hostname localhost" >> ${WORKSPACE}/config echo " volume volume1 ${WORKSPACE}/h2v1" >> ${WORKSPACE}/config echo " volume volume2 ${WORKSPACE}/h2v2" >> ${WORKSPACE}/config mkdir ${WORKSPACE}/h2v1 echo h2v1 > ${WORKSPACE}/h2v1/h2v1.txt mkdir ${WORKSPACE}/h2v2 echo h2v2 > ${WORKSPACE}/h2v2/h2v2.txt echo "| Backup hosts serially" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup while read time action device host volume; do case "$host" in host1 | host2 ) group=host1 ;; * ) group="$host" ;; esac case $action in start ) if [ -e ${WORKSPACE}/${group}.active ]; then echo >&2 "ERROR: group ${group} not serialized" exit 1 else touch ${WORKSPACE}/${group}.active fi ;; stop ) rm -f ${WORKSPACE}/${group}.active ;; esac done < ${WORKSPACE}/wrap.log rsbackup-10.0/tests/issue37000077500000000000000000000055351440730431700156510ustar00rootroot00000000000000#! /bin/sh # Copyright © 2017 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup linux_set_acl() { setfacl -m u:root:0 ${WORKSPACE}/volume1/has_acl getfacl -c ${WORKSPACE}/volume1/has_acl > ${WORKSPACE}/acl.subject } linux_get_acl() { getfacl -c ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00/has_acl > ${WORKSPACE}/acl.backup } macos_set_acl() { #chmod +a "root deny read" ${WORKSPACE}/volume1/has_acl #ls -le ${WORKSPACE}/volume1/has_acl | sed '1d' > ${WORKSPACE}/acl.subject echo BROKEN > ${WORKSPACE}/acl.subject } macos_get_acl() { #ls -le ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00/has_acl| sed '1d' > ${WORKSPACE}/acl.backup echo BROKEN > ${WORKSPACE}/acl.backup } case $(uname -s) in Linux ) SET_ACL=linux_set_acl GET_ACL=linux_get_acl ;; Darwin ) SET_ACL=macos_set_acl GET_ACL=macos_get_acl ;; * ) echo >&2 "ERROR: I don't know how to set ACLs on this platform." exit 1 ;; esac # Create a file with a couple of extended attributes touch ${WORKSPACE}/volume1/has_xattr if xattr -w user.alpha foo ${WORKSPACE}/volume1/has_xattr; then : else # https://github.com/ewxrjk/rsbackup/issues/105 # # In my CI environment the above command fails with EOPNOTSUPP. # It's not clear why. The oddities for the CI environment are: # - it's a sid userland on a bullseye kernel # - it's a docker container # - it's running in a tmpfs echo >&2 "ERROR: xattr failed: skipping" exit 77 # skip fi xattr -w user.beta bar ${WORKSPACE}/volume1/has_xattr # Capture what that looks like xattr -l ${WORKSPACE}/volume1/has_xattr > ${WORKSPACE}/xattr.subject # Create a file with an ACL touch ${WORKSPACE}/volume1/has_acl $SET_ACL RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/onevolume.txt --html ${WORKSPACE}/got/onevolume.html host1:volume1 # Inspect the backed-up extended attributes xattr -l ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00/has_xattr > ${WORKSPACE}/attr.backup # Inspect the backed-up ACL $GET_ACL # Check the original matches the backup compare ${WORKSPACE}/xattr.subject ${WORKSPACE}/attr.backup compare ${WORKSPACE}/acl.subject ${WORKSPACE}/acl.backup rsbackup-10.0/tests/issue43000077500000000000000000000053241440730431700156420ustar00rootroot00000000000000#! /bin/bash # Copyright © 2018 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| Failing hook" set +e STDERR=${WORKSPACE}/got/overridden-stderr.txt STDOUT=${WORKSPACE}/got/overridden-stdout.txt PRE_BACKUP_HOOK_STATUS=1 PRE_BACKUP_HOOK_STDERR=failhook-stderr RUN=failhook RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup host1:volume1 status=$? set -e # If the hook fails then rsbackup should fail too if [ $status != 1 ]; then echo >&2 "FAILED: rsbackup run with failing hook exited with status $status" exit 1 fi absent ${WORKSPACE}/failhook-dev-pre.ran absent ${WORKSPACE}/failhook-dev-post.ran exists ${WORKSPACE}/failhook-pre.acted absent ${WORKSPACE}/failhook-post.acted absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store1/host1/volume3 absent ${WORKSPACE}/store2/host1/volume2 absent ${WORKSPACE}/store2/host1/volume3 compare ${srcdir:-.}/expect/issue43/failhookerr.txt ${WORKSPACE}/got/overridden-stderr.txt compare ${srcdir:-.}/expect/issue43/failhookout.txt ${WORKSPACE}/got/overridden-stdout.txt rm -f ${WORKSPACE}/hookdata echo "| Temporarily failed hook" STDERR=${WORKSPACE}/got/overridden-stderr.txt STDOUT=${WORKSPACE}/got/overridden-stdout.txt PRE_BACKUP_HOOK_STATUS=75 PRE_BACKUP_HOOK_STDERR=tempfailhook-stderr RUN=tempfailhook RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup host1:volume1 absent ${WORKSPACE}/tempfailhook-dev-pre.ran absent ${WORKSPACE}/tempfailhook-dev-post.ran exists ${WORKSPACE}/tempfailhook-pre.acted absent ${WORKSPACE}/tempfailhook-post.acted absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store1/host1/volume3 absent ${WORKSPACE}/store2/host1/volume2 absent ${WORKSPACE}/store2/host1/volume3 compare ${srcdir:-.}/expect/issue43/tempfailhook.txt ${WORKSPACE}/got/overridden-stderr.txt compare ${srcdir:-.}/expect/issue43/failhookout.txt ${WORKSPACE}/got/overridden-stdout.txt rsbackup-10.0/tests/issue55000077500000000000000000000017461440730431700156510ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "database ${WORKSPACE}/alt.db" >> ${WORKSPACE}/config RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/onevolume.txt --html ${WORKSPACE}/got/onevolume.html host1:volume1 exists ${WORKSPACE}/alt.db absent ${WORKSPACE}/logs/backups.db rsbackup-10.0/tests/issue70000077500000000000000000000045241440730431700156430ustar00rootroot00000000000000#! /bin/sh # Copyright © 2011, 2012, 2014, 2015, 2017, 2019 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup get_inode() { ls -li "$1" | awk '{print $1}' } # Suppress backup linking for volume 2 while IFS="" read -r line; do case "$line" in *"volume volume2"* ) echo "$line" echo " rsync-link-dest false" ;; * ) echo "$line" ;; esac done < ${WORKSPACE}/config > ${WORKSPACE}/config.new mv ${WORKSPACE}/config.new ${WORKSPACE}/config echo "| Create first backup" RUN=all RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup echo "| Create second backup" RUN=all RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup echo "| Modify file" echo >> ${WORKSPACE}/volume1/file1 echo "| Create third backup" RUN=all RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup echo "| Check that backup linking works" i1_1=$(get_inode "${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00/file1") i1_2=$(get_inode "${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00/file1") i1_3=$(get_inode "${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00/file1") if [ $i1_1 != $i1_2 ]; then echo >&2 "ERROR: inode mismatch file1: 1/2" exit 1 fi if [ $i1_2 == $i1_3 ]; then echo >&2 "ERROR: unexpected inode match file1: 2/3" exit 1 fi i4_1=$(get_inode "${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00/dir2/file4") i4_2=$(get_inode "${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00/dir2/file4") i4_3=$(get_inode "${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00/dir2/file4") if [ $i4_1 == $i4_2 ]; then echo >&2 "ERROR: unexpected inode match file4: 1/2" exit 1 fi if [ $i4_2 == $i4_3 ]; then echo >&2 "ERROR: unexpected inode match file4: 2/3" exit 1 fi cleanup rsbackup-10.0/tests/issue71000077500000000000000000000035331440730431700156430ustar00rootroot00000000000000#! /bin/bash # Copyright © 2020 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup ( # Make the hook global echo pre-volume-hook ${srcdir:-.}/hook echo post-volume-hook ${srcdir:-.}/hook while IFS="" read -r line; do case "$line" in *-volume-hook* ) # Remove volume-level references to the hook ;; *"volume volume1"* ) # Suppress the hook for volume1 echo "$line" echo " pre-volume-hook" echo " post-volume-hook" ;; * ) echo "$line" ;; esac done < ${WORKSPACE}/config ) > ${WORKSPACE}/config.new mv ${WORKSPACE}/config.new ${WORKSPACE}/config # Backup volume1; hook should not run RUN=h1v1 RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup host1:volume1 absent ${WORKSPACE}/h1v1-pre.ran absent ${WORKSPACE}/h1v1-post.ran # Backup volume2; hook should run RUN=h1v2 RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup host1:volume2 exists ${WORKSPACE}/h1v2-pre.ran exists ${WORKSPACE}/h1v2-post.ran # Backup volume3; hook should run RUN=h1v3 RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup host1:volume3 exists ${WORKSPACE}/h1v3-pre.ran exists ${WORKSPACE}/h1v3-post.ran rsbackup-10.0/tests/partial000077500000000000000000000040511440730431700157730ustar00rootroot00000000000000#! /bin/sh # Copyright © 2017 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup # Create a wrapper which exits with the warning status cat > ${WORKSPACE}/rsync < ${WORKSPACE}/config.edit grep -v rsync-command ${WORKSPACE}/config >> ${WORKSPACE}/config.edit mv ${WORKSPACE}/config.edit ${WORKSPACE}/config echo "| Create backup for one volume" RUN=volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/onevolume.txt --html ${WORKSPACE}/got/onevolume.html host1:volume1 exists ${WORKSPACE}/volume1-dev-pre.ran exists ${WORKSPACE}/volume1-dev-post.ran exists ${WORKSPACE}/volume1-dev-pre.ran exists ${WORKSPACE}/volume1-dev-post.ran exists ${WORKSPACE}/volume1-pre.acted exists ${WORKSPACE}/volume1-post.acted exists ${WORKSPACE}/volume1-pre.acted exists ${WORKSPACE}/volume1-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00.incomplete absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store1/host1/volume3 absent ${WORKSPACE}/store2/host1/volume2 absent ${WORKSPACE}/store2/host1/volume3 compare ${srcdir:-.}/expect/backup/onevolume.txt ${WORKSPACE}/got/onevolume.txt compare ${srcdir:-.}/expect/backup/onevolume.html ${WORKSPACE}/got/onevolume.html cleanup rsbackup-10.0/tests/prune000077500000000000000000000205141440730431700154720ustar00rootroot00000000000000#! /bin/sh # Copyright © 2011, 2012, 2014-2016 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup ## Check that min-backups is honored correctly echo "| Create backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/created.txt compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/prune/created.txt ${WORKSPACE}/got/created.txt echo "| Null prune" RUN=null RSBACKUP_TIME=318211200 s ${RSBACKUP} --prune --text ${WORKSPACE}/got/null.txt absent ${WORKSPACE}/null-pre.ran absent ${WORKSPACE}/null-pos.ran absent ${WORKSPACE}/null-dev-pre.ran absent ${WORKSPACE}/null-dev-post.ran absent ${WORKSPACE}/null-dev-pre.acted absent ${WORKSPACE}/null-dev-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/prune/null.txt ${WORKSPACE}/got/null.txt sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned FROM backup" > ${WORKSPACE}/got/null-db.txt compare ${srcdir:-.}/expect/prune/null-db.txt ${WORKSPACE}/got/null-db.txt echo "| Create second backup" RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/createsecond.txt # volume1: 1980-01-01 1980-01-02 # volume2: 1980-01-01 1980-01-02 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${srcdir:-.}/expect/prune/createsecond.txt ${WORKSPACE}/got/createsecond.txt echo "| --dry-run should do nothing" RUN=dryrun RSBACKUP_TIME=318211200 s ${RSBACKUP} --prune --dry-run --text ${WORKSPACE}/got/dryrun.txt absent ${WORKSPACE}/dryrun-pre.ran absent ${WORKSPACE}/dryrun-post.ran exists ${WORKSPACE}/dryrun-dev-pre.ran exists ${WORKSPACE}/dryrun-dev-post.ran absent ${WORKSPACE}/dryrun-dev-pre.acted absent ${WORKSPACE}/dryrun-dev-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${srcdir:-.}/expect/prune/dryrun.txt ${WORKSPACE}/got/dryrun.txt sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned FROM backup" > ${WORKSPACE}/got/dryrun-db.txt compare ${srcdir:-.}/expect/prune/dryrun-db.txt ${WORKSPACE}/got/dryrun-db.txt echo "| Prune affecting volume1" RUN=prune1 RSBACKUP_TIME=318211200 s ${RSBACKUP} --verbose --prune --text ${WORKSPACE}/got/volume1.txt absent ${WORKSPACE}/prune1-pre.ran absent ${WORKSPACE}/prune1-post.ran exists ${WORKSPACE}/prune1-dev-pre.ran exists ${WORKSPACE}/prune1-dev-post.ran exists ${WORKSPACE}/prune1-dev-pre.acted exists ${WORKSPACE}/prune1-dev-post.acted absent ${WORKSPACE}/store1/host1/volume1/1980-01-01 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 sed < ${WORKSPACE}/got/volume1.txt > ${WORKSPACE}/got/volume1.sed.txt "s,${PWD}/w-prune,,g" compare ${srcdir:-.}/expect/prune/volume1.txt ${WORKSPACE}/got/volume1.sed.txt # volume1: 1980-01-02 # volume2: 1980-01-01 1980-01-02 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/volume1-db.txt compare ${srcdir:-.}/expect/prune/volume1-db.txt ${WORKSPACE}/got/volume1-db.txt ## Check that prune-age is honored correctly echo "| Create third backup" RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/third.txt # volume1: 1980-01-02 1980-01-03 # volume2: 1980-01-01 1980-01-02 1980-01-03 RSBACKUP_TIME=315705600 s ${RSBACKUP} --prune absent ${WORKSPACE}/store1/host1/volume1/1980-01-01 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 sed < ${WORKSPACE}/got/third.txt > ${WORKSPACE}/got/third.sed.txt "s,${PWD}/w-prune,,g" compare ${srcdir:-.}/expect/prune/third.txt ${WORKSPACE}/got/third.sed.txt echo "| Prune affecting unselected volume" RUN=prune2 RSBACKUP_TIME=315792000 s ${RSBACKUP} --prune --text ${WORKSPACE}/got/unselected.txt host1:volume1 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/prune2-dev-pre.ran absent ${WORKSPACE}/prune2-dev-post.ran absent ${WORKSPACE}/prune2-dev-pre.acted absent ${WORKSPACE}/prune2-dev-post.acted compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 sed < ${WORKSPACE}/got/unselected.txt > ${WORKSPACE}/got/unselected.sed.txt "s,${PWD}/w-prune,,g" compare ${srcdir:-.}/expect/prune/unselected.txt ${WORKSPACE}/got/unselected.sed.txt sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/unselected-db.txt compare ${srcdir:-.}/expect/prune/unselected-db.txt ${WORKSPACE}/got/unselected-db.txt echo "| Prune affecting volume2" RUN=prune3 RSBACKUP_TIME=315792000 s ${RSBACKUP} --prune --text ${WORKSPACE}/got/volume2.txt host1:volume2 absent ${WORKSPACE}/store1/host1/volume1/1980-01-01 absent ${WORKSPACE}/store1/host1/volume2/1980-01-01 exists ${WORKSPACE}/prune3-dev-pre.ran exists ${WORKSPACE}/prune3-dev-post.ran exists ${WORKSPACE}/prune3-dev-pre.acted exists ${WORKSPACE}/prune3-dev-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 sed < ${WORKSPACE}/got/volume2.txt > ${WORKSPACE}/got/volume2.sed.txt "s,${PWD}/w-prune,,g" compare ${srcdir:-.}/expect/prune/volume2.txt ${WORKSPACE}/got/volume2.sed.txt sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/volume2-db.txt compare ${srcdir:-.}/expect/prune/volume2-db.txt ${WORKSPACE}/got/volume2-db.txt echo "| Prune affecting everything" RUN=prune4 RSBACKUP_TIME=347155200 s ${RSBACKUP} --prune sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/everything-db.txt compare ${srcdir:-.}/expect/prune/everything-db.txt ${WORKSPACE}/got/everything-db.txt echo "| Repeat prune affecting everything" # should clean up prune logs RUN=prune5 RSBACKUP_TIME=349833600 s ${RSBACKUP} --prune sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/later-db.txt compare ${srcdir:-.}/expect/prune/later-db.txt ${WORKSPACE}/got/later-db.txt cleanup rsbackup-10.0/tests/prune-timeout000077500000000000000000000044441440730431700171620ustar00rootroot00000000000000#! /bin/sh # Copyright © 2020 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup # Create a 'slow' rm mkdir ${WORKSPACE}/bin cat > ${WORKSPACE}/bin/rm << __EOF__ #! /bin/sh set -e sleep 600 exec /bin/rm "$@" __EOF__ chmod +x ${WORKSPACE}/bin/rm echo "prune-timeout 1s" >> ${WORKSPACE}/config echo "| Create first backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup echo "| Create second backup" RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup echo "| Create third backup" RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup find ${WORKSPACE} echo "| Prune that takes too long" echo "rm ${WORKSPACE}/bin/rm" >> ${WORKSPACE}/config set +e RSBACKUP_TIME=318211200 s ${RSBACKUP} --verbose --prune --text ${WORKSPACE}/got/volume1.txt status=$? set -e # Timed-out prunes are 'normal', so we don't indicate an error. if [ $status != 0 ]; then echo "ERROR: unexpected status $status" >&2 exit 1 fi # Should not have removed anything compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 # Backups should be in sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned FROM backup" > ${WORKSPACE}/got/prunetimeout-db.txt compare ${srcdir:-.}/expect/prunetimeout/prunetimeout-db.txt ${WORKSPACE}/got/prunetimeout-db.txt rsbackup-10.0/tests/pruneage000077500000000000000000000015511440730431700161470ustar00rootroot00000000000000#! /bin/sh # Copyright © 2015 Richard Kettlewell. # # 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 . set -e # Same as prune but using the new style of prune policy parameter # setting PRUNE_AGE="prune-parameter prune-age" MIN_BACKUPS="prune-parameter min-backups" . ${srcdir:-.}/prune rsbackup-10.0/tests/prunedecay000077500000000000000000000177451440730431700165140ustar00rootroot00000000000000#! /bin/sh # Copyright © 2015 Richard Kettlewell. # # 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 . set -e PRUNE_POLICY=decay DECAY_LIMIT=8d . ${srcdir:-.}/setup.sh setup # With the default configuration the buckets look like this: # 0/1 days old - with decay-start # 2 days old - B0 # 3/4 days old - B1 # 5-8 days old - B2 echo "| Day 1" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=315532800 s ${RSBACKUP} --prune # D1, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 echo "| Day 2" RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=315619200 s ${RSBACKUP} --prune # D1, 1 days old, within decay-start # D2, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 echo "| Day 3" RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=315705600 s ${RSBACKUP} --prune # D1, 2 days old, in B0 # D2, 1 days old, within decay-start # D3, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 echo "| Day 4" RSBACKUP_TIME=315792000 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=315792000 s ${RSBACKUP} --prune # D1, 3 days old, in B1 # D2, 2 days old, in B0 # D3, 1 days old, within decay-start # D4, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 echo "| Day 5" RSBACKUP_TIME=315878400 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=315878400 s ${RSBACKUP} --prune # D1, 4 days old, in B1 # D2, 3 days old, in B1, not oldest, prunable # D3, 2 days old, in B0 # D4, 1 days old, within decay-start # D5, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-05T00:00:00 echo "| Day 6" RSBACKUP_TIME=315964800 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=315964800 s ${RSBACKUP} --prune # D1, 5 days old, in B2 # (D2 gone) # D3, 3 days old, in B1 # D4, 2 days old, in B0 # D5, 1 days old, within decay-start # D6, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-05T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-06T00:00:00 echo "| Day 7" RSBACKUP_TIME=316051200 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=316051200 s ${RSBACKUP} --prune # D1, 6 days old, in B2 # (D2 gone) # D3, 4 days old, in B1 # D4, 3 days old, in B1, not oldest, prunable # D5, 2 days old, in B0 # D6, 1 days old, within decay-start # D7, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-05T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-06T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-07T00:00:00 echo "| Day 8" RSBACKUP_TIME=316137600 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=316137600 s ${RSBACKUP} --prune # D1, 7 days old, in B2 # (D2 gone) # D3, 5 days old, in B2, not oldest, prunable # (D4 gone) # D5, 3 days old, in B1 # D6, 2 days old, in B0 # D7, 1 days old, within decay-start # D8, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-05T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-06T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-07T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-08T00:00:00 echo "| Day 9" RSBACKUP_TIME=316224000 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=316224000 s ${RSBACKUP} --prune # D1, 8 days old, in B2 # (D2 gone) # (D3 gone) # (D4 gone) # D5, 4 days old, in B1 # D6, 3 days old, in B1, not oldest, prunable # D7, 2 days old, in B0 # D8, 1 days old, within decay-start # D9, 0 days old, within decay-start compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-05T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-06T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-07T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-08T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-09T00:00:00 echo "| Day 10" RSBACKUP_TIME=316310400 s ${RSBACKUP} --backup host1:volume1 RSBACKUP_TIME=316310400 s ${RSBACKUP} --prune # D1, 9 days old, in B3, beyond decay-limit, prunable # (D2 gone) # (D3 gone) # (D4 gone) # D5, 5 days old, in B2 # (D6 gone) # D7, 3 days old, in B1 # D8, 2 days old, in B0 # D9, 1 days old, within decay-start # D10, 0 days old, within decay-start absent ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-04T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-05T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-06T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-07T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-08T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-09T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-10T00:00:00 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/prunedecay-db.txt compare ${srcdir:-.}/expect/prunedecay/prunedecay-db.txt ${WORKSPACE}/got/prunedecay-db.txt cleanup rsbackup-10.0/tests/pruneexec000077500000000000000000000044161440730431700163420ustar00rootroot00000000000000#! /bin/sh # Copyright © 2015 Richard Kettlewell. # # 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 . set -e PRUNE_POLICY=exec MIN_BACKUPS=none PRUNE_AGE=none PRUNE_PATH=${srcdir}/pruner.sh . ${srcdir:-.}/setup.sh setup echo "| Create backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup echo "| Create second backup" RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup echo "| Create third backup" RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 echo "| Prune" RUN=pruneexec RSBACKUP_TIME=315792000 s ${RSBACKUP} --prune compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/pruneexec-db.txt compare ${srcdir:-.}/expect/pruneexec/pruneexec-db.txt ${WORKSPACE}/got/pruneexec-db.txt cleanup rsbackup-10.0/tests/prunenever000077500000000000000000000035351440730431700165360ustar00rootroot00000000000000#! /bin/sh # Copyright © 2011, 2012, 2014, 2015 Richard Kettlewell. # # 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 . set -e PRUNE_POLICY=never MIN_BACKUPS=none PRUNE_AGE=none . ${srcdir:-.}/setup.sh setup echo "| Create first backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup echo "| Create second backup" RSBACKUP_TIME=315619200 s ${RSBACKUP} --backup echo "| Create third backup" RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup echo "| Prune affecting everything" RUN=prune4never RSBACKUP_TIME=347155200 s ${RSBACKUP} --prune compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-02T00:00:00 compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-02T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,time,pruned,log FROM backup WHERE pruned != 0" > ${WORKSPACE}/got/neverprune-db.txt compare ${srcdir:-.}/expect/prunenever/neverprune-db.txt ${WORKSPACE}/got/neverprune-db.txt cleanup rsbackup-10.0/tests/pruner.sh000077500000000000000000000026101440730431700162620ustar00rootroot00000000000000#! /bin/sh # Copyright © 2015 Richard Kettlewell. # # 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 . set -e assert() { if [ "$2" != "$3" ]; then echo "$1: expected '$2' but got '$3'" >&2 exit 1 fi } assert PRUNE_HOST host1 "$PRUNE_HOST" #ssert PRUNE_ONDEVICE "3 2 1" "$PRUNE_ONDEVICE" assert PRUNE_ONDEVICE "315532800 315619200 315705600" "$PRUNE_ONDEVICE" case "$PRUNE_VOLUME" in volume[12] ) expect_device1=6 expect_device2=5 ;; volume3 ) expect_device1=bogus expect_device2=3 ;; * ) echo "PRUNE_VOLUME: got '$PRUNE_VOLUME'" >&2 exit 1 ;; esac case "$PRUNE_DEVICE" in device1 ) assert PRUNE_TOTAL ${expect_device1} "$PRUNE_TOTAL" ;; device2 ) assert PRUNE_TOTAL ${expect_device2} "$PRUNE_TOTAL" ;; * ) echo "PRUNE_DEVICE: got '$PRUNE_DEVICE'" >&2 exit 1 ;; esac echo 315619200:zap rsbackup-10.0/tests/retire-device000077500000000000000000000055631440730431700170770ustar00rootroot00000000000000#! /bin/sh # Copyright © 2011, 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| Create backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/create.txt # volume1: 1980-01-01 # volume2: 1980-01-01 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/created-db.txt compare ${srcdir:-.}/expect/retire-device/created-db.txt ${WORKSPACE}/got/created-db.txt exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume2/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-device/create.txt ${WORKSPACE}/got/create.txt echo "| Edit config" sed < ${WORKSPACE}/config > ${WORKSPACE}/config.new 's/^device device2//' mv ${WORKSPACE}/config.new ${WORKSPACE}/config echo "| --dry-run should do nothing" RSBACKUP_TIME=315532800 s ${RSBACKUP} --retire-device --dry-run --text ${WORKSPACE}/got/dryrun.txt device2 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/created-db-bis.txt compare ${srcdir:-.}/expect/retire-device/created-db.txt ${WORKSPACE}/got/created-db-bis.txt exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume2/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-device/dryrun.txt ${WORKSPACE}/got/dryrun.txt echo "| Retire device2" RSBACKUP_TIME=315532800 s ${RSBACKUP} --retire-device --text ${WORKSPACE}/got/device2.txt device2 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/device2-db.txt compare ${srcdir:-.}/expect/retire-device/device2-db.txt ${WORKSPACE}/got/device2-db.txt exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume2/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-device/device2.txt ${WORKSPACE}/got/device2.txt cleanup rsbackup-10.0/tests/retire-forget000077500000000000000000000071411440730431700171200ustar00rootroot00000000000000#! /bin/sh # Copyright © 2017 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| Create backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/create.txt --html ${WORKSPACE}/got/create.html # volume1: 1980-01-01 # volume2: 1980-01-01 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/created-db.txt compare ${srcdir:-.}/expect/retire-volume/created-db.txt ${WORKSPACE}/got/created-db.txt exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-volume/create.txt ${WORKSPACE}/got/create.txt compare ${srcdir:-.}/expect/retire-volume/create.html ${WORKSPACE}/got/create.html echo "| Edit config" sed < ${WORKSPACE}/config > ${WORKSPACE}/config.new 's/^ *volume volume2.*//;s/^ *min-backups 2//' mv ${WORKSPACE}/config.new ${WORKSPACE}/config echo "| --dry-run should do nothing" RSBACKUP_TIME=315532800 RUN=dryrun s ${RSBACKUP} --retire --forget-only --dry-run --text ${WORKSPACE}/got/dryrun.txt --html ${WORKSPACE}/got/dryrun.html host1:volume2 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/created-db-bis.txt compare ${srcdir:-.}/expect/retire-volume/created-db.txt ${WORKSPACE}/got/created-db-bis.txt exists ${WORKSPACE}/dryrun-dev-pre.ran exists ${WORKSPACE}/dryrun-dev-post.ran absent ${WORKSPACE}/dryrun-dev-pre.acted absent ${WORKSPACE}/dryrun-dev-post.acted exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-volume/dryrun.txt ${WORKSPACE}/got/dryrun.txt compare ${srcdir:-.}/expect/retire-volume/dryrun.html ${WORKSPACE}/got/dryrun.html echo "| forget volume2" RSBACKUP_TIME=315532800 RUN=retire s ${RSBACKUP} --verbose --retire --forget-only --force --text ${WORKSPACE}/got/volume2.txt --html ${WORKSPACE}/got/volume2.html host1:volume2 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/volume2-db.txt compare ${srcdir:-.}/expect/retire-volume/volume2-db.txt ${WORKSPACE}/got/volume2-db.txt exists ${WORKSPACE}/dryrun-dev-pre.ran exists ${WORKSPACE}/dryrun-dev-post.ran absent ${WORKSPACE}/dryrun-dev-pre.acted absent ${WORKSPACE}/dryrun-dev-post.acted exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-volume/dryrun.txt ${WORKSPACE}/got/dryrun.txt compare ${srcdir:-.}/expect/retire-volume/dryrun.html ${WORKSPACE}/got/dryrun.html rsbackup-10.0/tests/retire-volume000077500000000000000000000117301440730431700171400ustar00rootroot00000000000000#! /bin/sh # Copyright © 2011, 2012, 2014-15, 2017 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "| Create backup" RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/create.txt --html ${WORKSPACE}/got/create.html # volume1: 1980-01-01 # volume2: 1980-01-01 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/created-db.txt compare ${srcdir:-.}/expect/retire-volume/created-db.txt ${WORKSPACE}/got/created-db.txt exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-volume/create.txt ${WORKSPACE}/got/create.txt compare ${srcdir:-.}/expect/retire-volume/create.html ${WORKSPACE}/got/create.html echo "| Edit config" sed < ${WORKSPACE}/config > ${WORKSPACE}/config.new 's/^ *volume volume2.*//;s/^ *min-backups 2//' mv ${WORKSPACE}/config.new ${WORKSPACE}/config echo "| --dry-run should do nothing" RSBACKUP_TIME=315532800 RUN=dryrun s ${RSBACKUP} --retire --dry-run --text ${WORKSPACE}/got/dryrun.txt --html ${WORKSPACE}/got/dryrun.html host1:volume2 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/created-db-bis.txt compare ${srcdir:-.}/expect/retire-volume/created-db.txt ${WORKSPACE}/got/created-db-bis.txt exists ${WORKSPACE}/dryrun-dev-pre.ran exists ${WORKSPACE}/dryrun-dev-post.ran absent ${WORKSPACE}/dryrun-dev-pre.acted absent ${WORKSPACE}/dryrun-dev-post.acted exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store1/host1/volume2/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 compare ${srcdir:-.}/expect/retire-volume/dryrun.txt ${WORKSPACE}/got/dryrun.txt compare ${srcdir:-.}/expect/retire-volume/dryrun.html ${WORKSPACE}/got/dryrun.html echo "| retire volume2" RSBACKUP_TIME=315532800 RUN=retire s ${RSBACKUP} --verbose --retire --force --text ${WORKSPACE}/got/volume2.txt --html ${WORKSPACE}/got/volume2.html host1:volume2 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status FROM backup" > ${WORKSPACE}/got/volume2-db.txt compare ${srcdir:-.}/expect/retire-volume/volume2-db.txt ${WORKSPACE}/got/volume2-db.txt exists ${WORKSPACE}/retire-dev-pre.ran exists ${WORKSPACE}/retire-dev-post.ran exists ${WORKSPACE}/retire-dev-pre.acted exists ${WORKSPACE}/retire-dev-post.acted exists ${WORKSPACE}/store1/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume2 exists ${WORKSPACE}/store2/host1/volume1/1980-01-01T00:00:00 absent ${WORKSPACE}/store2/host1/volume2 compare ${srcdir:-.}/expect/retire-volume/volume2.txt ${WORKSPACE}/got/volume2.txt compare ${srcdir:-.}/expect/retire-volume/volume2.html ${WORKSPACE}/got/volume2.html echo "| No negative selections for retire" if RSBACKUP_TIME=315532800 RUN=retire s ${RSBACKUP} --verbose --retire --force host1 -host1:volume3 2>${WORKSPACE}/errors; then echo "Unexpected success" exit 1 fi echo "| Cannot retire all hosts" if RSBACKUP_TIME=315532800 RUN=retire s ${RSBACKUP} --verbose --retire --force '*' 2>${WORKSPACE}/errors; then echo "Unexpected success" exit 1 fi echo "| Edit config" sed < ${WORKSPACE}/config > ${WORKSPACE}/config.new 's/^ *volume volume2.*//;s/^ *min-backups 2//' mv ${WORKSPACE}/config.new ${WORKSPACE}/config echo "| Retire all volumes" RSBACKUP_TIME=315532800 RUN=retire s ${RSBACKUP} --verbose --retire --force --text ${WORKSPACE}/got/all.txt --html ${WORKSPACE}/got/all.html host1 sqlite3 ${WORKSPACE}/logs/backups.db "SELECT host,volume,device,id,rc,status,log FROM backup" > ${WORKSPACE}/got/all-db.txt compare ${srcdir:-.}/expect/retire-volume/retire-volume-3.txt ${WORKSPACE}/got/all-db.txt exists ${WORKSPACE}/retire-dev-pre.ran exists ${WORKSPACE}/retire-dev-post.ran exists ${WORKSPACE}/retire-dev-pre.acted exists ${WORKSPACE}/retire-dev-post.acted absent ${WORKSPACE}/store1/host1/volume1 absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store2/host1/volume1 absent ${WORKSPACE}/store2/host1/volume2 compare ${srcdir:-.}/expect/retire-volume/all.txt ${WORKSPACE}/got/all.txt compare ${srcdir:-.}/expect/retire-volume/all.html ${WORKSPACE}/got/all.html cleanup rsbackup-10.0/tests/rsync-wrap000077500000000000000000000016631440730431700164520ustar00rootroot00000000000000#! /bin/sh # Copyright © 2019 Richard Kettlewell. # # 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 . set -e echo $(date +%s) start "$RSBACKUP_DEVICE" "$RSBACKUP_HOST" "$RSBACKUP_VOLUME" >> ${WORKSPACE}/wrap.log sleep 0.1 ${RSYNC} "$@" status=$? echo $(date +%s) stop "$RSBACKUP_DEVICE" "$RSBACKUP_HOST" "$RSBACKUP_VOLUME" >> ${WORKSPACE}/wrap.log exit $status rsbackup-10.0/tests/setup.sh000077500000000000000000000133221440730431700161110ustar00rootroot00000000000000# Copyright © 2011, 2012, 2014-18 Richard Kettlewell. # # 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 . export WORKSPACE="${PWD}/w-${0##*/}" RSBACKUP="${VALGRIND} ${PWD}/../src/rsbackup --config ${WORKSPACE}/config ${VERBOSE_OPT}" BACKUP_POLICY="${BACKUP_POLICY:-daily}" PRUNE_POLICY="${PRUNE_POLICY:-age}" PRUNE_AGE="${PRUNE_AGE:-prune-parameter prune-age}" MIN_BACKUPS="${MIN_BACKUPS:-prune-parameter min-backups}" setup() { echo echo "* ==== $0 ====" # Figure out what the 'real' rsync is case $(uname -s) in Darwin ) RSYNC=/usr/bin/rsync ;; * ) RSYNC=rsync ;; esac export RSYNC RSYNC_COMMAND=${RSYNC_COMMAND:-$RSYNC} rm -rf ${WORKSPACE} mkdir ${WORKSPACE} rm -f ${WORKSPACE}/config mkdir ${WORKSPACE}/store1 echo device1 > ${WORKSPACE}/store1/device-id echo "store --no-mounted ${WORKSPACE}/store1" >> ${WORKSPACE}/config echo "device \"device1\"" >> ${WORKSPACE}/config mkdir ${WORKSPACE}/store2 echo device2 > ${WORKSPACE}/store2/device-id echo "store --no-mounted ${WORKSPACE}/store2" >>${WORKSPACE}/config echo "device device2" >> ${WORKSPACE}/config echo "public true" >> ${WORKSPACE}/config echo "pre-device-hook ${srcdir:-.}/hook" >> ${WORKSPACE}/config echo "post-device-hook ${srcdir:-.}/hook" >> ${WORKSPACE}/config echo "keep-prune-logs 1d" >> ${WORKSPACE}/config echo "backup-policy ${BACKUP_POLICY}" >> ${WORKSPACE}/config [ -n "$BACKUP_INTERVAL" ] && echo "backup-parameter min-interval ${BACKUP_INTERVAL}" >> ${WORKSPACE}/config echo "prune-policy ${PRUNE_POLICY}" >> ${WORKSPACE}/config [ -n "$PRUNE_PATH" ] && echo "prune-parameter path ${PRUNE_PATH}" >> ${WORKSPACE}/config [ -n "$DECAY_LIMIT" ] && echo "prune-parameter decay-limit ${DECAY_LIMIT}" >> ${WORKSPACE}/config mkdir ${WORKSPACE}/logs echo "logs ${WORKSPACE}/logs" >> ${WORKSPACE}/config echo "lock ${WORKSPACE}/lock" >> ${WORKSPACE}/config # Exclude graph from report echo 'report "title:Backup report (${RSBACKUP_DATE})"' >> ${WORKSPACE}/config echo 'report + "h1:Backup report (${RSBACKUP_DATE})"' >> ${WORKSPACE}/config echo 'report + h2:Warnings?warnings warnings' >> ${WORKSPACE}/config echo 'report + h2:Summary summary' >> ${WORKSPACE}/config echo 'report + h2:Logfiles logs' >> ${WORKSPACE}/config echo 'report + "h3:Pruning logs" prune-logs' >> ${WORKSPACE}/config echo 'report + "p:Generated ${RSBACKUP_CTIME}"' >> ${WORKSPACE}/config echo "rsync-command ${RSYNC_COMMAND}" >> ${WORKSPACE}/config # Apple's rsync is ancient case $(uname -s) in Darwin ) echo 'rsync-extra-options --extended-attributes' >> ${WORKSPACE}/config ;; esac echo "host host1" >> ${WORKSPACE}/config echo " hostname localhost" >> ${WORKSPACE}/config [ "${PRUNE_AGE}" != none ] && echo " ${PRUNE_AGE} 2d" >> ${WORKSPACE}/config echo " volume volume1 ${WORKSPACE}/volume1" >> ${WORKSPACE}/config [ "${MIN_BACKUPS}" != none ] && echo " ${MIN_BACKUPS} 1" >> ${WORKSPACE}/config echo " pre-volume-hook ${srcdir:-.}/hook" >> ${WORKSPACE}/config echo " post-volume-hook ${srcdir:-.}/hook" >> ${WORKSPACE}/config echo " check-file file1" >> ${WORKSPACE}/config echo " volume volume2 ${WORKSPACE}/volume2" >> ${WORKSPACE}/config [ "${MIN_BACKUPS}" != none ] && echo " ${MIN_BACKUPS} 2" >> ${WORKSPACE}/config echo " volume volume3 ${WORKSPACE}/volume3" >> ${WORKSPACE}/config [ "${MIN_BACKUPS}" != none ] && echo " ${MIN_BACKUPS} 2" >> ${WORKSPACE}/config echo " devices *2" >> ${WORKSPACE}/config mkdir ${WORKSPACE}/volume1 echo one > ${WORKSPACE}/volume1/file1 mkdir ${WORKSPACE}/volume1/dir1 dd if=/dev/zero bs=4096 count=1 of=${WORKSPACE}/volume1/dir1/file2 mkdir ${WORKSPACE}/volume2 dd if=/dev/zero bs=1024 count=2048 of=${WORKSPACE}/volume2/file3 mkdir ${WORKSPACE}/volume2/dir2 echo four > ${WORKSPACE}/volume2/dir2/file4 echo five > ${WORKSPACE}/volume2/dir2/file5 mkdir ${WORKSPACE}/volume3 echo six > ${WORKSPACE}/volume3/file6 mkdir -p ${WORKSPACE}/got } cleanup() { rm -rf "${WORKSPACE}" } compare() { if diff -ruN "$1" "$2" > ${WORKSPACE}/diffs; then : else echo "*** $1 and $2 unexpectedly differ" cat ${WORKSPACE}/diffs if ${TEST_PATCH:-false}; then cp "$2" "$1" else exit 1 fi fi } exists() { if ! [ -e "$1" ]; then echo "*** $1 does not exist" exit 1 fi } absent() { if [ -e "$1" ]; then echo "*** $1 unexpectedly exists" exit 1 fi if [ -e "$1.incomplete" ]; then echo "*** $1.incomplete unexpectedly exists" exit 1 fi } s() { echo ">" "$@" "#" ${RSBACKUP_TIME} >&2 if [ "$STDOUT" != "" ]; then exec 3>&1 exec >"$STDOUT" fi if [ "$STDERR" != "" ]; then exec 4>&2 exec 2>"$STDERR" fi ok=true if RUN="${RUN}" RSBACKUP_TIME="${RSBACKUP_TIME}" "$@"; then ok=true else ok=false fi if [ "$STDOUT" != "" ]; then exec 1>&3 cat "$STDOUT" >&1 fi if [ "$STDERR" != "" ]; then exec 2>&4 cat "$STDERR" >&2 fi ${ok} } fails() { echo ">" "$@" "#" ${RSBACKUP_TIME} >&2 if "$@"; then echo "# unexpectedly succeeded" >&2 false else echo "# failed as expected" >&2 fi } exec 3>&2 rsbackup-10.0/tests/store000077500000000000000000000065671440730431700155110ustar00rootroot00000000000000#! /usr/bin/env bash # Copyright © 2011, 2012, 2014-15, 2018 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup mv ${WORKSPACE}/store1 ${WORKSPACE}/store3 echo "| Stores must be mount points normally" set +e STDERR=${WORKSPACE}/got/notmounted.txt RUN=store RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --store ${WORKSPACE}/store3 status=$? set -e if [ $status != 1 ]; then echo >&2 "FAILED: store mount check failed" exit 1 fi sed < ${WORKSPACE}/got/notmounted.txt > ${WORKSPACE}/got/notmounted-sed.txt "s,${WORKSPACE},,g" compare ${srcdir:-.}/expect/store/notmounted.txt ${WORKSPACE}/got/notmounted-sed.txt echo "| Create backup with overridden store" STDERR=${WORKSPACE}/got/overridden-stderr.txt RUN=store RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --unmounted-store ${WORKSPACE}/store3 --text ${WORKSPACE}/got/overridden.txt --html ${WORKSPACE}/got/overridden.html exists ${WORKSPACE}/store-pre.ran exists ${WORKSPACE}/store-post.ran compare ${WORKSPACE}/volume1 ${WORKSPACE}/store3/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store3/host1/volume2/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1 absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store2/host1/volume1 absent ${WORKSPACE}/store2/host1/volume2 compare ${srcdir:-.}/expect/store/overridden.txt ${WORKSPACE}/got/overridden.txt compare ${srcdir:-.}/expect/store/overridden.html ${WORKSPACE}/got/overridden.html sed < ${WORKSPACE}/got/overridden-stderr.txt > ${WORKSPACE}/got/overridden-stderr-sed.txt "s,${WORKSPACE},,g" compare ${srcdir:-.}/expect/store/overridden-stderr.txt ${WORKSPACE}/got/overridden-stderr-sed.txt echo "| Create backup with overridden duplicate store" cp -a ${WORKSPACE}/store3 ${WORKSPACE}/store1 rm -rf ${WORKSPACE}/store1/host1 STDERR=${WORKSPACE}/got/duplicate-stderr.txt RUN=store RSBACKUP_TIME=315532800 s ${RSBACKUP} --backup --unmounted-store ${WORKSPACE}/store3 --text ${WORKSPACE}/got/duplicate.txt --html ${WORKSPACE}/got/duplicate.html exists ${WORKSPACE}/store-pre.ran exists ${WORKSPACE}/store-post.ran compare ${WORKSPACE}/volume1 ${WORKSPACE}/store3/host1/volume1/1980-01-01T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store3/host1/volume2/1980-01-01T00:00:00 absent ${WORKSPACE}/store1/host1/volume1 absent ${WORKSPACE}/store1/host1/volume2 absent ${WORKSPACE}/store2/host1/volume1 absent ${WORKSPACE}/store2/host1/volume2 sed < ${WORKSPACE}/got/duplicate-stderr.txt > ${WORKSPACE}/got/duplicate-stderr-sed.txt "s,${WORKSPACE},,g" compare ${srcdir:-.}/expect/store/duplicate.txt ${WORKSPACE}/got/duplicate.txt compare ${srcdir:-.}/expect/store/duplicate.html ${WORKSPACE}/got/duplicate.html compare ${srcdir:-.}/expect/store/duplicate-stderr.txt ${WORKSPACE}/got/duplicate-stderr-sed.txt cleanup rsbackup-10.0/tests/style000077500000000000000000000032321440730431700154770ustar00rootroot00000000000000#! /bin/sh # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/setup.sh setup echo "stylesheet ${WORKSPACE}/t.css" >>${WORKSPACE}/config echo "color-good 0x000000" >> ${WORKSPACE}/config echo "color-bad 0xFFFFFF" >> ${WORKSPACE}/config echo "alternative style sheet" > ${WORKSPACE}/t.css echo "not real" >> ${WORKSPACE}/t.css echo "| Create backup for everything with alternative stylesheet" RUN=all RSBACKUP_TIME=315705600 s ${RSBACKUP} --backup --text ${WORKSPACE}/got/styled.txt --html ${WORKSPACE}/got/styled.html exists ${WORKSPACE}/all-pre.acted exists ${WORKSPACE}/all-post.acted compare ${WORKSPACE}/volume1 ${WORKSPACE}/store1/host1/volume1/1980-01-03T00:00:00 compare ${WORKSPACE}/volume2 ${WORKSPACE}/store1/host1/volume2/1980-01-03T00:00:00 absent ${WORKSPACE}/store1/host1/volume3 compare ${WORKSPACE}/volume3 ${WORKSPACE}/store2/host1/volume3/1980-01-03T00:00:00 compare ${srcdir:-.}/expect/style/styled.txt ${WORKSPACE}/got/styled.txt compare ${srcdir:-.}/expect/style/styled.html ${WORKSPACE}/got/styled.html cleanup rsbackup-10.0/tools/000077500000000000000000000000001440730431700144075ustar00rootroot00000000000000rsbackup-10.0/tools/Makefile.am000066400000000000000000000040271440730431700164460ustar00rootroot00000000000000# Copyright © 2011, 2012, 2014, 2015 Richard Kettlewell. # # 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 . bin_SCRIPTS=rsbackup.cron rsbackup-mount rsbackup-snapshot-hook EXTRA_DIST=rsbackup.config rsbackup.cron.in \ rsbackup.defaults rsbackup.devices \ rsbackup-mount.in rsbackup-snapshot-hook.in \ ${TESTS} CRONJOBS=rsbackup.daily rsbackup.hourly rsbackup.weekly rsbackup.monthly noinst_SCRIPTS=${CRONJOBS} TESTS=t-bashisms t-hook-ok t-hook-fsck-ok t-hook-fsck-fail \ t-hook-notlv t-hook-lvlayer t-hook-post t-hook-nosnap t-hook-dryrun rsbackup.cron: rsbackup.cron.in Makefile rm -f $@.new sed 's/_version_/${VERSION}/g' < ${srcdir}/rsbackup.cron.in > $@.new chmod 555 $@.new mv -f $@.new $@ rsbackup-mount: rsbackup-mount.in Makefile rm -f $@.new sed 's/_version_/${VERSION}/g' < ${srcdir}/rsbackup-mount.in > $@.new chmod 555 $@.new mv -f $@.new $@ rsbackup-snapshot-hook: rsbackup-snapshot-hook.in Makefile rm -f $@.new sed 's/_version_/${VERSION}/g' < ${srcdir}/rsbackup-snapshot-hook.in > $@.new chmod 555 $@.new mv -f $@.new $@ ${CRONJOBS}: Makefile rm -f $@.new echo '#! /bin/sh' > $@.new echo 'set -e' >> $@.new echo 'test -x ${bindir}/rsbackup.cron || exit 0' >> $@.new echo 'exec ${bindir}/rsbackup.cron $(subst rsbackup.,,$@)' >> $@.new chmod 555 $@.new mv -f $@.new $@ clean-local: rm -f *.new distclean-local: rm -f rsbackup.cron rsbackup-mount rsbackup-snapshot-hook rm -f rsbackup.hourly rsbackup.daily rsbackup.weekly rsbackup.monthly rsbackup-10.0/tools/rsbackup-mount.in000077500000000000000000000075461440730431700177300ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2011, 2012, 2014 Richard Kettlewell. # # 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 . set -e action=mount act="" keyfile="" while [ $# -gt 0 ]; do case "$1" in --unmount | -u ) shift action=unmount ;; --check | -c ) shift action=check ;; --dry-run | -n ) shift act="echo" ;; --key-file | -k ) shift keyfile="$1" shift ;; --help | -h ) cat <&2 exit 1 ;; * ) break ;; esac done . /etc/rsbackup/devices if [ $# = 0 ]; then set $devices auto=true else auto=false fi # Set the parameters for the device as follows: # # uuid= # encrypted= # luks_device= (if encrypted=true) # luks_device_attached= # luks_device_open= device_parameters() { dev="$1" uuid=$(eval echo \$${dev}_uuid) plain=$(eval echo \$${dev}_plain) if [ -z "$plain" ]; then encrypted=true luks_device=/dev/disk/by-uuid/$uuid if [ -e ${luks_device} ]; then luks_device_attached=true if [ -e /dev/mapper/$device ]; then luks_device_open=true else luks_device_open=false fi else luks_device_attached=false fi else encrypted=false fi } case $action in mount ) for device; do device_parameters $device if $encrypted && ! $luks_device_attached && $auto; then echo >&2 "WARNING: $device not attached, skipping" continue fi if $encrypted && ! $luks_device_open; then if $luks_device_attached; then echo "Decrypting $device:" if [ -n "$keyfile" ]; then $act cryptsetup luksOpen --key-file "$keyfile" "${luks_device}" $device else $act cryptsetup luksOpen "${luks_device}" $device fi else echo >&2 "ERROR: $device not attached" exit 1 fi fi if [ -r /$device ] && [ -x /$device ]; then if [ ! -e /$device/device-id ]; then $act mount /$device else if ! $auto; then echo >&2 "WARNING: /$device/device-id already exists" fi fi else echo >&2 "ERROR: /$device is not accessible" exit 1 fi done ;; unmount ) for device; do if [ -e /$device/device-id ]; then $act umount /$device fi if [ -e /dev/mapper/$device ]; then $act cryptsetup luksClose $device fi done ;; check ) for device; do device_parameters $device if $encrypted && $luks_device_attached; then if ! $luks_device_open; then echo "$device is attached but not decrypted" elif [ ! -e /$device/device-id ]; then echo "$device is attached and open, but does not seem to be mounted" fi fi done ;; esac rsbackup-10.0/tools/rsbackup-snapshot-hook.in000077500000000000000000000121151440730431700213470ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2012, 2014, 2015, 2017 Richard Kettlewell. # # 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 . set -e SNAPS=/var/lib/rsbackup/snapshots DIVISOR=5 # By default LVM whines about any FDs it doesn't know about export LVM_SUPPRESS_FD_WARNINGS=1 x() { fatal_errors=true case "$1" in +e ) shift fatal_errors=false ;; esac echo "HOOK: EXEC:" "$@" >&2 if $fatal_errors; then "$@" else set +e "$@" status=$? set -e fi } while [ $# -gt 0 ]; do case "$1" in --help | -h ) cat <&2 exit 1 ;; esac done # The path where the snapshot will be mounted snap=$SNAPS/$RSBACKUP_VOLUME # How to execute commands on the host case $RSBACKUP_SSH_TARGET in localhost ) remote="" ;; * ) remote="ssh $RSBACKUP_SSH_TARGET" ;; esac # Only use snapshots if configured to do so if ${RSBACKUP_ACT:-false} && $remote test -e $snap; then # Identify the device name devname=$($remote df ${RSBACKUP_VOLUME_PATH}|awk '/^\// { print $1}') # Canonicalize the device name to something LVM commands will recognise # (in case df gave us something like /dev/dm-0). # (dmsetup will barf if $devname isn't an LV) dmname=$($remote dmsetup info --noheadings -c -o name "${devname}") if [ -z "${dmname}" ]; then echo >&2 "ERROR: no info from dmsetup for device ${devname}" exit 1 fi # Assume the name returned by dmsetup corresponds to a thing in /dev/mapper dev=/dev/mapper/"${dmname}" # Pull out components. # (whitespace separators are good enough: not permitted in LV/VG names, # and if an empty component is followed by a non-empty one, something's # gone badly wrong) dmsplit=$($remote dmsetup splitname --noheadings --separator ' ' "${dmname}" LVM) read -r vg lv lvlayer <&2 "ERROR: failed to parse ${dmname} as LVM name (dmsetup splitname gave \"$dmsplit\")" exit 1 elif [ ! -z "${lvlayer}" ]; then # All three components nonempty means we've ended up with an LVM # internal layer, which is probably a mistake, which we shouldn't # compound by continuing. # (An LVM internal layer / sub-LV almost certainly shouldn't have been # mounted; LVM invents them when it needs to compose multiple devmapper # functionalities, for instance when creating a snapshot, or in some # RAID situations -- see e.g. lvconvert(8) and lvmraid(7).) echo >&2 "ERROR: cowardly refusing to work with sub-LV (${dmname} has non-empty layer component \"$lvlayer\")" exit 1 fi snaplv="${lv}.snap" # Predict the snapshot device path. (Use the LVM-created alias rather # than /dev/mapper/ so we don't have to contend with hyphen-stuffing.) snapdev="/dev/${vg}/${snaplv}" case ${RSBACKUP_HOOK} in pre-volume-hook ) # Tidy up any leftovers if $remote [ -e $snapdev ]; then x $remote umount $snap >&2 || true x $remote lvremove --force $snapdev >&2 || true fi # Find out the size of the source volume lvsz=$($remote lvdisplay $dev | awk '/Current LE/ { print $3 }') lvname=$($remote lvdisplay $dev | awk '/LV Path/ { print $3 }') if [ "$lvname" = "" ]; then lvname=$($remote lvdisplay $dev | awk '/LV Name/ { print $3 }') fi snaplvsz=$(($lvsz / $DIVISOR)) # Create and mount the snapshot x $remote lvcreate --extents $snaplvsz --name $snaplv --snapshot $lvname >&2 # Snapshots may need fscking before mounting # fsck status is a bitmap; 1 means that errors were corrected. # All the other nonzero bits are fatal. x +e $remote fsck -a $snapdev >&2 case $status in 0 | 1 ) ;; * ) x $remote lvremove --force $snapdev >&2 exit $status ;; esac x $remote mount -o ro $snapdev $snap >&2 # Backup from the snapshot, not the master echo $snap ;; post-volume-hook ) # Tidy up x $remote umount $snap >&2 x $remote lvremove --force $snapdev >&2 ;; esac fi rsbackup-10.0/tools/rsbackup.config000066400000000000000000000002141440730431700174050ustar00rootroot00000000000000# Location of lockfile lock /var/run/rsbackup.lock # User configuration include /etc/rsbackup/local # Hosts include /etc/rsbackup/hosts.d rsbackup-10.0/tools/rsbackup.cron.in000077500000000000000000000075101440730431700175170ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2011, 2014, 2015, 2019 Richard Kettlewell. # # 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 . set -e # Functions ------------------------------------------------------------------- verbosely() { echo "$@" >&2 "$@" } # Defaults -------------------------------------------------------------------- backup=hourly prune=daily prune_incomplete=weekly report=daily # Initial settings ------------------------------------------------------------ backupargs="" pruneargs="" reportargs="" wait="" dryrun="" verbose="" nicely="" # Parse command line ---------------------------------------------------------- while [ $# -gt 0 ]; do case "$1" in -n | --dry-run ) dryrun="--dry-run" shift ;; -v | --verbose ) verbose=verbosely shift ;; -h | --help ) cat <&2 "rsbackup.cron: unknown option '$1'" exit 1 ;; * ) break ;; esac done # Get frequency frequency="$1" # Read configuration ---------------------------------------------------------- . /etc/rsbackup/defaults # Warn about obsolete configuration ------------------------------------------- if [ "$hourly$daily$weekly$monthly" != "" ]; then echo "WARNING: obsolete configuration found in /etc/rsbackup/defaults" >&2 fi # Validate frequency ---------------------------------------------------------- # For lower frequencies, we always wait until we can take the lock. case "$frequency" in hourly ) ;; daily | weekly | monthly ) wait="--wait" ;; * ) echo >&2 "rsbackup.cron: unrecognize frequency '$frequency'" exit 1 esac # Decide whether to backup ---------------------------------------------------- if [ "$backup" = "$frequency" ]; then backupargs="$backupargs --backup" fi # Decide whether to prune ----------------------------------------------------- # Prune old backups if [ "$prune" = "$frequency" ]; then pruneargs="$pruneargs --prune" fi # Prune incomplete backups if [ "$prune_incomplete" = "$frequency" ]; then pruneargs="$pruneargs --prune-incomplete" fi # Decide whether to generate a report ----------------------------------------- # Generate an email report if [ "$report" = "$frequency" ] && [ "$email" != "" ]; then reportargs="$reportargs --email $email" fi # Act ------------------------------------------------------------------------- # We separate pruning from backup up because otherwise only the # backed-up hosts are pruned. if [ "x$backupargs" != x ]; then $verbose $nicely rsbackup $wait $dryrun $backupargs || true fi if [ "x$pruneargs" != x ]; then $verbose $nicely rsbackup $wait $dryrun $pruneargs || true fi if [ "x$reportargs" != x ]; then $verbose $nicely rsbackup $wait $dryrun $reportargs || true fi # That's all ------------------------------------------------------------------ rsbackup-10.0/tools/rsbackup.defaults000066400000000000000000000014561440730431700177600ustar00rootroot00000000000000# # Set backup=hourly|daily|weekly|monthly to control frequency of # backup attempts. (Use backup policies for fine-grained control over # when backups happen.) # backup=hourly # # Set report=hourly|daily|weekly|monthly to control frequency of # email reports. (Hourly is probably a bit much!) Only effective # if email is not "". # report=daily # # Set email=ADDRESS to have the report emailed to that address. # email=root # # Set prune=hourly|daily|weekly|monthly|never to control frequency of # automated pruning of old backups # prune=daily # # Set prune_incomplete=hourly|daily|weekly|monthly|never to control # frequency of automated pruning of incomplete backups # prune_incomplete=weekly # # Prefix to the rsbackup command # Use 'nice' and/or 'ionice' here. Remember to quote correctly. # nicely= rsbackup-10.0/tools/rsbackup.devices000066400000000000000000000003411440730431700175630ustar00rootroot00000000000000# List names of devices here devices="" # For each device, define DEVICE_uuid # e.g. devicename_uuid=8f4171f0-007d-4083-a40c-407e5f9c24dd # For any device that isn't encrypted, define DEVICE_plain # e.g. devicename_plain=1 rsbackup-10.0/tools/t-bashisms000077500000000000000000000015351440730431700164130ustar00rootroot00000000000000#! /bin/sh # # Copyright © 2014 Richard Kettlewell. # # 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 . set -e if type checkbashisms >/dev/null 2>&1; then for s in rsbackup.cron rsbackup-mount rsbackup-snapshot-hook; do checkbashisms -x -p "$s" done else exit 77 fi rsbackup-10.0/tools/t-hook-dryrun000077500000000000000000000025471440730431700170670ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-not-run df fake_cmd --must-not-run lvdisplay fake_cmd --must-not-run lvcreate fake_cmd --must-not-run fsck fake_cmd --must-not-run mount fake_cmd --must-not-run umount fake_cmd --must-not-run lvremove fake_cmd --must-not-run ssh fake_cmd --must-not-run dmsetup RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=false \ fake_run --must-output-empty \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-fsck-fail000077500000000000000000000040261440730431700173750ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-run df "echo /dev/mapper/vg-lv" \ --must-args /path/to/volume fake_cmd --must-run lvremove \ --must-args --force /dev/vg/lv.snap fake_cmd --must-run lvdisplay \ "echo ' Current LE 8192'; echo ' LV Path /dev/vg/lv'" \ --must-args /dev/mapper/vg-lv fake_cmd --must-run lvcreate \ --must-args --extents 1638 --name lv.snap --snapshot /dev/vg/lv fake_cmd --must-run fsck "exit 2" \ --must-args -a /dev/vg/lv.snap fake_cmd --must-run _dmsetup_info "echo vg-lv" \ --must-args info --noheadings -c -o name /dev/mapper/vg-lv fake_cmd --must-run _dmsetup_splitname "echo \"vg lv \"" \ --must-args splitname --noheadings --separator ' ' vg-lv LVM # Hack to mock two dmsetup verbs 'dmsetup info' / 'dmsetup splitname': fake_cmd dmsetup '_dmsetup_$1 "$@"' fake_cmd --must-not-run umount fake_cmd --must-not-run mount fake_cmd --must-not-run ssh RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-exit 2 --must-output-empty \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-fsck-ok000077500000000000000000000040561440730431700170760ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-run df "echo /dev/dm-0" \ --must-args /path/to/volume fake_cmd --must-run lvdisplay \ "echo ' Current LE 8192'; echo ' LV Path /dev/vg/lv'" \ --must-args /dev/mapper/vg-lv fake_cmd --must-run lvcreate \ --must-args --extents 1638 --name lv.snap --snapshot /dev/vg/lv fake_cmd --must-run fsck "exit 1" \ --must-args -a /dev/vg/lv.snap fake_cmd --must-run mount \ --must-args -o ro /dev/vg/lv.snap ${fake_work}/snaps/rsb-volume fake_cmd --must-run _dmsetup_info "echo vg-lv" \ --must-args info --noheadings -c -o name /dev/dm-0 fake_cmd --must-run _dmsetup_splitname "echo \"vg lv \"" \ --must-args splitname --noheadings --separator ' ' vg-lv LVM # Hack to mock two dmsetup verbs 'dmsetup info' / 'dmsetup splitname': fake_cmd dmsetup '_dmsetup_$1 "$@"' fake_cmd --must-not-run umount fake_cmd --must-not-run lvremove fake_cmd --must-not-run ssh RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-output "${fake_work}/snaps/rsb-volume" \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-lvlayer000077500000000000000000000034371440730431700172210ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-run df "echo /dev/mapper/vg-lv-internal" \ --must-args /path/to/volume fake_cmd --must-run _dmsetup_info "echo vg-lv-internal" \ --must-args info --noheadings -c -o name /dev/mapper/vg-lv-internal fake_cmd --must-run _dmsetup_splitname "echo \"vg lv internal\"" \ --must-args splitname --noheadings --separator ' ' vg-lv-internal LVM # Hack to mock two dmsetup verbs 'dmsetup info' / 'dmsetup splitname': fake_cmd dmsetup '_dmsetup_$1 "$@"' fake_cmd --must-not-run umount fake_cmd --must-not-run mount fake_cmd --must-not-run ssh fake_cmd --must-not-run lvremove fake_cmd --must-not-run lvdisplay fake_cmd --must-not-run lvcreate fake_cmd --must-not-run fsck RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-exit 1 --must-output-empty \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-nosnap000077500000000000000000000025021440730431700170310ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps fake_reset fake_cmd --must-not-run df fake_cmd --must-not-run lvdisplay fake_cmd --must-not-run lvcreate fake_cmd --must-not-run fsck fake_cmd --must-not-run mount fake_cmd --must-not-run umount fake_cmd --must-not-run lvremove fake_cmd --must-not-run ssh fake_cmd --must-not-run dmsetup RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-output-empty \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-notlv000077500000000000000000000032511440730431700166770ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-run df "echo /dev/sda1" \ --must-args /path/to/volume fake_cmd --must-run _dmsetup_info "echo >&2 Device sda1 not found; exit 1" \ --must-args info --noheadings -c -o name /dev/sda1 fake_cmd --must-not-run _dmsetup_splitname # Hack to mock two dmsetup verbs 'dmsetup info' / 'dmsetup splitname': fake_cmd dmsetup '_dmsetup_$1 "$@"' fake_cmd --must-not-run umount fake_cmd --must-not-run mount fake_cmd --must-not-run ssh fake_cmd --must-not-run lvremove fake_cmd --must-not-run lvdisplay fake_cmd --must-not-run lvcreate fake_cmd --must-not-run fsck RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-exit 1 --must-output-empty \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-ok000077500000000000000000000043011440730431700161430ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-run df "echo /dev/mapper/vg--with--hyphens-lv" \ --must-args /path/to/volume fake_cmd --must-run lvdisplay \ "echo ' Current LE 8192'; echo ' LV Path /dev/vg-with-hyphens/lv'" \ --must-args /dev/mapper/vg--with--hyphens-lv fake_cmd --must-run lvcreate \ --must-args --extents 1638 --name lv.snap --snapshot /dev/vg-with-hyphens/lv fake_cmd --must-run fsck \ --must-args -a /dev/vg-with-hyphens/lv.snap fake_cmd --must-run mount \ --must-args -o ro /dev/vg-with-hyphens/lv.snap ${fake_work}/snaps/rsb-volume fake_cmd --must-run _dmsetup_info "echo vg--with--hyphens-lv" \ --must-args info --noheadings -c -o name /dev/mapper/vg--with--hyphens-lv fake_cmd --must-run _dmsetup_splitname "echo \"vg-with-hyphens lv \"" \ --must-args splitname --noheadings --separator ' ' vg--with--hyphens-lv LVM # Hack to mock two dmsetup verbs 'dmsetup info' / 'dmsetup splitname': fake_cmd dmsetup '_dmsetup_$1 "$@"' fake_cmd --must-not-run umount fake_cmd --must-not-run lvremove fake_cmd --must-not-run ssh RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=pre-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-output "${fake_work}/snaps/rsb-volume" \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check rsbackup-10.0/tools/t-hook-post000077500000000000000000000034371440730431700165300ustar00rootroot00000000000000#! /usr/bin/env bash # # Copyright © 2014, 2015 Richard Kettlewell. # # 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 . set -e . ${srcdir:-.}/../scripts/fakeshell.sh fake_init mkdir ${fake_work}/snaps mkdir ${fake_work}/snaps/rsb-volume fake_reset fake_cmd --must-run df "echo /dev/mapper/vg-lv" \ --must-args /path/to/volume fake_cmd --must-run umount \ --must-args ${fake_work}/snaps/rsb-volume fake_cmd --must-run lvremove \ --must-args --force /dev/vg/lv.snap fake_cmd --must-run _dmsetup_info "echo vg-lv" \ --must-args info --noheadings -c -o name /dev/mapper/vg-lv fake_cmd --must-run _dmsetup_splitname "echo \"vg lv \"" \ --must-args splitname --noheadings --separator ' ' vg-lv LVM # Hack to mock two dmsetup verbs 'dmsetup info' / 'dmsetup splitname': fake_cmd dmsetup '_dmsetup_$1 "$@"' fake_cmd --must-not-run lvcreate fake_cmd --must-not-run fsck fake_cmd --must-not-run mount fake_cmd --must-not-run ssh RSBACKUP_VOLUME=rsb-volume \ RSBACKUP_VOLUME_PATH=/path/to/volume \ RSBACKUP_SSH_TARGET=localhost \ RSBACKUP_HOOK=post-volume-hook \ RSBACKUP_ACT=true \ fake_run --must-output-empty \ ./rsbackup-snapshot-hook -s ${fake_work}/snaps fake_check