├── .gitignore ├── CHANGES.md ├── COPYING ├── LICENSE ├── README.md ├── doc ├── .gitignore ├── Makefile ├── bulk.rst ├── conf.py ├── definitions.rst ├── index.rst ├── install.rst ├── intro.rst ├── license.html ├── linking.rst ├── man.rst ├── man │ ├── dantalian-clean.1.rst │ ├── dantalian-export.1.rst │ ├── dantalian-import.1.rst │ ├── dantalian-init-library.1.rst │ ├── dantalian-link.1.rst │ ├── dantalian-list.1.rst │ ├── dantalian-load.1.rst │ ├── dantalian-rename-all.1.rst │ ├── dantalian-rename.1.rst │ ├── dantalian-save.1.rst │ ├── dantalian-search.1.rst │ ├── dantalian-swap.1.rst │ ├── dantalian-tag.1.rst │ ├── dantalian-unlink-all.1.rst │ ├── dantalian-unlink.1.rst │ ├── dantalian-unload.1.rst │ ├── dantalian-untag.1.rst │ └── dantalian.1.rst ├── searching.rst └── tagging.rst ├── pylintrc ├── setup.py ├── src └── dantalian │ ├── __init__.py │ ├── base.py │ ├── bulk.py │ ├── dtags.py │ ├── findlib.py │ ├── library.py │ ├── main │ ├── __init__.py │ ├── argparse.py │ └── commands.py │ ├── oserrors.py │ ├── pathlib.py │ ├── tagging.py │ └── tagnames.py └── tests ├── __init__.py ├── base_test.py ├── findlib_test.py ├── library_test.py ├── tagnames_test.py └── testlib.py /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.git* 3 | __pycache__ 4 | *.egg-info 5 | 6 | /build 7 | /dist 8 | /MANIFEST 9 | /_site 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # CHANGES 2 | 3 | ## version 1.0 4 | 5 | This version changes more things than not, so just re-read the documentation. 6 | 7 | ## version 0.6 8 | 9 | * Commands can now take multiple arguments where applicable (e.g., 10 | `mktag`). 11 | * Add `revert` command for reverting ("unconverting") converted 12 | directories. It only works on directories that only have one tag. 13 | * `mktag` and `rmtag` are changed to only work on unique tag qualifiers, 14 | to avoid ambiguity. The old functionality using paths can be done 15 | simply using `mkdir` and `rmdir` (or `rm -r`). 16 | * New multi-tag, multi-file `tag` and `untag` commands using the `-t` 17 | and `-f` flags. 18 | * Revamped command line argument parsing. 19 | * Library initialization moved further down execution process. Poorly 20 | formed commands should now fail faster, without initializing the 21 | library first. 22 | * dantalian now uses `-print0` when calling `find`. Thus, it should now 23 | be safe to use Dantalian with filenames and directory names that 24 | contain newlines. You shouldn't use newlines in filenames ever, but 25 | now dantalian supports it. 26 | * Added `--print0` option to `tags`, `find` commands. 27 | * Rewrote documentation. 28 | 29 | ## version 0.5 30 | 31 | * New FUSE mount tree/node system. Nodes are made/deleted dynamically 32 | in a FUSE mounted library. Changes are saved on unmount and loaded on 33 | mount. Tree is dumped as a JSON file, so is editable by hand if 34 | necessary. 35 | * Wrote FUSE syscall specifications. 36 | * Added `rmnode` socket command. 37 | * Added unit tests. 38 | * Bugfixes. 39 | * Documentation improvements. 40 | * Added `mktag` and `rmtag` commands. 41 | 42 | ## version 0.4 43 | 44 | * Wrote the library specification and revised the library module. 45 | Behavior should be better defined now in edge cases. 46 | * Added multi-tag switch to `tag` and `untag` commands. 47 | * Added `clean` command. 48 | * Added socket/fuse commands. Framework has been laid to interact 49 | directly with virtual mounted FUSE libraries. 50 | * Added `mknode` command. 51 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dantalian README 2 | 3 | Project Website: 4 | 5 | Dantalian is a Python 3 library to assist file organization and tagging 6 | using hard links. 7 | 8 | Comprehensive documentation can be found online at 9 | 10 | 11 | ## Features 12 | 13 | - Organizes using the plain file system. 14 | - Extremely flexible (no tag limit, no tag naming restrictions, 15 | hierarchical tags, no file naming restrictions, can tag all files and 16 | directories). 17 | - Transparent to other applications, since it works directly with the 18 | file system. 19 | 20 | ## Requirements 21 | 22 | Dantalian works on contiguous POSIX-compatible file systems. Specific 23 | requirements may vary, but for most Linux users there should not be any 24 | problems. 25 | 26 | ## Installation 27 | 28 | Install using packages from your distribution if available. 29 | Otherwise, see below for manual installation. 30 | 31 | ### Arch Linux 32 | 33 | - [dantalian](https://aur.archlinux.org/packages/dantalian/) 34 | - [dantalian-git](https://aur.archlinux.org/packages/dantalian-git/) 35 | 36 | ### Manual installation 37 | 38 | Dependencies: 39 | 40 | - [Python 3](http://www.python.org/) 41 | 42 | Build dependencies: 43 | 44 | - [setuptools](https://pypi.python.org/pypi/setuptools) 45 | - [Sphinx](http://sphinx-doc.org/index.html) (for documentation) 46 | 47 | Installation is simple. Obtain the sources, then run: 48 | 49 | $ python setup.py install 50 | 51 | This will most likely require root, and will install Dantalian globally 52 | on the system. Otherwise, you can use virtualenv, or install it for the 53 | user: 54 | 55 | $ python setup.py install --user 56 | 57 | By default this will install to `~/.local/bin`. 58 | 59 | It is recommended to install the man pages as well. The man pages can 60 | be built like so: 61 | 62 | $ cd doc 63 | $ make man 64 | 65 | The man pages can be found in `doc/_build/man`. How they are 66 | installed depends on your system. On Arch Linux, man pages are 67 | installed in `/usr/share/man` as gzipped archives, so you would do 68 | the following: 69 | 70 | $ cd doc/_build/man 71 | $ gzip ./* 72 | # install ./* /usr/share/man/man1 73 | 74 | ## Usage 75 | 76 | Dantalian can be used either as a Python library or a standalone program. 77 | 78 | Note that using Dantalian separately may be slow due to Python's nature. 79 | When performing bulk operations, consider using Dantalian as a library 80 | in a Python script instead of invoking Dantalian repeatedly in a shell 81 | script. 82 | 83 | Check the documentation for more information. 84 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dantalian.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dantalian.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/dantalian" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dantalian" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/bulk.rst: -------------------------------------------------------------------------------- 1 | Bulk operations 2 | =============== 3 | 4 | .. module:: dantalian.bulk 5 | 6 | Bulk operations are defined in :mod:`dantalian.bulk`. These functions operate 7 | on multiple file or entire directory trees. 8 | 9 | .. function:: clean_symlinks(dirpath) 10 | 11 | Remove all broken symlinks in the given directory tree. 12 | 13 | .. function:: rename_all(rootpath, top, path, name) 14 | 15 | Rename all links to the given file or directory. 16 | 17 | Attempt to rename all links to the target under the rootpath to the given 18 | name, finding a name as necessary. If there are multiple links in a 19 | directory, the first will be renamed and the rest unlinked. 20 | 21 | :param str rootpath: Base path for tagname conversions. 22 | :param str top: Path of search directory. 23 | :param str path: Path of target to rename. 24 | :param str name: New filename. 25 | 26 | .. function:: unlink_all(rootpath, top, path) 27 | 28 | Unlink all links to the target file or directory. This can be used to 29 | completely remove a file instead of needing to manually unlink each of its 30 | links. 31 | 32 | :param str rootpath: Base path for tagname conversions. 33 | :param str top: Path of search directory. 34 | :param str path: Path of target. 35 | 36 | Import and export 37 | ----------------- 38 | 39 | .. function:: import_tags(rootpath, path_tag_map) 40 | 41 | Import a path tag map, such as one returned from :func:`export_tags`. 42 | 43 | Tags each path with the given tagnames, thus "importing" tag data. 44 | 45 | :param str rootpath: Base path for tag conversions. 46 | :param dict path_tag_map: Mapping of paths to lists of tagnames. 47 | 48 | .. function:: export_tags(rootpath, top, full=False) 49 | 50 | Export a path tag map. 51 | 52 | Each file will only have one key path mapping to a list of tags. If `full` 53 | is ``True``, each file will have one key path for each one of that file's 54 | links, all mapping to the same list of tags. 55 | 56 | Example without `full`:: 57 | 58 | {'foo/file': ['//foo', '//bar']} 59 | 60 | With ``full``:: 61 | 62 | {'foo/file': ['//foo', '//bar'], 63 | 'bar/file': ['//foo', '//bar']} 64 | 65 | :param str rootpath: Base path for tag conversions. 66 | :param str top: Top of directory tree to export. 67 | :param bool full: Whether to include all paths to a file. Defaults to False. 68 | :returns: Mapping of paths to lists of tagnames. 69 | :rtype: dict 70 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # dantalian documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jan 7 20:57:28 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | import re 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.viewcode'] 30 | try: 31 | import sphinx.ext.ext_ctags 32 | except ImportError: 33 | pass 34 | else: 35 | extensions.append('sphinx.ext.ext_ctags') 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = 'Dantalian' 51 | copyright = '2015 Allen Li' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '1.0' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '1.0' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | try: 102 | import sphinx_rtd_theme 103 | except ImportError: 104 | html_theme = 'default' 105 | else: 106 | html_theme = "sphinx_rtd_theme" 107 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | #html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | #html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | #html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | #html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | #html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | #html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | #html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | #html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | #html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | #html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | #html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | #html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | #html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'dantaliandoc' 181 | 182 | 183 | # -- Options for LaTeX output -------------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, author, documentclass [howto/manual]). 198 | latex_documents = [ 199 | ('index', 'dantalian.tex', 'Dantalian Documentation', 'Allen Li', 'manual'), 200 | ] 201 | 202 | # The name of an image file (relative to this directory) to place at the top of 203 | # the title page. 204 | #latex_logo = None 205 | 206 | # For "manual" documents, if this is true, then toplevel headings are parts, 207 | # not chapters. 208 | #latex_use_parts = False 209 | 210 | # If true, show page references after internal links. 211 | #latex_show_pagerefs = False 212 | 213 | # If true, show URL addresses after external links. 214 | #latex_show_urls = False 215 | 216 | # Documents to append as an appendix to all manuals. 217 | #latex_appendices = [] 218 | 219 | # If false, no module index is generated. 220 | #latex_domain_indices = True 221 | 222 | 223 | # -- Options for manual page output -------------------------------------------- 224 | 225 | HEADER_PATTERN = re.compile( 226 | r'(?P[-a-z]+)' 227 | r'\((?P
\d+)\)' 228 | r' +-- +' 229 | r'(?P\w.*)\s*$' 230 | ) 231 | 232 | 233 | def generate_man_pages(): 234 | files = (x for x in os.listdir('man') if x.endswith('.rst')) 235 | for filename in files: # filename doesn't have .rst extension 236 | full_path = os.path.join('man', filename) 237 | with open(full_path) as f: 238 | header = f.readline().rstrip(); 239 | match = HEADER_PATTERN.match(header) 240 | yield (full_path[:-4], match.group('name'), match.group('description'), 241 | ['Allen Li'], match.group('section')) 242 | 243 | # One entry per manual page. List of tuples 244 | # (source start file, name, description, authors, manual section). 245 | man_pages = list(generate_man_pages()) 246 | 247 | # If true, show URL addresses after external links. 248 | #man_show_urls = False 249 | 250 | 251 | # -- Options for Texinfo output ------------------------------------------------ 252 | 253 | # Grouping the document tree into Texinfo files. List of tuples 254 | # (source start file, target name, title, author, 255 | # dir menu entry, description, category) 256 | texinfo_documents = [ 257 | ('index', 'dantalian', 'Dantalian Documentation', 258 | 'Allen Li', 'dantalian', 'One line description of project.', 259 | 'Miscellaneous'), 260 | ] 261 | 262 | # Documents to append as an appendix to all manuals. 263 | #texinfo_appendices = [] 264 | 265 | # If false, no module index is generated. 266 | #texinfo_domain_indices = True 267 | 268 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 269 | #texinfo_show_urls = 'footnote' 270 | 271 | # vim: set tw=80: 272 | -------------------------------------------------------------------------------- /doc/definitions.rst: -------------------------------------------------------------------------------- 1 | Definitions 2 | =========== 3 | 4 | This section contains definitions of terms used in the documentation. 5 | 6 | General 7 | ------- 8 | 9 | The following are general terms you should be familiar with, but are provided 10 | here for clarification and refernce. 11 | 12 | .. glossary:: 13 | 14 | pathname 15 | path 16 | A string, consisting of filenames separated with forward slashes. 17 | 18 | basename 19 | The part of a path after the last forward slash in it. If the path ends 20 | in a forward slash, then the basename is the empty string. 21 | 22 | dirname 23 | The part of a path before the last forward slash in it. 24 | 25 | filename 26 | A string, which in a directory maps to a link. Cannot contain forward 27 | slashes. Filenames are components of paths. 28 | 29 | hard link 30 | link 31 | A directory entry pointing to a file. 32 | 33 | file 34 | A file in the file system, consisting of its inode and corresponding data 35 | blocks. A file has at least one link pointing to it. 36 | 37 | directory 38 | A special type of file, which maps filenames to links and can only have 39 | one link referring to it. 40 | 41 | symbolic link 42 | symlink 43 | A special type of link, which contains a string instead of pointing to a 44 | file. The string is used as a pathname. 45 | 46 | Dantalian-specific 47 | ------------------ 48 | 49 | The following are terms that are used by Dantalian internally and in this 50 | documentation. 51 | 52 | .. glossary:: 53 | 54 | tagname 55 | A special type of pathname which begins with at least two forward 56 | slashes. After stripping all forward slashes from the beginning of a 57 | tagname, the remaining string is considered a pathname relative to a 58 | given rootpath. See :ref:`tagnames`. 59 | 60 | rootpath 61 | A pathname that is used to resolve a tagname. See :ref:`tagnames`. 62 | 63 | library 64 | A directory which contains a link to a directory with the filename 65 | `.dantalian`. See :ref:`libraries`. 66 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Dantalian Documentation 2 | ======================= 3 | 4 | .. epigraph:: 5 | 6 | | *"I ask of thee, art thou mankind?"* 7 | | *"Nay, I am the world, the world inside the gourd."* 8 | | — *The Mystic Archives of Dantalian* 9 | 10 | Website 11 | http://darkfeline.github.io/dantalian/ 12 | 13 | This is the documentation for Dantalian |release|, built on |today|. 14 | 15 | Dantalian is a Python 3 library for file organization and tagging using hard 16 | links. 17 | 18 | Contents 19 | -------- 20 | 21 | .. toctree:: 22 | :maxdepth: 1 23 | 24 | intro 25 | install 26 | definitions 27 | linking 28 | searching 29 | tagging 30 | bulk 31 | man 32 | 33 | Copyright 34 | --------- 35 | 36 | Copyright (C) 2015 Allen Li 37 | 38 | Permission is granted to copy, distribute and/or modify this document under the 39 | terms of the GNU Free Documentation License, Version 1.3 or any later version 40 | published by the Free Software Foundation; with no Invariant Sections, no 41 | Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included 42 | in the section entitled "GNU Free Documentation License". 43 | 44 | :download:`GNU Free Documentation License ` 45 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install using packages from your distribution if available. 5 | Otherwise, see below for manual installation. 6 | 7 | Arch Linux 8 | ---------- 9 | 10 | - `dantalian `_ 11 | - `dantalian-git `_ 12 | 13 | Manual installation 14 | ------------------- 15 | 16 | Dependencies: 17 | 18 | - `Python 3 `_ 19 | 20 | Build dependencies: 21 | 22 | - `setuptools `_ 23 | - `Sphinx `_ (for documentation) 24 | 25 | Installation is simple. Obtain the sources, then run:: 26 | 27 | $ python setup.py install 28 | 29 | This will most likely require root, and will install Dantalian globally 30 | on the system. Otherwise, you can use virtualenv, or install it for the 31 | user:: 32 | 33 | $ python setup.py install --user 34 | 35 | It is recommended to install the man pages as well. The man pages can 36 | be built like so:: 37 | 38 | $ cd doc 39 | $ make man 40 | 41 | The man pages can be found in ``doc/_build/man``. How they are installed 42 | depends on your system. On Arch Linux, man pages are installed in 43 | ``/usr/share/man`` as gzipped archives, so you would do the following:: 44 | 45 | $ cd doc/_build/man 46 | $ gzip ./* 47 | # install ./* /usr/share/man/man1 48 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Dantalian is a Python 3 library that provides convenient functions for file 5 | organization and tagging using hard links. 6 | 7 | Dantalian is extremely flexible, imposing no constraints on usage; read the 8 | documentation carefully so you understand what you are doing. 9 | 10 | Dantalian can also be used as a standalone program. However, using Dantalian 11 | separately may be slow due to Python's nature. When performing bulk 12 | operations, consider using Dantalian as a library in a Python script instead of 13 | invoking Dantalian repeatedly in a shell script. 14 | -------------------------------------------------------------------------------- /doc/license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GNU Free Documentation License v1.3 - GNU Project - Free Software Foundation (FSF) 6 | 26 | 27 | 28 | 29 |

GNU Free Documentation License

30 | 31 |

Version 1.3, 3 November 2008

32 | 33 |

Copyright © 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. 34 | <http://fsf.org/> 35 |

Everyone is permitted to copy and distribute verbatim copies 36 | of this license document, but changing it is not allowed.

37 | 38 |

0. PREAMBLE

39 | 40 |

The purpose of this License is to make a manual, textbook, or other 41 | functional and useful document "free" in the sense of freedom: to 42 | assure everyone the effective freedom to copy and redistribute it, 43 | with or without modifying it, either commercially or noncommercially. 44 | Secondarily, this License preserves for the author and publisher a way 45 | to get credit for their work, while not being considered responsible 46 | for modifications made by others.

47 | 48 |

This License is a kind of "copyleft", which means that derivative 49 | works of the document must themselves be free in the same sense. It 50 | complements the GNU General Public License, which is a copyleft 51 | license designed for free software.

52 | 53 |

We have designed this License in order to use it for manuals for free 54 | software, because free software needs free documentation: a free 55 | program should come with manuals providing the same freedoms that the 56 | software does. But this License is not limited to software manuals; 57 | it can be used for any textual work, regardless of subject matter or 58 | whether it is published as a printed book. We recommend this License 59 | principally for works whose purpose is instruction or reference.

60 | 61 |

1. APPLICABILITY AND DEFINITIONS

62 | 63 |

This License applies to any manual or other work, in any medium, that 64 | contains a notice placed by the copyright holder saying it can be 65 | distributed under the terms of this License. Such a notice grants a 66 | world-wide, royalty-free license, unlimited in duration, to use that 67 | work under the conditions stated herein. The "Document", below, 68 | refers to any such manual or work. Any member of the public is a 69 | licensee, and is addressed as "you". You accept the license if you 70 | copy, modify or distribute the work in a way requiring permission 71 | under copyright law.

72 | 73 |

A "Modified Version" of the Document means any work containing the 74 | Document or a portion of it, either copied verbatim, or with 75 | modifications and/or translated into another language.

76 | 77 |

A "Secondary Section" is a named appendix or a front-matter section of 78 | the Document that deals exclusively with the relationship of the 79 | publishers or authors of the Document to the Document's overall 80 | subject (or to related matters) and contains nothing that could fall 81 | directly within that overall subject. (Thus, if the Document is in 82 | part a textbook of mathematics, a Secondary Section may not explain 83 | any mathematics.) The relationship could be a matter of historical 84 | connection with the subject or with related matters, or of legal, 85 | commercial, philosophical, ethical or political position regarding 86 | them.

87 | 88 |

The "Invariant Sections" are certain Secondary Sections whose titles 89 | are designated, as being those of Invariant Sections, in the notice 90 | that says that the Document is released under this License. If a 91 | section does not fit the above definition of Secondary then it is not 92 | allowed to be designated as Invariant. The Document may contain zero 93 | Invariant Sections. If the Document does not identify any Invariant 94 | Sections then there are none.

95 | 96 |

The "Cover Texts" are certain short passages of text that are listed, 97 | as Front-Cover Texts or Back-Cover Texts, in the notice that says that 98 | the Document is released under this License. A Front-Cover Text may 99 | be at most 5 words, and a Back-Cover Text may be at most 25 words.

100 | 101 |

A "Transparent" copy of the Document means a machine-readable copy, 102 | represented in a format whose specification is available to the 103 | general public, that is suitable for revising the document 104 | straightforwardly with generic text editors or (for images composed of 105 | pixels) generic paint programs or (for drawings) some widely available 106 | drawing editor, and that is suitable for input to text formatters or 107 | for automatic translation to a variety of formats suitable for input 108 | to text formatters. A copy made in an otherwise Transparent file 109 | format whose markup, or absence of markup, has been arranged to thwart 110 | or discourage subsequent modification by readers is not Transparent. 111 | An image format is not Transparent if used for any substantial amount 112 | of text. A copy that is not "Transparent" is called "Opaque".

113 | 114 |

Examples of suitable formats for Transparent copies include plain 115 | ASCII without markup, Texinfo input format, LaTeX input format, SGML 116 | or XML using a publicly available DTD, and standard-conforming simple 117 | HTML, PostScript or PDF designed for human modification. Examples of 118 | transparent image formats include PNG, XCF and JPG. Opaque formats 119 | include proprietary formats that can be read and edited only by 120 | proprietary word processors, SGML or XML for which the DTD and/or 121 | processing tools are not generally available, and the 122 | machine-generated HTML, PostScript or PDF produced by some word 123 | processors for output purposes only.

124 | 125 |

The "Title Page" means, for a printed book, the title page itself, 126 | plus such following pages as are needed to hold, legibly, the material 127 | this License requires to appear in the title page. For works in 128 | formats which do not have any title page as such, "Title Page" means 129 | the text near the most prominent appearance of the work's title, 130 | preceding the beginning of the body of the text.

131 | 132 |

The "publisher" means any person or entity that distributes copies of 133 | the Document to the public.

134 | 135 |

A section "Entitled XYZ" means a named subunit of the Document whose 136 | title either is precisely XYZ or contains XYZ in parentheses following 137 | text that translates XYZ in another language. (Here XYZ stands for a 138 | specific section name mentioned below, such as "Acknowledgements", 139 | "Dedications", "Endorsements", or "History".) To "Preserve the Title" 140 | of such a section when you modify the Document means that it remains a 141 | section "Entitled XYZ" according to this definition.

142 | 143 |

The Document may include Warranty Disclaimers next to the notice which 144 | states that this License applies to the Document. These Warranty 145 | Disclaimers are considered to be included by reference in this 146 | License, but only as regards disclaiming warranties: any other 147 | implication that these Warranty Disclaimers may have is void and has 148 | no effect on the meaning of this License.

149 | 150 |

2. VERBATIM COPYING

151 | 152 |

You may copy and distribute the Document in any medium, either 153 | commercially or noncommercially, provided that this License, the 154 | copyright notices, and the license notice saying this License applies 155 | to the Document are reproduced in all copies, and that you add no 156 | other conditions whatsoever to those of this License. You may not use 157 | technical measures to obstruct or control the reading or further 158 | copying of the copies you make or distribute. However, you may accept 159 | compensation in exchange for copies. If you distribute a large enough 160 | number of copies you must also follow the conditions in section 3.

161 | 162 |

You may also lend copies, under the same conditions stated above, and 163 | you may publicly display copies.

164 | 165 |

3. COPYING IN QUANTITY

166 | 167 |

If you publish printed copies (or copies in media that commonly have 168 | printed covers) of the Document, numbering more than 100, and the 169 | Document's license notice requires Cover Texts, you must enclose the 170 | copies in covers that carry, clearly and legibly, all these Cover 171 | Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on 172 | the back cover. Both covers must also clearly and legibly identify 173 | you as the publisher of these copies. The front cover must present 174 | the full title with all words of the title equally prominent and 175 | visible. You may add other material on the covers in addition. 176 | Copying with changes limited to the covers, as long as they preserve 177 | the title of the Document and satisfy these conditions, can be treated 178 | as verbatim copying in other respects.

179 | 180 |

If the required texts for either cover are too voluminous to fit 181 | legibly, you should put the first ones listed (as many as fit 182 | reasonably) on the actual cover, and continue the rest onto adjacent 183 | pages.

184 | 185 |

If you publish or distribute Opaque copies of the Document numbering 186 | more than 100, you must either include a machine-readable Transparent 187 | copy along with each Opaque copy, or state in or with each Opaque copy 188 | a computer-network location from which the general network-using 189 | public has access to download using public-standard network protocols 190 | a complete Transparent copy of the Document, free of added material. 191 | If you use the latter option, you must take reasonably prudent steps, 192 | when you begin distribution of Opaque copies in quantity, to ensure 193 | that this Transparent copy will remain thus accessible at the stated 194 | location until at least one year after the last time you distribute an 195 | Opaque copy (directly or through your agents or retailers) of that 196 | edition to the public.

197 | 198 |

It is requested, but not required, that you contact the authors of the 199 | Document well before redistributing any large number of copies, to 200 | give them a chance to provide you with an updated version of the 201 | Document.

202 | 203 |

4. MODIFICATIONS

204 | 205 |

You may copy and distribute a Modified Version of the Document under 206 | the conditions of sections 2 and 3 above, provided that you release 207 | the Modified Version under precisely this License, with the Modified 208 | Version filling the role of the Document, thus licensing distribution 209 | and modification of the Modified Version to whoever possesses a copy 210 | of it. In addition, you must do these things in the Modified Version:

211 | 212 |
    213 | 214 | 215 |
  • A. Use in the Title Page (and on the covers, if any) a title distinct 216 | from that of the Document, and from those of previous versions 217 | (which should, if there were any, be listed in the History section 218 | of the Document). You may use the same title as a previous version 219 | if the original publisher of that version gives permission. 220 |
  • 221 | 222 |
  • B. List on the Title Page, as authors, one or more persons or entities 223 | responsible for authorship of the modifications in the Modified 224 | Version, together with at least five of the principal authors of the 225 | Document (all of its principal authors, if it has fewer than five), 226 | unless they release you from this requirement. 227 |
  • 228 | 229 |
  • C. State on the Title page the name of the publisher of the 230 | Modified Version, as the publisher. 231 |
  • 232 | 233 |
  • D. Preserve all the copyright notices of the Document. 234 |
  • 235 | 236 |
  • E. Add an appropriate copyright notice for your modifications 237 | adjacent to the other copyright notices. 238 |
  • 239 | 240 |
  • F. Include, immediately after the copyright notices, a license notice 241 | giving the public permission to use the Modified Version under the 242 | terms of this License, in the form shown in the Addendum below. 243 |
  • 244 | 245 |
  • G. Preserve in that license notice the full lists of Invariant Sections 246 | and required Cover Texts given in the Document's license notice. 247 |
  • 248 | 249 |
  • H. Include an unaltered copy of this License. 250 |
  • 251 | 252 |
  • I. Preserve the section Entitled "History", Preserve its Title, and add 253 | to it an item stating at least the title, year, new authors, and 254 | publisher of the Modified Version as given on the Title Page. If 255 | there is no section Entitled "History" in the Document, create one 256 | stating the title, year, authors, and publisher of the Document as 257 | given on its Title Page, then add an item describing the Modified 258 | Version as stated in the previous sentence. 259 |
  • 260 | 261 |
  • J. Preserve the network location, if any, given in the Document for 262 | public access to a Transparent copy of the Document, and likewise 263 | the network locations given in the Document for previous versions 264 | it was based on. These may be placed in the "History" section. 265 | You may omit a network location for a work that was published at 266 | least four years before the Document itself, or if the original 267 | publisher of the version it refers to gives permission. 268 |
  • 269 | 270 |
  • K. For any section Entitled "Acknowledgements" or "Dedications", 271 | Preserve the Title of the section, and preserve in the section all 272 | the substance and tone of each of the contributor acknowledgements 273 | and/or dedications given therein. 274 |
  • 275 | 276 |
  • L. Preserve all the Invariant Sections of the Document, 277 | unaltered in their text and in their titles. Section numbers 278 | or the equivalent are not considered part of the section titles. 279 |
  • 280 | 281 |
  • M. Delete any section Entitled "Endorsements". Such a section 282 | may not be included in the Modified Version. 283 |
  • 284 | 285 |
  • N. Do not retitle any existing section to be Entitled "Endorsements" 286 | or to conflict in title with any Invariant Section. 287 |
  • 288 | 289 |
  • O. Preserve any Warranty Disclaimers.
  • 290 | 291 |
292 | 293 |

If the Modified Version includes new front-matter sections or 294 | appendices that qualify as Secondary Sections and contain no material 295 | copied from the Document, you may at your option designate some or all 296 | of these sections as invariant. To do this, add their titles to the 297 | list of Invariant Sections in the Modified Version's license notice. 298 | These titles must be distinct from any other section titles.

299 | 300 |

You may add a section Entitled "Endorsements", provided it contains 301 | nothing but endorsements of your Modified Version by various 302 | parties—for example, statements of peer review or that the text has 303 | been approved by an organization as the authoritative definition of a 304 | standard.

305 | 306 |

You may add a passage of up to five words as a Front-Cover Text, and a 307 | passage of up to 25 words as a Back-Cover Text, to the end of the list 308 | of Cover Texts in the Modified Version. Only one passage of 309 | Front-Cover Text and one of Back-Cover Text may be added by (or 310 | through arrangements made by) any one entity. If the Document already 311 | includes a cover text for the same cover, previously added by you or 312 | by arrangement made by the same entity you are acting on behalf of, 313 | you may not add another; but you may replace the old one, on explicit 314 | permission from the previous publisher that added the old one.

315 | 316 |

The author(s) and publisher(s) of the Document do not by this License 317 | give permission to use their names for publicity for or to assert or 318 | imply endorsement of any Modified Version.

319 | 320 |

5. COMBINING DOCUMENTS

321 | 322 |

You may combine the Document with other documents released under this 323 | License, under the terms defined in section 4 above for modified 324 | versions, provided that you include in the combination all of the 325 | Invariant Sections of all of the original documents, unmodified, and 326 | list them all as Invariant Sections of your combined work in its 327 | license notice, and that you preserve all their Warranty Disclaimers.

328 | 329 |

The combined work need only contain one copy of this License, and 330 | multiple identical Invariant Sections may be replaced with a single 331 | copy. If there are multiple Invariant Sections with the same name but 332 | different contents, make the title of each such section unique by 333 | adding at the end of it, in parentheses, the name of the original 334 | author or publisher of that section if known, or else a unique number. 335 | Make the same adjustment to the section titles in the list of 336 | Invariant Sections in the license notice of the combined work.

337 | 338 |

In the combination, you must combine any sections Entitled "History" 339 | in the various original documents, forming one section Entitled 340 | "History"; likewise combine any sections Entitled "Acknowledgements", 341 | and any sections Entitled "Dedications". You must delete all sections 342 | Entitled "Endorsements".

343 | 344 |

6. COLLECTIONS OF DOCUMENTS

345 | 346 |

You may make a collection consisting of the Document and other 347 | documents released under this License, and replace the individual 348 | copies of this License in the various documents with a single copy 349 | that is included in the collection, provided that you follow the rules 350 | of this License for verbatim copying of each of the documents in all 351 | other respects.

352 | 353 |

You may extract a single document from such a collection, and 354 | distribute it individually under this License, provided you insert a 355 | copy of this License into the extracted document, and follow this 356 | License in all other respects regarding verbatim copying of that 357 | document.

358 | 359 |

7. AGGREGATION WITH INDEPENDENT WORKS

360 | 361 |

A compilation of the Document or its derivatives with other separate 362 | and independent documents or works, in or on a volume of a storage or 363 | distribution medium, is called an "aggregate" if the copyright 364 | resulting from the compilation is not used to limit the legal rights 365 | of the compilation's users beyond what the individual works permit. 366 | When the Document is included in an aggregate, this License does not 367 | apply to the other works in the aggregate which are not themselves 368 | derivative works of the Document.

369 | 370 |

If the Cover Text requirement of section 3 is applicable to these 371 | copies of the Document, then if the Document is less than one half of 372 | the entire aggregate, the Document's Cover Texts may be placed on 373 | covers that bracket the Document within the aggregate, or the 374 | electronic equivalent of covers if the Document is in electronic form. 375 | Otherwise they must appear on printed covers that bracket the whole 376 | aggregate.

377 | 378 |

8. TRANSLATION

379 | 380 |

Translation is considered a kind of modification, so you may 381 | distribute translations of the Document under the terms of section 4. 382 | Replacing Invariant Sections with translations requires special 383 | permission from their copyright holders, but you may include 384 | translations of some or all Invariant Sections in addition to the 385 | original versions of these Invariant Sections. You may include a 386 | translation of this License, and all the license notices in the 387 | Document, and any Warranty Disclaimers, provided that you also include 388 | the original English version of this License and the original versions 389 | of those notices and disclaimers. In case of a disagreement between 390 | the translation and the original version of this License or a notice 391 | or disclaimer, the original version will prevail.

392 | 393 |

If a section in the Document is Entitled "Acknowledgements", 394 | "Dedications", or "History", the requirement (section 4) to Preserve 395 | its Title (section 1) will typically require changing the actual 396 | title.

397 | 398 |

9. TERMINATION

399 | 400 |

You may not copy, modify, sublicense, or distribute the Document 401 | except as expressly provided under this License. Any attempt 402 | otherwise to copy, modify, sublicense, or distribute it is void, and 403 | will automatically terminate your rights under this License.

404 | 405 |

However, if you cease all violation of this License, then your license 406 | from a particular copyright holder is reinstated (a) provisionally, 407 | unless and until the copyright holder explicitly and finally 408 | terminates your license, and (b) permanently, if the copyright holder 409 | fails to notify you of the violation by some reasonable means prior to 410 | 60 days after the cessation.

411 | 412 |

Moreover, your license from a particular copyright holder is 413 | reinstated permanently if the copyright holder notifies you of the 414 | violation by some reasonable means, this is the first time you have 415 | received notice of violation of this License (for any work) from that 416 | copyright holder, and you cure the violation prior to 30 days after 417 | your receipt of the notice.

418 | 419 |

Termination of your rights under this section does not terminate the 420 | licenses of parties who have received copies or rights from you under 421 | this License. If your rights have been terminated and not permanently 422 | reinstated, receipt of a copy of some or all of the same material does 423 | not give you any rights to use it.

424 | 425 |

10. FUTURE REVISIONS OF THIS LICENSE

426 | 427 |

The Free Software Foundation may publish new, revised versions of the 428 | GNU Free Documentation License from time to time. Such new versions 429 | will be similar in spirit to the present version, but may differ in 430 | detail to address new problems or concerns. See 431 | http://www.gnu.org/copyleft/.

432 | 433 |

Each version of the License is given a distinguishing version number. 434 | If the Document specifies that a particular numbered version of this 435 | License "or any later version" applies to it, you have the option of 436 | following the terms and conditions either of that specified version or 437 | of any later version that has been published (not as a draft) by the 438 | Free Software Foundation. If the Document does not specify a version 439 | number of this License, you may choose any version ever published (not 440 | as a draft) by the Free Software Foundation. If the Document 441 | specifies that a proxy can decide which future versions of this 442 | License can be used, that proxy's public statement of acceptance of a 443 | version permanently authorizes you to choose that version for the 444 | Document.

445 | 446 |

11. RELICENSING

447 | 448 |

"Massive Multiauthor Collaboration Site" (or "MMC Site") means any 449 | World Wide Web server that publishes copyrightable works and also 450 | provides prominent facilities for anybody to edit those works. A 451 | public wiki that anybody can edit is an example of such a server. A 452 | "Massive Multiauthor Collaboration" (or "MMC") contained in the site 453 | means any set of copyrightable works thus published on the MMC site.

454 | 455 |

"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0 456 | license published by Creative Commons Corporation, a not-for-profit 457 | corporation with a principal place of business in San Francisco, 458 | California, as well as future copyleft versions of that license 459 | published by that same organization.

460 | 461 |

"Incorporate" means to publish or republish a Document, in whole or in 462 | part, as part of another Document.

463 | 464 |

An MMC is "eligible for relicensing" if it is licensed under this 465 | License, and if all works that were first published under this License 466 | somewhere other than this MMC, and subsequently incorporated in whole or 467 | in part into the MMC, (1) had no cover texts or invariant sections, and 468 | (2) were thus incorporated prior to November 1, 2008.

469 | 470 |

The operator of an MMC Site may republish an MMC contained in the site 471 | under CC-BY-SA on the same site at any time before August 1, 2009, 472 | provided the MMC is eligible for relicensing.

473 | 474 |

ADDENDUM: How to use this License for your documents

475 | 476 |

To use this License in a document you have written, include a copy of 477 | the License in the document and put the following copyright and 478 | license notices just after the title page:

479 | 480 |
    Copyright (C)  YEAR  YOUR NAME.
481 |     Permission is granted to copy, distribute and/or modify this document
482 |     under the terms of the GNU Free Documentation License, Version 1.3
483 |     or any later version published by the Free Software Foundation;
484 |     with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
485 |     A copy of the license is included in the section entitled "GNU
486 |     Free Documentation License".
487 | 
488 | 489 |

If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, 490 | replace the "with … Texts." line with this:

491 | 492 |
    with the Invariant Sections being LIST THEIR TITLES, with the
493 |     Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
494 | 
495 | 496 |

If you have Invariant Sections without Cover Texts, or some other 497 | combination of the three, merge those two alternatives to suit the 498 | situation.

499 | 500 |

If your document contains nontrivial examples of program code, we 501 | recommend releasing these examples in parallel under your choice of 502 | free software license, such as the GNU General Public License, 503 | to permit their use in free software. 504 |

505 | 506 | 507 | -------------------------------------------------------------------------------- /doc/linking.rst: -------------------------------------------------------------------------------- 1 | Basic linking 2 | ============= 3 | 4 | This section describes Dantalian's basic linking features. 5 | 6 | .. module:: dantalian.base 7 | 8 | Dantalian's fundamental linking functionality is contained in 9 | :mod:`dantalian.base`. The main functions defined in this module are 10 | :func:`link`, :func:`unlink`, and :func:`rename`, which are analogous to 11 | their counterparts in the standard :mod:`os` module, except that they have been 12 | extended to work with directories (see :ref:`dir-linking`). 13 | :mod:`dantalian.base` additionally includes helper functions to compensate for 14 | the implementation of these extended features. 15 | 16 | .. function:: link(rootpath, src, dst) 17 | 18 | Link `src` to `dst`. See :ref:`dir-linking` for how directories are linked. 19 | 20 | :param str rootpath: Path for tagname conversions. 21 | :param str src: Source path. 22 | :param str dst: Destination path. 23 | 24 | .. function:: unlink(rootpath, path) 25 | 26 | Unlink the given path. See :ref:`dir-linking` for how directories are 27 | unlinked. 28 | 29 | If the directory does not have any extra links, :exc:`IsADirectoryError` is 30 | raised. 31 | 32 | If `path` is the actual directory and the directory dies have extra links, 33 | the directory is swapped out using :func:`swap_dir` 34 | 35 | :param str rootpath: Path for tagname conversions. 36 | :param str path: Target path. 37 | :raises IsADirectoryError: Target is a directory without any other links. 38 | 39 | .. note:: 40 | 41 | This function does not work recursively for directories. For example, 42 | unlinking a directory ``foo`` that contains a link ``bar`` to another 43 | directory will not properly update ``bar``'s ``.dtags`` file. 44 | 45 | .. function:: rename(rootpath, src, dst) 46 | 47 | Renames the given link. Implemented as and functionally equivalent to:: 48 | 49 | link(rootpath, src, dst) 50 | unlink(rootpath, src) 51 | 52 | :param str rootpath: Path for tagname conversions. 53 | :param str src: Source path. 54 | :param str dst: Destination path. 55 | 56 | .. note:: 57 | 58 | This will not overwrite files, unlike :func:`os.rename`. 59 | 60 | .. note:: 61 | 62 | This function does not work recursively for directories. For example, 63 | renaming a directory ``foo`` that contains a link ``bar`` to another 64 | directory will not properly update ``bar``'s ``.dtags`` file. 65 | 66 | The following function is provided for convenience. 67 | 68 | .. function:: list_links(top, path) 69 | 70 | Traverse the directory tree, finding all of the links to the target file. 71 | 72 | :param str top: Path of directory to begin search. 73 | :param str path: Path of target file. 74 | :return: Generator yielding paths. 75 | 76 | .. note:: 77 | 78 | This function returns a generator that lazily traverses the file system. 79 | Any changes to the file system will affect the generator's execution. 80 | 81 | .. _dir-linking: 82 | 83 | Directory linking 84 | ----------------- 85 | 86 | Directory linking is implemented in Dantalian using symlinks and a file named 87 | :file:`.dtags` in each tagged directory. Dantalian assumes that the status of 88 | symlinks in the file system are consistent with the contents of the 89 | :file:`.dtags` files, except for a number of administrative functions. 90 | 91 | A directory is linked thus, given a target path `path` and a rootpath 92 | `rootpath`: A symlink is created at `path`, whose target is the absolute path 93 | to the directory. A tagname is created given `path` and `rootpath`, which is 94 | added to the file named :file:`.dtags` in the directory. 95 | 96 | Similarly, a directory is unlinked thus, given a target path `path` and a 97 | rootpath `rootpath`: The symlink at `path` is removed, and the tagname created 98 | given `path` and `rootpath` is removed from the :file:`.dtags` file in the 99 | directory. Unlinking a directory that has no such extra links is invalid. 100 | 101 | The following function is provided for convenience. 102 | 103 | .. function:: swap_dir(rootpath, path) 104 | 105 | Swap a symlink with its target directory. More specifically, given that an 106 | actual directory with path ``foo`` is also linked at ``bar``, calling this 107 | function on ``bar`` will move the actual directory to ``bar``, creating a 108 | symlink at ``foo``, and updating the :file:`.dtags` file appropriately. 109 | 110 | This is useful when the actual directory, not a symlink, is needed somewhere. 111 | 112 | :param str rootpath: Path for tagname conversions. 113 | :param str path: Target path. 114 | :raises ValueError: Target is not a symlink to a directory. 115 | 116 | The following are administrative functions that do not necessarily assume that 117 | symlink state is consistent with :file:`.dtags` state and are used to repair 118 | and maintain such state consistency. 119 | 120 | .. function:: save_dtags(rootpath, top, dirpath) 121 | 122 | Save the current state of symlinks to the target directory in its ``.dtags`` 123 | file, overwriting its current ``.dtags`` state. The file system search is 124 | done recursively from `top`. 125 | 126 | This is useful for "committing" file system changes to ``.dtags`` files. 127 | 128 | :param str rootpath: Path for tagname conversions. 129 | :param str top: Path of search directory. 130 | :param str dirpath: Path of target directory. 131 | 132 | .. function:: load_dtags(rootpath, dirpath) 133 | 134 | Create symlinks according to the directory's ``.dtags`` file. 135 | 136 | This is useful in conjunction with :func:`unload_dtags` for moving directory 137 | trees around without worrying about symlink targets. 138 | 139 | :param str rootpath: Path for tagname conversions. 140 | :param str dirpath: Path of target directory. 141 | 142 | .. function:: unload_dtags(rootpath, dirpath) 143 | 144 | Remove symlinks according to the directory's ``.dtags`` file. 145 | 146 | This is useful in conjunction with :func:`load_dtags` for moving directory 147 | trees around without worrying about symlink targets. 148 | 149 | :param str rootpath: Path for tagname conversions. 150 | :param str dirpath: Path of target directory. 151 | 152 | .. _tagnames: 153 | 154 | Tagnames 155 | -------- 156 | 157 | .. module:: dantalian.tagnames 158 | 159 | Tagnames are a special type of pathnames used by Dantalian internally. They 160 | begin with at least two forward slashes. After stripping all forward slashes 161 | from the beginning of a tagname, the remaining string is considered a pathname 162 | relative to a given rootpath. 163 | 164 | Tagnames are used in :file:`.dtags` files for tagging directories, as well as 165 | as shortcuts for the standalone script. 166 | 167 | :mod:`dantalian.tagnames` contains functions for working with tagnames. Even 168 | though the transformation between tagnames and pathnames is relatively simple, 169 | use the functions provided in this module to ensure consistent behavior. 170 | 171 | .. function:: is_tag(name) 172 | 173 | Check if the given path is a tagname. 174 | 175 | :param str name: Pathname. 176 | :returns: Whether the given path is a tagname. 177 | :rtype: bool 178 | 179 | .. function:: path2tag(rootpath, pathname) 180 | 181 | Convert a pathname to a tagname. 182 | 183 | This function will also normalize the given path before converting it to a 184 | tagname. 185 | 186 | :param str rootpath: Path for tagname conversions. 187 | :param str pathname: Pathname. 188 | :returns: Tagname. 189 | :rtype: str 190 | 191 | .. function:: tag2path(rootpath, tagname) 192 | 193 | Convert a tagname to a pathname. 194 | 195 | This function doesn't normalize the resulting path. 196 | 197 | :param str rootpath: Path for tagname conversions. 198 | :param str tagname: Tagname. 199 | :returns: Pathname. 200 | :rtype: str 201 | 202 | .. function:: path(rootpath, name) 203 | 204 | Return the given tagname or pathname as a pathname. 205 | 206 | In other words, convert the given name to a pathname if it is tagname. 207 | 208 | .. function:: tag(rootpath, name) 209 | 210 | Return the given tagname or pathname as a tagname. 211 | 212 | In other words, convert the given name to a tagname if it is not a tagname. 213 | 214 | .. _libraries: 215 | 216 | Libraries 217 | --------- 218 | 219 | .. module:: dantalian.library 220 | 221 | Libraries are special directories Dantalian uses to make file management more 222 | convenient. A library is a directory that contains a subdirectory named 223 | :file:`.dantalian`. 224 | 225 | Currently, libraries exist to provide a clear `rootpath` to be used by 226 | Dantalian's various linking function. The standalone Dantalian script will 227 | search parent directories for a library to use as a root for many commands so 228 | that you do not have to explicitly provide one yourself. Other scripts using 229 | Dantalian as a library can also take advantage of libraries as anchor points. 230 | 231 | Currently, :file:`.dantalian` is not used for anything beyond identifying 232 | libraries, but in the future, it may be used for caching search results or 233 | other caching or data storage purposes. 234 | 235 | :mod:`dantalian.library` contains functions for working with libraries. 236 | 237 | .. function:: is_library(dirpath) 238 | 239 | Return whether the given directory is a library. 240 | 241 | :param str dirpath: Path to directory. 242 | :returns: Whether directory is library. 243 | :rtype: bool 244 | 245 | .. function:: find_library(dirpath='.') 246 | 247 | Find a library. Starting from the given path, search up the file system. 248 | Return the path of the first library found, including the initially given 249 | path. Returns ``None`` if no library is found. 250 | 251 | :param str dirpath: Path to search. 252 | :returns: Path or None 253 | :rtype: str or None 254 | 255 | .. function:: init_library(dirpath) 256 | 257 | Initialize a library. Does nothing if the given directory is already a 258 | library. 259 | 260 | :param str dirpath: Path to directory. 261 | 262 | .. function:: get_resource(dirpath, resource_path) 263 | 264 | Get the path of a resource stored in the library. 265 | 266 | May be used in the future for library data or cache storage. 267 | -------------------------------------------------------------------------------- /doc/man.rst: -------------------------------------------------------------------------------- 1 | Command reference (man pages) 2 | ============================= 3 | 4 | Documentation for the standalone Dantalian script and its commands are contain in the manual page, which is duplicated and linked below. 5 | 6 | .. toctree:: 7 | :titlesonly: 8 | 9 | man/dantalian.1 10 | man/dantalian-link.1 11 | man/dantalian-unlink.1 12 | man/dantalian-rename.1 13 | man/dantalian-swap.1 14 | man/dantalian-save.1 15 | man/dantalian-load.1 16 | man/dantalian-unload.1 17 | man/dantalian-list.1 18 | man/dantalian-search.1 19 | man/dantalian-init-library.1 20 | man/dantalian-tag.1 21 | man/dantalian-untag.1 22 | man/dantalian-clean.1 23 | man/dantalian-rename-all.1 24 | man/dantalian-unlink-all.1 25 | man/dantalian-import.1 26 | man/dantalian-export.1 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-clean.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-clean(1) -- Clean broken symlinks 2 | =========================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **clean** [*options*] [*dir*] 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Remove all broken symlinks. Use current directory if no path is given. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | 19 | SEE ALSO 20 | -------- 21 | 22 | dantalian(1) 23 | Main man page 24 | -------------------------------------------------------------------------------- /doc/man/dantalian-export.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-export(1) -- Export tags 2 | ================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **export** [*options*] *dir* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Export JSON tag data to stdout. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | --full Export full tag data; check documentation for more info. 21 | 22 | SEE ALSO 23 | -------- 24 | 25 | dantalian(1) 26 | Main man page 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-import.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-import(1) -- Import tags 2 | ================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **import** [*options*] 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Import JSON tag data from stdin. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | 21 | SEE ALSO 22 | -------- 23 | 24 | dantalian(1) 25 | Main man page 26 | -------------------------------------------------------------------------------- /doc/man/dantalian-init-library.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-init-library(1) -- Initialize library 2 | =============================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **init-library** [*options*] [*path*] 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Initialize library. Use current directory if no path is given. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | 19 | SEE ALSO 20 | -------- 21 | 22 | dantalian(1) 23 | Main man page 24 | -------------------------------------------------------------------------------- /doc/man/dantalian-link.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-link(1) -- Link file or directory 2 | =========================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **link** [*options*] *src* *dst* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Link *src* to *dst* 13 | 14 | Replacement for ln(1) that works with directories. 15 | 16 | OPTIONS 17 | ------- 18 | 19 | -h, --help Print help information. 20 | --root=PATH Specify the root directory of the library to use. If not 21 | specified, try to find a library automatically. 22 | 23 | SEE ALSO 24 | -------- 25 | 26 | dantalian(1) 27 | Main man page 28 | -------------------------------------------------------------------------------- /doc/man/dantalian-list.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-list(1) -- List links 2 | =============================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **list** [*options*] *path* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | List all links of the given file or directory. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | --tags List tagnames instead of pathnames. 21 | 22 | SEE ALSO 23 | -------- 24 | 25 | dantalian(1) 26 | Main man page 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-load.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-load(1) -- Load dtags 2 | =============================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **load** [*options*] *dir* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Load file system symlink information from a directory's dtags file. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | --all Recursively load for all directories. 21 | 22 | SEE ALSO 23 | -------- 24 | 25 | dantalian(1) 26 | Main man page 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-rename-all.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-rename-all(1) -- Rename all links 2 | =========================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **rename-all** [*options*] *path* *name* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Rename all links of a file or directory. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | 21 | SEE ALSO 22 | -------- 23 | 24 | dantalian(1) 25 | Main man page 26 | -------------------------------------------------------------------------------- /doc/man/dantalian-rename.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-rename(1) -- Rename file or directory 2 | =============================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **rename** [*options*] *src* *dst* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Rename file or directory. 13 | 14 | Replacement for mv(1) that works with directory links. 15 | 16 | OPTIONS 17 | ------- 18 | 19 | -h, --help Print help information. 20 | --root=PATH Specify the root directory of the library to use. If not 21 | specified, try to find a library automatically. 22 | 23 | SEE ALSO 24 | -------- 25 | 26 | dantalian(1) 27 | Main man page 28 | -------------------------------------------------------------------------------- /doc/man/dantalian-save.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-save(1) -- Save dtags 2 | =============================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **save** [*options*] *dir* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Save file system symlink information to a directory's dtags file. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | --all Recursively save for all directories. 21 | 22 | SEE ALSO 23 | -------- 24 | 25 | dantalian(1) 26 | Main man page 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-search.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-search(1) -- Search tags 2 | ================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **search** [*options*] *query*... 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Do a tag query search. Queries are parsed using the Dantalian library; see 13 | documentation for details. 14 | 15 | OPTIONS 16 | ------- 17 | 18 | -h, --help Print help information. 19 | --root=PATH Specify the root directory of the library to use. If not 20 | specified, try to find a library automatically. 21 | 22 | SEE ALSO 23 | -------- 24 | 25 | dantalian(1) 26 | Main man page 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-swap.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-swap(1) -- Swap directory links 2 | ========================================= 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **swap** [*options*] *dir* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Swap out a directory symlink for the actual directory. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | 21 | SEE ALSO 22 | -------- 23 | 24 | dantalian(1) 25 | Main man page 26 | -------------------------------------------------------------------------------- /doc/man/dantalian-tag.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-tag(1) -- Tag files 2 | ============================= 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **tag** [*options*] -f *file*... -- *tag*... 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Tag all of the given files with all of the given tags. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | 21 | SEE ALSO 22 | -------- 23 | 24 | dantalian(1) 25 | Main man page 26 | 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-unlink-all.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-unlink-all(1) -- Unlink all links 2 | =========================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **unlink-all** [*options*] *path*... 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Unlink all links of the given files or directories. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | 21 | SEE ALSO 22 | -------- 23 | 24 | dantalian(1) 25 | Main man page 26 | -------------------------------------------------------------------------------- /doc/man/dantalian-unlink.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-unlink(1) -- Unlink file or directory 2 | =============================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **unlink** [*options*] *file*... 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Unlink files and/or directories. 13 | 14 | Replacement for rm(1) that works with directories. 15 | 16 | OPTIONS 17 | ------- 18 | 19 | -h, --help Print help information. 20 | --root=PATH Specify the root directory of the library to use. If not 21 | specified, try to find a library automatically. 22 | 23 | SEE ALSO 24 | -------- 25 | 26 | dantalian(1) 27 | Main man page 28 | -------------------------------------------------------------------------------- /doc/man/dantalian-unload.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-unload(1) -- Unload dtags 2 | =================================== 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **unload** [*options*] *dir* 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Unload file system symlinks using a directory's dtags file. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | --all Recursively unload for all directories. 21 | 22 | SEE ALSO 23 | -------- 24 | 25 | dantalian(1) 26 | Main man page 27 | -------------------------------------------------------------------------------- /doc/man/dantalian-untag.1.rst: -------------------------------------------------------------------------------- 1 | dantalian-untag(1) -- Untag files 2 | ================================= 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** **untag** [*options*] -f *file*... -- *tag*... 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | Remove all of the given tags from all of the given files. 13 | 14 | OPTIONS 15 | ------- 16 | 17 | -h, --help Print help information. 18 | --root=PATH Specify the root directory of the library to use. If not 19 | specified, try to find a library automatically. 20 | 21 | SEE ALSO 22 | -------- 23 | 24 | dantalian(1) 25 | Main man page 26 | 27 | -------------------------------------------------------------------------------- /doc/man/dantalian.1.rst: -------------------------------------------------------------------------------- 1 | dantalian(1) -- file management using hard links 2 | ================================================ 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | **dantalian** [*options*] *command* [*args*] 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | **dantalian** is a standalone script for accessing Dantalian functionality. 13 | 14 | Dantalian is a Python 3 library to assist file organization and tagging using 15 | hard links. 16 | 17 | The commands here are generally equivalent to the respective functions in the 18 | Dantalian library, with some command line sugar. Therefore, make sure to read 19 | the documentation in addition to the man pages! 20 | 21 | OPTIONS 22 | ------- 23 | 24 | -h, --help Print help information. 25 | 26 | COMMANDS 27 | -------- 28 | 29 | Base commands 30 | ^^^^^^^^^^^^^ 31 | 32 | dantalian-link(1) 33 | Link file or directory. 34 | 35 | dantalian-unlink(1) 36 | Unlink file or directory. 37 | 38 | dantalian-rename(1) 39 | Rename file or directory. 40 | 41 | dantalian-swap(1) 42 | Swap symlink with its directory. 43 | 44 | dantalian-save(1) 45 | Save dtags. 46 | 47 | dantalian-load(1) 48 | Load dtags. 49 | 50 | dantalian-unload(1) 51 | Unoad dtags. 52 | 53 | dantalian-list(1) 54 | List links. 55 | 56 | Search commands 57 | ^^^^^^^^^^^^^^^ 58 | 59 | dantalian-search(1) 60 | Do tag query search. 61 | 62 | Library commands 63 | ^^^^^^^^^^^^^^^^ 64 | 65 | dantalian-init-library(1) 66 | Initialize library. 67 | 68 | Tagging commands 69 | ^^^^^^^^^^^^^^^^ 70 | 71 | dantalian-tag(1) 72 | Tag file or directory. 73 | 74 | dantalian-untag(1) 75 | Untag file or directory. 76 | 77 | Bulk commands 78 | ^^^^^^^^^^^^^ 79 | 80 | dantalian-clean(1) 81 | Clean up broken symlinks. 82 | 83 | dantalian-rename-all(1) 84 | Rename all links of a file. 85 | 86 | dantalian-unlink-all(1) 87 | Unlink all links of a file. 88 | 89 | dantalian-import(1) 90 | Import tag data. 91 | 92 | dantalian-export(1) 93 | Export tag data. 94 | 95 | SEE ALSO 96 | -------- 97 | 98 | Online documentation 99 | http://dantalian.readthedocs.io/ 100 | 101 | Project website 102 | http://darkfeline.github.io/dantalian/ 103 | -------------------------------------------------------------------------------- /doc/searching.rst: -------------------------------------------------------------------------------- 1 | Searching 2 | ========= 3 | 4 | .. module:: dantalian.findlib 5 | 6 | The :mod:`dantalian.findlib` module implements tag queries. See :ref:`tagging` 7 | for more information about tags. 8 | 9 | Queries are represented as a tree of :class:`SearchNodes`. 10 | 11 | Example usage:: 12 | 13 | from dantalian import findlib 14 | 15 | # Find files which are tagged foo and bar 16 | paths = findlib.search(findlib.parse_query('AND foo bar END')) 17 | 18 | .. function:: search(search_node) 19 | 20 | Return a list of result paths for a given search query. 21 | 22 | .. function:: parse_query(rootpath, query) 23 | 24 | Parse a query string into a query node tree. 25 | 26 | Parent node syntax:: 27 | 28 | NODE foo [bar...] END 29 | 30 | where NODE is AND, OR, or MINUS 31 | 32 | Tokens beginning with a backslash are used directly in :class:`DirNode`s. 33 | Everything else parses to a :class:`DirNode`. 34 | 35 | Tagnames are converted to paths using the given `rootpath`. 36 | 37 | Query strings look like:: 38 | 39 | 'AND foo bar OR spam eggs END AND \AND \OR \END \\\END END END' 40 | 41 | which parses to:: 42 | 43 | AndNode([ 44 | DirNode('foo'), 45 | DirNode('bar'), 46 | OrNode([ 47 | DirNode('spam'), 48 | DirNode('eggs'), 49 | ]), 50 | AndNode([ 51 | DirNode('AND'), 52 | DirNode('OR'), 53 | DirNode('END'), 54 | DirNode('\\END'), 55 | ]), 56 | ]) 57 | 58 | Query nodes 59 | ----------- 60 | 61 | Query nodes are used to represent a search query. Query node trees can be 62 | built manually using the node classes or by using :func:`parse_query` 63 | 64 | .. class:: SearchNode 65 | 66 | An abstract interface for all query nodes. 67 | 68 | .. method:: get_results(self) 69 | 70 | Abstract method. Returns the results of query represented by the current 71 | node. 72 | 73 | :returns: A dictionary mapping inode objects to paths. 74 | 75 | .. class:: GroupNode(children) 76 | 77 | Abstract class for query nodes that have a list of child nodes, 78 | i.e. non-leaf nodes. 79 | 80 | :param list children: List of children nodes. 81 | 82 | .. class:: AndNode(children) 83 | 84 | Query node that merges the results of its children nodes by set intersection. 85 | 86 | :param list children: List of children nodes. 87 | 88 | .. class:: OrNode(children) 89 | 90 | Query node that merges the results of its children nodes by set union. 91 | 92 | :param list children: List of children nodes. 93 | 94 | .. class:: MinusNode(children) 95 | 96 | Query node that merges the results of its children nodes by set difference: 97 | the results of its first child minus the results of the rest of its 98 | children. 99 | 100 | :param list children: List of children nodes. 101 | 102 | .. class:: DirNode(dirpath) 103 | 104 | Query node that returns a directory's contents as results. These are the 105 | leaf nodes in a query search tree. 106 | -------------------------------------------------------------------------------- /doc/tagging.rst: -------------------------------------------------------------------------------- 1 | .. _tagging: 2 | 3 | Tagging 4 | ======= 5 | 6 | .. module:: dantalian.tagging 7 | 8 | Dantalian provides a simple implementation of tagging with hard links using the 9 | module :mod:`dantalian.tagging`. Tagging works thusly: 10 | 11 | Objects can be arbitrarily tagged with tags. Objects can be both files and 12 | directories, and tags can only be directories. An object is tagged with a 13 | given tag when it has a link in the corresponding directory. Similarly, an 14 | object is untagged by removing all of its links in the corresponding directory. 15 | 16 | .. function:: tag(rootpath, path, directory) 17 | 18 | Tag a file (or directory) with a directory. In effect, this tries to link 19 | `path` inside `directory` using :func:`dantalian.base.link`. It will try to 20 | use the same :term:`basename` as the given file if possible; if not, it will 21 | try to find a similar name that is free. 22 | 23 | .. function:: untag(rootpath, path, directory) 24 | 25 | Untag a file (or directory) from a directory. Essentially calls 26 | :func:`dantalian.base.unlink` on all links of the target file in the given 27 | directory. 28 | 29 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=I 3 | 4 | [TYPECHECK] 5 | ignored-modules=dantalian.oserrors 6 | 7 | [REPORTS] 8 | reports=no 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='dantalian', 5 | version='1.0.0', 6 | description='File tagging with hard links', 7 | long_description='', 8 | keywords='', 9 | url='http://darkfeline.github.io/dantalian/', 10 | author='Allen Li', 11 | author_email='darkfeline@felesatra.moe', 12 | classifiers=[ 13 | 'Development Status :: 4 - Beta', 14 | 'Environment :: Console', 15 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 16 | 'Intended Audience :: Developers', 17 | 'Intended Audience :: End Users/Desktop', 18 | 'Programming Language :: Python :: 3.5', 19 | ], 20 | 21 | package_dir={'': 'src'}, 22 | packages=find_packages('src'), 23 | entry_points={ 24 | 'console_scripts': [ 25 | 'dantalian = dantalian.main:main', 26 | ], 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/dantalian/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This is the dantalian package.""" 19 | 20 | from . import base 21 | from . import bulk 22 | from . import tagging 23 | from . import library 24 | -------------------------------------------------------------------------------- /src/dantalian/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module defines basic interaction with a semantic filesystem. 19 | 20 | This module primarily extends link(), unlink(), and rename() to work as though 21 | they support directory linking. The rest of the functions exist as 22 | implementation details to manage directory linking with symlinks and dtags. 23 | 24 | """ 25 | 26 | from itertools import chain 27 | import os 28 | import posixpath 29 | 30 | from dantalian import dtags 31 | from dantalian import oserrors 32 | from dantalian import pathlib 33 | from dantalian import tagnames 34 | 35 | 36 | def link(rootpath, src, dst): 37 | """Link src to dst. 38 | 39 | Args: 40 | rootpath: Path for tagname conversions. 41 | src: Source path. 42 | dst: Destination path. 43 | 44 | """ 45 | if posixpath.isdir(src): 46 | src = pathlib.readlink(src) 47 | os.symlink(posixpath.abspath(src), dst) 48 | dtags.add_tag(src, tagnames.path2tag(rootpath, dst)) 49 | else: 50 | os.link(src, dst) 51 | 52 | 53 | def unlink(rootpath, path): 54 | """Unlink given path. 55 | 56 | If the target is a directory without any other links, raise OSError. 57 | 58 | """ 59 | target = path 60 | # We unlink the target. However, if it is a directory, we want to swap it 61 | # out for one of its symlinks, then unlink the symlink. If the directory 62 | # doesn't have any tags, then we fail. 63 | if posixpath.isdir(target): 64 | if not posixpath.islink(target): 65 | tags = dtags.list_tags(target) 66 | if not tags: 67 | raise oserrors.is_a_directory(target) 68 | swap_candidate = tagnames.tag2path(rootpath, tags[0]) 69 | swap_dir(rootpath, swap_candidate) 70 | assert posixpath.islink(target) 71 | dtags.remove_tag(target, tagnames.path2tag(rootpath, target)) 72 | os.unlink(target) 73 | 74 | 75 | def rename(rootpath, src, dst): 76 | """Rename src to dst and fix tags for directories. 77 | 78 | Doesn't overwrite an existing file at dst. 79 | 80 | Args: 81 | rootpath: Rootpath for tagname conversions. 82 | src: Source path. 83 | dst: Destination path. 84 | """ 85 | link(rootpath, src, dst) 86 | unlink(rootpath, src) 87 | 88 | 89 | def swap_dir(rootpath, path): 90 | """Swap a symlink with its target directory. 91 | 92 | Args: 93 | rootpath: Rootpath for tag conversions. 94 | path: Path of target symlink. 95 | 96 | """ 97 | target = path 98 | if posixpath.islink(target) and posixpath.isdir(target): 99 | here = target 100 | there = pathlib.readlink(target) 101 | # here is the symlink 102 | # there is the dir 103 | here_tag = tagnames.path2tag(rootpath, here) 104 | there_tag = tagnames.path2tag(rootpath, there) 105 | dtags.remove_tag(here, here_tag) 106 | dtags.add_tag(here, there_tag) 107 | os.unlink(here) 108 | # here is now nothing 109 | # there is now the dir 110 | os.rename(there, here) 111 | # here is now the dir 112 | # there is now nothing 113 | os.symlink(here, there) 114 | else: 115 | raise ValueError('{} is not a symlink to a directory'.format(target)) 116 | 117 | 118 | def list_links(top, path): 119 | """List all links to the target file. 120 | 121 | Args: 122 | top: Path to top of directory tree to search. 123 | path: Path of file. 124 | 125 | Returns: 126 | Generator yielding paths. 127 | """ 128 | target = path 129 | for (dirpath, dirnames, filenames) in os.walk(top): 130 | for name in chain(dirnames, filenames): 131 | filepath = posixpath.join(dirpath, name) 132 | if posixpath.samefile(target, filepath): 133 | yield filepath 134 | 135 | 136 | def save_dtags(rootpath, top, dirpath): 137 | """Save symlinks to a directory's dtags, overwriting it. 138 | 139 | Args: 140 | rootpath: Path for tag conversions. 141 | top: Path of directory in which to search. 142 | dirpath: Path of directory whose dtags to update. 143 | 144 | """ 145 | dirpath = pathlib.readlink(dirpath) 146 | tags = [tagnames.path2tag(rootpath, path) 147 | for path in list_links(top, dirpath)] 148 | dir_tagname = tagnames.path2tag(rootpath, dirpath) 149 | tags = [tagname 150 | for tagname in tags 151 | if tagname != dir_tagname] 152 | dtags.set_tags(dirpath, tags) 153 | 154 | 155 | def load_dtags(rootpath, dirpath): 156 | """Create symlinks for a directory using its dtags.""" 157 | tags = dtags.list_tags(dirpath) 158 | dirpath = pathlib.readlink(dirpath) 159 | target = posixpath.abspath(dirpath) 160 | for tagname in tags: 161 | dstpath = tagnames.tag2path(rootpath, tagname) 162 | os.symlink(target, dstpath) 163 | 164 | 165 | def unload_dtags(rootpath, dirpath): 166 | """Remove symlinks using a directory's dtags.""" 167 | tags = dtags.list_tags(dirpath) 168 | dirpath = pathlib.readlink(dirpath) 169 | for tagname in tags: 170 | tagpath = tagnames.tag2path(rootpath, tagname) 171 | if posixpath.samefile(dirpath, tagpath): 172 | os.unlink(tagpath) 173 | -------------------------------------------------------------------------------- /src/dantalian/bulk.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module contains functions that work on multiple files and whole 19 | directory trees. 20 | 21 | """ 22 | 23 | from collections import defaultdict 24 | from itertools import chain 25 | import logging 26 | import os 27 | import posixpath 28 | import shutil 29 | 30 | from dantalian import base 31 | from dantalian import oserrors 32 | from dantalian import pathlib 33 | from dantalian import tagging 34 | from dantalian import tagnames 35 | 36 | _LOGGER = logging.getLogger(__name__) 37 | 38 | 39 | def clean_symlinks(dirpath): 40 | """Remove all broken symlinks under the given directory.""" 41 | # Broken symlinks appear as files, so we skip directories. 42 | for dirpath, _, filenames in os.walk(dirpath): 43 | for filename in filenames: 44 | path = posixpath.join(dirpath, filename) 45 | if posixpath.islink(path) and not posixpath.exists(path): 46 | os.unlink(path) 47 | 48 | 49 | def rename_all(rootpath, top, path, name): 50 | """Rename all links to the file or directory. 51 | 52 | Attempt to rename all links to the target under the rootpath to the given 53 | name, finding a name as necessary. If there are multiple links in a 54 | directory, the first will be renamed and the rest unlinked. 55 | 56 | Args: 57 | rootpath: Base path for tag conversions. 58 | top: Path of search directory. 59 | path: Path to target. 60 | name: New filename. 61 | 62 | """ 63 | target = path 64 | newname = name 65 | seen = set() 66 | for filepath in base.list_links(top, target): 67 | dirname = posixpath.dirname(filepath) 68 | if dirname in seen: 69 | base.unlink(rootpath, filepath) 70 | continue 71 | # pylint: disable=cell-var-from-loop 72 | pathlib.free_name_do(dirname, newname, 73 | lambda dst: base.rename(rootpath, filepath, dst)) 74 | seen.add(dirname) 75 | 76 | 77 | def unlink_all(rootpath, top, path): 78 | """Unlink all links to the target file-or-directory. 79 | 80 | Unlink all links to the target under top. 81 | 82 | Args: 83 | rootpath: Base path for tag conversions and search. 84 | top: Path of search directory. 85 | path: Path to target. 86 | 87 | """ 88 | target = path 89 | if posixpath.isdir(target): 90 | target = pathlib.readlink(target) 91 | base.unload_dtags(rootpath, target) 92 | shutil.rmtree(target) 93 | else: 94 | for path in base.list_links(top, target): 95 | base.unlink(rootpath, path) 96 | 97 | 98 | def import_tags(rootpath, path_tag_map): 99 | """Import tags. 100 | 101 | Essentially runs tag() for all paths for all tagnames. 102 | 103 | Args: 104 | rootpath: Base path for tag conversions. 105 | path_tag_map: Mapping of paths to lists of tagnames. 106 | 107 | """ 108 | for (path, tags) in path_tag_map.items(): 109 | for tagname in tags: 110 | tagging.tag(rootpath, path, tagname) 111 | 112 | 113 | def export_tags(rootpath, top, full=False): 114 | """Export tags. 115 | 116 | Returns a dictionary that maps pathnames to lists of tagnames. 117 | 118 | Each file will only have one key path mapping to a list of tags. If 119 | full=True, each file will have one key path for each one of that file's 120 | links, all mapping to the same list of tags. 121 | 122 | Args: 123 | rootpath: Base path for tag conversions. 124 | top: Top of directory tree to export. 125 | full: Whether to include all paths to a file. Defaults to False. 126 | Returns: 127 | Dictionary mapping pathnames to lists of tagnames. 128 | 129 | """ 130 | stat_tag_map = _export_stat_map(rootpath, top) 131 | results = dict() 132 | if full: 133 | for tags in stat_tag_map.values(): 134 | tags = list(tags) 135 | for tagname in tags: 136 | path = tagnames.tag2path(rootpath, tagname) 137 | results[path] = tags 138 | else: 139 | for tags in stat_tag_map.values(): 140 | tags = list(tags) 141 | path = tagnames.tag2path(rootpath, tags[0]) 142 | results[path] = tags 143 | return results 144 | 145 | 146 | def _export_stat_map(rootpath, top): 147 | """Export a map of stat objects to sets of tags. 148 | 149 | Args: 150 | rootpath: Base path for tag conversions. 151 | top: Top of directory tree to export. 152 | Returns: 153 | Dictionary mapping stat objects to sets of tagnames. 154 | 155 | """ 156 | stat_tag_map = defaultdict(set) 157 | for dirpath, dirnames, filenames in os.walk(top): 158 | for filename in chain(dirnames, filenames): 159 | path = posixpath.join(dirpath, filename) 160 | stat = os.stat(path) 161 | tagname = tagnames.path2tag(rootpath, path) 162 | stat_tag_map[stat].add(tagname) 163 | return stat_tag_map 164 | -------------------------------------------------------------------------------- /src/dantalian/dtags.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module defines interaction with directory dtags. 19 | 20 | A function that has any side effects other than manipulating dtags files does 21 | not belong in here. 22 | 23 | """ 24 | 25 | import os 26 | import posixpath 27 | 28 | _DTAGS_FILE = '.dtags' 29 | 30 | 31 | def _dtags_file(dirpath): 32 | """Get the path of a directory's dtags file.""" 33 | return posixpath.join(dirpath, _DTAGS_FILE) 34 | 35 | 36 | def open_dtags(dirpath, mode='r'): 37 | """Open dtags file of directory with given mode.""" 38 | tags_file = _dtags_file(dirpath) 39 | try: 40 | return open(tags_file, mode) 41 | except FileNotFoundError: 42 | os.mknod(tags_file) 43 | return open(tags_file, mode) 44 | 45 | 46 | def write_tag(file, tagname): 47 | """Write tag to file at current position.""" 48 | file.write(tagname + '\n') 49 | 50 | 51 | def write_tags(file, tags): 52 | """Write a list of tags to a file object.""" 53 | file.seek(0) 54 | for tag in tags: 55 | write_tag(file, tag) 56 | file.truncate() 57 | 58 | 59 | def read_tags(file): 60 | """Read tags from file object, leaving position at end. 61 | 62 | Returns: 63 | List of tagnames. 64 | 65 | """ 66 | return file.read().splitlines() 67 | 68 | 69 | def add_tag(dirpath, tagname): 70 | """Add tag to directory's dtags if not already added.""" 71 | with open_dtags(dirpath, 'r+') as file: 72 | tags = read_tags(file) 73 | if tagname in tags: 74 | return 75 | write_tag(file, tagname) 76 | 77 | 78 | def remove_tag(dirpath, tagname): 79 | """Remove tag from directory's dtags if it exists.""" 80 | with open_dtags(dirpath, 'r+') as file: 81 | tags = read_tags(file) 82 | if tagname not in tags: 83 | return 84 | tags = [tag 85 | for tag in tags 86 | if tag != tagname] 87 | write_tags(file, tags) 88 | 89 | 90 | def list_tags(dirpath): 91 | """Return a list of a directory's dtags.""" 92 | with open_dtags(dirpath, 'r+') as file: 93 | return read_tags(file) 94 | 95 | 96 | def set_tags(dirpath, tags): 97 | """Set a directory's tags to the provided list.""" 98 | with open_dtags(dirpath, 'r+') as file: 99 | write_tags(file, tags) 100 | 101 | 102 | def rename_all(dirpath, name): 103 | """Rename all dtags of the given directory. 104 | 105 | Rename all of the dtags' basenames. 106 | 107 | """ 108 | with open_dtags(dirpath, 'r+') as file: 109 | tags = read_tags(file) 110 | tags = [posixpath.join(posixpath.dirname(tag), name) for tag in tags] 111 | write_tags(file, tags) 112 | -------------------------------------------------------------------------------- /src/dantalian/findlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module implements searching and queries.""" 19 | 20 | # pylint: disable=too-few-public-methods 21 | 22 | import abc 23 | from collections import deque 24 | import functools 25 | import logging 26 | import os 27 | import shlex 28 | 29 | from dantalian import pathlib 30 | from dantalian import tagnames 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | 35 | def search(search_node): 36 | """Return paths by tag query. 37 | 38 | Args: 39 | search_node: Root Node of search query tree 40 | 41 | Returns: 42 | List of paths. 43 | """ 44 | return list(search_node.get_results().values()) 45 | 46 | 47 | class SearchNode(metaclass=abc.ABCMeta): 48 | 49 | """Abstract interface for search query nodes. 50 | 51 | Methods: 52 | get_results(): Get results of node query. 53 | """ 54 | 55 | # pylint: disable=no-init 56 | 57 | @abc.abstractmethod 58 | def get_results(self): 59 | """Return a dictionary mapping inode objects to paths.""" 60 | 61 | 62 | class GroupNode(SearchNode, metaclass=abc.ABCMeta): 63 | 64 | """Abstract class for nodes that have a list of child nodes.""" 65 | 66 | # pylint: disable=abstract-method 67 | 68 | def __init__(self, children): 69 | self.children = children 70 | 71 | def __eq__(self, other): 72 | return (self.__class__ is other.__class__ and 73 | len(self.children) == len(other.children) and 74 | all(ours == theirs 75 | for (ours, theirs) in zip(self.children, other.children))) 76 | 77 | 78 | class AndNode(GroupNode): 79 | 80 | """ 81 | AndNode merges the results of its children nodes by set intersection. 82 | """ 83 | 84 | def get_results(self): 85 | results = self.children[0].get_results() 86 | inodes = (set(node.get_results()) for node in self.children) 87 | inodes = functools.reduce(set.intersection, inodes) 88 | return dict((inode, results[inode]) for inode in inodes) 89 | 90 | 91 | class OrNode(GroupNode): 92 | 93 | """ 94 | OrNode merges the results of its children nodes by set union. 95 | """ 96 | 97 | def get_results(self): 98 | results = {} 99 | for node in self.children: 100 | pathmap = node.get_results() 101 | for inode in pathmap: 102 | if inode not in results: 103 | results[inode] = pathmap[inode] 104 | return results 105 | 106 | 107 | class MinusNode(GroupNode): 108 | 109 | """ 110 | MinusNode returns the results of its first child minus the results of the 111 | rest of its children. 112 | """ 113 | 114 | def get_results(self): 115 | results = self.children[0].get_results() 116 | for node in self.children[1:]: 117 | pathmap = node.get_results() 118 | for inode in pathmap: 119 | if inode in results: 120 | del results[inode] 121 | return results 122 | 123 | 124 | class DirNode(SearchNode): 125 | 126 | """ 127 | DirNode returns the inodes and paths of the contents of its directory. 128 | """ 129 | 130 | def __init__(self, dirpath): 131 | self.dirpath = dirpath 132 | 133 | def __eq__(self, other): 134 | return (self.__class__ is other.__class__ and 135 | self.dirpath == other.dirpath) 136 | 137 | @staticmethod 138 | def _get_inode(filepath): 139 | """Return inode and path pair.""" 140 | return (os.stat(filepath), filepath) 141 | 142 | def get_results(self): 143 | return dict(self._get_inode(filepath) 144 | for filepath in pathlib.listdirpaths(self.dirpath)) 145 | 146 | 147 | def parse_query(rootpath, query): 148 | r"""Parse query string into query node tree. 149 | 150 | See documentation for details. 151 | 152 | Args: 153 | rootpath: Rootpath for tag conversions. 154 | query: Search query string. 155 | 156 | """ 157 | tokens = deque(shlex.split(query)) 158 | parse_stack = [] 159 | parse_list = [] 160 | while tokens: 161 | token = tokens.popleft() 162 | _LOGGER.debug("Parsing token %s", token) 163 | if token[0] == '\\': 164 | token = token[1:] 165 | parse_list.append(DirNode(token)) 166 | elif token == 'AND': 167 | parse_stack.append(parse_list) 168 | parse_stack.append(AndNode) 169 | parse_list = [] 170 | elif token == 'OR': 171 | parse_stack.append(parse_list) 172 | parse_stack.append(OrNode) 173 | parse_list = [] 174 | elif token == 'MINUS': 175 | parse_stack.append(parse_list) 176 | parse_stack.append(MinusNode) 177 | parse_list = [] 178 | elif token == 'END': 179 | node_type = parse_stack.pop() 180 | node = node_type(parse_list) 181 | parse_list = parse_stack.pop() 182 | parse_list.append(node) 183 | else: 184 | token = tagnames.path(rootpath, token) 185 | parse_list.append(DirNode(token)) 186 | if len(parse_list) != 1: 187 | raise ParseError(parse_stack, parse_list, 188 | "Not exactly one node at top of parse") 189 | return parse_list[0] 190 | 191 | 192 | class ParseError(Exception): 193 | 194 | """Error parsing query.""" 195 | 196 | def __init__(self, parse_stack, parse_list, msg=''): 197 | super().__init__() 198 | self.parse_stack = parse_stack 199 | self.parse_list = parse_list 200 | self.msg = msg 201 | 202 | def __str__(self): 203 | return "{}\nstack={}\nlist={}".format( 204 | self.msg, self.parse_stack, self.parse_list) 205 | 206 | def __repr__(self): 207 | return "ParseError({!r}, {!r}, {!r})".format( 208 | self.parse_stack, self.parse_list, self.msg) 209 | -------------------------------------------------------------------------------- /src/dantalian/library.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module defines interaction with libraries.""" 19 | 20 | import os 21 | import posixpath 22 | 23 | 24 | _ROOTDIR = '.dantalian' 25 | 26 | def is_library(dirpath): 27 | """Return whether dirpath refers to a library.""" 28 | return posixpath.isdir(posixpath.join(dirpath, _ROOTDIR)) 29 | 30 | 31 | def find_library(dirpath='.'): 32 | """Find library. 33 | 34 | Return the path of the first library found above the given path. Return 35 | None if no library is found. 36 | 37 | """ 38 | dirpath = posixpath.abspath(dirpath) 39 | _, dirpath = posixpath.splitdrive(dirpath) 40 | while True: 41 | if is_library(dirpath): 42 | return dirpath 43 | elif dirpath in ('/', ''): 44 | return None 45 | else: 46 | dirpath, _ = posixpath.split(dirpath) 47 | 48 | 49 | def init_library(dirpath): 50 | """Initialize library.""" 51 | rootdir = posixpath.join(dirpath, _ROOTDIR) 52 | if not posixpath.isdir(rootdir): 53 | os.mkdir(rootdir) 54 | 55 | 56 | def get_resource(dirpath, resource_path): 57 | """Get the path of a resource for a library.""" 58 | return posixpath.join(dirpath, _ROOTDIR, resource_path) 59 | -------------------------------------------------------------------------------- /src/dantalian/main/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This package implements the Dantalian program.""" 19 | 20 | import logging 21 | 22 | from . import argparse 23 | 24 | 25 | def main(): 26 | """Entry function.""" 27 | # Set up logging. 28 | root_logger = logging.getLogger() 29 | handler = logging.StreamHandler() 30 | root_logger.addHandler(handler) 31 | # Parse arguments. 32 | parser = argparse.make_parser() 33 | args = parser.parse_args() 34 | if args.debug: 35 | handler.setFormatter(logging.Formatter( 36 | '%(asctime)s %(levelname)s @%(name)s %(message)s')) 37 | # handler default is pass all 38 | root_logger.setLevel('DEBUG') 39 | else: 40 | handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) 41 | handler.setLevel('WARNING') 42 | # root logger default is WARNING 43 | # Run command. 44 | try: 45 | func = args.func 46 | except AttributeError: 47 | parser.print_help() 48 | else: 49 | func(args) 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /src/dantalian/main/argparse.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | Entry point. 20 | """ 21 | 22 | import argparse 23 | 24 | from . import commands 25 | 26 | def _add_root(parser): 27 | """Add rootpath argument.""" 28 | parser.add_argument('--root', metavar='ROOT') 29 | 30 | def _make_base(subparsers): 31 | """Add base command parsers.""" 32 | # link 33 | parser = subparsers.add_parser('link', usage='%(prog)s SRC DST') 34 | _add_root(parser) 35 | parser.add_argument('src') 36 | parser.add_argument('dst') 37 | parser.set_defaults(func=commands.link) 38 | 39 | # unlink 40 | parser = subparsers.add_parser('unlink', usage='%(prog)s SRC DST') 41 | _add_root(parser) 42 | parser.add_argument('files', nargs='+') 43 | parser.set_defaults(func=commands.unlink) 44 | 45 | # rename 46 | parser = subparsers.add_parser('rename', usage='%(prog)s SRC DST') 47 | _add_root(parser) 48 | parser.add_argument('src') 49 | parser.add_argument('dst') 50 | parser.set_defaults(func=commands.rename) 51 | 52 | # swap 53 | parser = subparsers.add_parser('swap', usage='%(prog)s DIR') 54 | _add_root(parser) 55 | parser.add_argument('dir') 56 | parser.set_defaults(func=commands.swap) 57 | 58 | # save 59 | parser = subparsers.add_parser('save', usage='%(prog)s DIR') 60 | _add_root(parser) 61 | parser.add_argument('--all', action='store_true') 62 | parser.add_argument('dir') 63 | parser.set_defaults(func=commands.save) 64 | 65 | # load 66 | parser = subparsers.add_parser('load', usage='%(prog)s DIR') 67 | _add_root(parser) 68 | parser.add_argument('--all', action='store_true') 69 | parser.add_argument('dir') 70 | parser.set_defaults(func=commands.load) 71 | 72 | # unload 73 | parser = subparsers.add_parser('unload', usage='%(prog)s DIR') 74 | _add_root(parser) 75 | parser.add_argument('--all', action='store_true') 76 | parser.add_argument('dir') 77 | parser.set_defaults(func=commands.unload) 78 | 79 | 80 | def make_parser(): 81 | 82 | """Make argument parser. 83 | 84 | Argument parser is reusable, so keep the parser around instead of remaking 85 | it. 86 | 87 | You can use it to parse and run an argument list like so: 88 | 89 | >>> parser = make_parser() 90 | >>> args = parser.parse_args(['tag', 'foo', 'bar']) 91 | >>> args.func(args) 92 | """ 93 | 94 | top_parser = argparse.ArgumentParser() 95 | top_parser.add_argument('--debug', action='store_true') 96 | subparsers = top_parser.add_subparsers(title='Commands') 97 | 98 | ########################################################################### 99 | # base 100 | _make_base(subparsers) 101 | 102 | ########################################################################### 103 | # magic list 104 | parser = subparsers.add_parser('list', usage='%(prog)s PATH') 105 | _add_root(parser) 106 | parser.add_argument('--tags', action='store_true') 107 | parser.add_argument('path') 108 | parser.set_defaults(func=commands.magic_list) 109 | 110 | ########################################################################### 111 | # search 112 | parser = subparsers.add_parser('search', usage='%(prog)s QUERY') 113 | _add_root(parser) 114 | parser.add_argument('query', nargs='+') 115 | parser.set_defaults(func=commands.search) 116 | 117 | ########################################################################### 118 | # library 119 | parser = subparsers.add_parser('init-library', usage='%(prog)s [PATH]') 120 | parser.add_argument('path', nargs='?', default='.') 121 | parser.set_defaults(func=commands.init_library) 122 | 123 | ########################################################################### 124 | # tagging 125 | # tag 126 | parser = subparsers.add_parser( 127 | 'tag', 128 | usage='%(prog)s -f FILE [FILE ...] -- TAG [TAG ...]') 129 | _add_root(parser) 130 | parser.add_argument('-f', nargs='+', dest='files', required=True, 131 | metavar='FILE') 132 | parser.add_argument('tags', nargs='+') 133 | parser.set_defaults(func=commands.tag) 134 | 135 | # untag 136 | parser = subparsers.add_parser( 137 | 'untag', 138 | usage='%(prog)s -f FILE [FILE ...] -- TAG [TAG ...]') 139 | _add_root(parser) 140 | parser.add_argument('-f', nargs='+', dest='files', required=True, 141 | metavar='FILE') 142 | parser.add_argument('tags', nargs='+') 143 | parser.set_defaults(func=commands.untag) 144 | 145 | ########################################################################### 146 | # bulk 147 | # clean 148 | parser = subparsers.add_parser('clean', usage='%(prog)s [DIR]') 149 | parser.add_argument('dir', default='.') 150 | parser.set_defaults(func=commands.clean) 151 | 152 | # rename_all 153 | parser = subparsers.add_parser('rename-all', usage='%(prog)s PATH NAME') 154 | _add_root(parser) 155 | parser.add_argument('path') 156 | parser.add_argument('name') 157 | parser.set_defaults(func=commands.rename_all) 158 | 159 | # unlink_all 160 | parser = subparsers.add_parser('unlink-all', 161 | usage='%(prog)s PATH [PATH ...]') 162 | _add_root(parser) 163 | parser.add_argument('paths', nargs='+') 164 | parser.set_defaults(func=commands.unlink_all) 165 | 166 | # import 167 | parser = subparsers.add_parser('import', usage='%(prog)s') 168 | _add_root(parser) 169 | parser.set_defaults(func=commands.import_tags) 170 | 171 | # export 172 | parser = subparsers.add_parser('export', usage='%(prog)s DIR') 173 | _add_root(parser) 174 | parser.add_argument('dir') 175 | parser.add_argument('--full', action='store_true') 176 | parser.set_defaults(func=commands.export_tags) 177 | 178 | return top_parser 179 | -------------------------------------------------------------------------------- /src/dantalian/main/commands.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module contains command implementations.""" 19 | 20 | import json 21 | import logging 22 | import os 23 | import posixpath 24 | import sys 25 | 26 | from dantalian import base 27 | from dantalian import bulk 28 | from dantalian import dtags 29 | from dantalian import findlib 30 | from dantalian import library 31 | from dantalian import tagging 32 | from dantalian import tagnames 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | # pylint: disable=missing-docstring 37 | 38 | 39 | def _get_rootpath(args): 40 | """Unpack rootpath argument.""" 41 | if args.root: 42 | return args.root 43 | else: 44 | return library.find_library('.') 45 | 46 | 47 | def _tag_convert(args, *keys): 48 | """Convert argument values from tagnames to paths. 49 | 50 | Also convert values from lists of tagnames to lists of paths. Also do 51 | _get_rootpath because it's convenient. 52 | 53 | """ 54 | rootpath = _get_rootpath(args) 55 | for key in keys: 56 | value = getattr(args, key) 57 | if isinstance(value, list): 58 | value = [tagnames.path(rootpath, name) for name in value] 59 | else: 60 | value = tagnames.path(rootpath, value) 61 | setattr(args, key, value) 62 | return rootpath 63 | 64 | 65 | ############################################################################## 66 | # base 67 | def link(args): 68 | rootpath = _tag_convert(args, 'src', 'dst') 69 | base.link(rootpath, args.src, args.dst) 70 | 71 | 72 | def unlink(args): 73 | rootpath = _tag_convert(args, 'files') 74 | for file in args.files: 75 | try: 76 | base.unlink(rootpath, file) 77 | except OSError as err: 78 | _LOGGER.error(err) 79 | 80 | 81 | def rename(args): 82 | rootpath = _tag_convert(args, 'src', 'dst') 83 | base.rename(rootpath, args.src, args.dst) 84 | 85 | 86 | def swap(args): 87 | rootpath = _tag_convert(args, 'dir') 88 | base.swap_dir(rootpath, args.dir) 89 | 90 | 91 | def _do_all_dirs(top, callback): 92 | """Call function for all directories.""" 93 | for (dirpath, dirnames, _) in os.walk(top): 94 | for dirname in dirnames: 95 | path = posixpath.join(dirpath, dirname) 96 | callback(path) 97 | 98 | 99 | def save(args): 100 | rootpath = _tag_convert(args, 'dir') 101 | if args.all: 102 | _do_all_dirs(args.dir, 103 | lambda path: base.save_dtags(rootpath, rootpath, path)) 104 | else: 105 | base.save_dtags(rootpath, rootpath, args.dir) 106 | 107 | 108 | def load(args): 109 | rootpath = _tag_convert(args, 'dir') 110 | if args.all: 111 | _do_all_dirs(args.dir, lambda path: base.load_dtags(rootpath, path)) 112 | else: 113 | base.load_dtags(rootpath, args.dir) 114 | 115 | 116 | def unload(args): 117 | rootpath = _tag_convert(args, 'dir') 118 | if args.all: 119 | _do_all_dirs(args.dir, lambda path: base.unload_dtags(rootpath, path)) 120 | else: 121 | base.unload_dtags(rootpath, args.dir) 122 | 123 | 124 | ############################################################################## 125 | def magic_list(args): 126 | rootpath = _tag_convert(args, 'path') 127 | path = args.path 128 | if posixpath.isdir(path) and args.tags: 129 | results = dtags.list_tags(path) 130 | else: 131 | results = base.list_links(rootpath, path) 132 | for item in results: 133 | print(item) 134 | 135 | 136 | ############################################################################## 137 | def search(args): 138 | rootpath = _get_rootpath(args) 139 | query = ' '.join(args.query) 140 | query_tree = findlib.parse_query(rootpath, query) 141 | results = findlib.search(query_tree) 142 | for entry in results: 143 | print(entry) 144 | 145 | 146 | ############################################################################## 147 | # library 148 | def init_library(args): 149 | library.init_library(args.path) 150 | 151 | 152 | ############################################################################## 153 | # tagging 154 | def tag(args): 155 | rootpath = _tag_convert(args, 'files', 'tags') 156 | for current_file in args.files: 157 | for current_tag in args.tags: 158 | try: 159 | tagging.tag(rootpath, current_file, current_tag) 160 | except OSError as err: 161 | _LOGGER.error(err) 162 | 163 | 164 | def untag(args): 165 | rootpath = _tag_convert(args, 'files', 'tags') 166 | for current_file in args.files: 167 | for current_tag in args.tags: 168 | try: 169 | tagging.untag(rootpath, current_file, current_tag) 170 | except OSError as err: 171 | _LOGGER.error(err) 172 | 173 | 174 | ############################################################################### 175 | # bulk 176 | def clean(args): 177 | bulk.clean_symlinks(args.dir) 178 | 179 | 180 | def rename_all(args): 181 | rootpath = _tag_convert(args, 'path') 182 | bulk.rename_all(rootpath, rootpath, args.path, args.name) 183 | 184 | 185 | def unlink_all(args): 186 | rootpath = _tag_convert(args, 'paths') 187 | bulk.unlink_all(rootpath, rootpath, args.path) 188 | 189 | 190 | def import_tags(args): 191 | rootpath = _get_rootpath(args) 192 | path_tag_map = json.load(sys.stdin) 193 | bulk.import_tags(rootpath, path_tag_map) 194 | 195 | 196 | def export_tags(args): 197 | rootpath = _get_rootpath(args) 198 | path_tag_map = bulk.export_tags(rootpath, args.dir, args.full) 199 | json.dump(path_tag_map, sys.stdout) 200 | -------------------------------------------------------------------------------- /src/dantalian/oserrors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Allen Li 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | oserrors.py 17 | =========== 18 | 19 | This module contains helpers for properly raising various OSError subclasses. 20 | 21 | >>> raise file_exists() 22 | Traceback (most recent call last): 23 | ... 24 | FileExistsError: [Errno 17] File exists 25 | >>> raise file_exists('foo') 26 | Traceback (most recent call last): 27 | ... 28 | FileExistsError: [Errno 17] File exists: 'foo' 29 | >>> raise file_exists('foo', 'bar') 30 | Traceback (most recent call last): 31 | ... 32 | FileExistsError: [Errno 17] File exists: 'foo' -> 'bar' 33 | 34 | Error codes and message text is handled automatically, and the attributes 35 | filename and filename2 are set on the error object; see Python docs on OSError 36 | for details. 37 | 38 | BlockingIOError takes an extra argument to set the attribute 39 | characters_written; see Python docs on BlockingIOError for details. 40 | 41 | >>> raise blocking_io_eagain('foo', 'bar', 5) 42 | Traceback (most recent call last): 43 | ... 44 | BlockingIOError: [Errno 11] Resource temporarily unavailable: 'foo' -> 'bar' 45 | 46 | Argument can be passed by parameter name: 47 | 48 | >>> raise blocking_io_eagain(filename='foo', filename2='bar', written=5) 49 | Traceback (most recent call last): 50 | ... 51 | BlockingIOError: [Errno 11] Resource temporarily unavailable: 'foo' -> 'bar' 52 | >>> raise blocking_io_eagain(written=5) 53 | Traceback (most recent call last): 54 | ... 55 | BlockingIOError: [Errno 11] Resource temporarily unavailable 56 | 57 | You can also build an OSError builder functionally: 58 | 59 | >>> import errno 60 | >>> enoent_error = oserror(errno.ENOENT) 61 | >>> raise enoent_error('foo') 62 | Traceback (most recent call last): 63 | ... 64 | FileNotFoundError: [Errno 2] No such file or directory: 'foo' 65 | 66 | """ 67 | 68 | import errno 69 | import os 70 | 71 | _ERRORS = ( 72 | ('blocking_io_eagain', errno.EAGAIN), 73 | ('blocking_io_ealready', errno.EALREADY), 74 | ('blocking_io_ewouldblock', errno.EWOULDBLOCK), 75 | ('blocking_io_einprogress', errno.EINPROGRESS), 76 | ('child_process', errno.ECHILD), 77 | ('broken_pipe_eshutdown', errno.ESHUTDOWN), 78 | ('broken_pipe_epipe', errno.EPIPE), 79 | ('connection_aborted', errno.ECONNABORTED), 80 | ('connection_refused', errno.ECONNREFUSED), 81 | ('connection_reset', errno.ECONNRESET), 82 | ('file_exists', errno.EEXIST), 83 | ('file_not_found', errno.ENOENT), 84 | ('interrupted', errno.EINTR), 85 | ('is_a_directory', errno.EISDIR), 86 | ('not_a_directory', errno.ENOTDIR), 87 | ('permission_eacces', errno.EACCES), 88 | ('permission_eperm', errno.EPERM), 89 | ('process_lookup', errno.ESRCH), 90 | ('timeout', errno.ETIMEDOUT), 91 | ) 92 | 93 | # pylint: disable=missing-docstring 94 | 95 | 96 | def oserror(err): 97 | """Return an OSError builder function. 98 | 99 | Args: 100 | err: An error number, e.g., errno.ENOENT 101 | 102 | """ 103 | def build_oserror(filename=None, filename2=None, written=None): 104 | error = OSError(err, os.strerror(err)) 105 | if filename is not None: 106 | error.filename = filename 107 | if filename2 is not None: 108 | error.filename2 = filename2 109 | if written is not None: 110 | error.characters_written = written 111 | return error 112 | return build_oserror 113 | 114 | 115 | def _init(): 116 | globals_ = globals() 117 | for name, err in _ERRORS: 118 | globals_[name] = oserror(err) 119 | 120 | _init() 121 | -------------------------------------------------------------------------------- /src/dantalian/pathlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This module contains shared path-related functionality. 20 | """ 21 | 22 | from itertools import count 23 | import os 24 | import posixpath 25 | 26 | 27 | def readlink(path): 28 | """Follow all symlinks and return the target of the last link.""" 29 | while posixpath.islink(path): 30 | path = os.readlink(path) 31 | return path 32 | 33 | 34 | def listdirpaths(path): 35 | """Like os.listdir(), except return pathnames instead of filenames. 36 | 37 | Returns: 38 | A generator yielding paths. 39 | 40 | """ 41 | for entry in os.listdir(path): 42 | yield posixpath.join(path, entry) 43 | 44 | 45 | def free_name(dirpath, name): 46 | """Find a free filename in the given directory. 47 | 48 | Given a desired filename, this function attempts to find a filename 49 | that is not currently being used in the given directory, adding an 50 | incrementing index to the filename as necessary. 51 | 52 | Note that the returned filename might not work due to race 53 | conditions. Program accordingly. 54 | 55 | Args: 56 | dirpath: Pathname of directory to look in. 57 | name: Desired filename. 58 | 59 | Returns: 60 | Filename. 61 | 62 | """ 63 | files = os.listdir(dirpath) 64 | if name not in files: 65 | return name 66 | base, ext = posixpath.splitext(name) 67 | i = count(1) 68 | while True: 69 | name = ''.join((base, '.', str(next(i)), ext)) 70 | if name not in files: 71 | return name 72 | 73 | 74 | def free_name_do(dirpath, name, callback): 75 | """Repeatedly attempt to do something while finding a free filename. 76 | 77 | Returns: 78 | Path of successful new name. 79 | """ 80 | while True: 81 | dst = posixpath.join(dirpath, free_name(dirpath, name)) 82 | try: 83 | callback(dst) 84 | except FileExistsError: 85 | continue 86 | else: 87 | return dst 88 | -------------------------------------------------------------------------------- /src/dantalian/tagging.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module contains tagging functions, i.e. tagging with a directory.""" 19 | 20 | import logging 21 | import posixpath 22 | 23 | from dantalian import base 24 | from dantalian import pathlib 25 | 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | def tag(rootpath, path, directory): 30 | """Tag file with a directory. 31 | 32 | Args: 33 | rootpath: Rootpath to resolve tagnames. 34 | path: Path of file or directory to tag. 35 | directory: Directory path. 36 | """ 37 | pathlib.free_name_do(directory, posixpath.basename(path), 38 | lambda dst: base.link(rootpath, path, dst)) 39 | 40 | 41 | def untag(rootpath, path, directory): 42 | """Untag file from a directory. 43 | 44 | Args: 45 | rootpath: Rootpath to resolve tagnames. 46 | path: Path of file or directory to tag. 47 | directory: Directory path. 48 | """ 49 | target = path 50 | to_unlink = [] 51 | for filepath in pathlib.listdirpaths(directory): 52 | if posixpath.samefile(target, filepath): 53 | to_unlink.append(filepath) 54 | for filepath in to_unlink: 55 | base.unlink(rootpath, filepath) 56 | -------------------------------------------------------------------------------- /src/dantalian/tagnames.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """This module defines interaction with tagnames.""" 19 | 20 | import posixpath 21 | 22 | 23 | def is_tag(name): 24 | """Check if name is a tagname.""" 25 | return name.startswith('//') 26 | 27 | 28 | def path2tag(rootpath, pathname): 29 | """Convert a pathname to a tagname. 30 | 31 | Normalizes the path before converting to a tagname. 32 | 33 | """ 34 | return '//' + posixpath.relpath(pathname, rootpath) 35 | 36 | 37 | def tag2path(rootpath, tagname): 38 | """Convert a tagname to a pathname. 39 | 40 | Doesn't normalize the resulting path. 41 | 42 | """ 43 | return posixpath.join(rootpath, tagname.lstrip('/')) 44 | 45 | 46 | def path(rootpath, name): 47 | """Return tagname or pathname as a pathname.""" 48 | if is_tag(name): 49 | name = tag2path(rootpath, name) 50 | return name 51 | 52 | 53 | def tag(rootpath, name): 54 | """Return tagname or pathname as a tagname.""" 55 | if not is_tag(name): 56 | name = path2tag(rootpath, name) 57 | return name 58 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This package contains the unit tests for the dantalian package. 20 | """ 21 | -------------------------------------------------------------------------------- /tests/base_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This module contains unit tests for dantalian.base 20 | """ 21 | 22 | import os 23 | import posixpath 24 | from unittest.mock import patch 25 | 26 | from dantalian import base 27 | 28 | from . import testlib 29 | 30 | # pylint: disable=missing-docstring 31 | 32 | 33 | class TestLink(testlib.FSMixin, testlib.SameFileMixin): 34 | 35 | def setUp(self): 36 | super().setUp() 37 | os.mkdir('bag') 38 | os.mknod('apple') 39 | 40 | def test_link(self): 41 | base.link(self.root, 'apple', 'bag/apple') 42 | self.assertSameFile('apple', 'bag/apple') 43 | 44 | 45 | class TestLinkDir(testlib.FSMixin, testlib.SameFileMixin): 46 | 47 | def setUp(self): 48 | super().setUp() 49 | os.mkdir('bag') 50 | os.mkdir('apple') 51 | 52 | def test_link_dir(self): 53 | with patch('dantalian.dtags.add_tag', autospec=True) as mock_func: 54 | base.link(self.root, 'apple', 'bag/apple') 55 | self.assertSameFile('apple', 'bag/apple') 56 | mock_func.assert_called_with('apple', '//bag/apple') 57 | 58 | 59 | class TestUnlink(testlib.FSMixin, testlib.SameFileMixin): 60 | 61 | def setUp(self): 62 | super().setUp() 63 | os.mkdir('bag') 64 | os.mknod('apple') 65 | os.link('apple', 'bag/apple') 66 | 67 | def test_unlink(self): 68 | base.unlink(self.root, 'bag/apple') 69 | self.assertFalse(posixpath.exists('bag/apple')) 70 | 71 | 72 | class TestUnlinkDir(testlib.FSMixin, testlib.SameFileMixin): 73 | 74 | def setUp(self): 75 | super().setUp() 76 | os.mkdir('bag') 77 | os.mkdir('apple') 78 | os.symlink(posixpath.abspath('apple'), 'bag/apple') 79 | 80 | def test_unlink_dir(self): 81 | with patch('dantalian.dtags.remove_tag', autospec=True) as mock_func: 82 | base.unlink(self.root, 'bag/apple') 83 | self.assertFalse(posixpath.exists('bag/apple')) 84 | mock_func.assert_called_with('bag/apple', '//bag/apple') 85 | 86 | 87 | class TestSwapDir(testlib.FSMixin, testlib.SameFileMixin): 88 | 89 | def setUp(self): 90 | super().setUp() 91 | os.mkdir('bag') 92 | os.mkdir('apple') 93 | os.symlink(posixpath.abspath('apple'), 'bag/apple') 94 | 95 | def test_swap_dir(self): 96 | with patch('dantalian.dtags.add_tag', autospec=True) as mock_add, \ 97 | patch('dantalian.dtags.remove_tag', autospec=True) as mock_rm: 98 | base.swap_dir(self.root, 'bag/apple') 99 | self.assertTrue(posixpath.islink('apple')) 100 | self.assertTrue(posixpath.isdir('bag/apple')) 101 | mock_rm.assert_called_with('bag/apple', '//bag/apple') 102 | mock_add.assert_called_with('bag/apple', '//apple') 103 | -------------------------------------------------------------------------------- /tests/findlib_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This module contains unit tests for dantalian.findlib 20 | """ 21 | 22 | import os 23 | import posixpath 24 | from unittest import TestCase 25 | 26 | from dantalian import findlib 27 | 28 | from . import testlib 29 | 30 | # pylint: disable=missing-docstring 31 | 32 | 33 | class QueryMixin(TestCase): 34 | 35 | """TestCase mixin with convenient assertions.""" 36 | 37 | # pylint: disable=invalid-name 38 | # pylint: disable=too-few-public-methods 39 | 40 | def assertSameQuery(self, node1, node2): 41 | """Assert two query node trees are equal.""" 42 | self.assertEqual(node1, node2) 43 | if isinstance(node1, findlib.GroupNode): 44 | for i in range(len(node1.children)): 45 | self.assertSameQuery(node1.children[i], node2.children[i]) 46 | 47 | 48 | class TestLibraryParsing(QueryMixin): 49 | 50 | root = 'foobar' 51 | 52 | def test_parse_and(self): 53 | tree = findlib.parse_query(self.root, "AND A B C END") 54 | self.assertSameQuery(tree, findlib.AndNode( 55 | [findlib.DirNode("A"), 56 | findlib.DirNode("B"), 57 | findlib.DirNode("C")])) 58 | 59 | def test_parse_or(self): 60 | tree = findlib.parse_query(self.root, "OR A B C END") 61 | self.assertSameQuery(tree, findlib.OrNode( 62 | [findlib.DirNode("A"), 63 | findlib.DirNode("B"), 64 | findlib.DirNode("C")])) 65 | 66 | def test_parse_minus(self): 67 | tree = findlib.parse_query(self.root, "MINUS A B C END") 68 | self.assertSameQuery(tree, findlib.MinusNode( 69 | [findlib.DirNode("A"), 70 | findlib.DirNode("B"), 71 | findlib.DirNode("C")])) 72 | 73 | def test_parse_and_escape(self): 74 | tree = findlib.parse_query(self.root, 75 | r"AND '\AND' '\\AND' '\\\AND' END") 76 | self.assertSameQuery(tree, findlib.AndNode( 77 | [findlib.DirNode(r'AND'), 78 | findlib.DirNode(r'\AND'), 79 | findlib.DirNode(r'\\AND')])) 80 | 81 | def test_parse_end_escape(self): 82 | tree = findlib.parse_query(self.root, r"AND A B \\END END") 83 | self.assertSameQuery(tree, findlib.AndNode( 84 | [findlib.DirNode(r'A'), 85 | findlib.DirNode(r'B'), 86 | findlib.DirNode(r'END')])) 87 | 88 | def test_parse_and_or(self): 89 | tree = findlib.parse_query(self.root, "AND A B C OR spam eggs END END") 90 | self.assertSameQuery(tree, findlib.AndNode( 91 | [findlib.DirNode("A"), 92 | findlib.DirNode("B"), 93 | findlib.DirNode("C"), 94 | findlib.OrNode( 95 | [findlib.DirNode('spam'), 96 | findlib.DirNode('eggs')]) 97 | ])) 98 | 99 | def test_parse_tags(self): 100 | tree = findlib.parse_query(self.root, "AND A /B //C //D/E END") 101 | self.assertSameQuery(tree, findlib.AndNode( 102 | [findlib.DirNode("A"), 103 | findlib.DirNode("/B"), 104 | findlib.DirNode(posixpath.join(self.root, 'C')), 105 | findlib.DirNode(posixpath.join(self.root, 'D/E')), 106 | ])) 107 | 108 | 109 | class TestSearch(testlib.FSMixin): 110 | 111 | def setUp(self): 112 | super().setUp() 113 | os.makedirs('A') 114 | os.makedirs('B') 115 | os.makedirs('C') 116 | os.mknod('A/a') 117 | os.mknod('A/b') 118 | os.mknod('A/c') 119 | os.mknod('C/d') 120 | os.link('A/b', 'B/b') 121 | os.link('A/c', 'B/c') 122 | os.link('A/c', 'C/c') 123 | 124 | def test_and(self): 125 | results = findlib.search( 126 | findlib.AndNode( 127 | [findlib.DirNode('A'), 128 | findlib.DirNode('B'), 129 | findlib.DirNode('C')] 130 | ) 131 | ) 132 | self.assertListEqual( 133 | results, 134 | ['A/c'], 135 | ) 136 | 137 | def test_or(self): 138 | results = findlib.search( 139 | findlib.OrNode( 140 | [findlib.DirNode('A'), 141 | findlib.DirNode('B'), 142 | findlib.DirNode('C')] 143 | ) 144 | ) 145 | self.assertListEqual( 146 | sorted(results), 147 | sorted(['A/a', 'A/b', 'A/c', 'C/d']), 148 | ) 149 | 150 | def test_minus(self): 151 | results = findlib.search( 152 | findlib.MinusNode( 153 | [findlib.DirNode('A'), 154 | findlib.DirNode('B'), 155 | findlib.DirNode('C')] 156 | ) 157 | ) 158 | self.assertListEqual( 159 | results, 160 | ['A/a'] 161 | ) 162 | 163 | def test_dir(self): 164 | results = findlib.search( 165 | findlib.DirNode('A') 166 | ) 167 | self.assertListEqual( 168 | sorted(results), 169 | sorted(['A/a', 'A/b', 'A/c']), 170 | ) 171 | -------------------------------------------------------------------------------- /tests/library_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This module contains unit tests for dantalian.library 20 | """ 21 | 22 | import os 23 | import posixpath 24 | 25 | from dantalian import library 26 | 27 | from . import testlib 28 | 29 | # pylint: disable=missing-docstring 30 | 31 | 32 | class TestLibraryRoot(testlib.FSMixin): 33 | 34 | def setUp(self): 35 | super().setUp() 36 | os.makedirs('A/.dantalian') 37 | os.mknod('A/.dantalian/foo') 38 | os.makedirs('A/foo/bar') 39 | os.makedirs('B/foo/bar') 40 | 41 | def test_is_library(self): 42 | self.assertTrue(library.is_library('A')) 43 | self.assertFalse(library.is_library('B')) 44 | 45 | def test_find_library(self): 46 | self.assertEqual(library.find_library('A/foo/bar'), 47 | posixpath.abspath('A')) 48 | 49 | def test_init_library(self): 50 | library.init_library('B') 51 | self.assertTrue(library.is_library('B')) 52 | 53 | def test_get_resource(self): 54 | self.assertEqual(library.get_resource('A', 'foo'), 'A/.dantalian/foo') 55 | -------------------------------------------------------------------------------- /tests/tagnames_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This module contains unit tests for dantalian.tagnames 20 | """ 21 | 22 | from unittest import TestCase 23 | 24 | from dantalian import tagnames 25 | 26 | # pylint: disable=missing-docstring 27 | 28 | 29 | class TestTagnames(TestCase): 30 | 31 | def test_is_tag(self): 32 | self.assertTrue(tagnames.is_tag('//foo/tag')) 33 | self.assertTrue(tagnames.is_tag('/////foo/tag')) 34 | self.assertFalse(tagnames.is_tag('/foo/tag')) 35 | self.assertFalse(tagnames.is_tag('foo/tag')) 36 | 37 | def test_path2tag(self): 38 | self.assertEqual(tagnames.path2tag('/foo', '/foo/bar'), '//bar') 39 | 40 | def test_tag2path(self): 41 | self.assertEqual(tagnames.tag2path('/foo', '//bar'), '/foo/bar') 42 | self.assertEqual(tagnames.tag2path('/foo', '///bar'), '/foo/bar') 43 | 44 | def test_path(self): 45 | self.assertEqual(tagnames.path('/foo', '//bar'), '/foo/bar') 46 | self.assertEqual(tagnames.path('/foo', '/bar'), '/bar') 47 | 48 | def test_tag(self): 49 | self.assertEqual(tagnames.tag('/foo', '//bar'), '//bar') 50 | self.assertEqual(tagnames.tag('/foo', '/foo/bar'), '//bar') 51 | -------------------------------------------------------------------------------- /tests/testlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Allen Li 2 | # 3 | # This file is part of Dantalian. 4 | # 5 | # Dantalian is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Dantalian is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Dantalian. If not, see . 17 | 18 | """ 19 | This module contains TestCase mixin classes that implement functionality 20 | shared between other test modules. 21 | 22 | """ 23 | 24 | import os 25 | import posixpath 26 | import shutil 27 | import tempfile 28 | from unittest import TestCase 29 | 30 | 31 | class SameFileMixin(TestCase): 32 | 33 | """TestCase mixin with convenient assertions.""" 34 | 35 | # pylint: disable=invalid-name 36 | 37 | def assertSameFile(self, file1, file2): 38 | """Assert both files are the same by inode.""" 39 | self.assertTrue(posixpath.samefile(file1, file2)) 40 | 41 | def assertNotSameFile(self, file1, file2): 42 | """Assert files are not the same. 43 | 44 | Assertion fails if first file does not exist. 45 | """ 46 | self.assertTrue(posixpath.exists(file1)) 47 | if posixpath.exists(file2): 48 | self.assertFalse(posixpath.samefile(file1, file2)) 49 | 50 | 51 | class FSMixin(TestCase): 52 | 53 | """TestCase mixin with convenient assertions. 54 | 55 | File system setup mixin. 56 | 57 | Example: 58 | 59 | class Foo(FSMixin): 60 | def setUp(self): 61 | super().setUp() 62 | # Do any file system initialization here 63 | os.makedir('foo') 64 | os.mknod('foo/bar') 65 | 66 | """ 67 | 68 | # pylint: disable=invalid-name 69 | 70 | def setUp(self): 71 | self.__olddir = os.getcwd() 72 | self.__tmproot = tempfile.mkdtemp() 73 | os.chdir(self.__tmproot) 74 | 75 | @property 76 | def root(self): 77 | """Get path of text fixture directory root.""" 78 | return self.__tmproot 79 | 80 | def tearDown(self): 81 | os.chdir(self.__olddir) 82 | shutil.rmtree(self.__tmproot) 83 | --------------------------------------------------------------------------------