├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── djangodav ├── __init__.py ├── acls.py ├── auth │ ├── __init__.py │ ├── rest.py │ ├── tasty.py │ └── tests.py ├── base │ ├── __init__.py │ ├── locks.py │ ├── resources.py │ └── tests │ │ ├── __init__.py │ │ ├── resources.py │ │ └── tests.py ├── db │ ├── __init__.py │ └── resources.py ├── fs │ ├── __init__.py │ ├── resources.py │ └── tests.py ├── locks.py ├── models.py ├── responses.py ├── templates │ └── djangodav │ │ └── index.html ├── utils.py └── views │ ├── __init__.py │ ├── tests.py │ └── views.py ├── docs ├── auth.rst ├── classes.rst ├── difference.rst ├── howto.rst ├── known_issues.rst └── motivation.rst ├── runtests.py ├── samples ├── __init__.py ├── auth │ ├── __init__.py │ └── views │ │ ├── __init__.py │ │ ├── rest.py │ │ └── tasty.py ├── db │ ├── __init__.py │ ├── models.py │ └── resources.py ├── fs │ ├── __init__.py │ ├── models.py │ ├── resources.py │ └── views.py ├── manage.py ├── requirements.txt ├── settings.py └── urls.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | *.log 23 | *.pot 24 | *.pyc 25 | env 26 | env3 27 | .idea 28 | .vscode 29 | *.sqlite 30 | migrations 31 | __pycache__ 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.8" 5 | env: 6 | - DJANGO_VERSION=1.11.29 7 | - DJANGO_VERSION=3.1.2 8 | matrix: 9 | exclude: 10 | - python: "2.7" 11 | env: DJANGO_VERSION=3.1.2 12 | - python: "3.8" 13 | env: DJANGO_VERSION=1.11.29 14 | install: pip install -q Django==$DJANGO_VERSION djangorestframework mock lxml 15 | script: python setup.py test 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | graft djangodav/templates 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DjangoDav 2 | ========= 3 | 4 | Production ready WebDav extension for Django. 5 | 6 | .. image:: https://travis-ci.org/meteozond/djangodav.svg 7 | 8 | Motivation 9 | ---------- 10 | 11 | Django is a very popular tool which provides data representation and management. One of the key goals is to provide 12 | machine access to it. Most popular production ready tools provide json based api access. Which have their own 13 | advantages and disadvantages. 14 | 15 | WebDav today is a standard for cooperative document management. Its clients are built in the modern operation systems 16 | and supported by the world popular services. But it very important to remember that it's not only about file storage, 17 | WebDab provides a set of methods to deal with tree structured objects of any kind. 18 | 19 | Providing WebDav access to Django resources opens new horizons for building Web2.0 apps, with inplace edition and 20 | providing native operation system access to the stored objects. 21 | 22 | 23 | Difference with SmartFile django-webdav 24 | --------------------------------------- 25 | 26 | Base resource functionality was separated into BaseResource class from the storage 27 | functionality which developers free to choose from provided or implement themselves. 28 | 29 | Improved class dependencies. Resource class don’t know anything about url or server, its 30 | goal is only to store content and provide proper access. 31 | 32 | Removed properties helper class. View is now responsible for xml generation, and resource 33 | provides actual property list. 34 | 35 | Server is now inherited from Django Class Based View, and renamed to DavView. 36 | 37 | Key methods covered with tests. 38 | 39 | Removed redundant request handler. 40 | 41 | Added FSResource and DBResource to provide file system and data base access. 42 | 43 | Xml library usage is replaced with lxml to achieve proper xml generation code readability. 44 | 45 | 46 | How to create simple filesystem webdav resource 47 | ----------------------------------------------- 48 | 49 | 1. Create resource.py 50 | ~~~~~~~~~~~~~~~~~~~~~ 51 | 52 | .. code:: python 53 | 54 | from django.conf import settings 55 | from djangodav.base.resources import MetaEtagMixIn 56 | from djangodav.fs.resources import DummyFSDAVResource 57 | 58 | class MyDavResource(MetaEtagMixIn, DummyFSDAVResource): 59 | root = '/path/to/folder' 60 | 61 | 62 | 2. Register WebDav view in urls.py 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | .. code:: python 66 | 67 | from djangodav.acls import FullAcl 68 | from djangodav.locks import DummyLock 69 | from djangodav.views import DavView 70 | 71 | from django.conf.urls import patterns 72 | 73 | from .resource import MyDavResource 74 | 75 | urlpatterns = patterns('', 76 | (r'^fsdav(?P.*)$', DavView.as_view(resource_class=MyDavResource, lock_class=DummyLock, 77 | acl_class=FullAcl)), 78 | ) 79 | -------------------------------------------------------------------------------- /djangodav/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | VERSION = (0, 0, 1, 'beta', 25) 23 | __version__ = "0.0.1b25" 24 | -------------------------------------------------------------------------------- /djangodav/acls.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | 23 | class DavAcl(object): 24 | """Represents all the permissions that a user might have on a resource. This 25 | makes it easy to implement virtual permissions.""" 26 | def __init__(self, read=False, write=False, delete=False, full=None): 27 | if full is not None: 28 | self.read = self.write = self.delete = \ 29 | self.create = self.relocate = full 30 | self.read = read 31 | self.write = write 32 | self.delete = delete 33 | 34 | 35 | class ReadOnlyAcl(DavAcl): 36 | def __init__(self, read=True, write=False, delete=False, full=None): 37 | super(ReadOnlyAcl, self).__init__(read, write, delete, full) 38 | 39 | 40 | class FullAcl(DavAcl): 41 | def __init__(self, read=True, write=True, delete=True, full=None): 42 | super(FullAcl, self).__init__(read, write, delete, full) 43 | -------------------------------------------------------------------------------- /djangodav/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /djangodav/auth/rest.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from django.http import HttpResponse 22 | from django.utils.decorators import method_decorator 23 | from django.views.decorators.csrf import csrf_exempt 24 | from djangodav.responses import HttpResponseUnAuthorized 25 | 26 | try: 27 | import rest_framework 28 | from rest_framework.exceptions import APIException 29 | except ImportError: 30 | rest_framework = None 31 | 32 | 33 | class RequestWrapper(object): 34 | """ simulates django-rest-api request wrapper """ 35 | def __init__(self, request): 36 | self._request = request 37 | def __getattr__(self, attr): 38 | return getattr(self._request, attr) 39 | 40 | 41 | class RestAuthViewMixIn(object): 42 | authentications = NotImplemented 43 | 44 | @method_decorator(csrf_exempt) 45 | def dispatch(self, request, *args, **kwargs): 46 | assert rest_framework is not None, "django rest framework is not installed." 47 | if request.method.lower() != 'options': 48 | user_auth_tuple = None 49 | for auth in self.authentications: 50 | try: 51 | user_auth_tuple = auth.authenticate(RequestWrapper(request)) 52 | except APIException as e: 53 | return HttpResponse(e.detail, status=e.status_code) 54 | else: 55 | if user_auth_tuple is None: 56 | continue # try next authenticator 57 | else: 58 | break # we got auth, so stop trying 59 | 60 | if user_auth_tuple is not None: 61 | user, auth = user_auth_tuple 62 | else: 63 | resp = HttpResponseUnAuthorized("Not Authorised") 64 | resp['WWW-Authenticate'] = self.authentications[0].authenticate_header(request) 65 | return resp 66 | 67 | request.user = user 68 | request.auth = auth 69 | return super(RestAuthViewMixIn, self).dispatch(request, *args, **kwargs) 70 | -------------------------------------------------------------------------------- /djangodav/auth/tasty.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from django.http import HttpResponse 22 | from django.utils.decorators import method_decorator 23 | from django.views.decorators.csrf import csrf_exempt 24 | from djangodav.responses import HttpResponseUnAuthorized 25 | 26 | 27 | class TastypieAuthViewMixIn(object): 28 | authentication = NotImplemented 29 | 30 | @method_decorator(csrf_exempt) 31 | def dispatch(self, request, *args, **kwargs): 32 | 33 | if request.method.lower() != 'options': 34 | auth_result = self.authentication.is_authenticated(request) 35 | 36 | if isinstance(auth_result, HttpResponse): 37 | return auth_result 38 | 39 | if auth_result is not True: 40 | return HttpResponseUnAuthorized() 41 | 42 | return super(TastypieAuthViewMixIn, self).dispatch(request, *args, **kwargs) 43 | -------------------------------------------------------------------------------- /djangodav/auth/tests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os.path import dirname 3 | from base64 import b64encode 4 | 5 | from django.test import TestCase 6 | from django.test.client import RequestFactory 7 | try: 8 | from django.utils.unittest import skipUnless 9 | except ImportError: 10 | from unittest import skipUnless 11 | 12 | from djangodav.views import DavView 13 | from djangodav.fs.resources import DummyReadFSDavResource 14 | from djangodav.auth.rest import RestAuthViewMixIn 15 | 16 | try: 17 | import rest_framework 18 | from rest_framework.authentication import SessionAuthentication as RestSessionAuthentication, \ 19 | BasicAuthentication as RestBasicAuthentication 20 | except ImportError: 21 | rest_framework = None 22 | 23 | from djangodav.responses import HttpResponseUnAuthorized 24 | from django.contrib.auth import get_user_model 25 | from djangodav.locks import DummyLock 26 | from djangodav.acls import ReadOnlyAcl 27 | from django.http.response import HttpResponse 28 | 29 | class TestFSResource(DummyReadFSDavResource): 30 | """ just a test resource """ 31 | root = dirname(__file__) 32 | 33 | class TestDAVView(RestAuthViewMixIn, DavView): 34 | """ dummy view """ 35 | resource_class= TestFSResource 36 | acl_class = ReadOnlyAcl 37 | lock_class = DummyLock 38 | 39 | class RestAuthTest(TestCase): 40 | """ test authentication through Django Rest Framework. This will 41 | only work when RestFramework is actually installed. """ 42 | 43 | def setUp(self): 44 | self.user = get_user_model()(username='root', is_active=True) 45 | self.user.set_password('test') 46 | self.user.save() 47 | 48 | @skipUnless(rest_framework, "required Django Rest Framework") 49 | def assertIsAuthorized(self, response): 50 | self.assertIsInstance(response, HttpResponse) 51 | self.assertNotIsInstance(response, HttpResponseUnAuthorized) 52 | 53 | @skipUnless(rest_framework, "required Django Rest Framework") 54 | def assertIsNotAuthorized(self, response): 55 | self.assertIsInstance(response, HttpResponseUnAuthorized) 56 | 57 | @skipUnless(rest_framework, "required Django Rest Framework") 58 | def assertHasAuthenticateHeader(self, response): 59 | self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"') 60 | 61 | @skipUnless(rest_framework, "required Django Rest Framework") 62 | def test_auth_session(self): 63 | """ test whether we can authenticate through Django session """ 64 | 65 | class RestAuthDavView(TestDAVView): 66 | authentications = (RestSessionAuthentication(),) 67 | v = RestAuthDavView.as_view() 68 | 69 | # get with no authentication yields HttpResponseUnAuthorized 70 | request = RequestFactory().get('/') 71 | response = v(request, '/') 72 | self.assertIsNotAuthorized(response) 73 | 74 | # get with authentication goes through 75 | request = RequestFactory().get('/') 76 | # in the regular case, session handling would be done in 77 | # middleware. RestSessionAuthentication only checks for 78 | # request.user, so we just fake that 79 | request.user = self.user 80 | response = v(request, '/') 81 | self.assertIsAuthorized(response) 82 | 83 | @skipUnless(rest_framework, "required Django Rest Framework") 84 | def test_auth_basic(self): 85 | """ test whether we can authenticate through Basic auth """ 86 | 87 | class RestAuthDavView(TestDAVView): 88 | authentications = (RestBasicAuthentication(),) 89 | v = RestAuthDavView.as_view() 90 | 91 | # get with no authentication yields HttpResponseUnAuthorized 92 | request = RequestFactory().get('/') 93 | response = v(request, '/') 94 | self.assertIsNotAuthorized(response) 95 | self.assertHasAuthenticateHeader(response) 96 | 97 | # get with session authentication does not get through 98 | request = RequestFactory().get('/') 99 | request.user = self.user 100 | response = v(request, '/') 101 | self.assertIsNotAuthorized(response) 102 | 103 | # get with basic authentication goes through 104 | if sys.version_info < (3, 0, 0): #py2 105 | b64encode_str = b64encode('root:test') 106 | else: 107 | b64encode_str = b64encode(b'root:test').decode('utf-8') 108 | request = RequestFactory().get('/', **{'HTTP_AUTHORIZATION': 'Basic %s' % b64encode_str}) 109 | response = v(request, '/') 110 | self.assertIsAuthorized(response) 111 | 112 | @skipUnless(rest_framework, "required Django Rest Framework") 113 | def test_auth_multiple(self): 114 | """ test whether we can authenticate through either Session or Basic auth """ 115 | 116 | class RestAuthDavView(TestDAVView): 117 | authentications = (RestBasicAuthentication(), RestSessionAuthentication(),) 118 | v = RestAuthDavView.as_view() 119 | 120 | # get with no authentication yields HttpResponseUnAuthorized 121 | request = RequestFactory().get('/') 122 | response = v(request, '/') 123 | self.assertIsNotAuthorized(response) 124 | self.assertHasAuthenticateHeader(response) 125 | 126 | # get with session authentication goes through 127 | request = RequestFactory().get('/') 128 | request.user = self.user 129 | response = v(request, '/') 130 | self.assertIsAuthorized(response) 131 | 132 | # get with basic authentication goes through 133 | if sys.version_info < (3, 0, 0): #py2 134 | b64encode_str = b64encode('root:test') 135 | else: 136 | b64encode_str = b64encode(b'root:test').decode('utf-8') 137 | request = RequestFactory().get('/', **{'HTTP_AUTHORIZATION': 'Basic %s' % b64encode_str}) 138 | response = v(request, '/') 139 | self.assertIsAuthorized(response) 140 | -------------------------------------------------------------------------------- /djangodav/base/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /djangodav/base/locks.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | 23 | class BaseLock(object): 24 | def __init__(self, resource): 25 | self.resource = resource 26 | 27 | def get(self): 28 | """Gets all active locks for the requested resource. Returns a list of locks.""" 29 | raise NotImplementedError() 30 | 31 | def acquire(self, lockscope, locktype, depth, timeout, owner): 32 | """Creates a new lock for the given resource.""" 33 | raise NotImplementedError() 34 | 35 | def release(self, token): 36 | """Releases the lock referenced by the given lock id.""" 37 | raise NotImplementedError() 38 | 39 | def del_locks(self): 40 | """Releases all locks for the given resource.""" 41 | raise NotImplementedError() 42 | -------------------------------------------------------------------------------- /djangodav/base/resources.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from hashlib import md5 22 | from mimetypes import guess_type 23 | 24 | from django.utils.encoding import force_bytes 25 | from django.utils.http import urlquote 26 | from djangodav.utils import rfc3339_date, rfc1123_date, safe_join 27 | 28 | 29 | class BaseDavResource(object): 30 | ALL_PROPS = ['getcontentlength', 'creationdate', 'getlastmodified', 'resourcetype', 'displayname'] 31 | 32 | LIVE_PROPERTIES = [ 33 | '{DAV:}getetag', '{DAV:}getcontentlength', '{DAV:}creationdate', 34 | '{DAV:}getlastmodified', '{DAV:}resourcetype', '{DAV:}displayname' 35 | ] 36 | 37 | def __init__(self, path): 38 | self.path = [] 39 | path = path.strip("/") 40 | if path: 41 | self.path = path.split("/") 42 | 43 | def get_path(self): 44 | return ("/" if self.path else "") + "/".join(self.path) + ("/" * (self.is_collection)) 45 | 46 | def get_escaped_path(self): 47 | path = [urlquote(p) for p in self.path] 48 | return ("/" if path else "") + "/".join(path) + ("/" * self.is_collection) 49 | 50 | @property 51 | def displayname(self): 52 | if not self.path: 53 | return None 54 | return self.path[-1] 55 | 56 | @property 57 | def is_root(self): 58 | return not bool(self.path) 59 | 60 | def get_parent_path(self): 61 | path = self.path[:-1] 62 | return "/" + "/".join(path) + "/" if path else "" 63 | 64 | def get_parent(self): 65 | return self.clone(self.get_parent_path()) 66 | 67 | def get_descendants(self, depth=1, include_self=True): 68 | """Return an iterator of all descendants of this resource.""" 69 | if include_self: 70 | yield self 71 | # If depth is less than 0, then it started out as -1. 72 | # We need to keep recursing until we hit 0, or forever 73 | # in case of infinity. 74 | if depth != 0: 75 | for child in self.get_children(): 76 | for desc in child.get_descendants(depth=depth-1, include_self=True): 77 | yield desc 78 | 79 | @property 80 | def getcontentlength(self): 81 | raise NotImplementedError() 82 | 83 | @property 84 | def creationdate(self): 85 | """Return the create time as rfc3339_date.""" 86 | return rfc3339_date(self.get_created()) 87 | 88 | @property 89 | def getlastmodified(self): 90 | """Return the modified time as http_date.""" 91 | return rfc1123_date(self.get_modified()) 92 | 93 | def get_created(self): 94 | """Return the create time as datetime object.""" 95 | raise NotImplementedError() 96 | 97 | def get_modified(self): 98 | """Return the modified time as datetime object.""" 99 | raise NotImplementedError() 100 | 101 | @property 102 | def getetag(self): 103 | raise NotImplementedError() 104 | 105 | def copy(self, destination, depth=-1): 106 | if self.is_collection: 107 | if not destination.exists or not destination.is_collection: 108 | destination.create_collection() 109 | self.copy_collection(destination, depth) 110 | else: 111 | if destination.is_object: 112 | destination.delete() 113 | self.copy_object(destination) 114 | 115 | def copy_collection(self, destination, depth=-1): 116 | """Called to copy a resource to a new location. Overwrite is assumed, the DAV server 117 | will refuse to copy to an existing resource otherwise. This method needs to gracefully 118 | handle a pre-existing destination of any type. It also needs to respect the depth 119 | parameter. depth == -1 is infinity.""" 120 | # If depth is less than 0, then it started out as -1. 121 | # We need to keep recursing until we hit 0, or forever 122 | # in case of infinity. 123 | if depth != 0: 124 | for child in self.get_children(): 125 | child.copy(self.clone(safe_join(destination.get_path(), child.displayname)), 126 | depth=depth-1) 127 | 128 | def copy_object(self, destination): 129 | raise NotImplemented() 130 | 131 | def move(self, destination): 132 | if self.is_collection: 133 | if not destination.exists or not destination.is_collection: 134 | destination.create_collection() 135 | self.move_collection(destination) 136 | else: 137 | if destination.is_object: 138 | destination.delete() 139 | self.move_object(destination) 140 | 141 | def move_collection(self, destination): 142 | """Called to move a resource to a new location. Overwrite is assumed, the DAV server 143 | will refuse to move to an existing resource otherwise. This method needs to gracefully 144 | handle a pre-existing destination of any type.""" 145 | for child in self.get_children(): 146 | child.move(self.clone(safe_join(destination.get_path(), child.displayname))) 147 | self.delete() 148 | 149 | def clone(self, *args, **kwargs): 150 | return self.__class__(*args, **kwargs) 151 | 152 | def move_object(self, destination): 153 | raise NotImplemented() 154 | 155 | def write(self, content): 156 | raise NotImplementedError() 157 | 158 | def read(self): 159 | raise NotImplementedError() 160 | 161 | @property 162 | def is_collection(self): 163 | raise NotImplementedError() 164 | 165 | @property 166 | def content_type(self): 167 | return guess_type(self.displayname)[0] 168 | 169 | @property 170 | def is_object(self): 171 | raise NotImplementedError() 172 | 173 | @property 174 | def exists(self): 175 | raise NotImplementedError() 176 | 177 | def get_children(self): 178 | raise NotImplementedError() 179 | 180 | def delete(self): 181 | raise NotImplementedError() 182 | 183 | def create_collection(self): 184 | raise NotImplementedError() 185 | 186 | 187 | class MetaEtagMixIn(object): 188 | @property 189 | def getetag(self): 190 | """Calculate an etag for this resource. The default implementation uses an md5 sub of the 191 | absolute path modified time and size. Can be overridden if resources are not stored in a 192 | file system. The etag is used to detect changes to a resource between HTTP calls. So this 193 | needs to change if a resource is modified.""" 194 | hashsum = md5() 195 | hashsum.update(force_bytes(self.displayname)) 196 | hashsum.update(force_bytes(self.creationdate)) 197 | hashsum.update(force_bytes(self.getlastmodified)) 198 | hashsum.update(force_bytes(self.getcontentlength)) 199 | return hashsum.hexdigest() 200 | -------------------------------------------------------------------------------- /djangodav/base/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /djangodav/base/tests/resources.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from datetime import datetime 22 | from django.utils.timezone import utc 23 | from djangodav.base.resources import BaseDavResource 24 | from mock import Mock, MagicMock 25 | 26 | 27 | class MockResource(MagicMock, BaseDavResource): 28 | exists = True 29 | get_created = Mock(return_value=datetime(1983, 12, 24, 6, tzinfo=utc)) 30 | get_modified = Mock(return_value=datetime(2014, 12, 24, 6, tzinfo=utc)) 31 | getcontentlength = 0 32 | 33 | def __init__(self, path, *args, **kwargs): 34 | super(MockResource, self).__init__(spec=BaseDavResource, *args, **kwargs) 35 | BaseDavResource.__init__(self, path) 36 | 37 | 38 | class MockObject(MockResource): 39 | getetag = "0" * 40 40 | is_object = True 41 | is_collection = False 42 | getcontentlength = 42 43 | 44 | 45 | class MockCollection(MockResource): 46 | is_object = False 47 | is_collection = True 48 | 49 | 50 | class MissingMockResource(MockResource): 51 | exists = False 52 | 53 | 54 | class MissingMockObject(MissingMockResource): 55 | is_object = True 56 | is_collection = False 57 | getcontentlength = 42 58 | 59 | 60 | class MissingMockCollection(MissingMockResource): 61 | is_object = False 62 | is_collection = True 63 | -------------------------------------------------------------------------------- /djangodav/base/tests/tests.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from django.test import TestCase 22 | from djangodav.base.resources import BaseDavResource 23 | from djangodav.base.tests.resources import MockCollection, MockObject, MissingMockCollection 24 | from mock import patch, Mock 25 | 26 | 27 | class TestBaseDavResource(TestCase): 28 | 29 | def setUp(self): 30 | self.resource = BaseDavResource("/path/to/name") 31 | 32 | def test_path(self): 33 | self.assertEqual(self.resource.path, ['path', 'to', 'name']) 34 | 35 | @patch('djangodav.base.resources.BaseDavResource.is_collection', True) 36 | def test_get_path_collection(self): 37 | self.assertEqual(self.resource.get_path(), '/path/to/name/') 38 | 39 | @patch('djangodav.base.resources.BaseDavResource.is_collection', False) 40 | def test_get_path_object(self): 41 | self.assertEqual(self.resource.get_path(), '/path/to/name') 42 | 43 | @patch('djangodav.base.resources.BaseDavResource.get_children', Mock(return_value=[])) 44 | def test_get_descendants(self): 45 | self.assertEqual(list(self.resource.get_descendants(depth=1, include_self=True)), [self.resource]) 46 | 47 | def test_get_parent_path(self): 48 | self.assertEqual(self.resource.get_parent_path(), '/path/to/') 49 | 50 | def test_displayname(self): 51 | self.assertEqual(self.resource.displayname, 'name') 52 | 53 | def test_move_collection(self): 54 | child = MockObject('/path/to/src/child', move=Mock()) 55 | src = MockCollection('/path/to/src/', get_children=Mock(return_value=[child]), delete=Mock()) 56 | dst = MissingMockCollection('/path/to/dst/', create_collection=Mock()) 57 | 58 | src.move(dst) 59 | 60 | src.delete.assert_called_with() 61 | dst.create_collection.assert_called_with() 62 | self.assertEqual(child.move.call_args[0][0].path, ['path', 'to', 'dst', 'child']) 63 | 64 | def test_move_collection_collision(self): 65 | child = MockObject('/path/to/src/child', move=Mock()) 66 | src = MockCollection('/path/to/src/', get_children=Mock(return_value=[child]), delete=Mock()) 67 | dst = MockCollection('/path/to/dst/', create_collection=Mock()) 68 | 69 | src.move(dst) 70 | 71 | src.delete.assert_called_with() 72 | self.assertEqual(dst.create_collection.call_count, 0) 73 | self.assertEqual(child.move.call_args[0][0].path, ['path', 'to', 'dst', 'child']) 74 | 75 | def test_copy_collection(self): 76 | child = MockObject('/path/to/src/child', copy=Mock()) 77 | src = MockCollection('/path/to/src/', get_children=Mock(return_value=[child]), delete=Mock()) 78 | dst = MissingMockCollection('/path/to/dst/', create_collection=Mock()) 79 | 80 | src.copy(dst) 81 | 82 | dst.create_collection.assert_called_with() 83 | self.assertEqual(child.copy.call_args[0][0].path, ['path', 'to', 'dst', 'child']) 84 | 85 | def test_copy_collection_collision(self): 86 | child = MockObject('/path/to/src/child', copy=Mock()) 87 | src = MockCollection('/path/to/src/', get_children=Mock(return_value=[child]), delete=Mock()) 88 | dst = MockCollection('/path/to/dst/', create_collection=Mock()) 89 | 90 | src.copy_collection(dst) 91 | 92 | self.assertEqual(dst.create_collection.call_count, 0) 93 | self.assertEqual(child.copy.call_args[0][0].path, ['path', 'to', 'dst', 'child']) 94 | 95 | def test_copy_collection_depth_0(self): 96 | child = MockObject('/path/to/src/child', copy=Mock()) 97 | src = MockCollection('/path/to/src/', get_children=Mock(return_value=[child]), delete=Mock()) 98 | dst = MissingMockCollection('/path/to/dst/', create_collection=Mock()) 99 | 100 | src.copy(dst, 0) 101 | 102 | dst.create_collection.assert_called_with() 103 | self.assertEqual(child.copy.call_count, 0) 104 | -------------------------------------------------------------------------------- /djangodav/db/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /djangodav/db/resources.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from operator import and_ 22 | from functools import reduce 23 | from django.core.exceptions import ObjectDoesNotExist 24 | from django.db.models import Q 25 | from django.utils.functional import cached_property 26 | from django.utils.timezone import now 27 | from djangodav.base.resources import BaseDavResource 28 | from djangodav.utils import url_join 29 | 30 | 31 | class BaseDBDavResource(BaseDavResource): 32 | collection_model = None 33 | object_model = None 34 | 35 | collection_attribute = 'parent' 36 | created_attribute = 'created' 37 | modified_attribute = 'modified' 38 | name_attribute = 'name' 39 | size_attribute = 'size' 40 | 41 | collection_select_related = tuple() 42 | object_select_related = tuple() 43 | 44 | collection_prefetch_related = tuple() 45 | object_prefetch_related = tuple() 46 | 47 | def __init__(self, path, **kwargs): 48 | if 'obj' in kwargs: # Accepting ready object to reduce db requests 49 | self.__dict__['obj'] = kwargs.pop('obj') 50 | super(BaseDBDavResource, self).__init__(path) 51 | 52 | @cached_property 53 | def obj(self): 54 | raise NotImplementedError 55 | 56 | @property 57 | def getcontentlength(self): 58 | return getattr(self.obj, self.size_attribute) 59 | 60 | def get_created(self): 61 | if self.is_root: 62 | return now() 63 | return getattr(self.obj, self.created_attribute) 64 | 65 | def get_modified(self): 66 | if self.is_root: 67 | return now() 68 | return getattr(self.obj, self.modified_attribute) 69 | 70 | @property 71 | def is_collection(self): 72 | return self.is_root or isinstance(self.obj, self.collection_model) 73 | 74 | @property 75 | def is_object(self): 76 | return isinstance(self.obj, self.object_model) 77 | 78 | @cached_property 79 | def exists(self): 80 | return self.is_root or bool(self.obj) 81 | 82 | def get_model_lookup_kwargs(self, **kwargs): 83 | return self.get_model_kwargs(**kwargs) 84 | 85 | def get_model_kwargs(self, **kwargs): 86 | return kwargs 87 | 88 | def get_children(self): 89 | """Return an iterator of all direct children of this resource.""" 90 | if not self.exists or isinstance(self.obj, self.object_model): 91 | return 92 | 93 | models = [ 94 | [self.collection_model, self.collection_select_related, self.collection_prefetch_related], 95 | [self.object_model, self.object_select_related, self.object_prefetch_related] 96 | ] 97 | for model, select_related, prefetch_related in models: 98 | qs = model.objects 99 | if select_related: 100 | qs = qs.select_related(*select_related) 101 | if prefetch_related: 102 | qs = qs.prefetch_related(*prefetch_related) 103 | kwargs = self.get_model_lookup_kwargs(**{self.collection_attribute: self.obj}) 104 | for child in qs.filter(**kwargs): 105 | yield self.clone( 106 | url_join(*(self.path + [child.name])), 107 | obj=child # Sending ready object to reduce db requests 108 | ) 109 | 110 | def read(self): 111 | raise NotImplementedError 112 | 113 | def write(self, content): 114 | raise NotImplementedError 115 | 116 | def delete(self): 117 | if not self.obj: 118 | return 119 | self.obj.delete() 120 | 121 | 122 | class NameLookupDBDavMixIn(object): 123 | """Object lookup by joining collections tables to fit given path""" 124 | 125 | def __init__(self, path, **kwargs): 126 | self.possible_collection = path.endswith("/") 127 | super(NameLookupDBDavMixIn, self).__init__(path, **kwargs) 128 | 129 | def get_object(self): 130 | return self.get_model_by_path('object', self.path) 131 | 132 | def get_collection(self): 133 | return self.get_model_by_path('collection', self.path) 134 | 135 | def create_collection(self): 136 | name = self.path[-1] 137 | parent = self.clone("/".join(self.path[:-1])).obj 138 | kwargs = self.get_model_kwargs(**{self.collection_attribute: parent, 'name': name}) 139 | self.collection_model.objects.create(**kwargs) 140 | 141 | @cached_property 142 | def obj(self): 143 | if not self.path: 144 | return None 145 | 146 | if self.possible_collection: # Reducing queries 147 | attempts = [self.get_collection, self.get_object] 148 | else: 149 | attempts = [self.get_object, self.get_collection] 150 | 151 | for get_object in attempts: 152 | try: 153 | return get_object() 154 | except ObjectDoesNotExist: 155 | continue 156 | 157 | def get_model_by_path(self, model_attr, path): 158 | if not path: 159 | return None 160 | 161 | args = [] 162 | i = 0 163 | for part in reversed(path): 164 | args.append(Q(**{"__".join(([self.collection_attribute] * i) + [self.name_attribute]): part})) 165 | i += 1 166 | qs = getattr(self, "%s_model" % model_attr).objects.filter(**self.get_model_lookup_kwargs()) 167 | 168 | select_related = ["__".join([self.collection_attribute] * i) for i in range(1, len(path))] 169 | select_related += getattr(self, "%s_select_related" % model_attr) 170 | if select_related: 171 | qs = qs.select_related(*select_related) 172 | 173 | prefetch_related = getattr(self, "%s_prefetch_related" % model_attr) 174 | if prefetch_related: 175 | qs = qs.prefetch_related(*prefetch_related) 176 | 177 | args.append(Q(**{"__".join([self.collection_attribute] * len(path)): None})) 178 | try: 179 | return qs.filter(reduce(and_, args))[0] 180 | except IndexError: 181 | raise qs.model.DoesNotExist() 182 | 183 | def copy_object(self, destination): 184 | self.obj.pk = None 185 | name = destination.path[-1] 186 | collection = self.clone(destination.get_parent_path()).obj 187 | setattr(self.obj, self.name_attribute, name) 188 | setattr(self.obj, self.collection_attribute, collection) 189 | setattr(self.obj, self.created_attribute, now()) 190 | setattr(self.obj, self.modified_attribute, now()) 191 | self.obj.save(force_insert=True) 192 | 193 | def move_object(self, destination): 194 | name = destination.path[-1] 195 | collection = self.clone(destination.get_parent_path()).obj 196 | setattr(self.obj, self.name_attribute, name) 197 | setattr(self.obj, self.collection_attribute, collection) 198 | setattr(self.obj, self.modified_attribute, now()) 199 | self.obj.save(update_fields=[self.name_attribute, self.collection_attribute, self.modified_attribute]) 200 | -------------------------------------------------------------------------------- /djangodav/fs/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /djangodav/fs/resources.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | import hashlib 22 | import mimetypes 23 | from sys import getfilesystemencoding 24 | import os 25 | import datetime 26 | import shutil 27 | import urllib 28 | 29 | from django.http import HttpResponse 30 | from django.utils.http import http_date 31 | 32 | from djangodav.base.resources import BaseDavResource 33 | from djangodav.responses import ResponseException 34 | from djangodav.utils import safe_join, url_join 35 | 36 | 37 | fs_encoding = getfilesystemencoding() 38 | 39 | 40 | class BaseFSDavResource(BaseDavResource): 41 | """Implements an interface to the file system. This can be subclassed to provide 42 | a virtual file system (like say in MySQL). This default implementation simply uses 43 | python's os library to do most of the work.""" 44 | 45 | root = None 46 | 47 | def get_abs_path(self): 48 | """Return the absolute path of the resource. Used internally to interface with 49 | an actual file system. If you override all other methods, this one will not 50 | be used.""" 51 | return os.path.join(self.root, *self.path) 52 | 53 | @property 54 | def getcontentlength(self): 55 | """Return the size of the resource in bytes.""" 56 | return os.path.getsize(self.get_abs_path()) 57 | 58 | def get_created(self): 59 | """Return the create time as datetime object.""" 60 | return datetime.datetime.fromtimestamp(os.stat(self.get_abs_path()).st_ctime) 61 | 62 | def get_modified(self): 63 | """Return the modified time as datetime object.""" 64 | return datetime.datetime.fromtimestamp(os.stat(self.get_abs_path()).st_mtime) 65 | 66 | @property 67 | def is_collection(self): 68 | """Return True if this resource is a directory (collection in WebDAV parlance).""" 69 | return os.path.isdir(self.get_abs_path()) 70 | 71 | @property 72 | def is_object(self): 73 | """Return True if this resource is a file (resource in WebDAV parlance).""" 74 | return os.path.isfile(self.get_abs_path()) 75 | 76 | @property 77 | def exists(self): 78 | """Return True if this resource exists.""" 79 | return os.path.exists(self.get_abs_path()) 80 | 81 | def get_children(self): 82 | """Return an iterator of all direct children of this resource.""" 83 | if os.path.isdir(self.get_abs_path()): 84 | for child in os.listdir(self.get_abs_path()): 85 | try: 86 | is_unicode = isinstance(child, unicode) 87 | except NameError: # Python 3 fix 88 | is_unicode = isinstance(child, str) 89 | if not is_unicode: 90 | child = child.decode(fs_encoding) 91 | yield self.clone(url_join(*(self.path + [child]))) 92 | 93 | def write(self, content): 94 | raise NotImplementedError 95 | 96 | def read(self): 97 | raise NotImplementedError 98 | 99 | def delete(self): 100 | """Delete the resource, recursive is implied.""" 101 | if self.is_collection: 102 | for child in self.get_children(): 103 | child.delete() 104 | os.rmdir(self.get_abs_path()) 105 | elif self.is_object: 106 | os.remove(self.get_abs_path()) 107 | 108 | def create_collection(self): 109 | """Create a directory in the location of this resource.""" 110 | os.mkdir(self.get_abs_path()) 111 | 112 | def copy_object(self, destination, depth=0): 113 | shutil.copy(self.get_abs_path(), destination.get_abs_path()) 114 | 115 | def move_object(self, destination): 116 | os.rename(self.get_abs_path(), destination.get_abs_path()) 117 | 118 | 119 | class DummyReadFSDavResource(BaseFSDavResource): 120 | def read(self): 121 | with open(self.get_abs_path(), 'rb') as f: 122 | return f.read() 123 | 124 | 125 | class DummyWriteFSDavResource(BaseFSDavResource): 126 | def write(self, request): 127 | with open(self.get_abs_path(), 'wb') as dst: 128 | shutil.copyfileobj(request, dst) 129 | 130 | 131 | class DummyFSDAVResource(DummyReadFSDavResource, DummyWriteFSDavResource, BaseFSDavResource): 132 | pass 133 | 134 | 135 | class SendFileFSDavResource(BaseFSDavResource): 136 | quote = False 137 | 138 | def read(self): 139 | response = HttpResponse() 140 | full_path = self.get_abs_path().encode('utf-8') 141 | if self.quote: 142 | full_path = urllib.quote(full_path) 143 | response['X-SendFile'] = full_path 144 | response['Content-Type'] = mimetypes.guess_type(self.displayname) 145 | response['Content-Length'] = self.getcontentlength 146 | response['Last-Modified'] = http_date(self.getlastmodified) 147 | response['ETag'] = self.getetag 148 | raise ResponseException(response) 149 | 150 | 151 | class RedirectFSDavResource(BaseFSDavResource): 152 | prefix = "/" 153 | 154 | def read(self): 155 | response = HttpResponse() 156 | response['X-Accel-Redirect'] = url_join(self.prefix, self.get_path().encode('utf-8')) 157 | response['X-Accel-Charset'] = 'utf-8' 158 | response['Content-Type'] = mimetypes.guess_type(self.displayname) 159 | response['Content-Length'] = self.getcontentlength 160 | response['Last-Modified'] = http_date(self.getlastmodified) 161 | response['ETag'] = self.getetag 162 | raise ResponseException(response) 163 | -------------------------------------------------------------------------------- /djangodav/fs/tests.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from django.test import TestCase 22 | from djangodav.fs.resources import BaseFSDavResource 23 | from mock import patch 24 | 25 | 26 | class TestFSDavResource(TestCase): 27 | class FSDavResource(BaseFSDavResource): 28 | base_url = 'http://testserver/base/' 29 | root = '/some/folder/' 30 | 31 | def setUp(self): 32 | self.resource = self.FSDavResource("/path/to/name") 33 | 34 | @patch('djangodav.fs.resources.os.path.isdir') 35 | def test_is_collection(self, isdir): 36 | isdir.return_value = True 37 | self.assertTrue(self.resource.is_collection) 38 | isdir.assert_called_with('/some/folder/path/to/name') 39 | 40 | @patch('djangodav.fs.resources.os.path.isfile') 41 | def test_isfile(self, isfile): 42 | isfile.return_value = True 43 | self.assertTrue(self.resource.is_object) 44 | isfile.assert_called_with('/some/folder/path/to/name') 45 | 46 | @patch('djangodav.fs.resources.os.path.exists') 47 | def test_isfile(self, exists): 48 | exists.return_value = True 49 | self.assertTrue(self.resource.exists) 50 | exists.assert_called_with('/some/folder/path/to/name') 51 | 52 | @patch('djangodav.fs.resources.os.path.getsize') 53 | def test_get_size(self, getsize): 54 | getsize.return_value = 42 55 | self.assertEquals(self.resource.getcontentlength, 42) 56 | getsize.assert_called_with('/some/folder/path/to/name') 57 | 58 | def test_get_abs_path(self): 59 | self.assertEquals(self.resource.get_abs_path(), '/some/folder/path/to/name') 60 | 61 | @patch('djangodav.fs.resources.os.path.isdir') 62 | @patch('djangodav.fs.resources.os.listdir') 63 | def test_get_children(self, listdir, isdir): 64 | listdir.return_value=['child1', 'child2'] 65 | children = list(self.resource.get_children()) 66 | self.assertEqual(children[0].path, ['path', 'to', 'name', 'child1']) 67 | self.assertEqual(children[1].path, ['path', 'to', 'name', 'child2']) 68 | listdir.assert_called_with('/some/folder/path/to/name') 69 | -------------------------------------------------------------------------------- /djangodav/locks.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from uuid import uuid4 22 | 23 | from django.utils.encoding import force_text 24 | 25 | from djangodav.base.locks import BaseLock 26 | 27 | 28 | class DummyLock(BaseLock): 29 | def get(self, *args, **kwargs): 30 | pass 31 | 32 | def acquire(self, *args, **kwargs): 33 | return force_text(uuid4()) 34 | 35 | def release(self, token): 36 | return True 37 | 38 | def del_locks(self): 39 | pass 40 | -------------------------------------------------------------------------------- /djangodav/models.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /djangodav/responses.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | try: 23 | import httplib 24 | except ImportError: 25 | from http import client as httplib 26 | from django.http import HttpResponse 27 | 28 | 29 | # When possible, code returns an HTTPResponse sub-class. In some situations, we want to be able 30 | # to raise an exception to control the response (error conditions within utility functions). In 31 | # this case, we provide HttpError sub-classes for raising. 32 | 33 | 34 | class ResponseException(Exception): 35 | """A base HTTP error class. This allows utility functions to raise an HTTP error so that 36 | when used inside a handler, the handler can simply call the utility and the correct 37 | HttpResponse will be issued to the client.""" 38 | 39 | def __init__(self, response, *args, **kwargs): 40 | super(ResponseException, self).__init__('Response excepted', *args, **kwargs) 41 | self.response = response 42 | 43 | 44 | class HttpResponsePreconditionFailed(HttpResponse): 45 | status_code = httplib.PRECONDITION_FAILED 46 | 47 | 48 | class HttpResponseMediatypeNotSupported(HttpResponse): 49 | status_code = httplib.UNSUPPORTED_MEDIA_TYPE 50 | 51 | 52 | class HttpResponseMultiStatus(HttpResponse): 53 | status_code = httplib.MULTI_STATUS 54 | 55 | 56 | class HttpResponseNotImplemented(HttpResponse): 57 | status_code = httplib.NOT_IMPLEMENTED 58 | 59 | 60 | class HttpResponseBadGateway(HttpResponse): 61 | status_code = httplib.BAD_GATEWAY 62 | 63 | 64 | class HttpResponseCreated(HttpResponse): 65 | status_code = httplib.CREATED 66 | 67 | 68 | class HttpResponseNoContent(HttpResponse): 69 | status_code = httplib.NO_CONTENT 70 | 71 | 72 | class HttpResponseConflict(HttpResponse): 73 | status_code = httplib.CONFLICT 74 | 75 | 76 | class HttpResponseLocked(HttpResponse): 77 | status_code = httplib.LOCKED 78 | 79 | 80 | class HttpResponseUnAuthorized(HttpResponse): 81 | status_code = httplib.UNAUTHORIZED 82 | -------------------------------------------------------------------------------- /djangodav/templates/djangodav/index.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Portions (c) 2014, Alexander Klimenko 3 | All rights reserved. 4 | 5 | Copyright (c) 2011, SmartFile 6 | All rights reserved. 7 | 8 | This file is part of DjangoDav. 9 | 10 | DjangoDav is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published 12 | by the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | DjangoDav is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with DjangoDav. If not, see . 22 | {% endcomment %} 23 | 24 | 25 | 26 | 27 |

Index of {{ resource.get_displaypath }}

28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% for child in resource.get_children %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% endfor %} 47 | 48 |
NameCreatedLast modifiedSize

Parent Directory
{{ child.displayname }}{{ child.get_created|date:"d-M-Y H:i" }}{{ child.get_modified|date:"d-M-Y H:i" }}{{ child.getcontentlength|filesizeformat }}

49 |

DjangoDav - https://github.com/meteozond/djangodav/

50 | 51 | 52 | -------------------------------------------------------------------------------- /djangodav/utils.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | 23 | import datetime, time, calendar 24 | from wsgiref.handlers import format_date_time 25 | 26 | from django.utils.encoding import force_text 27 | from django.utils.feedgenerator import rfc2822_date 28 | 29 | try: 30 | from email.utils import parsedate_tz 31 | except ImportError: 32 | from email.Utils import parsedate_tz 33 | import lxml.builder as lb 34 | 35 | # Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 36 | FORMAT_RFC_822 = '%a, %d %b %Y %H:%M:%S GMT' 37 | # Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 38 | FORMAT_RFC_850 = '%A %d-%b-%y %H:%M:%S GMT' 39 | # Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format 40 | FORMAT_ASC = '%a %b %d %H:%M:%S %Y' 41 | 42 | WEBDAV_NS = "DAV:" 43 | 44 | WEBDAV_NSMAP = {'D': WEBDAV_NS} 45 | 46 | D = lb.ElementMaker(namespace=WEBDAV_NS, nsmap=WEBDAV_NSMAP) 47 | 48 | 49 | def get_property_tag_list(res, *names): 50 | props = [] 51 | for name in names: 52 | tag = get_property_tag(res, name) 53 | if tag is None: 54 | continue 55 | props.append(tag) 56 | return props 57 | 58 | 59 | def get_property_tag(res, name): 60 | if name == 'resourcetype': 61 | if res.is_collection: 62 | return D(name, D.collection) 63 | return D(name) 64 | try: 65 | if hasattr(res, name): 66 | return D(name, force_text(getattr(res, name))) 67 | except AttributeError: 68 | return 69 | 70 | 71 | def safe_join(root, *paths): 72 | """The provided os.path.join() does not work as desired. Any path starting with / 73 | will simply be returned rather than actually being joined with the other elements.""" 74 | if not root.startswith('/'): 75 | root = '/' + root 76 | for path in paths: 77 | while root.endswith('/'): 78 | root = root[:-1] 79 | while path.startswith('/'): 80 | path = path[1:] 81 | root += '/' + path 82 | return root 83 | 84 | 85 | def url_join(base, *paths): 86 | """Assuming base is the scheme and host (and perhaps path) we will join the remaining 87 | path elements to it.""" 88 | paths = safe_join(*paths) if paths else "" 89 | while base.endswith('/'): 90 | base = base[:-1] 91 | return base + paths 92 | 93 | 94 | def ns_split(tag): 95 | """Splits the namespace and property name from a clark notation property name.""" 96 | if tag.startswith("{") and "}" in tag: 97 | ns, name = tag.split("}", 1) 98 | return ns[1:-1], name 99 | return "", tag 100 | 101 | 102 | def ns_join(ns, name): 103 | """Joins a namespace and property name into clark notation.""" 104 | return '{%s:}%s' % (ns, name) 105 | 106 | 107 | def rfc3339_date(dt): 108 | if not dt: 109 | return '' 110 | return dt.strftime('%Y-%m-%dT%H:%M:%SZ') 111 | 112 | 113 | def rfc1123_date(dt): 114 | if not dt: 115 | return '' 116 | return rfc2822_date(dt) 117 | 118 | 119 | def parse_time(timestring): 120 | value = None 121 | for fmt in (FORMAT_RFC_822, FORMAT_RFC_850, FORMAT_ASC): 122 | try: 123 | value = time.strptime(timestring, fmt) 124 | except ValueError: 125 | pass 126 | if value is None: 127 | try: 128 | # Sun Nov 6 08:49:37 1994 +0100 ; ANSI C's asctime() format with timezone 129 | value = parsedate_tz(timestring) 130 | except ValueError: 131 | pass 132 | if value is None: 133 | return 134 | return calendar.timegm(value) 135 | -------------------------------------------------------------------------------- /djangodav/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import * 2 | -------------------------------------------------------------------------------- /djangodav/views/tests.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | import sys 22 | from lxml.etree import ElementTree 23 | from django.http import HttpResponse, HttpRequest, Http404 24 | from djangodav.acls import FullAcl 25 | from djangodav.locks import DummyLock 26 | from djangodav.responses import ResponseException 27 | from lxml import etree 28 | 29 | from djangodav.base.tests.resources import MockCollection, MockObject, MissingMockCollection, MissingMockObject 30 | from djangodav.fs.tests import * 31 | from djangodav.utils import D, WEBDAV_NSMAP, rfc1123_date 32 | from djangodav.views import DavView 33 | from mock import Mock 34 | 35 | 36 | class TestView(TestCase): 37 | def setUp(self): 38 | self.blank_collection = MockCollection( 39 | path='/blank_collection/', 40 | get_descendants=Mock(return_value=[]), 41 | get_parent=lambda: self.top_collection 42 | ) 43 | self.sub_object = MockObject( 44 | path='/collection/sub_object', 45 | getcontentlength=42, 46 | get_descendants=Mock(return_value=[]), 47 | get_parent=lambda: self.top_collection 48 | ) 49 | self.missing_sub_object = MissingMockObject( 50 | path='/collection/missing_sub_object', 51 | getcontentlength=42, 52 | get_descendants=Mock(return_value=[]), 53 | get_parent=lambda: self.top_collection 54 | ) 55 | self.missing_sub_collection = MissingMockCollection( 56 | path='/collection/missing_sub_collection', 57 | get_descendants=Mock(return_value=[]), 58 | get_parent=lambda: self.top_collection 59 | ) 60 | self.sub_collection = MockCollection( 61 | path='/collection/sub_colection/', 62 | get_descendants=Mock(return_value=[]), 63 | get_parent=lambda: self.top_collection 64 | ) 65 | self.top_collection = MockCollection( 66 | path='/collection/', 67 | get_descendants=Mock(return_value=[self.sub_object, self.sub_collection]) 68 | ) 69 | 70 | def test_get_collection_redirect(self): 71 | actual_path = '/collection/' 72 | wrong_path = '/collection' 73 | v = DavView(path=wrong_path, acl_class=FullAcl) 74 | v.__dict__['resource'] = MockCollection(actual_path) 75 | request = Mock(META={'SERVERNANE': 'testserver'}, build_absolute_uri=Mock(return_value=wrong_path)) 76 | resp = v.get(request, wrong_path, 'xbody') 77 | self.assertEqual(resp.status_code, 302) 78 | self.assertEqual(actual_path, resp['Location']) 79 | 80 | def test_get_object_redirect(self): 81 | actual_path = '/object.mp4' 82 | wrong_path = '/object.mp4/' 83 | r = DavView(path=wrong_path, acl_class=FullAcl) 84 | r.__dict__['resource'] = MockObject(actual_path) 85 | request = Mock(META={'SERVERNANE': 'testserver'}, build_absolute_uri=Mock(return_value=wrong_path)) 86 | resp = r.get(request, wrong_path, 'xbody') 87 | self.assertEqual(resp.status_code, 302) 88 | self.assertEqual(resp['Location'], actual_path) 89 | 90 | def test_missing(self): 91 | path = '/object.mp4' 92 | r = DavView(path=path, acl_class=FullAcl) 93 | r.__dict__['resource'] = MissingMockCollection(path) 94 | request = Mock(META={'SERVERNANE': 'testserver'}, build_absolute_uri=Mock(return_value=path)) 95 | self.assertRaises(Http404, r.get, request, path, 'xbody') 96 | 97 | def test_propfind_listing(self): 98 | self.top_collection.get_descendants.return_value += [self.top_collection] 99 | request = Mock(META={}) 100 | path = '/collection/' 101 | v = DavView(base_url='/base/', path=path, request=request, acl_class=FullAcl, xml_pretty_print=True) 102 | v.__dict__['resource'] = self.top_collection 103 | resp = v.propfind(request, path, None) 104 | self.assertEqual(resp.status_code, 207) 105 | self.assertEqual(resp.content, 106 | etree.tostring(D.multistatus( 107 | D.response( 108 | D.href('/base/collection/sub_object'), 109 | D.propstat( 110 | D.prop( 111 | D.getcontentlength("42"), 112 | D.creationdate("1983-12-24T06:00:00Z"), 113 | D.getlastmodified("Wed, 24 Dec 2014 06:00:00 +0000"), 114 | D.resourcetype(), 115 | D.displayname("sub_object"), 116 | ), 117 | D.status("HTTP/1.1 200 OK") 118 | ) 119 | ), 120 | D.response( 121 | D.href('/base/collection/sub_colection/'), 122 | D.propstat( 123 | D.prop( 124 | D.getcontentlength("0"), 125 | D.creationdate("1983-12-24T06:00:00Z"), 126 | D.getlastmodified("Wed, 24 Dec 2014 06:00:00 +0000"), 127 | D.resourcetype(D.collection()), 128 | D.displayname("sub_colection"), 129 | ), 130 | D.status("HTTP/1.1 200 OK") 131 | ) 132 | ), 133 | D.response( 134 | D.href('/base/collection/'), 135 | D.propstat( 136 | D.prop( 137 | D.getcontentlength("0"), 138 | D.creationdate("1983-12-24T06:00:00Z"), 139 | D.getlastmodified("Wed, 24 Dec 2014 06:00:00 +0000"), 140 | D.resourcetype(D.collection()), 141 | D.displayname("collection"), 142 | ), 143 | D.status("HTTP/1.1 200 OK") 144 | ) 145 | ), 146 | ), pretty_print=True, xml_declaration=True, encoding='utf-8') 147 | ) 148 | 149 | def test_propfind_exact_names(self): 150 | self.sub_object.get_descendants.return_value += [self.sub_object] 151 | request = Mock(META={}) 152 | path = 'collection/sub_object' 153 | v = DavView(base_url='/base/', path=path, request=request, acl_class=FullAcl, xml_pretty_print=True) 154 | v.__dict__['resource'] = self.sub_object 155 | resp = v.propfind(request, path, 156 | etree.XPathDocumentEvaluator(ElementTree( 157 | D.propfind( 158 | D.prop( 159 | D.displayname(), 160 | D.resourcetype(), 161 | ) 162 | ) 163 | ), namespaces=WEBDAV_NSMAP) 164 | ) 165 | self.assertEqual(resp.status_code, 207) 166 | self.assertEqual(resp.content, 167 | etree.tostring(D.multistatus( 168 | D.response( 169 | D.href('/base/collection/sub_object'), 170 | D.propstat( 171 | D.prop( 172 | D.displayname("sub_object"), 173 | D.resourcetype(), 174 | ), 175 | D.status("HTTP/1.1 200 OK") 176 | ) 177 | ), 178 | ), pretty_print=True, xml_declaration=True, encoding='utf-8') 179 | ) 180 | 181 | def test_propfind_allprop(self): 182 | self.sub_object.get_descendants.return_value += [self.sub_object] 183 | request = Mock(META={}) 184 | path = 'collection/sub_object' 185 | v = DavView(base_url='/base/', path=path, request=request, acl_class=FullAcl, xml_pretty_print=True) 186 | v.__dict__['resource'] = self.sub_object 187 | resp = v.propfind(request, path, 188 | etree.XPathDocumentEvaluator(ElementTree( 189 | D.propfind( 190 | D.allprop() 191 | ) 192 | ), namespaces=WEBDAV_NSMAP) 193 | ) 194 | self.assertEqual(resp.status_code, 207) 195 | self.assertEqual(resp.content, 196 | etree.tostring(D.multistatus( 197 | D.response( 198 | D.href('/base/collection/sub_object'), 199 | D.propstat( 200 | D.prop( 201 | D.getcontentlength("42"), 202 | D.creationdate("1983-12-24T06:00:00Z"), 203 | D.getlastmodified("Wed, 24 Dec 2014 06:00:00 +0000"), 204 | D.resourcetype(), 205 | D.displayname("sub_object"), 206 | ), 207 | D.status("HTTP/1.1 200 OK") 208 | ) 209 | ), 210 | ), pretty_print=True, xml_declaration=True, encoding='utf-8') 211 | ) 212 | 213 | 214 | def test_propfind_all_names(self): 215 | self.sub_object.get_descendants.return_value += [self.sub_object] 216 | request = Mock(META={}) 217 | path = 'collection/sub_object' 218 | v = DavView(base_url='/base/', path=path, request=request, acl_class=FullAcl, xml_pretty_print=True) 219 | v.__dict__['resource'] = self.sub_object 220 | resp = v.propfind(request, path, 221 | etree.XPathDocumentEvaluator(ElementTree( 222 | D.propfind( 223 | D.propname() 224 | ) 225 | ), namespaces=WEBDAV_NSMAP) 226 | ) 227 | self.assertEqual(resp.status_code, 207) 228 | self.assertEqual(resp.content, 229 | etree.tostring(D.multistatus( 230 | D.response( 231 | D.href('/base/collection/sub_object'), 232 | D.propstat( 233 | D.prop( 234 | D.getcontentlength(), 235 | D.creationdate(), 236 | D.getlastmodified(), 237 | D.resourcetype(), 238 | D.displayname(), 239 | ), 240 | D.status("HTTP/1.1 200 OK") 241 | ) 242 | ), 243 | ), pretty_print=True, xml_declaration=True, encoding='utf-8') 244 | ) 245 | 246 | def test_dispatch(self): 247 | request = Mock( 248 | spec=HttpRequest, 249 | META={ 250 | 'PATH_INFO': '/base/path/', 251 | 'CONTENT_TYPE': 'text/xml', 252 | 'CONTENT_LENGTH': '44' 253 | }, 254 | method='GET', 255 | read=Mock(side_effect=[u"\n", u"", u""]) 256 | ) 257 | v = DavView(request=request, get=Mock(return_value=HttpResponse()), _allowed_methods=Mock(return_value=['GET'])) 258 | v.dispatch(request, '/path/') 259 | self.assertIsNotNone(v.xbody) 260 | self.assertEqual(v.base_url, '/base') 261 | self.assertEqual(v.path, '/path/') 262 | 263 | def test_allowed_object(self): 264 | v = DavView() 265 | v.__dict__['resource'] = self.sub_object 266 | self.assertListEqual(v._allowed_methods(), ['HEAD', 'OPTIONS', 'PROPFIND', 'LOCK', 'UNLOCK', 'GET', 'DELETE', 'PROPPATCH', 'COPY', 'MOVE', 'PUT', 'MKCOL']) 267 | 268 | def test_allowed_collection(self): 269 | v = DavView() 270 | v.__dict__['resource'] = self.top_collection 271 | self.assertListEqual(v._allowed_methods(), ['HEAD', 'OPTIONS', 'PROPFIND', 'LOCK', 'UNLOCK', 'GET', 'DELETE', 'PROPPATCH', 'COPY', 'MOVE', 'PUT', 'MKCOL']) 272 | 273 | def test_allowed_missing_collection(self): 274 | v = DavView() 275 | parent = MockCollection('/path/to/obj') 276 | v.__dict__['resource'] = MissingMockCollection('/path/', get_parent=Mock(return_value=parent)) 277 | self.assertListEqual(v._allowed_methods(), ['HEAD', 'OPTIONS', 'PROPFIND', 'LOCK', 'UNLOCK', 'GET', 'DELETE', 'PROPPATCH', 'COPY', 'MOVE', 'PUT', 'MKCOL']) 278 | 279 | def test_allowed_missing_parent(self): 280 | v = DavView() 281 | parent = MissingMockCollection('/path/to/obj') 282 | v.__dict__['resource'] = MissingMockCollection('/path/', get_parent=Mock(return_value=parent)) 283 | self.assertListEqual(v._allowed_methods(), ['HEAD', 'OPTIONS', 'PROPFIND', 'LOCK', 'UNLOCK', 'GET', 'DELETE', 'PROPPATCH', 'COPY', 'MOVE', 'PUT', 'MKCOL']) 284 | 285 | def test_options_root(self): 286 | path = '/' 287 | v = DavView(path=path, acl_class=FullAcl) 288 | v.__dict__['resource'] = MockObject(path) 289 | resp = v.options(None, path) 290 | self.assertEqual(sorted(resp.items()), [ 291 | ('Content-Length', '0'), 292 | ('Content-Type', 'text/xml; charset="utf-8"'), 293 | ('DAV', '1,2'), 294 | ]) 295 | 296 | def test_options_obj(self): 297 | path = '/obj' 298 | v = DavView(path=path, _allowed_methods=Mock(return_value=['ALL']), acl_class=FullAcl) 299 | v.__dict__['resource'] = MockObject(path) 300 | resp = v.options(None, path) 301 | self.assertEqual(sorted(resp.items()), [ 302 | ('Allow', 'ALL'), 303 | ('Allow-Ranges', 'bytes'), 304 | ('Content-Length', '0'), 305 | ('Content-Type', 'text/xml; charset="utf-8"'), 306 | ('DAV', '1,2'), 307 | ]) 308 | 309 | def test_options_collection(self): 310 | path = '/collection/' 311 | v = DavView(path=path, _allowed_methods=Mock(return_value=['ALL']), acl_class=FullAcl) 312 | v.__dict__['resource'] = MockCollection(path) 313 | resp = v.options(None, path) 314 | self.assertEqual(sorted(resp.items()), [ 315 | ('Allow', 'ALL'), 316 | ('Content-Length', '0'), 317 | ('Content-Type', 'text/xml; charset="utf-8"'), 318 | ('DAV', '1,2'), 319 | ]) 320 | 321 | def test_get_obj(self): 322 | path = '/obj.txt' 323 | v = DavView(path=path, _allowed_methods=Mock(return_value=['ALL']), acl_class=FullAcl) 324 | v.__dict__['resource'] = MockObject(path, read=Mock(return_value="C" * 42)) 325 | resp = v.get(None, path, acl_class=FullAcl) 326 | self.assertEqual(resp['Etag'], "0" * 40) 327 | self.assertEqual(resp['Content-Type'], "text/plain") 328 | self.assertEqual(resp['Last-Modified'], "Wed, 24 Dec 2014 06:00:00 +0000") 329 | if sys.version_info < (3, 0, 0): #py2 330 | self.assertEqual(resp.content, "C" * 42) 331 | else: 332 | self.assertEqual(resp.content.decode('utf-8'), "C" * 42) 333 | 334 | @patch('djangodav.views.render', Mock(return_value=HttpResponse('listing'))) 335 | def test_head_object(self): 336 | path = '/object.txt' 337 | v = DavView(path=path, base_url='/base', _allowed_methods=Mock(return_value=['ALL']), acl_class=FullAcl) 338 | v.__dict__['resource'] = MockObject(path) 339 | resp = v.head(None, path) 340 | self.assertEqual("text/plain", resp['Content-Type']) 341 | self.assertEqual("Wed, 24 Dec 2014 06:00:00 +0000", resp['Last-Modified']) 342 | if sys.version_info < (3, 0, 0): #py2 343 | self.assertEqual("", resp.content) 344 | else: 345 | self.assertEqual("", resp.content.decode('utf-8')) 346 | self.assertEqual("0", resp['Content-Length']) 347 | 348 | @patch('djangodav.views.views.render', Mock(return_value=HttpResponse('listing'))) 349 | def test_get_collection(self): 350 | path = '/collection/' 351 | v = DavView(path=path, acl_class=FullAcl, base_url='/base', _allowed_methods=Mock(return_value=['ALL'])) 352 | v.__dict__['resource'] = MockCollection(path) 353 | resp = v.get(None, path) 354 | if sys.version_info < (3, 0, 0): #py2 355 | self.assertEqual("listing", resp.content) 356 | else: 357 | self.assertEqual("listing", resp.content.decode('utf-8')) 358 | self.assertEqual("Wed, 24 Dec 2014 06:00:00 +0000", resp['Last-Modified']) 359 | 360 | def test_head_collection(self): 361 | path = '/collection/' 362 | v = DavView(path=path, acl_class=FullAcl, base_url='/base', _allowed_methods=Mock(return_value=['ALL'])) 363 | v.__dict__['resource'] = MockCollection(path) 364 | resp = v.head(None, path) 365 | if sys.version_info < (3, 0, 0): #py2 366 | self.assertEqual("", resp.content) 367 | else: 368 | self.assertEqual("", resp.content.decode('utf-8')) 369 | self.assertEqual("Wed, 24 Dec 2014 06:00:00 +0000", resp['Last-Modified']) 370 | self.assertEqual("0", resp['Content-Length']) 371 | 372 | def test_put_new(self): 373 | path = '/collection/missing_sub_object' 374 | v = DavView(path=path, acl_class=FullAcl, resource_class=Mock()) 375 | v.__dict__['resource'] = self.missing_sub_object 376 | self.missing_sub_object.write = Mock() 377 | request = HttpRequest() 378 | resp = v.put(request, path) 379 | self.missing_sub_object.write.assert_called_with(request) 380 | self.assertEqual(201, resp.status_code) 381 | 382 | def test_put_exists(self): 383 | path = '/collection/missing_sub_object' 384 | v = DavView(path=path, acl_class=FullAcl, resource_class=Mock()) 385 | v.__dict__['resource'] = self.sub_object 386 | self.sub_object.write = Mock() 387 | request = HttpRequest() 388 | resp = v.put(request, path) 389 | self.sub_object.write.assert_called_with(request) 390 | self.assertEqual(204, resp.status_code) 391 | 392 | def test_put_collection(self): 393 | path = '/collection/missing_sub_object' 394 | v = DavView(path=path, acl_class=FullAcl, resource_class=Mock()) 395 | v.__dict__['resource'] = self.sub_collection 396 | self.sub_collection.write = Mock() 397 | request = HttpRequest() 398 | resp = v.put(request, path) 399 | self.assertFalse(self.sub_collection.write.called) 400 | self.assertEqual(405, resp.status_code) 401 | 402 | def test_mkcol_new(self): 403 | path = '/collection/missing_sub_collection' 404 | v = DavView(path=path, acl_class=FullAcl, resource_class=Mock()) 405 | v.__dict__['resource'] = self.missing_sub_collection 406 | self.missing_sub_collection.create_collection = Mock() 407 | request = HttpRequest() 408 | resp = v.mkcol(request, path) 409 | self.missing_sub_collection.create_collection.assert_called_with() 410 | self.assertEqual(201, resp.status_code) 411 | 412 | def test_mkcol_exists(self): 413 | path = '/collection/sub_collection' 414 | v = DavView(path=path, acl_class=FullAcl, resource_class=Mock()) 415 | v.__dict__['resource'] = self.sub_collection 416 | self.sub_collection.create_collection = Mock() 417 | request = HttpRequest() 418 | resp = v.mkcol(request, path) 419 | self.assertFalse(self.sub_collection.create_collection.called) 420 | self.assertEqual(405, resp.status_code) 421 | 422 | def test_mkcol_object(self): 423 | path = '/collection/sub_object' 424 | v = DavView(path=path, acl_class=FullAcl, resource_class=Mock()) 425 | v.__dict__['resource'] = self.sub_object 426 | self.sub_object.create_collection = Mock() 427 | request = HttpRequest() 428 | resp = v.mkcol(request, path) 429 | self.assertFalse(self.sub_object.create_collection.called) 430 | self.assertEqual(405, resp.status_code) 431 | 432 | def test_delete_exists(self): 433 | target = self.sub_object 434 | v = DavView(path=target.get_path(), acl_class=FullAcl, resource_class=Mock(), lock_class=DummyLock) 435 | v.__dict__['resource'] = target 436 | request = HttpRequest() 437 | target.delete = Mock() 438 | resp = v.delete(request, target.get_path()) 439 | self.assertTrue(target.delete.called) 440 | self.assertEqual(204, resp.status_code) 441 | 442 | def test_delete_missing(self): 443 | target = self.missing_sub_object 444 | v = DavView(path=target.get_path(), acl_class=FullAcl, resource_class=Mock(), lock_class=DummyLock) 445 | v.__dict__['resource'] = target 446 | request = HttpRequest() 447 | target.delete = Mock() 448 | self.assertRaises(Http404, v.delete, request, target.get_path()) 449 | self.assertFalse(target.delete.called) 450 | 451 | def test_copy_new(self): 452 | src = self.sub_object 453 | src.copy = Mock(return_value=None) 454 | dst = self.missing_sub_object 455 | request = HttpRequest() 456 | request.META['HTTP_DESTINATION'] = "http://testserver%s" % dst.get_path() 457 | request.META['SERVER_NAME'] = 'testserver' 458 | request.META['SERVER_PORT'] = '80' 459 | request.META['HTTP_DEPTH'] = 'infinity' 460 | v = DavView(base_url='http://testserver', request=request, path=src.get_path(), acl_class=FullAcl, resource_class=Mock(), lock_class=DummyLock) 461 | v.resource_class = Mock(return_value=dst) 462 | v.__dict__['resource'] = src 463 | resp = v.copy(request, src.get_path(), None) 464 | self.assertEqual(201, resp.status_code) 465 | self.assertTrue(src.copy.called) 466 | 467 | def test_copy_overwrite(self): 468 | src = self.sub_object 469 | src.copy = Mock(return_value=None) 470 | dst = self.blank_collection 471 | dst.delete = Mock(return_value=None) 472 | request = HttpRequest() 473 | request.META['HTTP_DESTINATION'] = "http://testserver%s" % dst.get_escaped_path() 474 | request.META['SERVER_NAME'] = 'testserver' 475 | request.META['SERVER_PORT'] = '80' 476 | request.META['HTTP_DEPTH'] = 'infinity' 477 | v = DavView(base_url='http://testserver', request=request, path=src.get_path(), acl_class=FullAcl, resource_class=Mock(), lock_class=DummyLock) 478 | v.resource_class = Mock(return_value=dst) 479 | v.__dict__['resource'] = src 480 | resp = v.copy(request, src.get_path(), None) 481 | self.assertEqual(204, resp.status_code) 482 | self.assertTrue(src.copy.called) 483 | self.assertTrue(dst.delete.called) 484 | 485 | def test_move_new(self): 486 | src = self.sub_object 487 | src.move = Mock(return_value=None) 488 | dst = self.missing_sub_object 489 | dst.delete = Mock(return_value=None) 490 | request = HttpRequest() 491 | request.META['HTTP_DESTINATION'] = "http://testserver%s" % dst.get_escaped_path() 492 | request.META['SERVER_NAME'] = 'testserver' 493 | request.META['SERVER_PORT'] = '80' 494 | v = DavView(base_url='http://testserver', request=request, path=src.get_path(), acl_class=FullAcl, resource_class=Mock(), lock_class=DummyLock) 495 | v.resource_class = Mock(return_value=dst) 496 | v.__dict__['resource'] = src 497 | resp = v.move(request, src.get_path(), None) 498 | self.assertEqual(201, resp.status_code) 499 | self.assertTrue(src.move.called) 500 | self.assertFalse(dst.delete.called) 501 | 502 | def test_move_overwrite(self): 503 | src = self.sub_object 504 | src.move = Mock(return_value=None) 505 | dst = self.blank_collection 506 | dst.delete = Mock(return_value=None) 507 | request = HttpRequest() 508 | request.META['HTTP_DESTINATION'] = "http://testserver%s" % dst.get_escaped_path() 509 | request.META['SERVER_NAME'] = 'testserver' 510 | request.META['SERVER_PORT'] = '80' 511 | v = DavView(base_url='http://testserver', request=request, path=src.get_path(), acl_class=FullAcl, resource_class=Mock(), lock_class=DummyLock) 512 | v.resource_class = Mock(return_value=dst) 513 | v.__dict__['resource'] = src 514 | resp = v.move(request, src.get_path(), None) 515 | self.assertEqual(204, resp.status_code) 516 | self.assertTrue(src.move.called) 517 | self.assertTrue(dst.delete.called) 518 | -------------------------------------------------------------------------------- /djangodav/views/views.py: -------------------------------------------------------------------------------- 1 | import urllib, re 2 | import sys 3 | try: 4 | import urlparse 5 | except ImportError: 6 | from urllib import parse as urlparse 7 | from sys import version_info as python_version 8 | from lxml import etree 9 | 10 | from django.utils.encoding import force_text 11 | from django.utils.timezone import now 12 | from django.http import HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseBadRequest, \ 13 | HttpResponseNotModified, HttpResponseRedirect, Http404 14 | from django.utils.decorators import method_decorator 15 | from django.utils.functional import cached_property 16 | from django.utils.http import parse_etags 17 | from django.shortcuts import render 18 | from django.views.decorators.csrf import csrf_exempt 19 | from django.views.generic import View 20 | 21 | from djangodav.responses import ResponseException, HttpResponsePreconditionFailed, HttpResponseCreated, HttpResponseNoContent, \ 22 | HttpResponseConflict, HttpResponseMediatypeNotSupported, HttpResponseBadGateway, \ 23 | HttpResponseMultiStatus, HttpResponseLocked, HttpResponse 24 | from djangodav.utils import WEBDAV_NSMAP, D, url_join, get_property_tag_list, rfc1123_date 25 | from djangodav import VERSION as djangodav_version 26 | from django import VERSION as django_version, get_version 27 | 28 | PATTERN_IF_DELIMITER = re.compile(r'(<([^>]+)>)|(\(([^\)]+)\))') 29 | 30 | class DavView(View): 31 | resource_class = None 32 | lock_class = None 33 | acl_class = None 34 | template_name = 'djangodav/index.html' 35 | http_method_names = ['options', 'put', 'mkcol', 'head', 'get', 'delete', 'propfind', 'proppatch', 'copy', 'move', 'lock', 'unlock'] 36 | server_header = 'DjangoDav/%s Django/%s Python/%s' % ( 37 | get_version(djangodav_version), 38 | get_version(django_version), 39 | get_version(python_version) 40 | ) 41 | xml_pretty_print = False 42 | xml_encoding = 'utf-8' 43 | 44 | def no_access(self): 45 | return HttpResponseForbidden() 46 | 47 | @method_decorator(csrf_exempt) 48 | def dispatch(self, request, path, *args, **kwargs): 49 | if path: 50 | self.path = path 51 | self.base_url = request.META['PATH_INFO'][:-len(self.path)] 52 | else: 53 | self.path = '/' 54 | self.base_url = request.META['PATH_INFO'] 55 | 56 | meta = request.META.get 57 | self.xbody = kwargs['xbody'] = None 58 | if (request.method.lower() != 'put' 59 | and "/xml" in meta('CONTENT_TYPE', '') 60 | and meta('CONTENT_LENGTH', 0) != '' 61 | and int(meta('CONTENT_LENGTH', 0)) > 0): 62 | self.xbody = kwargs['xbody'] = etree.XPathDocumentEvaluator( 63 | etree.parse(request, etree.XMLParser(ns_clean=True)), 64 | namespaces=WEBDAV_NSMAP 65 | ) 66 | 67 | if request.method.upper() in self._allowed_methods(): 68 | handler = getattr(self, request.method.lower(), self.http_method_not_allowed) 69 | else: 70 | handler = self.http_method_not_allowed 71 | try: 72 | resp = handler(request, self.path, *args, **kwargs) 73 | except ResponseException as e: 74 | resp = e.response 75 | if not 'Allow' in resp: 76 | methods = self._allowed_methods() 77 | if methods: 78 | resp['Allow'] = ", ".join(methods) 79 | if not 'Date' in resp: 80 | resp['Date'] = rfc1123_date(now()) 81 | if self.server_header: 82 | resp['Server'] = self.server_header 83 | return resp 84 | 85 | def options(self, request, path, *args, **kwargs): 86 | if not self.has_access(self.resource, 'read'): 87 | return self.no_access() 88 | response = self.build_xml_response() 89 | response['DAV'] = '1,2' 90 | response['Content-Length'] = '0' 91 | if self.path in ('/', '*'): 92 | return response 93 | response['Allow'] = ", ".join(self._allowed_methods()) 94 | if self.resource.exists and self.resource.is_object: 95 | response['Allow-Ranges'] = 'bytes' 96 | return response 97 | 98 | def _allowed_methods(self): 99 | allowed = [ 100 | 'HEAD', 'OPTIONS', 'PROPFIND', 'LOCK', 'UNLOCK', 101 | 'GET', 'DELETE', 'PROPPATCH', 'COPY', 'MOVE', 'PUT', 'MKCOL', 102 | ] 103 | 104 | return allowed 105 | 106 | def get_access(self, resource): 107 | """Return permission as DavAcl object. A DavACL should have the following attributes: 108 | read, write, delete, create, relocate, list. By default we implement a read-only 109 | system.""" 110 | return self.acl_class(read=True, full=False) 111 | 112 | def has_access(self, resource, method): 113 | return getattr(self.get_access(resource), method) 114 | 115 | def get_resource_kwargs(self, **kwargs): 116 | return kwargs 117 | 118 | @cached_property 119 | def resource(self): 120 | return self.get_resource(path=self.path) 121 | 122 | def get_resource(self, **kwargs): 123 | return self.resource_class(**self.get_resource_kwargs(**kwargs)) 124 | 125 | def get_depth(self, default='1'): 126 | depth = str(self.request.META.get('HTTP_DEPTH', default)).lower() 127 | if not depth in ('0', '1', 'infinity'): 128 | raise ResponseException(HttpResponseBadRequest('Invalid depth header value %s' % depth)) 129 | if depth == 'infinity': 130 | depth = -1 131 | else: 132 | depth = int(depth) 133 | return depth 134 | 135 | def evaluate_conditions(self, res): 136 | if not res.exists: 137 | return 138 | etag = res.get_etag() 139 | mtime = res.get_mtime_stamp() 140 | cond_if_match = self.request.META.get('HTTP_IF_MATCH', None) 141 | if cond_if_match: 142 | etags = parse_etags(cond_if_match) 143 | if '*' in etags or etag in etags: 144 | raise ResponseException(HttpResponsePreconditionFailed()) 145 | cond_if_modified_since = self.request.META.get('HTTP_IF_MODIFIED_SINCE', False) 146 | if cond_if_modified_since: 147 | # Parse and evaluate, but don't raise anything just yet... 148 | # This might be ignored based on If-None-Match evaluation. 149 | cond_if_modified_since = parse_time(cond_if_modified_since) 150 | if cond_if_modified_since and cond_if_modified_since > mtime: 151 | cond_if_modified_since = True 152 | else: 153 | cond_if_modified_since = False 154 | cond_if_none_match = self.request.META.get('HTTP_IF_NONE_MATCH', None) 155 | if cond_if_none_match: 156 | etags = parse_etags(cond_if_none_match) 157 | if '*' in etags or etag in etags: 158 | if self.request.method in ('GET', 'HEAD'): 159 | raise ResponseException(HttpResponseNotModified()) 160 | raise ResponseException(HttpResponsePreconditionFailed()) 161 | # Ignore If-Modified-Since header... 162 | cond_if_modified_since = False 163 | cond_if_unmodified_since = self.request.META.get('HTTP_IF_UNMODIFIED_SINCE', None) 164 | if cond_if_unmodified_since: 165 | cond_if_unmodified_since = parse_time(cond_if_unmodified_since) 166 | if cond_if_unmodified_since and cond_if_unmodified_since <= mtime: 167 | raise ResponseException(HttpResponsePreconditionFailed()) 168 | if cond_if_modified_since: 169 | # This previously evaluated True and is not being ignored... 170 | raise ResponseException(HttpResponseNotModified()) 171 | # TODO: complete If header handling... 172 | cond_if = self.request.META.get('HTTP_IF', None) 173 | if cond_if: 174 | if not cond_if.startswith('<'): 175 | cond_if = '<*>' + cond_if 176 | #for (tmpurl, url, tmpcontent, content) in PATTERN_IF_DELIMITER.findall(cond_if): 177 | 178 | def get(self, request, path, head=False, *args, **kwargs): 179 | if not self.resource.exists: 180 | raise Http404("Resource doesn't exists") 181 | if not path.endswith("/") and self.resource.is_collection: 182 | return HttpResponseRedirect(request.build_absolute_uri() + "/") 183 | if path.endswith("/") and self.resource.is_object: 184 | return HttpResponseRedirect(request.build_absolute_uri().rstrip("/")) 185 | response = HttpResponse() 186 | if head: 187 | response['Content-Length'] = 0 188 | if not self.has_access(self.resource, 'read'): 189 | return self.no_access() 190 | if self.resource.is_object: 191 | response['Content-Type'] = self.resource.content_type 192 | response['ETag'] = self.resource.getetag 193 | if not head: 194 | response['Content-Length'] = self.resource.getcontentlength 195 | response.content = self.resource.read() 196 | elif not head: 197 | response = render(request, self.template_name, dict(resource=self.resource, base_url=self.base_url)) 198 | response['Last-Modified'] = self.resource.getlastmodified 199 | return response 200 | 201 | def head(self, request, path, *args, **kwargs): 202 | return self.get(request, path, head=True, *args, **kwargs) 203 | 204 | def put(self, request, path, *args, **kwargs): 205 | parent = self.resource.get_parent() 206 | if not parent.exists: 207 | return HttpResponseConflict("Resource doesn't exists") 208 | if self.resource.is_collection: 209 | return HttpResponseNotAllowed(list(set(self._allowed_methods()) - set(['MKCOL', 'PUT']))) 210 | if not self.resource.exists and not self.has_access(parent, 'write'): 211 | return self.no_access() 212 | if self.resource.exists and not self.has_access(self.resource, 'write'): 213 | return self.no_access() 214 | created = not self.resource.exists 215 | self.resource.write(request) 216 | if created: 217 | self.__dict__['resource'] = self.get_resource(path=self.resource.get_path()) 218 | return HttpResponseCreated() 219 | else: 220 | return HttpResponseNoContent() 221 | 222 | def delete(self, request, path, *args, **kwargs): 223 | if not self.resource.exists: 224 | raise Http404("Resource doesn't exists") 225 | if not self.has_access(self.resource, 'delete'): 226 | return self.no_access() 227 | self.lock_class(self.resource).del_locks() 228 | self.resource.delete() 229 | response = HttpResponseNoContent() 230 | self.__dict__['resource'] = self.get_resource(path=self.resource.get_path()) 231 | return response 232 | 233 | def mkcol(self, request, path, *args, **kwargs): 234 | if self.resource.exists: 235 | return HttpResponseNotAllowed(list(set(self._allowed_methods()) - set(['MKCOL', 'PUT']))) 236 | if not self.resource.get_parent().exists: 237 | return HttpResponseConflict() 238 | length = request.META.get('CONTENT_LENGTH', 0) 239 | if length and int(length) != 0: 240 | return HttpResponseMediatypeNotSupported() 241 | if not self.has_access(self.resource, 'write'): 242 | return self.no_access() 243 | self.resource.create_collection() 244 | self.__dict__['resource'] = self.get_resource(path=self.resource.get_path()) 245 | return HttpResponseCreated() 246 | 247 | def relocate(self, request, path, method, *args, **kwargs): 248 | if not self.resource.exists: 249 | raise Http404("Resource doesn't exists") 250 | if not self.has_access(self.resource, 'read'): 251 | return self.no_access() 252 | # dst = urlparse.unquote(request.META.get('HTTP_DESTINATION', '')).decode(self.xml_encoding) 253 | if sys.version_info < (3, 0, 0): #py2 254 | # in Python 2, urlparse requires bytestrings 255 | dst = urlparse.unquote(request.META.get('HTTP_DESTINATION', '')).decode(self.xml_encoding) 256 | else: 257 | # in Python 3, urlparse understands string 258 | dst = urlparse.unquote(request.META.get('HTTP_DESTINATION', '')) 259 | if not dst: 260 | return HttpResponseBadRequest('Destination header missing.') 261 | dparts = urlparse.urlparse(dst) 262 | sparts = urlparse.urlparse(request.build_absolute_uri()) 263 | if sparts.scheme != dparts.scheme or sparts.netloc != dparts.netloc: 264 | return HttpResponseBadGateway('Source and destination must have the same scheme and host.') 265 | # adjust path for our base url: 266 | dst = self.get_resource(path=dparts.path[len(self.base_url):]) 267 | if not dst.get_parent().exists: 268 | return HttpResponseConflict() 269 | if not self.has_access(self.resource, 'write'): 270 | return self.no_access() 271 | overwrite = request.META.get('HTTP_OVERWRITE', 'T') 272 | if overwrite not in ('T', 'F'): 273 | return HttpResponseBadRequest('Overwrite header must be T or F.') 274 | overwrite = (overwrite == 'T') 275 | if not overwrite and dst.exists: 276 | return HttpResponsePreconditionFailed('Destination exists and overwrite False.') 277 | dst_exists = dst.exists 278 | if dst_exists: 279 | self.lock_class(self.resource).del_locks() 280 | self.lock_class(dst).del_locks() 281 | dst.delete() 282 | errors = getattr(self.resource, method)(dst, *args, **kwargs) 283 | if errors: 284 | return self.build_xml_response(response_class=HttpResponseMultiStatus) # WAT? 285 | if dst_exists: 286 | return HttpResponseNoContent() 287 | return HttpResponseCreated() 288 | 289 | def copy(self, request, path, xbody): 290 | depth = self.get_depth() 291 | if depth != -1: 292 | return HttpResponseBadRequest() 293 | return self.relocate(request, path, 'copy', depth=depth) 294 | 295 | def move(self, request, path, xbody): 296 | if not self.has_access(self.resource, 'delete'): 297 | return self.no_access() 298 | return self.relocate(request, path, 'move') 299 | 300 | def lock(self, request, path, xbody=None, *args, **kwargs): 301 | # TODO Lock refreshing 302 | if not self.has_access(self.resource, 'write'): 303 | return self.no_access() 304 | 305 | if not xbody: 306 | return HttpResponseBadRequest('Lockinfo required') 307 | 308 | try: 309 | depth = int(request.META.get('HTTP_DEPTH', '0')) 310 | except ValueError: 311 | return HttpResponseBadRequest('Wrong depth') 312 | 313 | try: 314 | timeout = int(request.META.get('HTTP_LOCK_TIMEOUT', 'Seconds-600')[len('Seconds-'):]) 315 | except ValueError: 316 | return HttpResponseBadRequest('Wrong timeout') 317 | 318 | owner = None 319 | try: 320 | owner_obj = xbody('/D:lockinfo/D:owner')[0] # TODO: WEBDAV_NS 321 | except IndexError: 322 | owner_obj = None 323 | else: 324 | if owner_obj.text: 325 | owner = owner_obj.text 326 | if len(owner_obj): 327 | owner = owner_obj[0].text 328 | 329 | try: 330 | lockscope_obj = xbody('/D:lockinfo/D:lockscope/*')[0] # TODO: WEBDAV_NS 331 | except IndexError: 332 | return HttpResponseBadRequest('Lock scope required') 333 | else: 334 | lockscope = lockscope_obj.xpath('local-name()') 335 | 336 | try: 337 | locktype_obj = xbody('/D:lockinfo/D:locktype/*')[0] # TODO: WEBDAV_NS 338 | except IndexError: 339 | return HttpResponseBadRequest('Lock type required') 340 | else: 341 | locktype = locktype_obj.xpath('local-name()') 342 | 343 | token = self.lock_class(self.resource).acquire(lockscope, locktype, depth, timeout, owner) 344 | if not token: 345 | return HttpResponseLocked('Already locked') 346 | 347 | body = D.activelock(*([ 348 | D.locktype(locktype_obj), 349 | D.lockscope(lockscope_obj), 350 | D.depth(force_text(depth)), 351 | D.timeout("Second-%s" % timeout), 352 | D.locktoken(D.href('opaquelocktoken:%s' % token))] 353 | + ([owner_obj] if owner_obj is not None else []) 354 | )) 355 | 356 | return self.build_xml_response(body) 357 | 358 | def unlock(self, request, path, xbody=None, *args, **kwargss): 359 | if not self.has_access(self.resource, 'write'): 360 | return self.no_access() 361 | 362 | token = request.META.get('HTTP_LOCK_TOKEN') 363 | if not token: 364 | return HttpResponseBadRequest('Lock token required') 365 | if not self.lock_class(self.resource).release(token): 366 | return self.no_access() 367 | return HttpResponseNoContent() 368 | 369 | def propfind(self, request, path, xbody=None, *args, **kwargs): 370 | if not self.has_access(self.resource, 'read'): 371 | return self.no_access() 372 | 373 | if not self.resource.exists: 374 | raise Http404("Resource doesn't exists") 375 | 376 | if not self.get_access(self.resource): 377 | return self.no_access() 378 | 379 | get_all_props, get_prop, get_prop_names = True, False, False 380 | if xbody: 381 | get_prop = [p.xpath('local-name()') for p in xbody('/D:propfind/D:prop/*')] 382 | get_all_props = xbody('/D:propfind/D:allprop') 383 | get_prop_names = xbody('/D:propfind/D:propname') 384 | if int(bool(get_prop)) + int(bool(get_all_props)) + int(bool(get_prop_names)) != 1: 385 | return HttpResponseBadRequest() 386 | 387 | children = self.resource.get_descendants(depth=self.get_depth()) 388 | 389 | if get_prop_names: 390 | responses = [ 391 | D.response( 392 | D.href(url_join(self.base_url, child.get_escaped_path())), 393 | D.propstat( 394 | D.prop(*[ 395 | D(name) for name in child.ALL_PROPS 396 | ]), 397 | D.status('HTTP/1.1 200 OK'), 398 | ), 399 | ) 400 | for child in children 401 | ] 402 | else: 403 | responses = [ 404 | D.response( 405 | D.href(url_join(self.base_url, child.get_escaped_path())), 406 | D.propstat( 407 | D.prop( 408 | *get_property_tag_list(child, *(get_prop if get_prop else child.ALL_PROPS)) 409 | ), 410 | D.status('HTTP/1.1 200 OK'), 411 | ), 412 | ) 413 | for child in children 414 | ] 415 | 416 | body = D.multistatus(*responses) 417 | return self.build_xml_response(body, HttpResponseMultiStatus) 418 | 419 | def proppatch(self, request, path, xbody, *args, **kwargs): 420 | if not self.resource.exists: 421 | raise Http404("Resource doesn't exists") 422 | if not self.has_access(self.resource, 'write'): 423 | return self.no_access() 424 | depth = self.get_depth(default="0") 425 | if depth != 0: 426 | return HttpResponseBadRequest('Invalid depth header value %s' % depth) 427 | props = xbody('/D:propertyupdate/D:set/D:prop/*') 428 | body = D.multistatus( 429 | D.response( 430 | D.href(url_join(self.base_url, self.resource.get_escaped_path())), 431 | *[D.propstat( 432 | D.status('HTTP/1.1 200 OK'), 433 | D.prop(el.tag) 434 | ) for el in props] 435 | ) 436 | ) 437 | return self.build_xml_response(body, HttpResponseMultiStatus) 438 | 439 | def build_xml_response(self, tree=None, response_class=HttpResponse, **kwargs): 440 | if tree is not None: 441 | content = etree.tostring( 442 | tree, 443 | xml_declaration=True, 444 | pretty_print=self.xml_pretty_print, 445 | encoding=self.xml_encoding 446 | ) 447 | else: 448 | content = b'' 449 | return response_class( 450 | content, 451 | content_type='text/xml; charset="%s"' % self.xml_encoding, 452 | **kwargs 453 | ) 454 | -------------------------------------------------------------------------------- /docs/auth.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Authentication 3 | ============== 4 | 5 | Introduction 6 | ------------ 7 | 8 | WebDav like any other http api protocol accepts wide range of authentication methods: Basic, Digest, OAuth, OAuth2, etc. 9 | Most of them are already developed in the api other libraries. So we provide mixins you can use to bring in Django Rest 10 | Framework or Tastipie authentication layer support. 11 | 12 | 13 | Using Django REST framework authentication 14 | ------------------------------------------ 15 | 16 | Inherit your DavView from `RestAuthViewMixIn` and provide REST authentication instances as `authentications` property 17 | tuple. 18 | 19 | ..code: python 20 | 21 | from rest_framework.authentication import SessionAuthentication, BasicAuthentication 22 | from djangodav.views import DavView 23 | 24 | class AuthFsDavView(RestAuthViewMixIn, DavView): 25 | authentications = (BasicAuthentication(), SessionAuthentication()) 26 | 27 | 28 | Using Django Tastypie authentication 29 | ------------------------------------ 30 | 31 | Inherit your DavView from `TastypieAuthViewMixIn` and provide Tastpie authentication instance as `authentication` 32 | property tuple. 33 | 34 | ..code: python 35 | 36 | from djangodav.auth.tasty import TastypieAuthViewMixIn 37 | from tastypie.authentication import BasicAuthentication 38 | 39 | 40 | class RestAuthDavView(TastypieAuthViewMixIn, DavView): 41 | authentication = BasicAuthentication() 42 | 43 | resource_class = TempDirWebDavResource 44 | lock_class = DummyLock 45 | acl_class = FullAcl 46 | 47 | With Tastipie you can also use `MultiAuthentication` to provide several authentication methods. 48 | 49 | ..code: python 50 | 51 | from djangodav.auth.tasty import TastypieAuthViewMixIn 52 | from tastypie.authentication import BasicAuthentication, MultiAuthentication, SessionAuthentication 53 | 54 | 55 | class TastyAuthDavView(TastypieAuthViewMixIn, DavView): 56 | authentication = MultiAuthentication(BasicAuthentication(), SessionAuthentication()) 57 | 58 | resource_class = TempDirWebDavResource 59 | lock_class = DummyLock 60 | acl_class = FullAcl 61 | -------------------------------------------------------------------------------- /docs/classes.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | DjangoDav classes 3 | ================= 4 | 5 | 6 | views.DavView 7 | ------------- 8 | 9 | DavView is responsible for request handling. It routes http methods, and translates xml request body to internal 10 | representation and building xml responses. It uses DavLock class to provide resource locking data management and 11 | DavResource to manage resources. 12 | 13 | 14 | Locks 15 | ----- 16 | 17 | base.lock.BaseDavLock 18 | ~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | Provides access to locks data management. 21 | 22 | lock.DummyLock 23 | ~~~~~~~~~~~~~~ 24 | 25 | Provides lock emulation. 26 | 27 | 28 | Resources 29 | --------- 30 | 31 | Encapsulating storage functionality. Providing public objects management methods and available property list. 32 | 33 | 34 | base.resource.BaseDavResource 35 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | Provides base resource management functionality. Like data conversion and resource copy/move logic. 37 | 38 | 39 | fs.resource.BaseFSDavResource 40 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | 42 | Provides all filesystem operations accept reading and writing files. 43 | 44 | 45 | fs.resource.DummyWriteFSDavResource 46 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 47 | 48 | Provides through memory write to fs. 49 | 50 | 51 | fs.resource.DummyReadFSDavResource 52 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | 54 | Provides through memory read from fs. 55 | 56 | 57 | fs.resource.SendFileFSDavResource 58 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 59 | 60 | Uses SendFile functionality of Apache or Nginx web-server to provide resource reading. 61 | 62 | 63 | fs.resource.RedirectFSDavResource 64 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | 66 | Uses X-Redirect functionality of Nginx web-server to provide resource reading. 67 | 68 | 69 | db.resource.DBBaseResource 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | Provides base functionality to provide access to database resources. 73 | 74 | 75 | db.resource.NameLookupDBDavMixIn 76 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | 78 | Provides access to database resources by object names lookup. 79 | -------------------------------------------------------------------------------- /docs/difference.rst: -------------------------------------------------------------------------------- 1 | --------------------------------------- 2 | Difference with SmartFile django-webdav 3 | --------------------------------------- 4 | 5 | Base resource functionality was separated into BaseResource class from the storage 6 | functionality which developers free to choose from provided or implement themselves. 7 | 8 | Improved class dependencies. Resource class don’t know anything about url or server, its 9 | goal is only to store content and provide proper access. 10 | 11 | Removed properties helper class. View is now responsible for xml generation, and resource 12 | provides actual property list. 13 | 14 | Server is now inherited from Django Class Based View, and renamed to DavView. 15 | 16 | Key methods covered with tests. 17 | 18 | Removed redundant request handler. 19 | 20 | Added FSResource and DBResource to provide file system and data base access. 21 | 22 | Xml library usage is replaced with lxml to achieve proper xml generation code readability. 23 | -------------------------------------------------------------------------------- /docs/howto.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | How to 3 | ====== 4 | 5 | Create simple filesystem webdav resource 6 | ---------------------------------------- 7 | 8 | 1. Create resource.py 9 | ~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | .. code:: python 12 | 13 | from django.conf import settings 14 | from djangodav.base.resources import MetaEtagMixIn 15 | from djangodav.fs.resources import DummyFSDAVResource 16 | 17 | class MyDavResource(MetaEtagMixIn, DummyFSDAVResource): 18 | root = '/path/to/folder' 19 | 20 | 21 | 2. Register WebDav view in urls.py 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | .. code:: python 25 | 26 | from djangodav.acl import FullAcl 27 | from djangodav.lock import DummyLock 28 | from djangodav.views import DavView 29 | 30 | from django.conf.urls import patterns 31 | 32 | from .resource import TempDirWebDavResource 33 | 34 | urlpatterns = patterns('', 35 | (r'^fsdav(?P.*)$', DavView.as_view(resource_class=MyDavResource, lock_class=DummyLock, 36 | acl_class=FullAcl)), 37 | ) 38 | 39 | 40 | Create simple database webdav resource 41 | -------------------------------------- 42 | 43 | 1. Create models.py 44 | ~~~~~~~~~~~~~~~~~~~ 45 | 46 | .. code:: python 47 | 48 | from django.db import models 49 | from django.utils.timezone import now 50 | 51 | class BaseDavModel(models.Model): 52 | name = models.CharField(max_length=255) 53 | created = models.DateTimeField(default=now) 54 | modified = models.DateTimeField(default=now) 55 | 56 | class Meta: 57 | abstract = True 58 | 59 | 60 | class CollectionModel(BaseDavModel): 61 | parent = models.ForeignKey('self', blank=True, null=True) 62 | size = 0 63 | 64 | class Meta: 65 | unique_together = (('parent', 'name'),) 66 | 67 | 68 | class ObjectModel(BaseDavModel): 69 | parent = models.ForeignKey(CollectionModel, blank=True, null=True) 70 | size = models.IntegerField(default=0) 71 | content = models.TextField(default=u"") 72 | md5 = models.CharField(max_length=255) 73 | 74 | class Meta: 75 | unique_together = (('parent', 'name'),) 76 | 77 | 2. Create resource.py 78 | ~~~~~~~~~~~~~~~~~~~~~ 79 | 80 | .. code:: python 81 | 82 | from base64 import b64encode, b64decode 83 | from hashlib import md5 84 | 85 | from django.utils.timezone import now 86 | from djangodav.db.resources import NameLookupDBDavMixIn, BaseDBDavResource 87 | from samples.db.models import CollectionModel, ObjectModel 88 | 89 | class MyDBDavResource(NameLookupDBDavMixIn, BaseDBDavResource): 90 | collection_model = CollectionModel 91 | object_model = ObjectModel 92 | 93 | def write(self, content): 94 | size = len(content) 95 | hashsum = md5(content).hexdigest() 96 | content = b64encode(content) 97 | if not self.exists: 98 | self.object_model.objects.create( 99 | name=self.displayname, 100 | parent=self.get_parent().obj, 101 | md5=hashsum, 102 | size=size, 103 | content=content 104 | ) 105 | return 106 | self.obj.size = size 107 | self.obj.modified = now() 108 | self.obj.content = content 109 | self.md5 = hashsum 110 | self.obj.save(update_fields=['content', 'size', 'modified', 'md5']) 111 | 112 | def read(self): 113 | return b64decode(self.obj.content) 114 | 115 | @property 116 | def getetag(self): 117 | return self.obj.md5 118 | 119 | @property 120 | def getcontentlength(self): 121 | return self.obj.size 122 | 123 | 2. Register DavView in urls.py 124 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | 126 | .. code:: python 127 | 128 | from djangodav.acls import FullAcl 129 | from djangodav.locks import DummyLock 130 | 131 | from djangodav.views import DavView 132 | 133 | from django.conf.urls import patterns 134 | from samples.db.resource import MyDBDavResource 135 | 136 | 137 | urlpatterns = patterns('', 138 | # Mirroring tmp folder 139 | (r'^dbdav(?P.*)$', DavView.as_view(resource_class=MyDBDavResource, lock_class=DummyLock, acl_class=FullAcl)), 140 | ) 141 | -------------------------------------------------------------------------------- /docs/known_issues.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Known issues 3 | ============ 4 | 5 | 6 | Recreating collection on move 7 | ----------------------------- 8 | 9 | To provide easier usage we encapsulated highlevel copy and move methods, which provide graceful conflict resolution 10 | and don't know anything about end data representation, at the same time. If you need to provide native move, you can 11 | override these methods. 12 | 13 | Revert on error 14 | --------------- 15 | 16 | Webdav standart expects that all changes will be reverted if the operation can't be finished. Now it can be provided by 17 | DBResource with transaction based database. FSResource does't support transactions of any kind. 18 | -------------------------------------------------------------------------------- /docs/motivation.rst: -------------------------------------------------------------------------------- 1 | ---------- 2 | Motivation 3 | ---------- 4 | 5 | Django is a very popular tool which provides data representation and management. One of the key goals is to provide 6 | machine access to it. Most popular production ready tools provide json based api access. Which have their own 7 | advantages and disadvantages. 8 | 9 | WebDav today is a standard for cooperative document management. Its clients are built in the modern operation systems 10 | and supported by the world popular services. But it very important to remember that it's not only about file storage, 11 | WebDab provides a set of methods to deal with tree structured objects of any kind. 12 | 13 | Providing WebDav access to Django resources opens new horizons for building Web2.0 apps, with inplace edition and 14 | providing native operation system access to the stored objects. 15 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | import os 23 | import sys 24 | import django 25 | from django.conf import settings 26 | 27 | 28 | DEFAULT_SETTINGS = dict( 29 | INSTALLED_APPS = ( 30 | 'djangodav', 31 | 'django.contrib.auth', 32 | 'django.contrib.contenttypes', 33 | # 'djangodav.tests', 34 | ), 35 | DATABASES = dict( 36 | default = dict( 37 | ENGINE = 'django.db.backends.sqlite3' 38 | ) 39 | ), 40 | # ROOT_URLCONF = 'djangodav.tests.urls', 41 | MIDDLEWARE_CLASSES = (), 42 | TEMPLATES = [ 43 | { 44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 45 | 'APP_DIRS': True, 46 | }, 47 | ] 48 | ) 49 | 50 | 51 | def runtests(*test_args): 52 | if not settings.configured: 53 | settings.configure(**DEFAULT_SETTINGS) 54 | if hasattr(django, 'setup'): 55 | django.setup() 56 | if not test_args: 57 | test_args = ['djangodav'] 58 | 59 | parent = os.path.dirname(os.path.abspath(__file__)) 60 | sys.path.insert(0, parent) 61 | try: 62 | from django.test.runner import DiscoverRunner 63 | runner_class = DiscoverRunner 64 | except ImportError: 65 | from django.test.simple import DjangoTestSuiteRunner 66 | runner_class = DjangoTestSuiteRunner 67 | failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) 68 | sys.exit(failures) 69 | 70 | 71 | if __name__ == '__main__': 72 | runtests() 73 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /samples/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /samples/auth/views/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /samples/auth/views/rest.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from djangodav.views import DavView 22 | 23 | from djangodav.auth.rest import RestAuthViewMixIn 24 | from rest_framework.authentication import SessionAuthentication as RestSessionAuthentication, BasicAuthentication as RestBasicAuthentication 25 | 26 | 27 | class RestAuthDavView(RestAuthViewMixIn, DavView): 28 | authentications = (RestBasicAuthentication(), RestSessionAuthentication()) 29 | -------------------------------------------------------------------------------- /samples/auth/views/tasty.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from djangodav.views import DavView 22 | 23 | from djangodav.auth.tasty import TastypieAuthViewMixIn 24 | from tastypie.authentication import MultiAuthentication, BasicAuthentication as TastyBasicAuthentication, SessionAuthentication as TastySessionAuthentication 25 | 26 | 27 | class TastyAuthDavView(TastypieAuthViewMixIn, DavView): 28 | authentication = MultiAuthentication(TastyBasicAuthentication(), TastySessionAuthentication()) 29 | -------------------------------------------------------------------------------- /samples/db/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /samples/db/models.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | from django.db import models 23 | from django.utils.timezone import now 24 | 25 | 26 | class BaseDavModel(models.Model): 27 | name = models.CharField(max_length=255) 28 | created = models.DateTimeField(default=now) 29 | modified = models.DateTimeField(default=now) 30 | 31 | class Meta: 32 | abstract = True 33 | 34 | 35 | class CollectionModel(BaseDavModel): 36 | parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE) 37 | size = 0 38 | 39 | class Meta: 40 | unique_together = (('parent', 'name'),) 41 | 42 | 43 | class ObjectModel(BaseDavModel): 44 | parent = models.ForeignKey(CollectionModel, blank=True, null=True, on_delete=models.CASCADE) 45 | size = models.IntegerField(default=0) 46 | content = models.TextField(default=u"") 47 | md5 = models.CharField(max_length=255) 48 | 49 | class Meta: 50 | unique_together = (('parent', 'name'),) 51 | -------------------------------------------------------------------------------- /samples/db/resources.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from base64 import b64encode, b64decode 22 | from hashlib import md5 23 | 24 | from django.utils.timezone import now 25 | from djangodav.db.resources import NameLookupDBDavMixIn, BaseDBDavResource 26 | from samples.db.models import CollectionModel, ObjectModel 27 | 28 | 29 | class MyDBDavResource(NameLookupDBDavMixIn, BaseDBDavResource): 30 | collection_model = CollectionModel 31 | object_model = ObjectModel 32 | 33 | def write(self, content): 34 | size = len(content) 35 | hashsum = md5(content).hexdigest() 36 | content = b64encode(content) 37 | if not self.exists: 38 | self.object_model.objects.create( 39 | name=self.displayname, 40 | parent=self.get_parent().obj, 41 | md5=hashsum, 42 | size=size, 43 | content=content 44 | ) 45 | return 46 | self.obj.size = size 47 | self.obj.modified = now() 48 | self.obj.content = content 49 | self.md5 = hashsum 50 | self.obj.save(update_fields=['content', 'size', 'modified', 'md5']) 51 | 52 | def read(self): 53 | return b64decode(self.obj.content) 54 | 55 | @property 56 | def getetag(self): 57 | return self.obj.md5 58 | 59 | @property 60 | def getcontentlength(self): 61 | return self.obj.size 62 | -------------------------------------------------------------------------------- /samples/fs/__init__.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /samples/fs/models.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | from django.db import models 23 | -------------------------------------------------------------------------------- /samples/fs/resources.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from djangodav.base.resources import MetaEtagMixIn 3 | from djangodav.fs.resources import DummyFSDAVResource 4 | 5 | 6 | class TempDirWebDavResource(MetaEtagMixIn, DummyFSDAVResource): 7 | root = settings.WEBDAV_ROOT 8 | -------------------------------------------------------------------------------- /samples/fs/views.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | -------------------------------------------------------------------------------- /samples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /samples/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-tastypie 3 | djangorestframework 4 | lxml 5 | -------------------------------------------------------------------------------- /samples/settings.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | 22 | import os 23 | 24 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__name__))) 25 | 26 | DEBUG = True 27 | DEBUG_PROPAGATE_EXCEPTIONS = DEBUG 28 | 29 | ADMINS = ( 30 | # ('Your Name', 'your_email@domain.com'), 31 | ) 32 | 33 | MANAGERS = ADMINS 34 | 35 | DATABASES = { 36 | 'default': { 37 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 38 | 'NAME': 'sample.sqlite', # Or path to database file if using sqlite3. 39 | 'USER': '', # Not used with sqlite3. 40 | 'PASSWORD': '', # Not used with sqlite3. 41 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 42 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 43 | } 44 | } 45 | 46 | # Local time zone for this installation. Choices can be found here: 47 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 48 | # although not all choices may be available on all operating systems. 49 | # On Unix systems, a value of None will cause Django to use the same 50 | # timezone as the operating system. 51 | # If running in a Windows environment this must be set to the same as your 52 | # system time zone. 53 | TIME_ZONE = 'America/Chicago' 54 | 55 | # Language code for this installation. All choices can be found here: 56 | # http://www.i18nguy.com/unicode/language-identifiers.html 57 | LANGUAGE_CODE = 'en-us' 58 | 59 | SITE_ID = 1 60 | 61 | # If you set this to False, Django will make some optimizations so as not 62 | # to load the internationalization machinery. 63 | USE_I18N = True 64 | 65 | # If you set this to False, Django will not format dates, numbers and 66 | # calendars according to the current locale 67 | USE_L10N = True 68 | 69 | # Absolute filesystem path to the directory that will hold user-uploaded files. 70 | # Example: "/home/media/media.lawrence.com/" 71 | MEDIA_ROOT = '' 72 | 73 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 74 | # trailing slash if there is a path component (optional in other cases). 75 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 76 | MEDIA_URL = '' 77 | 78 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 79 | # trailing slash. 80 | # Examples: "http://foo.com/media/", "/media/". 81 | ADMIN_MEDIA_PREFIX = '/media/' 82 | 83 | # Make this unique, and don't share it with anybody. 84 | SECRET_KEY = '7*2a!%^qk_t05mopm0u@u9_)p-hcb7fy(1usg4^yi9%epjz%$y' 85 | 86 | MIDDLEWARE_CLASSES = ( 87 | 'django.middleware.common.CommonMiddleware', 88 | 'django.contrib.sessions.middleware.SessionMiddleware', 89 | 'django.middleware.csrf.CsrfViewMiddleware', 90 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 91 | 'django.contrib.messages.middleware.MessageMiddleware', 92 | ) 93 | 94 | ROOT_URLCONF = 'samples.urls' 95 | 96 | TEMPLATES = [ 97 | { 98 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 99 | 'DIRS': [ 100 | os.path.join(BASE_DIR, 'djangodav/templates'), 101 | ], 102 | 'APP_DIRS': True, 103 | 'OPTIONS': { 104 | 'context_processors': [ 105 | 'django.template.context_processors.debug', 106 | 'django.template.context_processors.request', 107 | 'django.contrib.auth.context_processors.auth', 108 | 'django.contrib.messages.context_processors.messages', 109 | ], 110 | }, 111 | }, 112 | ] 113 | 114 | INSTALLED_APPS = ( 115 | 'django.contrib.auth', 116 | 'django.contrib.contenttypes', 117 | 'django.contrib.sessions', 118 | 'django.contrib.sites', 119 | 'django.contrib.messages', 120 | 'djangodav', 121 | 'samples.db', 122 | # Uncomment the next line to enable the admin: 123 | # 'django.contrib.admin', 124 | # Uncomment the next line to enable admin documentation: 125 | # 'django.contrib.admindocs', 126 | ) 127 | 128 | LOGGING = { 129 | 'version': 1, 130 | 'disable_existing_loggers': False, 131 | 'filters': { 132 | 'require_debug_false': { 133 | '()': 'django.utils.log.RequireDebugFalse' 134 | } 135 | }, 136 | 'handlers': { 137 | 'mail_admins': { 138 | 'level': 'ERROR', 139 | 'filters': ['require_debug_false'], 140 | 'class': 'django.utils.log.AdminEmailHandler' 141 | } 142 | }, 143 | 'loggers': { 144 | 'django.request': { 145 | 'handlers': ['mail_admins'], 146 | 'level': 'ERROR', 147 | 'propagate': True, 148 | }, 149 | } 150 | } 151 | 152 | from tempfile import gettempdir 153 | WEBDAV_ROOT = gettempdir() 154 | -------------------------------------------------------------------------------- /samples/urls.py: -------------------------------------------------------------------------------- 1 | # Portions (c) 2014, Alexander Klimenko 2 | # All rights reserved. 3 | # 4 | # Copyright (c) 2011, SmartFile 5 | # All rights reserved. 6 | # 7 | # This file is part of DjangoDav. 8 | # 9 | # DjangoDav is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as published 11 | # by the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # DjangoDav is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with DjangoDav. If not, see . 21 | from djangodav.acls import FullAcl 22 | from djangodav.locks import DummyLock 23 | 24 | from djangodav.views import DavView 25 | 26 | from django.conf.urls import url 27 | 28 | from samples.fs.resources import TempDirWebDavResource 29 | from samples.db.resources import MyDBDavResource 30 | from samples.auth.views.rest import RestAuthDavView 31 | from samples.auth.views.tasty import TastyAuthDavView 32 | 33 | 34 | urlpatterns = [ 35 | # Mirroring tmp folder 36 | url(r'^fs(?P.*)$', DavView.as_view(resource_class=TempDirWebDavResource, lock_class=DummyLock, acl_class=FullAcl)), 37 | # Db file keeper 38 | url(r'^db(?P.*)$', DavView.as_view(resource_class=MyDBDavResource, lock_class=DummyLock, acl_class=FullAcl)), 39 | 40 | # REST framework auth 41 | url(r'^auth/rest(?P.*)$', RestAuthDavView.as_view(resource_class=TempDirWebDavResource, lock_class=DummyLock, acl_class=FullAcl)), 42 | # Tastypie auth 43 | url(r'^auth/tasty(?P.*)$', TastyAuthDavView.as_view(resource_class=TempDirWebDavResource, lock_class=DummyLock, acl_class=FullAcl)), 44 | ] 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Portions (c) 2014, Alexander Klimenko 4 | # All rights reserved. 5 | # 6 | # Copyright (c) 2011, SmartFile 7 | # All rights reserved. 8 | # 9 | # This file is part of DjangoDav. 10 | # 11 | # DjangoDav is free software: you can redistribute it and/or modify 12 | # it under the terms of the GNU Affero General Public License as published 13 | # by the Free Software Foundation, either version 3 of the License, or 14 | # (at your option) any later version. 15 | # 16 | # DjangoDav is distributed in the hope that it will be useful, 17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | # GNU Affero General Public License for more details. 20 | # 21 | # You should have received a copy of the GNU Affero General Public License 22 | # along with DjangoDav. If not, see . 23 | 24 | from setuptools import setup, find_packages 25 | 26 | 27 | setup( 28 | name='DjangoDav', 29 | version=__import__('djangodav').__version__, 30 | description=('A WebDAV server for Django.'), 31 | long_description = open('README.rst').read(), 32 | author='Alexander Klimenko', 33 | author_email='alex@erix.ru', 34 | url='https://github.com/meteozond/djangodav', 35 | packages=find_packages(), 36 | classifiers=[ 37 | 'Development Status :: 4 - Beta', 38 | 'Environment :: Web Environment', 39 | 'Framework :: Django', 40 | 'License :: OSI Approved :: GNU Affero General Public License v3', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | "Programming Language :: Python :: 2.7", 44 | "Programming Language :: Python :: 2.6", 45 | "Programming Language :: Python :: 3.4", 46 | 'Topic :: Software Development :: Libraries :: Python Modules' 47 | ], 48 | install_requires=["lxml", "Django>=1.3.0"], 49 | tests_require=["Django>=1.3.0", "mock==1.0.1"], 50 | include_package_data=True, 51 | zip_safe=False, 52 | test_suite='runtests.runtests' 53 | ) 54 | --------------------------------------------------------------------------------