Merged changes from wsgidav-dev
This commit is contained in:
parent
95f139c291
commit
a0e805913e
53 changed files with 3994 additions and 2608 deletions
10
.hgignore
Normal file
10
.hgignore
Normal file
|
@ -0,0 +1,10 @@
|
|||
glob:*.pyc
|
||||
glob:*.confc
|
||||
glob:build
|
||||
glob:WsgiDAV.egg-info/
|
||||
glob:docs/api
|
||||
glob:*.shelve
|
||||
glob:wsgidav.conf
|
||||
glob:dist
|
||||
glob:*.orig
|
||||
glob:.settings
|
17
.project
Normal file
17
.project
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>wsgidav_dev</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.python.pydev.PyDevBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.python.pydev.pythonNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
10
.pydevproject
Normal file
10
.pydevproject
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?eclipse-pydev version="1.0"?>
|
||||
|
||||
<pydev_project>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.4</pydev_property>
|
||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||
<path>/wsgidav_dev</path>
|
||||
</pydev_pathproperty>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
</pydev_project>
|
|
@ -9,6 +9,6 @@ ADDONS GUIDE
|
|||
|
||||
This section describes the addons available:
|
||||
|
||||
+ windowsdomaincontroller_
|
||||
+ nt_domain_controller_
|
||||
|
||||
|
9
CHANGES
Normal file
9
CHANGES
Normal file
|
@ -0,0 +1,9 @@
|
|||
=======
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
0.4.0 alpha
|
||||
===========
|
||||
|
||||
See http://code.google.com/p/wsgidav/wiki/ChangeLog04
|
||||
|
|
@ -3,10 +3,10 @@ WsgiDAV Developers Guide
|
|||
========================
|
||||
|
||||
:Authors: - Ho Chun Wei, fuzzybr80(at)gmail.com (original PyFileServer)
|
||||
- Martin Wendt
|
||||
- Martin Wendt (WsgiDAV)
|
||||
:Project: WsgiDAV, http://wsgidav.googlecode.com/
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
:Abstract: This document gives a brief introduction to the WsgiDAV application package.
|
||||
:Abstract: This document gives a brief introduction to the WsgiDAV application package (targeted to developers).
|
||||
|
||||
|
||||
.. contents:: Table Of Contents
|
||||
|
@ -36,15 +36,17 @@ WSGI application stack::
|
|||
|
|
||||
error_printer.ErrorPrinter (middleware)
|
||||
|
|
||||
httpauthentication.HTTPAuthenticator (middleware)
|
||||
http_authenticator.HTTPAuthenticator (middleware)
|
||||
| \- Uses a domain controller object
|
||||
|
|
||||
dir_browser.WsgiDavDirBrowser (middleware, optional)
|
||||
|
|
||||
requestresolver.RequestResolver (middleware)
|
||||
request_resolver.RequestResolver (middleware)
|
||||
|
|
||||
*-> requestserver.RequestServer (application)
|
||||
*-> request_server.RequestServer (application)
|
||||
\- Uses a DAVProvider object
|
||||
\- Uses a lock manager object
|
||||
and a property manager object
|
||||
|
||||
See the following sections for details.
|
||||
|
||||
|
@ -59,6 +61,7 @@ Main module, that handles the HTTP requests. This object is passed to the WSGI
|
|||
server and represents our WsgiDAV application to the outside.
|
||||
|
||||
On init:
|
||||
|
||||
Use the configuration dictionary to initialize lock manager, property manager,
|
||||
domain controller.
|
||||
|
||||
|
@ -69,11 +72,9 @@ On init:
|
|||
|
||||
For every request:
|
||||
|
||||
Note: The OPTIONS method for the '*' path is handled directly.
|
||||
|
||||
Find the registered DAV provider for the current request.
|
||||
|
||||
Add or modify info in the WSGI ``eniron``:
|
||||
Add or modify info in the WSGI ``environ``:
|
||||
|
||||
environ["SCRIPT_NAME"]
|
||||
Mount-point of the current share.
|
||||
|
@ -89,6 +90,7 @@ For every request:
|
|||
|
||||
Log the HTTP request, then pass the request to the first middleware.
|
||||
|
||||
Note: The OPTIONS method for the '*' path is handled directly.
|
||||
|
||||
|
||||
Middleware ``debug_filter.WsgiDavDebugFilter``
|
||||
|
@ -96,11 +98,13 @@ Middleware ``debug_filter.WsgiDavDebugFilter``
|
|||
Optional middleware for debugging.
|
||||
|
||||
On init:
|
||||
|
||||
Define HTTP methods and litmus tests, that should turn on the verbose mode
|
||||
(currently hard coded).
|
||||
|
||||
For every request:
|
||||
Increase value of ``eniron['verbose']``, if the request should be debugged.
|
||||
|
||||
Increase value of ``environ['verbose']``, if the request should be debugged.
|
||||
Also dump request and response headers and body.
|
||||
|
||||
Then pass the request to the next middleware.
|
||||
|
@ -119,7 +123,7 @@ For every request:
|
|||
Internal exceptions are converted to HTTP_INTERNAL_ERRORs.
|
||||
|
||||
|
||||
Middleware ``httpauthentication.HTTPAuthenticator``
|
||||
Middleware ``http_authenticator.HTTPAuthenticator``
|
||||
---------------------------------------------------
|
||||
Uses a domain controller to establish HTTP authentication.
|
||||
|
||||
|
@ -141,22 +145,26 @@ Middleware ``dir_browser.WsgiDavDirBrowser``
|
|||
Handles GET requests on collections to display a HTML directory listing.
|
||||
|
||||
On init:
|
||||
|
||||
-
|
||||
|
||||
For every request:
|
||||
|
||||
If path maps to a collection:
|
||||
Render collection members as directory (HTML table).
|
||||
|
||||
|
||||
Middleware ``requestresolver.RequestResolver``
|
||||
----------------------------------------------
|
||||
Middleware ``request_resolver.RequestResolver``
|
||||
-----------------------------------------------
|
||||
Find the mapped DAV-Provider, create a new RequestServer instance, and dispatch
|
||||
the request.
|
||||
|
||||
On init:
|
||||
|
||||
Store URL-to-DAV-Provider mapping.
|
||||
|
||||
For every request:
|
||||
|
||||
Setup ``environ["SCRIPT_NAME"]`` to request realm and and
|
||||
``environ["PATH_INFO"]`` to resource path.
|
||||
|
||||
|
@ -166,21 +174,22 @@ For every request:
|
|||
Note: The OPTIONS method for '*' is handled directly.
|
||||
|
||||
|
||||
Application ``requestserver.RequestServer``
|
||||
-------------------------------------------
|
||||
Application ``request_server.RequestServer``
|
||||
--------------------------------------------
|
||||
Handles one single WebDAV request.
|
||||
|
||||
On init:
|
||||
|
||||
Store a reference to the DAV-Provider object.
|
||||
|
||||
For every request:
|
||||
|
||||
Handle one single WebDAV method (PROPFIND, PROPPATCH, LOCK, ...) using a
|
||||
DAV-Provider instance. Then return the response body or raise an DAVError.
|
||||
|
||||
Note: this object only handles one single request.
|
||||
|
||||
|
||||
|
||||
API documentation
|
||||
=================
|
||||
Follow this link to browse the `API documentation`_.
|
||||
|
@ -197,23 +206,23 @@ All DAV providers must implement a common interface. This is usually done by
|
|||
deriving from the abstract base class ``dav_provider.DAVProvider``.
|
||||
|
||||
WsgiDAV comes with a DAV provider for file systems, called
|
||||
``fs_dav_provider.FilesystemProvider``. That is why WsgiDAV a WebDAV file
|
||||
``fs_dav_provider.FilesystemProvider``. That is why WsgiDAV is a WebDAV file
|
||||
server out-of-the-box.
|
||||
|
||||
There are also a few other modules that may serve as examples on how to plug-in
|
||||
your own custom DAV providers:
|
||||
|
||||
ReadOnlyFilesystemProvider
|
||||
fs_dav_provider.ReadOnlyFilesystemProvider
|
||||
Similar to ``FilesystemProvider``, but raise HTTP_FORBIDDEN on write access
|
||||
attempts.
|
||||
|
||||
DummyDAVProvider
|
||||
addons.dummy_dav_provider.DummyDAVProvider
|
||||
TODO
|
||||
|
||||
VirtualResourceProvider
|
||||
addons.virtual_dav_provider.VirtualResourceProvider
|
||||
TODO
|
||||
|
||||
SimpleMySQLResourceAbstractionLayer
|
||||
addons.mysql_dav_provider.MySQLBrowserProvider
|
||||
TODO
|
||||
|
||||
|
||||
|
@ -222,10 +231,16 @@ Property Managers
|
|||
DAV providers may use a property manager to support persistence for *dead
|
||||
properties*.
|
||||
|
||||
WsgiDAV comes with a default implementation based in shelve, called
|
||||
``property_manager.PropertyManager``.
|
||||
WsgiDAV comes with two default implementations, one based on a in-memory
|
||||
dictionary, and a persistent one based in shelve::
|
||||
|
||||
However, this may be replaced by a custom version, as long as the required
|
||||
property_manager.PropertyManager
|
||||
property_manager.ShelvePropertyManager
|
||||
|
||||
``PropertyManager`` is used by default, but ``ShelvePropertyManager`` can be
|
||||
enabled by uncommenting two lines in the configuration file.
|
||||
|
||||
In addition, this may be replaced by a custom version, as long as the required
|
||||
interface is implemented.
|
||||
|
||||
|
||||
|
@ -234,10 +249,16 @@ Lock Managers
|
|||
DAV providers may use a lock manager to support exclusive and shared write
|
||||
locking.
|
||||
|
||||
WsgiDAV comes with a default implementation based in shelve, called
|
||||
``lock_manager.LockManager``.
|
||||
WsgiDAV comes with two default implementations, one based on a in-memory
|
||||
dictionary, and a persistent one based in shelve::
|
||||
|
||||
However, this may be replaced by a custom version, as long as the required
|
||||
lock_manager.LockManager
|
||||
lock_manager.ShelveLockManager
|
||||
|
||||
``LockManager`` is used by default, but ``ShelveLockManager`` can be
|
||||
enabled by uncommenting two lines in the configuration file.
|
||||
|
||||
In addition, this may be replaced by a custom version, as long as the required
|
||||
interface is implemented.
|
||||
|
||||
|
||||
|
@ -252,7 +273,7 @@ the config file.
|
|||
However, this may be replaced by a custom version, as long as the required
|
||||
interface is implemented.
|
||||
|
||||
``wsgidav.addons.windowsdomaincontroller`` is an example for such an extension.
|
||||
``wsgidav.addons.nt_domain_controller`` is an example for such an extension.
|
||||
|
||||
|
||||
Other objects
|
||||
|
@ -341,7 +362,7 @@ You will find this terms / naming conventions in the source:
|
|||
URL (after the mount path was popped).
|
||||
The share path is the common URL prefix of this URL.
|
||||
|
||||
TODO: do we need to distinguish between server mount points ('mount path') and
|
||||
TODO: do we need to ditinguish between server mount points ('mount path') and
|
||||
WsgiDAV mount points ('share path')?
|
||||
|
||||
Constructed like
|
||||
|
@ -401,7 +422,7 @@ You will find this terms / naming conventions in the source:
|
|||
|
||||
|
||||
*reference URL*:
|
||||
Quoted, ISO-8859-1 encoded byte string.
|
||||
Quoted, UTF-8 encoded byte string.
|
||||
|
||||
This is basically the same as an URL, that was build from the *preferred path*.
|
||||
But this deals with 'virtual locations' as well.
|
||||
|
@ -430,7 +451,7 @@ You will find this terms / naming conventions in the source:
|
|||
|
||||
|
||||
*href*:
|
||||
**Quoted**, ISO-8859-1 encoded byte string.
|
||||
**Quoted**, UTF-8 encoded byte string.
|
||||
|
||||
Used in XML responses. We are using the path-absolute option. i.e. starting
|
||||
with '/'. (See http://www.webdav.org/specs/rfc4918.html#rfc.section.8.3)
|
||||
|
@ -439,4 +460,13 @@ You will find this terms / naming conventions in the source:
|
|||
href = quote(mountPath + preferredPath)
|
||||
Example:
|
||||
"/dav/public/my%20nice%20doc.txt"
|
||||
|
||||
|
||||
*filePath*:
|
||||
Unicode
|
||||
|
||||
Used by fs_dav_provider when serving files from the file system.
|
||||
(At least on Vista) os.path.exists(filePath) returns False, if a file name contains
|
||||
special characters, even if it is correctly UTF-8 encoded.
|
||||
So we convert to unicode.
|
||||
|
53
README.txt
53
README.txt
|
@ -2,24 +2,29 @@
|
|||
README
|
||||
======
|
||||
|
||||
:Module: pyfileserver
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com
|
||||
:Project: PyFileServer, http://pyfilesync.berlios.de/
|
||||
:Authors: - Ho Chun Wei, fuzzybr80(at)gmail.com (original PyFileServer)
|
||||
- Martin Wendt
|
||||
:Project: WsgiDAV, http://wsgidav.googlecode.com/
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
:Abstract: This document gives a brief introduction to the WsgiDAV application package.
|
||||
|
||||
|
||||
What is PyFileServer?
|
||||
=====================
|
||||
PyFileServer is a WSGI web application for sharing filesystem directories
|
||||
.. contents:: Table Of Contents
|
||||
|
||||
|
||||
|
||||
What is WsgiDAV?
|
||||
================
|
||||
WsgiDAV is a WSGI web application for sharing filesystem directories
|
||||
over WebDAV.
|
||||
|
||||
For a more detailed discussion of the package, go to the project page
|
||||
<http://pyfilesync.berlios.de/pyfileserver.html>
|
||||
<http://wsgidav.googlecode.com/>
|
||||
|
||||
|
||||
|
||||
Installing PyFileServer
|
||||
=======================
|
||||
Installing WsgiDAV
|
||||
==================
|
||||
|
||||
1. Get and install the latest release of Python, available from
|
||||
|
||||
|
@ -28,9 +33,9 @@ Installing PyFileServer
|
|||
Python 2.3 or later is required; Python 2.4.1 or later is
|
||||
recommended.
|
||||
|
||||
2. Use the latest PyFileServer release. Get the code from:
|
||||
2. Use the latest WsgiDAV release. Get the code from:
|
||||
|
||||
http://pyfilesync.berlios.de/pyfileserver.html
|
||||
http://wsgidav.googlecode.com/
|
||||
|
||||
|
||||
3. Unpack the archive in a temporary directory (**not** directly in
|
||||
|
@ -38,17 +43,17 @@ Installing PyFileServer
|
|||
|
||||
python setup.py install
|
||||
|
||||
PyFileServer requires the PyXML library <http://pyxml.sourceforge.net/>
|
||||
WsgiDAV requires the PyXML library <http://pyxml.sourceforge.net/>
|
||||
to run, and the installation process will install it if it is
|
||||
not present on the system.
|
||||
|
||||
|
||||
|
||||
Configuring PyFileServer
|
||||
========================
|
||||
Configuring WsgiDAV
|
||||
===================
|
||||
|
||||
PyFileServer reads its configuration from a user-specified configuration file.
|
||||
An example of this file is given in the package as 'PyFileServer-example.conf'.
|
||||
WsgiDAV reads its configuration from a user-specified configuration file.
|
||||
An example of this file is given in the package as 'WsgiDAV-example.conf'.
|
||||
|
||||
You should make a copy of this file to use as your configuration file. The file
|
||||
is self-documented and you can modify any settings as required.
|
||||
|
@ -56,10 +61,10 @@ is self-documented and you can modify any settings as required.
|
|||
Refer to the TUTORIAL documentation for an example.
|
||||
|
||||
|
||||
Running PyFileServer
|
||||
====================
|
||||
Running WsgiDAV
|
||||
===============
|
||||
|
||||
PyFileServer comes bundled with a simple wsgi webserver.
|
||||
WsgiDAV comes bundled with a simple wsgi webserver.
|
||||
|
||||
Running as standalone server
|
||||
----------------------------
|
||||
|
@ -69,8 +74,8 @@ To run as a standalone server using the bundled ext_wsgiutils_server.py::
|
|||
usage: python ext_wsgiutils_server.py [options] [config-file]
|
||||
|
||||
config-file:
|
||||
The configuration file for PyFileServer. if omitted, the application
|
||||
will look for a file named 'PyFileServer.conf' in the current directory
|
||||
The configuration file for WsgiDAV. if omitted, the application
|
||||
will look for a file named 'WsgiDAV.conf' in the current directory
|
||||
|
||||
options:
|
||||
--port=PORT Port to serve on (default: 8080)
|
||||
|
@ -86,10 +91,10 @@ Running using other web servers
|
|||
To run it with other WSGI web servers, you can::
|
||||
|
||||
from pyfileserver.mainappwrapper import PyFileApp
|
||||
publish_app = PyFileApp('PyFileServer.conf')
|
||||
publish_app = PyFileApp('WsgiDAV.conf')
|
||||
# construct the application with configuration file
|
||||
# if configuration file is omitted, the application
|
||||
# will look for a file named 'PyFileServer.conf'
|
||||
# will look for a file named 'WsgiDAV.conf'
|
||||
# in the current directory
|
||||
|
||||
where ``publish_app`` is the WSGI application to be run, it will be called with
|
||||
|
@ -109,7 +114,7 @@ Help and Documentation
|
|||
For further help or documentation, please refer to the project web page or
|
||||
send a query to the mailing list.
|
||||
|
||||
Project Page: PyFileServer <http://pyfilesync.berlios.de/pyfileserver.html>
|
||||
Project Page: WsgiDAV <http://wsgidav.googlecode.com/>
|
||||
|
||||
Mailing List: pyfilesync-users@lists.berlios.de (subscribe
|
||||
<http://lists.berlios.de/mailman/listinfo/pyfilesync-users>)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
class IDAVProvider(object):
|
||||
"""
|
||||
TODO: not sure, if we really need interfaces if we have an abstract base class.
|
||||
|
||||
For now, see wsgidav.DAVProvider.
|
||||
"""
|
|
@ -1,64 +0,0 @@
|
|||
class IDomainController(object):
|
||||
"""
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
+-------------------------------------------------------------------------------+
|
||||
This class is an interface for a PropertyManager. Implementations for the
|
||||
property manager in WsgiDAV include::
|
||||
|
||||
wsgidav.domain_controller.WsgiDAVDomainController
|
||||
wsgidav.addons.windowsdomaincontroller.SimpleWindowsDomainController
|
||||
|
||||
All methods must be implemented.
|
||||
|
||||
The environ variable here is the WSGI 'environ' dictionary. It is passed to
|
||||
all methods of the domain controller as a means for developers to pass information
|
||||
from previous middleware or server config (if required).
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Domain Controllers
|
||||
------------------
|
||||
|
||||
The HTTP basic and digest authentication schemes are based on the following
|
||||
concept:
|
||||
|
||||
Each requested relative URI can be resolved to a realm for authentication,
|
||||
for example:
|
||||
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
|
||||
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
|
||||
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
|
||||
and each realm would have a set of username and password pairs that would
|
||||
allow access to the resource.
|
||||
|
||||
A domain controller provides this information to the HTTPAuthenticator.
|
||||
"""
|
||||
|
||||
def getDomainRealm(self, inputURL, environ):
|
||||
"""
|
||||
resolves a relative url to the appropriate realm name
|
||||
"""
|
||||
|
||||
def requireAuthentication(self, realmname, environ):
|
||||
"""
|
||||
returns True if this realm requires authentication
|
||||
or False if it is available for general access
|
||||
"""
|
||||
|
||||
def isRealmUser(self, realmname, username, environ):
|
||||
"""
|
||||
returns True if this username is valid for the realm, False otherwise
|
||||
"""
|
||||
|
||||
def getRealmUserPassword(self, realmname, username, environ):
|
||||
"""
|
||||
returns the password for the given username for the realm.
|
||||
Used for digest authentication.
|
||||
"""
|
||||
|
||||
def authDomainUser(self, realmname, username, password, environ):
|
||||
"""
|
||||
returns True if this username/password pair is valid for the realm,
|
||||
False otherwise. Used for basic authentication.
|
||||
"""
|
|
@ -1,97 +0,0 @@
|
|||
|
||||
class LockManagerInterface(object):
|
||||
"""
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
| See wsgidav.lock_manager instead |
|
||||
+-------------------------------------------------------------------------------+
|
||||
This class is an interface for a LockManager. Implementations for the lock manager
|
||||
in WsgiDAV include::
|
||||
|
||||
wsgidav.lock_manager.LockManager
|
||||
|
||||
All methods must be implemented.
|
||||
|
||||
The url variable in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
"""
|
||||
|
||||
def generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockheadurl, timeout):
|
||||
"""
|
||||
returns a new locktoken for the following lock:
|
||||
username - username of user performing the lock
|
||||
locktype - only the locktype "write" is defined in the webdav specification
|
||||
lockscope - "shared" or "exclusive"
|
||||
lockdepth - depth of lock. "0" or "infinity"
|
||||
lockowner - a arbitrary field provided by the client at lock time
|
||||
lockheadurl - the url the lock is being performed on
|
||||
timeout - -1 for infinite, positive value for number of seconds.
|
||||
Could be None, fall back to a default.
|
||||
"""
|
||||
|
||||
def deleteLock(self, locktoken):
|
||||
"""
|
||||
deletes a lock specified by locktoken
|
||||
"""
|
||||
|
||||
def isTokenLockedByUser(self, locktoken, username):
|
||||
"""
|
||||
returns True if locktoken corresponds to a lock locked by username
|
||||
"""
|
||||
|
||||
def isUrlLocked(self, url):
|
||||
"""
|
||||
returns True if the resource at url is locked
|
||||
"""
|
||||
|
||||
def getUrlLockScope(self, url):
|
||||
"""
|
||||
returns the lockscope of all locks on url. 'shared' or 'exclusive'
|
||||
"""
|
||||
|
||||
def getLockProperty(self, locktoken, lockproperty):
|
||||
"""
|
||||
returns the value for the following properties for the lock specified by
|
||||
locktoken:
|
||||
'LOCKUSER', 'LOCKTYPE', 'LOCKSCOPE', 'LOCKDEPTH', 'LOCKOWNER', 'LOCKHEADURL'
|
||||
and
|
||||
'LOCKTIME' - number of seconds left on the lock.
|
||||
"""
|
||||
|
||||
def isUrlLockedByToken(self, url, locktoken):
|
||||
"""
|
||||
returns True if the resource at url is locked by lock specified by locktoken
|
||||
"""
|
||||
|
||||
def getTokenListForUrl(self, url):
|
||||
"""
|
||||
returns a list of locktokens corresponding to locks on url.
|
||||
"""
|
||||
|
||||
def getTokenListForUrlByUser(self, url, username):
|
||||
"""
|
||||
returns a list of locktokens corresponding to locks on url by user username.
|
||||
"""
|
||||
|
||||
def addUrlToLock(self, url, locktoken):
|
||||
"""
|
||||
adds url to be locked by lock specified by locktoken.
|
||||
|
||||
more than one url can be locked by a lock - depth infinity locks.
|
||||
"""
|
||||
|
||||
def removeAllLocksFromUrl(self, url):
|
||||
"""
|
||||
removes all locks from a url.
|
||||
|
||||
This usually happens when the resource specified by url is being deleted.
|
||||
"""
|
||||
|
||||
def refreshLock(self, locktoken, timeout):
|
||||
"""
|
||||
refreshes the lock specified by locktoken.
|
||||
|
||||
timeout : -1 for infinite, positive value for number of seconds.
|
||||
Could be None, fall back to a default.
|
||||
"""
|
|
@ -1,102 +0,0 @@
|
|||
|
||||
class PropertyManagerInterface(object):
|
||||
|
||||
"""
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
| See wsgidav.property_manager instead |
|
||||
+-------------------------------------------------------------------------------+
|
||||
This class is an interface for a PropertyManager. Implementations for the
|
||||
property manager in WsgiDAV include::
|
||||
|
||||
wsgidav.property_manager.PropertyManager
|
||||
|
||||
All methods must be implemented.
|
||||
|
||||
The url variables in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Properties and WsgiDAV
|
||||
---------------------------
|
||||
Properties of a resource refers to the attributes of the resource. A property
|
||||
is referenced by the property name and the property namespace. We usually
|
||||
refer to the property as ``{property namespace}property name``
|
||||
|
||||
Properties of resources as defined in webdav falls under three categories:
|
||||
|
||||
Live properties
|
||||
These properties are attributes actively maintained by the server, such as
|
||||
file size, or read permissions. if you are sharing a database record as a
|
||||
resource, for example, the attributes of the record could become the live
|
||||
properties of the resource.
|
||||
|
||||
The webdav specification defines the following properties that could be
|
||||
live properties (refer to webdav specification for details):
|
||||
{DAV:}creationdate
|
||||
{DAV:}displayname
|
||||
{DAV:}getcontentlanguage
|
||||
{DAV:}getcontentlength
|
||||
{DAV:}getcontenttype
|
||||
{DAV:}getetag
|
||||
{DAV:}getlastmodified
|
||||
{DAV:}resourcetype
|
||||
{DAV:}source
|
||||
|
||||
These properties are implemented by the abstraction layer.
|
||||
|
||||
Locking properties
|
||||
They refer to the two webdav-defined properties
|
||||
{DAV:}supportedlock and {DAV:}lockdiscovery
|
||||
|
||||
These properties are implemented by the locking library in
|
||||
``wsgidav.lock_manager`` and dead properties library in
|
||||
``wsgidav.property_manager``
|
||||
|
||||
Dead properties
|
||||
They refer to arbitrarily assigned properties not actively maintained.
|
||||
|
||||
These properties are implemented by the dead properties library in
|
||||
``wsgidav.property_manager``
|
||||
|
||||
"""
|
||||
|
||||
def getProperties(self, normurl):
|
||||
"""
|
||||
return a list of properties for url specified by normurl
|
||||
|
||||
return list is a list of tuples (a, b) where a is the property namespace
|
||||
and b the property name
|
||||
"""
|
||||
|
||||
def getProperty(self, normurl, propname, propns):
|
||||
"""
|
||||
return the value of the property for url specified by normurl where
|
||||
propertyname is propname and property namespace is propns
|
||||
"""
|
||||
|
||||
def writeProperty(self, normurl, propname, propns, propertyvalue):
|
||||
"""
|
||||
write propertyvalue as value of the property for url specified by
|
||||
normurl where propertyname is propname and property namespace is propns
|
||||
"""
|
||||
|
||||
def removeProperty(self, normurl, propname, propns):
|
||||
"""
|
||||
delete the property for url specified by normurl where
|
||||
propertyname is propname and property namespace is propns
|
||||
"""
|
||||
|
||||
def removeProperties(self, normurl):
|
||||
"""
|
||||
delete all properties from url specified by normurl
|
||||
"""
|
||||
|
||||
def copyProperties(self, origurl, desturl):
|
||||
"""
|
||||
copy all properties from url specified by origurl to url specified by desturl
|
||||
"""
|
||||
|
14
setup.cfg
Normal file
14
setup.cfg
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Set default to 'daily build'
|
||||
[egg_info]
|
||||
tag_build = .dev
|
||||
tag_date = 1
|
||||
|
||||
# Set sdist format to tar.gz
|
||||
# NOTE: tar doesn't seem to run on windows
|
||||
[sdist]
|
||||
#formats = gztar
|
||||
|
||||
# Define 'release' alias to strip '.dev-DATE'
|
||||
[aliases]
|
||||
release = egg_info -RDb ''
|
||||
|
34
setup.py
34
setup.py
|
@ -1,5 +1,6 @@
|
|||
# If true, then the svn revision won't be used to calculate the
|
||||
# revision (set to True for real releases)
|
||||
import os
|
||||
RELEASE = False
|
||||
|
||||
from wsgidav.version import __version__
|
||||
|
@ -8,22 +9,16 @@ from ez_setup import use_setuptools
|
|||
use_setuptools()
|
||||
|
||||
|
||||
|
||||
#manual check for dependencies: PyXML - somehow installer unable to find on PyPI.
|
||||
#import sys
|
||||
#try:
|
||||
# from lxml import etree #@UnusedImport
|
||||
#except ImportError:
|
||||
# print "Failed to detect lxml. lxml is required for WsgiDAV. Please install"
|
||||
# print "lxml <http://codespeak.net/lxml/> before installing WsgiDAV"
|
||||
# sys.exit(-1)
|
||||
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
# 'setup.py upload' fails on Vista, because .pypirc is searched on 'HOME' path
|
||||
if not "HOME" in os.environ and "HOMEPATH" in os.environ:
|
||||
os.environ.setdefault("HOME", os.environ.get("HOMEPATH", ""))
|
||||
print "Initializing HOME environment variable to '%s'" % os.environ["HOME"]
|
||||
|
||||
setup(name="WsgiDAV",
|
||||
version = __version__,
|
||||
author = "Ho Chun Wei, Martin Wendt",
|
||||
author = "Martin Wendt, Ho Chun Wei",
|
||||
author_email = "wsgidav@wwwendt.de",
|
||||
maintainer = "Martin Wendt",
|
||||
maintainer_email = "wsgidav@wwwendt.de",
|
||||
|
@ -33,7 +28,7 @@ setup(name="WsgiDAV",
|
|||
WsgiDAV is a WebDAV server for sharing files and other resources over the web.
|
||||
It is based on the WSGI interface <http://www.python.org/peps/pep-0333.html>.
|
||||
|
||||
It comes bundled with a simple WSIG web server.
|
||||
It comes bundled with a simple WSGI web server.
|
||||
|
||||
*This package is based on PyFileServer by Ho Chun Wei.*
|
||||
|
||||
|
@ -45,7 +40,7 @@ Project home: http://wsgidav.googlecode.com/
|
|||
#Development Status :: 4 - Beta
|
||||
#Development Status :: 5 - Production/Stable
|
||||
|
||||
classifiers = ["Development Status :: 3 - Alpha",
|
||||
classifiers = ["Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Information Technology",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
|
@ -60,15 +55,18 @@ Project home: http://wsgidav.googlecode.com/
|
|||
"Topic :: Internet :: WWW/HTTP :: WSGI :: Server",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
keywords = 'web wsgi webdav application server',
|
||||
# platforms=['Unix', 'Windows'],
|
||||
keywords = "web wsgi webdav application server",
|
||||
# platforms=["Unix", "Windows"],
|
||||
license = "LGPL",
|
||||
install_requires = ["lxml"],
|
||||
# install_requires = ["lxml"],
|
||||
packages = find_packages(exclude=[]),
|
||||
# package_data={'': ['*.txt', '*.html', '*.conf']},
|
||||
py_modules = ["ez_setup", ],
|
||||
|
||||
# package_data={"": ["*.txt", "*.html", "*.conf"]},
|
||||
# include_package_data = True, # TODO: PP
|
||||
zip_safe = False,
|
||||
extras_require = {},
|
||||
test_suite = "tests.test_all.run",
|
||||
entry_points = {
|
||||
"console_scripts" : ["wsgidav = wsgidav.server.run_server:run"],
|
||||
},
|
||||
|
|
14
tests/__init__.py
Normal file
14
tests/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
#__all__ = ['mainappwrapper',
|
||||
# 'request_server',
|
||||
# 'processrequesterrorhandler',
|
||||
## 'httpdatehelper',
|
||||
# 'util',
|
||||
# 'request_resolver',
|
||||
# 'http_authenticator',
|
||||
# 'domain_controller',
|
||||
# 'loadconfig_primitive',
|
||||
# 'property_manager',
|
||||
# 'lock_manager',
|
||||
# 'fileabstractionlayer',
|
||||
# 'websupportfuncs',
|
||||
# 'wsgiapp']
|
45
tests/cadavertestscript.txt
Normal file
45
tests/cadavertestscript.txt
Normal file
|
@ -0,0 +1,45 @@
|
|||
open http://localhost/test
|
||||
tester
|
||||
tester
|
||||
|
||||
ls
|
||||
rmcol cadaverTEST
|
||||
mkcol cadaverTEST
|
||||
cd cadaverTEST
|
||||
|
||||
mput *.txt
|
||||
mkcol container
|
||||
move *.txt container
|
||||
|
||||
mkcol MOVETEST
|
||||
mkcol COPYTEST
|
||||
|
||||
lock MOVETEST
|
||||
discover MOVETEST
|
||||
|
||||
lock COPYTEST
|
||||
discover COPYTEST
|
||||
|
||||
move container MOVETEST
|
||||
ls MOVETEST
|
||||
discover MOVETEST/container/LICENSE.txt
|
||||
|
||||
copy MOVETEST/container COPYTEST
|
||||
ls COPYTEST
|
||||
discover COPYTEST/container/LICENSE.txt
|
||||
|
||||
rmcol COPYTEST
|
||||
ls
|
||||
|
||||
unlock MOVETEST
|
||||
discover MOVETEST
|
||||
discover MOVETEST/container/LICENSE.txt
|
||||
|
||||
propnames MOVETEST/container/LICENSE.txt
|
||||
propget MOVETEST/container/LICENSE.txt
|
||||
propset MOVETEST/container/LICENSE.txt deadproperty testvalue
|
||||
propget MOVETEST/container/LICENSE.txt
|
||||
|
||||
rmcol MOVETEST
|
||||
|
||||
quit
|
4
tests/conftest.py
Normal file
4
tests/conftest.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
import pkg_resources
|
||||
pkg_resources.require('wsgidav')
|
24
tests/test_all.py
Normal file
24
tests/test_all.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
Unit tests for the WsgiDAV package.
|
||||
"""
|
||||
|
||||
from tests import test_lock_manager, test_property_manager, test_wsgidav_app,\
|
||||
test_util
|
||||
from unittest import TestSuite, TextTestRunner
|
||||
import sys
|
||||
|
||||
|
||||
def run():
|
||||
suite = TestSuite([test_util.suite(),
|
||||
test_lock_manager.suite(),
|
||||
test_property_manager.suite(),
|
||||
test_wsgidav_app.suite(),
|
||||
])
|
||||
failures = TextTestRunner(descriptions=0, verbosity=2).run(suite)
|
||||
sys.exit(failures)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
251
tests/test_lock_manager.py
Normal file
251
tests/test_lock_manager.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
|
||||
"""Unit test for lock_manager.py"""
|
||||
from tempfile import gettempdir
|
||||
from wsgidav.dav_error import DAVError
|
||||
import os
|
||||
from time import sleep
|
||||
from unittest import TestCase, TestSuite, TextTestRunner
|
||||
from wsgidav import lock_manager
|
||||
|
||||
#===============================================================================
|
||||
# BasicTest
|
||||
#===============================================================================
|
||||
class BasicTest(TestCase):
|
||||
"""Test lock_manager.LockManager()."""
|
||||
principal = "Joe Tester"
|
||||
owner = "joe.tester@example.com"
|
||||
root = "/dav/res"
|
||||
timeout = 10 * 60 # Default lock timeout 10 minutes
|
||||
|
||||
@classmethod
|
||||
def suite(cls):
|
||||
"""Return test case suite (so we can control the order)."""
|
||||
suite = TestSuite()
|
||||
suite.addTest(cls("testPreconditions"))
|
||||
suite.addTest(cls("testOpen"))
|
||||
suite.addTest(cls("testValidation"))
|
||||
suite.addTest(cls("testLock"))
|
||||
suite.addTest(cls("testTimeout"))
|
||||
suite.addTest(cls("testConflict"))
|
||||
return suite
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.lm = lock_manager.LockManager()
|
||||
self.lm._verbose = 1
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
self.lm._close()
|
||||
self.lm = None
|
||||
|
||||
|
||||
def _isLockDict(self, o):
|
||||
try:
|
||||
_ = o["root"]
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _isLockResultOK(self, resultTupleList):
|
||||
"""Return True, if result is [ (lockDict, None) ]."""
|
||||
try:
|
||||
return (len(resultTupleList) == 1
|
||||
and len(resultTupleList) == 2
|
||||
and self._isLockDict(resultTupleList[0][0])
|
||||
and resultTupleList[0][1] is None)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def _isLockResultFault(self, resultTupleList, status=None):
|
||||
"""Return True, if it is a valid result tuple containing a DAVError."""
|
||||
try:
|
||||
if len(resultTupleList) < 1:
|
||||
return False
|
||||
resultTuple = resultTupleList[0]
|
||||
if len(resultTuple) != 2 or not self._isLockDict(resultTuple[0]) or not isinstance(resultTuple[1], DAVError):
|
||||
return False
|
||||
elif status and status!=DAVError.value:
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def testPreconditions(self):
|
||||
"""Environment must be set."""
|
||||
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
|
||||
|
||||
|
||||
def testOpen(self):
|
||||
"""Lock manager should be lazy opening on first access."""
|
||||
lm = self.lm
|
||||
assert not lm._loaded, "LM must only be opened after first access"
|
||||
lm._generateLock(self.principal, "write", "exclusive", "infinity",
|
||||
self.owner,
|
||||
"/dav",
|
||||
10)
|
||||
assert lm._loaded, "LM must be opened after first access"
|
||||
|
||||
|
||||
def testValidation(self):
|
||||
"""Lock manager should raise errors on bad args."""
|
||||
lm = self.lm
|
||||
self.assertRaises(AssertionError,
|
||||
lm._generateLock, lm, "writeX", "exclusive", "infinity",
|
||||
self.owner, self.root, self.timeout)
|
||||
self.assertRaises(AssertionError,
|
||||
lm._generateLock, lm, "write", "exclusiveX", "infinity",
|
||||
self.owner, self.root, self.timeout)
|
||||
self.assertRaises(AssertionError,
|
||||
lm._generateLock, lm, "write", "exclusive", "infinityX",
|
||||
self.owner, self.root, self.timeout)
|
||||
self.assertRaises(AssertionError,
|
||||
lm._generateLock, lm, "write", "exclusive", "infinity",
|
||||
None, self.root, self.timeout)
|
||||
self.assertRaises(AssertionError,
|
||||
lm._generateLock, lm, "write", "exclusive", "infinity",
|
||||
self.owner, None, self.timeout)
|
||||
|
||||
assert lm._dict is None, "No locks should have been created by this test"
|
||||
|
||||
|
||||
def testLock(self):
|
||||
"""Lock manager should create and find locks."""
|
||||
lm = self.lm
|
||||
url = "/dav/res"
|
||||
# Create a new lock
|
||||
lockDict = lm._generateLock(self.principal, "write", "exclusive", "infinity",
|
||||
self.owner, url, self.timeout)
|
||||
# Check returned dictionary
|
||||
assert lockDict is not None
|
||||
assert lockDict["root"] == url
|
||||
assert lockDict["type"] == "write"
|
||||
assert lockDict["scope"] == "exclusive"
|
||||
assert lockDict["depth"] == "infinity"
|
||||
assert lockDict["owner"] == self.owner
|
||||
assert lockDict["principal"] == self.principal
|
||||
|
||||
# Test lookup
|
||||
tok = lockDict.get("token")
|
||||
assert lm.getLock(tok, "root") == url
|
||||
|
||||
lockDict = lm.getLock(tok)
|
||||
|
||||
assert lockDict is not None
|
||||
assert lockDict["root"] == url
|
||||
assert lockDict["type"] == "write"
|
||||
assert lockDict["scope"] == "exclusive"
|
||||
assert lockDict["depth"] == "infinity"
|
||||
assert lockDict["owner"] == self.owner
|
||||
assert lockDict["principal"] == self.principal
|
||||
|
||||
# We locked "/dav/res", did we?
|
||||
assert lm.isTokenLockedByUser(tok, self.principal)
|
||||
|
||||
res = lm.getUrlLockList(url, self.principal)
|
||||
assert len(res) == 1
|
||||
|
||||
res = lm.getUrlLockList(url, "another user")
|
||||
assert len(res) == 0
|
||||
|
||||
assert lm.isUrlLockedByToken("/dav/res", tok), "url not directly locked by locktoken."
|
||||
assert lm.isUrlLockedByToken("/dav/res/", tok), "url not directly locked by locktoken."
|
||||
assert lm.isUrlLockedByToken("/dav/res/sub", tok), "child url not indirectly locked"
|
||||
|
||||
assert not lm.isUrlLockedByToken("/dav/ressub", tok), "non-child url reported as locked"
|
||||
assert not lm.isUrlLockedByToken("/dav", tok), "parent url reported as locked"
|
||||
assert not lm.isUrlLockedByToken("/dav/", tok), "parent url reported as locked"
|
||||
|
||||
|
||||
def testTimeout(self):
|
||||
"""Locks should be purged after expiration date."""
|
||||
lm = self.lm
|
||||
timeout = 1
|
||||
lockDict = lm._generateLock(self.principal, "write", "exclusive", "infinity",
|
||||
self.owner, self.root, timeout)
|
||||
|
||||
assert lockDict is not None
|
||||
tok = lockDict.get("token")
|
||||
assert lm.getLock(tok, "root") == self.root
|
||||
|
||||
sleep(timeout - 0.5)
|
||||
lockDict = lm.getLock(tok)
|
||||
assert lockDict is not None, "Lock expired too early"
|
||||
|
||||
sleep(1)
|
||||
lockDict = lm.getLock(tok)
|
||||
assert lockDict is None, "Lock has not expired"
|
||||
|
||||
|
||||
def testConflict(self):
|
||||
"""Locks should prevent conflicts."""
|
||||
lm = self.lm
|
||||
tokenList = []
|
||||
# Create a lock for '/dav/res/'
|
||||
res = lm.acquire("/dav/res/", "write", "exclusive", "infinity",
|
||||
self.owner, self.timeout, self.principal, tokenList)
|
||||
assert self._isLockDict(res[0][0]) and res[0][1] is None, "Could not acquire lock"
|
||||
|
||||
# Try to lock with a slightly different URL (without trailing '/')
|
||||
res = lm.acquire("/dav/res", "write", "exclusive", "infinity",
|
||||
self.owner, self.timeout, "another principal", tokenList)
|
||||
assert self._isLockResultFault(res), "Could acquire a conflicting lock"
|
||||
|
||||
# Try to lock with another principal
|
||||
res = lm.acquire("/dav/res/", "write", "exclusive", "infinity",
|
||||
self.owner, self.timeout, "another principal", tokenList)
|
||||
assert self._isLockResultFault(res), "Could acquire a conflicting lock"
|
||||
|
||||
# Try to lock child with another principal
|
||||
res = lm.acquire("/dav/res/sub", "write", "exclusive", "infinity",
|
||||
self.owner, self.timeout, "another principal", tokenList)
|
||||
assert self._isLockResultFault(res), "Could acquire a conflicting child lock"
|
||||
|
||||
# Try to lock parent with same principal
|
||||
res = lm.acquire("/dav/", "write", "exclusive", "infinity",
|
||||
self.owner, self.timeout, self.principal, tokenList)
|
||||
assert self._isLockResultFault(res), "Could acquire a conflicting parent lock"
|
||||
|
||||
# Try to lock child with same principal
|
||||
res = lm.acquire("/dav/res/sub", "write", "exclusive", "infinity",
|
||||
self.owner, self.timeout, self.principal, tokenList)
|
||||
assert self._isLockResultFault(res), "Could acquire a conflicting child lock (same principal)"
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# ShelveTest
|
||||
#===============================================================================
|
||||
class ShelveTest(BasicTest):
|
||||
"""Test lock_manager.ShelveLockManager()."""
|
||||
|
||||
def setUp(self):
|
||||
self.path = os.path.join(gettempdir(), "wsgidav-locks.shelve")
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
self.lm = lock_manager.ShelveLockManager(self.path)
|
||||
self.lm._verbose = 1
|
||||
|
||||
def tearDown(self):
|
||||
self.lm._close()
|
||||
self.lm = None
|
||||
# os.remove(self.path)
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# suite
|
||||
#===============================================================================
|
||||
def suite():
|
||||
"""Return suites of all test cases."""
|
||||
return TestSuite([BasicTest.suite(),
|
||||
ShelveTest.suite(),
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# unittest.main()
|
||||
suite = suite()
|
||||
TextTestRunner(descriptions=0, verbosity=2).run(suite)
|
106
tests/test_property_manager.py
Normal file
106
tests/test_property_manager.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
|
||||
"""Unit test for lock_manager.py"""
|
||||
from tempfile import gettempdir
|
||||
from unittest import TestCase, TestSuite, TextTestRunner
|
||||
import os
|
||||
from wsgidav import property_manager
|
||||
|
||||
#===============================================================================
|
||||
# BasicTest
|
||||
#===============================================================================
|
||||
class BasicTest(TestCase):
|
||||
"""Test property_manager.PropertyManager()."""
|
||||
respath = "/dav/res"
|
||||
|
||||
@classmethod
|
||||
def suite(cls):
|
||||
"""Return test case suite (so we can control the order)."""
|
||||
suite = TestSuite()
|
||||
suite.addTest(cls("testPreconditions"))
|
||||
suite.addTest(cls("testOpen"))
|
||||
suite.addTest(cls("testValidation"))
|
||||
suite.addTest(cls("testReadWrite"))
|
||||
return suite
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.pm = property_manager.PropertyManager()
|
||||
self.pm._verbose = 2
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
self.pm._close()
|
||||
self.pm = None
|
||||
|
||||
|
||||
def testPreconditions(self):
|
||||
"""Environment must be set."""
|
||||
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
|
||||
|
||||
|
||||
def testOpen(self):
|
||||
"""Property manager should be lazy opening on first access."""
|
||||
pm = self.pm
|
||||
assert not pm._loaded, "PM must be closed until first access"
|
||||
pm.getProperties(self.respath)
|
||||
assert pm._loaded, "PM must be opened after first access"
|
||||
|
||||
|
||||
def testValidation(self):
|
||||
"""Property manager should raise errors on bad args."""
|
||||
pm = self.pm
|
||||
self.assertRaises(AssertionError,
|
||||
pm.writeProperty, None, "{ns1:}foo", "hurz", False)
|
||||
# name must have a namespace
|
||||
# self.assertRaises(AssertionError,
|
||||
# pm.writeProperty, "/dav/res", "foo", "hurz", False)
|
||||
self.assertRaises(AssertionError,
|
||||
pm.writeProperty, "/dav/res", None, "hurz", False)
|
||||
self.assertRaises(AssertionError,
|
||||
pm.writeProperty, "/dav/res", "{ns1:}foo", None, False)
|
||||
|
||||
assert pm._dict is None, "No properties should have been created by this test"
|
||||
|
||||
|
||||
def testReadWrite(self):
|
||||
"""Property manager should raise errors on bad args."""
|
||||
pm = self.pm
|
||||
url = "/dav/res"
|
||||
pm.writeProperty(url, "foo", "my name is joe")
|
||||
assert pm.getProperty(url, "foo") == "my name is joe"
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# ShelveTest
|
||||
#===============================================================================
|
||||
class ShelveTest(BasicTest):
|
||||
"""Test property_manager.ShelvePropertyManager()."""
|
||||
|
||||
def setUp(self):
|
||||
self.path = os.path.join(gettempdir(), "wsgidav-props.shelve")
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
self.pm = property_manager.ShelvePropertyManager(self.path)
|
||||
self.pm._verbose = 1
|
||||
|
||||
def tearDown(self):
|
||||
self.pm._close()
|
||||
self.pm = None
|
||||
# os.remove(self.path)
|
||||
|
||||
#===============================================================================
|
||||
# suite
|
||||
#===============================================================================
|
||||
def suite():
|
||||
"""Return suites of all test cases."""
|
||||
return TestSuite([BasicTest.suite(),
|
||||
ShelveTest.suite(),
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# unittest.main()
|
||||
suite = suite()
|
||||
TextTestRunner(descriptions=1, verbosity=2).run(suite)
|
69
tests/test_util.py
Normal file
69
tests/test_util.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
|
||||
"""Unit tests for wsgidav.util"""
|
||||
|
||||
from unittest import TestCase, TestSuite, TextTestRunner
|
||||
from wsgidav.util import *
|
||||
|
||||
class BasicTest(TestCase):
|
||||
"""Test ."""
|
||||
|
||||
@classmethod
|
||||
def suite(cls):
|
||||
"""Return test case suite (so we can control the order)."""
|
||||
suite = TestSuite()
|
||||
suite.addTest(cls("testPreconditions"))
|
||||
suite.addTest(cls("testBasics"))
|
||||
return suite
|
||||
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
||||
def testPreconditions(self):
|
||||
"""Environment must be set."""
|
||||
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
|
||||
|
||||
|
||||
def testBasics(self):
|
||||
"""Test basic tool functions."""
|
||||
assert not isChildUri("/a/b", "/a/")
|
||||
assert not isChildUri("/a/b", "/a/b")
|
||||
assert not isChildUri("/a/b", "/a/b/")
|
||||
assert not isChildUri("/a/b", "/a/bc")
|
||||
assert not isChildUri("/a/b", "/a/bc/")
|
||||
assert isChildUri("/a/b", "/a/b/c")
|
||||
assert isChildUri("/a/b", "/a/b/c")
|
||||
|
||||
assert not isEqualOrChildUri("/a/b", "/a/")
|
||||
assert isEqualOrChildUri("/a/b", "/a/b")
|
||||
assert isEqualOrChildUri("/a/b", "/a/b/")
|
||||
assert not isEqualOrChildUri("/a/b", "/a/bc")
|
||||
assert not isEqualOrChildUri("/a/b", "/a/bc/")
|
||||
assert isEqualOrChildUri("/a/b", "/a/b/c")
|
||||
assert isEqualOrChildUri("/a/b", "/a/b/c")
|
||||
|
||||
assert lstripstr("/dav/a/b", "/dav") == "/a/b"
|
||||
assert lstripstr("/dav/a/b", "/DAV") == "/dav/a/b"
|
||||
assert lstripstr("/dav/a/b", "/DAV", True) == "/a/b"
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# suite
|
||||
#===============================================================================
|
||||
def suite():
|
||||
"""Return suites of all test cases."""
|
||||
return TestSuite([BasicTest.suite(),
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# unittest.main()
|
||||
suite = suite()
|
||||
TextTestRunner(descriptions=0, verbosity=2).run(suite)
|
269
tests/test_wsgidav_app.py
Normal file
269
tests/test_wsgidav_app.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
Unit test for wsgidav.lock_manager.py
|
||||
|
||||
This test suite uses paste.fixture to send fake requests to the WSGI
|
||||
stack.
|
||||
|
||||
See http://pythonpaste.org/testing-applications.html
|
||||
and http://pythonpaste.org/modules/fixture.html
|
||||
"""
|
||||
from paste.fixture import TestApp #@UnresolvedImport
|
||||
from tempfile import gettempdir
|
||||
from wsgidav.wsgidav_app import DEFAULT_CONFIG, WsgiDAVApp
|
||||
from wsgidav.fs_dav_provider import FilesystemProvider
|
||||
#from wsgidav import util
|
||||
import os
|
||||
import unittest
|
||||
|
||||
#===============================================================================
|
||||
# ServerTest
|
||||
#===============================================================================
|
||||
class ServerTest(unittest.TestCase):
|
||||
"""Test wsgidav_app using paste.fixture."""
|
||||
|
||||
@classmethod
|
||||
def suite(cls):
|
||||
"""Return test case suite (so we can control the order)."""
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(cls("testPreconditions"))
|
||||
suite.addTest(cls("testDirBrowser"))
|
||||
suite.addTest(cls("testGetPut"))
|
||||
suite.addTest(cls("testEncoding"))
|
||||
suite.addTest(cls("testAuthentication"))
|
||||
return suite
|
||||
|
||||
|
||||
def _makeWsgiDAVApp(self, withAuthentication):
|
||||
self.rootpath = os.path.join(gettempdir(), "wsgidav-test")
|
||||
if not os.path.exists(self.rootpath):
|
||||
os.mkdir(self.rootpath)
|
||||
provider = FilesystemProvider(self.rootpath)
|
||||
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config.update({
|
||||
"provider_mapping": {"/": provider},
|
||||
"user_mapping": {},
|
||||
"verbose": 1,
|
||||
"enable_loggers": [],
|
||||
"propsmanager": None, # None: use property_manager.PropertyManager
|
||||
"locksmanager": None, # None: use lock_manager.LockManager
|
||||
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
|
||||
})
|
||||
|
||||
if withAuthentication:
|
||||
config["user_mapping"] = {"/": {"tester": {"password": "tester",
|
||||
"description": "",
|
||||
"roles": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
config["acceptbasic"] = True
|
||||
config["acceptdigest"] = False
|
||||
config["defaultdigest"] = False
|
||||
|
||||
return WsgiDAVApp(config)
|
||||
|
||||
|
||||
def setUp(self):
|
||||
wsgi_app = self._makeWsgiDAVApp(False)
|
||||
self.app = TestApp(wsgi_app)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# os.rmdir(self.rootpath)
|
||||
del self.app
|
||||
self.app = None
|
||||
|
||||
|
||||
def testPreconditions(self):
|
||||
"""Environment must be set."""
|
||||
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
|
||||
|
||||
|
||||
def testDirBrowser(self):
|
||||
"""Server must respond to GET on a collection."""
|
||||
app = self.app
|
||||
# Access collection (expect '200 Ok' with HTML response)
|
||||
res = app.get("/", status=200)
|
||||
assert "WsgiDAV - Index of /" in res, "Could not list root share"
|
||||
|
||||
# Access unmapped resource (expect '404 Not Found')
|
||||
res = app.get("/not-existing-124/", status=404)
|
||||
|
||||
|
||||
def testGetPut(self):
|
||||
"""Read and write file contents."""
|
||||
app = self.app
|
||||
|
||||
# Prepare file content
|
||||
data1 = "this is a file\nwith two lines"
|
||||
data2 = "this is another file\nwith three lines\nsee!"
|
||||
# Big file with 10 MB
|
||||
lines = []
|
||||
line = "." * (1000-6)
|
||||
for i in xrange(10*1000):
|
||||
lines.append("%04i: %s" % (i, line))
|
||||
data3 = "\n".join(lines)
|
||||
|
||||
# Remove old test files
|
||||
app.delete("/file1.txt", expect_errors=True)
|
||||
app.delete("/file2.txt", expect_errors=True)
|
||||
app.delete("/file3.txt", expect_errors=True)
|
||||
|
||||
# Access unmapped resource (expect '404 Not Found')
|
||||
app.delete("/file1.txt", status=404)
|
||||
app.get("/file1.txt", status=404)
|
||||
|
||||
# PUT a small file (expect '201 Created')
|
||||
app.put("/file1.txt", params=data1, status=201)
|
||||
|
||||
res = app.get("/file1.txt", status=200)
|
||||
assert res.body == data1, "GET file content different from PUT"
|
||||
|
||||
# PUT overwrites a small file (expect '204 No Content')
|
||||
app.put("/file1.txt", params=data2, status=204)
|
||||
|
||||
res = app.get("/file1.txt", status=200)
|
||||
assert res.body == data2, "GET file content different from PUT"
|
||||
|
||||
# PUT writes a big file (expect '201 Created')
|
||||
app.put("/file2.txt", params=data3, status=201)
|
||||
|
||||
# Request must not contain a body (expect '415 Media Type Not Supported')
|
||||
app.get("/file1.txt",
|
||||
headers={"Content-Length": str(len(data1))},
|
||||
params=data1,
|
||||
status=415)
|
||||
|
||||
|
||||
def testEncoding(self):
|
||||
"""Handle special characters."""
|
||||
app = self.app
|
||||
uniData = u"This is a file with special characters:\n" \
|
||||
+ u"Umlaute(äöüß)\n" \
|
||||
+ u"Euro(\u20AC)\n" \
|
||||
+ u"Male(\u2642)"
|
||||
|
||||
data = uniData.encode("utf8")
|
||||
|
||||
def __testrw(filename):
|
||||
# Write/read UTF-8 encoded file name
|
||||
# print util.stringRepr(filename)
|
||||
app.delete(filename, expect_errors=True)
|
||||
app.put(filename, params=data, status=201)
|
||||
res = app.get(filename, status=200)
|
||||
assert res.body == data, "GET file content different from PUT"
|
||||
|
||||
# filenames with umlauts
|
||||
__testrw("/file uml(äöüß).txt")
|
||||
# UTF-8 encoded filenames
|
||||
__testrw("/file euro(\xE2\x82\xAC).txt")
|
||||
__testrw("/file male(\xE2\x99\x82).txt")
|
||||
|
||||
|
||||
def testAuthentication(self):
|
||||
"""Require login."""
|
||||
# Re-create test app with authentication
|
||||
self.tearDown()
|
||||
wsgi_app = self._makeWsgiDAVApp(True)
|
||||
app = self.app = TestApp(wsgi_app)
|
||||
|
||||
# Anonymous access must fail (expect 401 Not Authorized)
|
||||
# Existing resource
|
||||
app.get("/file1.txt", status=401)
|
||||
# Non-existing resource
|
||||
app.get("/not_existing_file.txt", status=401)
|
||||
# Root container
|
||||
app.get("/", status=401)
|
||||
|
||||
# Try basic access authentication
|
||||
user = "tester"
|
||||
password = "tester"
|
||||
creds = (user + ":" + password).encode("base64").strip()
|
||||
headers = {"Authorization": "Basic %s" % creds,
|
||||
}
|
||||
# Existing resource
|
||||
app.get("/file1.txt", headers=headers, status=200)
|
||||
# Non-existing resource (expect 404 NotFound)
|
||||
app.get("/not_existing_file.txt", headers=headers, status=404)
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# WsgiDAVServerTest
|
||||
#===============================================================================
|
||||
#class WsgiDAVServerTest(unittest.TestCase):
|
||||
# """Test the built-in WsgiDAV server with cadaver."""
|
||||
#
|
||||
# @classmethod
|
||||
# def suite(cls):
|
||||
# """Return test case suite (so we can control the order)."""
|
||||
# suite = unittest.TestSuite()
|
||||
# suite.addTest(cls("testPreconditions"))
|
||||
# suite.addTest(cls("testOpen"))
|
||||
# return suite
|
||||
#
|
||||
#
|
||||
# def setUp(self):
|
||||
# config = DEFAULT_CONFIG.copy()
|
||||
# config.update({
|
||||
# "provider_mapping": {},
|
||||
# "user_mapping": {},
|
||||
# "host": "localhost",
|
||||
# "port": 8080,
|
||||
# "ext_servers": [
|
||||
# # "paste",
|
||||
# # "cherrypy",
|
||||
# # "wsgiref",
|
||||
# "wsgidav",
|
||||
# ],
|
||||
# "enable_loggers": [
|
||||
# ],
|
||||
#
|
||||
# "propsmanager": None, # None: use properry_manager.PropertyManager
|
||||
# "locksmanager": None, # None: use lock_manager.LockManager
|
||||
# "domaincontroller": None,
|
||||
# "verbose": 2,
|
||||
# })
|
||||
# self.app = WsgiDAVApp(config)
|
||||
#
|
||||
## from wsgidav.server.run_server import _runBuiltIn
|
||||
## _runBuiltIn(app, config)
|
||||
#
|
||||
#
|
||||
# def tearDown(self):
|
||||
# del self.app
|
||||
# self.app = None
|
||||
#
|
||||
#
|
||||
# def testPreconditions(self):
|
||||
# """Environment must be set."""
|
||||
# self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
|
||||
#
|
||||
#
|
||||
# def testOpen(self):
|
||||
# """Property manager should be lazy opening on first access."""
|
||||
# app = self.app
|
||||
# conf = self.app.config
|
||||
#
|
||||
#
|
||||
# def testValidation(self):
|
||||
# """Property manager should raise errors on bad args."""
|
||||
# conf = self.app.config
|
||||
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# suite
|
||||
#===============================================================================
|
||||
def suite():
|
||||
"""Return suites of all test cases."""
|
||||
return unittest.TestSuite([ServerTest.suite(),
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# unittest.main()
|
||||
suite = suite()
|
||||
unittest.TextTestRunner(descriptions=1, verbosity=2).run(suite)
|
5
tools/_map_w.bat
Normal file
5
tools/_map_w.bat
Normal file
|
@ -0,0 +1,5 @@
|
|||
net use W: http://127.0.0.1/temp/ /persistent:no /user:tester tester
|
||||
rem net use W: http://127.0.0.1/ /persistent:no /user:tester tester
|
||||
net use
|
||||
dir w:
|
||||
pause
|
3
tools/_unmap_w.bat
Normal file
3
tools/_unmap_w.bat
Normal file
|
@ -0,0 +1,3 @@
|
|||
net use W: /delete
|
||||
net use
|
||||
pause
|
|
@ -7,11 +7,13 @@ url: http://wsgidav.googlecode.com/
|
|||
# The list of modules to document. Modules can be named using
|
||||
# dotted names, module filenames, or package directory names.
|
||||
# This option may be repeated.
|
||||
modules: wsgidav_server
|
||||
;modules: wsgidav_server
|
||||
modules: wsgidav.interfaces
|
||||
modules: wsgidav.interfaces.propertymanagerinterface
|
||||
|
||||
# pyfileserver is specified in the command line
|
||||
# wsgidav is specified in the command line
|
||||
|
||||
;exclude=PATTERN
|
||||
exclude=tests, wsgidav.path, wsgidav.pickleshare
|
||||
|
||||
# Whether or not to include syntax highlighted source code in
|
||||
# the output (HTML only).
|
||||
|
@ -24,7 +26,7 @@ output: html
|
|||
|
||||
include-log: yes
|
||||
inheritance: grouped
|
||||
top: class-tree.html
|
||||
;top: class-tree.html
|
||||
|
||||
# Include all automatically generated graphs. These graphs are
|
||||
# generated using Graphviz dot.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#__all__ = ['windowsdomaincontroller',
|
||||
#__all__ = ['nt_domain_controller',
|
||||
# 'simplemysqlabstractionlayer',
|
||||
# ]
|
||||
|
|
|
@ -20,7 +20,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
from wsgidav.dav_provider import DAVProvider
|
||||
|
||||
|
@ -121,16 +121,16 @@ class DummyDAVProvider(DAVProvider):
|
|||
return super(DummyDAVProvider, self).getResourceInfo(path)
|
||||
|
||||
|
||||
def getPropertyNames(self, path, mode="allprop"):
|
||||
def getPropertyNames(self, davres, mode="allprop"):
|
||||
"""Return list of supported property names in Clark Notation.
|
||||
|
||||
@param mode: 'allprop': common properties, that should be send on 'allprop' requests.
|
||||
'propname': all available properties.
|
||||
"""
|
||||
return super(DummyDAVProvider, self).getPropertyNames(path, mode)
|
||||
return super(DummyDAVProvider, self).getPropertyNames(davres, mode)
|
||||
|
||||
|
||||
def getProperties(self, path, mode, nameList=None, namesOnly=False):
|
||||
def getProperties(self, davres, mode, nameList=None, namesOnly=False):
|
||||
"""Return properties as list of 2-tuples (name, value).
|
||||
|
||||
<name> is the property name in Clark notation.
|
||||
|
@ -146,11 +146,11 @@ class DummyDAVProvider(DAVProvider):
|
|||
@param nameList: list of property names in Clark Notation (only for mode 'named')
|
||||
@param namesOnly: return None for <value>
|
||||
"""
|
||||
return super(DummyDAVProvider, self).getProperties(path, mode, nameList, namesOnly)
|
||||
return super(DummyDAVProvider, self).getProperties(davres, mode, nameList, namesOnly)
|
||||
|
||||
|
||||
def getPropertyValue(self, path, name):
|
||||
return super(DummyDAVProvider, self).getPropertyValue(path, name)
|
||||
def getPropertyValue(self, path, propname, davres=None):
|
||||
return super(DummyDAVProvider, self).getPropertyValue(path, propname, davres)
|
||||
|
||||
|
||||
def setPropertyValue(self, path, name, value, dryRun=False):
|
||||
|
@ -200,7 +200,7 @@ class DummyDAVProvider(DAVProvider):
|
|||
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
def getSupportedLivePropertyNames(self, path):
|
||||
def getSupportedLivePropertyNames(self, davres):
|
||||
"""Return list of supported live properties in Clark Notation.
|
||||
|
||||
Do NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
|
||||
|
@ -208,7 +208,7 @@ class DummyDAVProvider(DAVProvider):
|
|||
return NotImplementedError() # Provider must override this
|
||||
|
||||
|
||||
def getLivePropertyValue(self, path, name):
|
||||
def getLivePropertyValue(self, davres, propname):
|
||||
"""Set list of supported live properties in Clark Notation.
|
||||
|
||||
Raise HTTP_NOT_FOUND if property is not supported.
|
||||
|
|
542
wsgidav/addons/mysql_dav_provider.py
Normal file
542
wsgidav/addons/mysql_dav_provider.py
Normal file
|
@ -0,0 +1,542 @@
|
|||
"""
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
Implementation of a DAV provider that provides a very basic, read-only
|
||||
resource layer emulation of a MySQL database.
|
||||
|
||||
This module is specific to the WsgiDAV application. It provides a
|
||||
classes ``MySQLBrowserProvider``.
|
||||
|
||||
Usage::
|
||||
|
||||
(see sample_wsgidav.conf)
|
||||
MySQLBrowserProvider(host, user, passwd, db)
|
||||
|
||||
host - host of database server
|
||||
user - username to access database
|
||||
passwd - passwd to access database
|
||||
db - name of database on database server
|
||||
|
||||
The ``MySQLBrowserProvider`` provides a very basic, read-only
|
||||
resource layer emulation of a MySQL database.
|
||||
It provides the following interface:
|
||||
|
||||
- the root collection shared consists of collections that correspond to
|
||||
table names
|
||||
|
||||
- in each table collection, there is a resource called "_ENTIRE_CONTENTS".
|
||||
This is a non-collection resource that returns a csv representation of the
|
||||
entire table
|
||||
|
||||
- if the table has a single primary key, each table record will also appear
|
||||
as a non-collection resource in the table collection using the primary key
|
||||
value as its name. This resource returns a csv representation of the record
|
||||
and will also include the record attributes as live properties with
|
||||
attribute name as property name and table name suffixed with colon as the
|
||||
property namespace
|
||||
|
||||
|
||||
This is a very basic interface and below is a by no means thorough summary of
|
||||
its limitations:
|
||||
|
||||
- Really only supports having numbers or strings as primary keys. The code uses
|
||||
a numeric or string comparison that may not hold up if the primary key is
|
||||
a date or some other datatype.
|
||||
|
||||
- There is no handling for cases like BLOBs as primary keys or such. Well, there is
|
||||
no handling for BLOBs in general.
|
||||
|
||||
- When returning contents, it buffers the entire contents! A bad way to return
|
||||
large tables. Ideally you would have a FileMixin that reads the database even
|
||||
as the application reads the file object....
|
||||
|
||||
- It takes too many database queries to return information.
|
||||
Ideally there should be some sort of caching for metadata at least, to avoid
|
||||
unnecessary queries to the database.
|
||||
|
||||
|
||||
Abstraction Layers must provide the methods as described in
|
||||
abstractionlayerinterface_
|
||||
|
||||
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
||||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
.. _abstractionlayerinterface : interfaces/abstractionlayerinterface.py
|
||||
"""
|
||||
from wsgidav.dav_provider import DAVProvider
|
||||
from wsgidav.dav_error import DAVError, HTTP_FORBIDDEN
|
||||
from wsgidav import util
|
||||
import MySQLdb
|
||||
import md5
|
||||
import time
|
||||
import csv
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
|
||||
class MySQLBrowserProvider(DAVProvider):
|
||||
|
||||
def __init__(self, host, user, passwd, db):
|
||||
super(MySQLBrowserProvider, self).__init__()
|
||||
self._host = host
|
||||
self._user = user
|
||||
self._passwd = passwd
|
||||
self._db = db
|
||||
self.connectCount = 0
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "%s for db '%s' on '%s' (user: '%s')'" % (self.__class__.__name__, self._db, self._host, self._user)
|
||||
|
||||
|
||||
def _splitPath(self, path):
|
||||
"""Return (tableName, primaryKey) tuple for a request path."""
|
||||
if path.strip() in (None, "", "/"):
|
||||
return (None, None)
|
||||
tableName, primKey = util.saveSplit(path.strip("/"), "/", 1)
|
||||
# _logger.debug("'%s' -> ('%s', '%s')" % (path, tableName, primKey))
|
||||
return (tableName, primKey)
|
||||
|
||||
|
||||
def _initConnection(self):
|
||||
self.connectCount += 1
|
||||
return MySQLdb.connect(host=self._host,
|
||||
user=self._user,
|
||||
passwd=self._passwd,
|
||||
db=self._db)
|
||||
|
||||
|
||||
def _getFieldList(self, conn, table_name):
|
||||
retlist = []
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
retlist.append(row["Field"])
|
||||
cursor.close ()
|
||||
return retlist
|
||||
|
||||
|
||||
def _isDataTypeNumeric(self, datatype):
|
||||
if datatype is None:
|
||||
return False
|
||||
#how many MySQL datatypes does it take to change a lig... I mean, store numbers
|
||||
numerictypes = ["BIGINT",
|
||||
"INTT",
|
||||
"MEDIUMINT",
|
||||
"SMALLINT",
|
||||
"TINYINT",
|
||||
"BIT",
|
||||
"DEC",
|
||||
"DECIMAL",
|
||||
"DOUBLE",
|
||||
"FLOAT",
|
||||
"REAL",
|
||||
"DOUBLE PRECISION",
|
||||
"INTEGER",
|
||||
"NUMERIC"]
|
||||
datatype = datatype.upper()
|
||||
for numtype in numerictypes:
|
||||
if datatype.startswith(numtype):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _existsRecordByPrimaryKey(self, conn, table_name, pri_key_value):
|
||||
pri_key = None
|
||||
pri_field_type = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
if row["Key"] == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = row["Field"]
|
||||
pri_field_type = row["Type"]
|
||||
else:
|
||||
return False #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
|
||||
isNumType = self._isDataTypeNumeric(pri_field_type)
|
||||
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
if isNumType:
|
||||
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
|
||||
else:
|
||||
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
|
||||
row = cursor.fetchone ()
|
||||
if row is None:
|
||||
cursor.close()
|
||||
return False
|
||||
cursor.close()
|
||||
return True
|
||||
|
||||
|
||||
def _getFieldByPrimaryKey(self, conn, table_name, pri_key_value, field_name):
|
||||
pri_key = None
|
||||
pri_field_type = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
if row["Key"] == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = row["Field"]
|
||||
pri_field_type = row["Type"]
|
||||
else:
|
||||
return None #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
|
||||
isNumType = self._isDataTypeNumeric(pri_field_type)
|
||||
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
if isNumType:
|
||||
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
|
||||
else:
|
||||
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
|
||||
row = cursor.fetchone ()
|
||||
if row is None:
|
||||
cursor.close()
|
||||
return None
|
||||
val = str(row[field_name])
|
||||
cursor.close()
|
||||
return val
|
||||
|
||||
|
||||
def _getRecordByPrimaryKey(self, conn, table_name, pri_key_value):
|
||||
dictRet = {}
|
||||
pri_key = None
|
||||
pri_field_type = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
if row["Key"] == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = row["Field"]
|
||||
pri_field_type = row["Type"]
|
||||
else:
|
||||
return None #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
|
||||
isNumType = self._isDataTypeNumeric(pri_field_type)
|
||||
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
if isNumType:
|
||||
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
|
||||
else:
|
||||
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
|
||||
row = cursor.fetchone ()
|
||||
if row is None:
|
||||
cursor.close()
|
||||
return None
|
||||
for fname in row.keys():
|
||||
dictRet[fname] = str(row[fname])
|
||||
cursor.close()
|
||||
return dictRet
|
||||
|
||||
|
||||
def _findPrimaryKey(self, conn, table_name):
|
||||
pri_key = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
fieldname = row["Field"]
|
||||
keyvalue = row["Key"]
|
||||
if keyvalue == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = fieldname
|
||||
else:
|
||||
return None #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
return pri_key
|
||||
|
||||
|
||||
def _listFields(self, conn, table_name, field_name):
|
||||
retlist = []
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
retlist.append(str(row[field_name]))
|
||||
cursor.close()
|
||||
return retlist
|
||||
|
||||
|
||||
def _listTables(self, conn):
|
||||
retlist = []
|
||||
cursor = conn.cursor ()
|
||||
cursor.execute ("SHOW TABLES")
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
retlist.append("%s" % (row[0]))
|
||||
cursor.close ()
|
||||
return retlist
|
||||
|
||||
|
||||
def getMemberNames(self, path):
|
||||
"""
|
||||
path - path identifier for the resource
|
||||
|
||||
returns a list of names of resources contained in the collection resource
|
||||
specified
|
||||
"""
|
||||
conn = self._initConnection()
|
||||
# resdata = path.strip(":").split(":")
|
||||
tableName, primKey = self._splitPath(path)
|
||||
# if len(resdata) == 1:
|
||||
if tableName is None:
|
||||
retlist = self._listTables(conn)
|
||||
# elif len(resdata) == 2:
|
||||
elif primKey is None:
|
||||
pri_key = self._findPrimaryKey(conn, tableName)
|
||||
if pri_key is not None:
|
||||
retlist = self._listFields(conn, tableName, pri_key)
|
||||
else:
|
||||
retlist = []
|
||||
retlist[0:0] = ["_ENTIRE_CONTENTS"]
|
||||
else:
|
||||
retlist = []
|
||||
conn.close()
|
||||
return retlist
|
||||
|
||||
|
||||
# def getSupportedInfoTypes(self, path):
|
||||
# """Return a list of supported information types.
|
||||
#
|
||||
# See DAVProvider.getSupportedInfoTypes()
|
||||
# """
|
||||
# infoTypes = ["created",
|
||||
# "contentType",
|
||||
# "etag",
|
||||
# "isCollection",
|
||||
# "displayName",
|
||||
# ]
|
||||
## if not self.isCollection(path):
|
||||
## pass
|
||||
#
|
||||
# return infoTypes
|
||||
|
||||
|
||||
def getInfoDict(self, path, typeList=None):
|
||||
"""Return info dictionary for path.
|
||||
|
||||
See DAVProvider.getInfoDict()
|
||||
"""
|
||||
# TODO: calling exists() makes directory browsing VERY slow.
|
||||
# At least compared to PyFileServer, which simply used string
|
||||
# functions to get displayType and displayRemarks
|
||||
if not self.exists(path):
|
||||
return None
|
||||
tableName, primKey = self._splitPath(path)
|
||||
|
||||
displayType = "Unknown"
|
||||
displayRemarks = ""
|
||||
contentType = "text/html"
|
||||
|
||||
# _logger.debug("getInfoDict(%s), nc=%s" % (path, self.connectCount))
|
||||
if tableName is None:
|
||||
displayType = "Database"
|
||||
elif primKey is None: # "database" and table name
|
||||
displayType = "Database Table"
|
||||
else:
|
||||
contentType = "text/csv"
|
||||
if primKey == "_ENTIRE_CONTENTS":
|
||||
displayType = "Database Table Contents"
|
||||
displayRemarks = "CSV Representation of Table Contents"
|
||||
else:
|
||||
displayType = "Database Record"
|
||||
displayRemarks = "Attributes available as properties"
|
||||
|
||||
# Avoid calling isCollection, since it would call isExisting -> _initConnection
|
||||
# isCollection = self.isCollection(path)
|
||||
isCollection = primKey is None
|
||||
|
||||
# Avoid calling getPreferredPath, since it would call isCollection -> _initConnection
|
||||
# name = util.getUriName(self.getPreferredPath(path))
|
||||
name = util.getUriName(path)
|
||||
|
||||
# supportedInfoTypes = ["created",
|
||||
# "contentType",
|
||||
# "etag",
|
||||
# "isCollection",
|
||||
# "displayName",
|
||||
# ]
|
||||
dict = {"contentLength": None,
|
||||
"contentType": contentType,
|
||||
"name": name,
|
||||
"displayName": name,
|
||||
"displayType": displayType,
|
||||
"modified": None,
|
||||
"created": time.time(),
|
||||
"etag": md5.new(path).hexdigest(),
|
||||
"supportRanges": False,
|
||||
"isCollection": isCollection,
|
||||
# "supportedInfoTypes": supportedInfoTypes,
|
||||
}
|
||||
# Some resource-only infos:
|
||||
if not isCollection:
|
||||
dict["modified"] = time.time()
|
||||
# _logger.debug("---> getInfoDict, nc=%s" % self.connectCount)
|
||||
return dict
|
||||
|
||||
|
||||
def exists(self, path):
|
||||
tableName, primKey = self._splitPath(path)
|
||||
if path == "/":
|
||||
return True
|
||||
|
||||
try:
|
||||
conn = self._initConnection()
|
||||
# Check table existence:
|
||||
tbllist = self._listTables(conn)
|
||||
if tableName not in tbllist:
|
||||
return False
|
||||
# Check table key existence:
|
||||
if primKey and primKey != "_ENTIRE_CONTENTS":
|
||||
return self._existsRecordByPrimaryKey(conn, tableName, primKey)
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def isCollection(self, path):
|
||||
_tableName, primKey = self._splitPath(path)
|
||||
return self.exists(path) and primKey is None
|
||||
|
||||
|
||||
def isResource(self, path):
|
||||
_tableName, primKey = self._splitPath(path)
|
||||
return self.exists(path) and primKey is not None
|
||||
|
||||
|
||||
def createEmptyResource(self, path):
|
||||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def createCollection(self, path):
|
||||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def deleteCollection(self, path):
|
||||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def openResourceForRead(self, path, davres=None):
|
||||
"""
|
||||
path - path identifier for the resource
|
||||
|
||||
returns a file-like object / stream containing the contents of the
|
||||
resource specified.
|
||||
|
||||
The application will close() the stream.
|
||||
"""
|
||||
filestream = StringIO()
|
||||
|
||||
# resdata = path.strip(":").split(":")
|
||||
tableName, primKey = self._splitPath(path)
|
||||
# if len(resdata) == 3:
|
||||
if primKey is not None:
|
||||
# table_name = resdata[1]
|
||||
conn = self._initConnection()
|
||||
listFields = self._getFieldList(conn, tableName)
|
||||
csvwriter = csv.DictWriter(filestream, listFields, extrasaction="ignore")
|
||||
dictFields = {}
|
||||
for field_name in listFields:
|
||||
dictFields[field_name] = field_name
|
||||
csvwriter.writerow(dictFields)
|
||||
|
||||
if primKey == "_ENTIRE_CONTENTS":
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("SELECT * from " + self._db + "." + tableName)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
csvwriter.writerow(row)
|
||||
cursor.close ()
|
||||
else:
|
||||
row = self._getRecordByPrimaryKey(conn, tableName, primKey)
|
||||
if row is not None:
|
||||
csvwriter.writerow(row)
|
||||
conn.close()
|
||||
|
||||
#this suffices for small dbs, but
|
||||
#for a production big database, I imagine you would have a FileMixin that
|
||||
#does the retrieving and population even as the file object is being read
|
||||
filestream.seek(0)
|
||||
return filestream
|
||||
|
||||
#filevalue = filestream.getvalue()
|
||||
#filestream.close()
|
||||
#return StringIO.StringIO(filevalue)
|
||||
|
||||
|
||||
|
||||
def openResourceForWrite(self, path, contenttype=None):
|
||||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def deleteResource(self, path):
|
||||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def copyResource(self, path, destrespath):
|
||||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def getPropertyValue(self, path, propname, davres=None):
|
||||
"""Return the value of a property.
|
||||
|
||||
The base implementation handles:
|
||||
|
||||
- ``{DAV:}lockdiscovery`` and ``{DAV:}supportedlock`` using the
|
||||
associated lock manager.
|
||||
- All other *live* properties (i.e. name starts with ``{DAV:}``) are
|
||||
delegated to self.getLivePropertyValue()
|
||||
- Finally, other properties are considered *dead*, and are handled using
|
||||
the associated property manager, if one is present.
|
||||
"""
|
||||
# Return table field as property
|
||||
# resdata = path.strip(":").split(":")
|
||||
tableName, primKey = self._splitPath(path)
|
||||
if primKey is not None:
|
||||
ns, localName = util.splitNamespace(propname)
|
||||
if ns == tableName:
|
||||
conn = self._initConnection()
|
||||
fieldlist = self._getFieldList(conn, tableName)
|
||||
if localName in fieldlist:
|
||||
val = self._getFieldByPrimaryKey(conn, tableName, primKey, localName)
|
||||
conn.close()
|
||||
return val
|
||||
conn.close()
|
||||
# else, let default implementation return supported live and dead properties
|
||||
return super(MySQLBrowserProvider, self).getPropertyValue(path, propname, davres)
|
||||
|
||||
|
||||
def getPropertyNames(self, davres, mode="allprop"):
|
||||
"""Return list of supported property names in Clark Notation.
|
||||
|
||||
Return supported live and dead properties. (See also DAVProvider.getPropertyNames().)
|
||||
|
||||
In addition, all table field names are returned as properties.
|
||||
"""
|
||||
# Let default implementation return supported live and dead properties
|
||||
propNames = super(MySQLBrowserProvider, self).getPropertyNames(davres, mode)
|
||||
# Add fieldnames as properties
|
||||
# resdata = path.strip(":").split(":")
|
||||
tableName, primKey = self._splitPath(davres.path)
|
||||
if primKey is not None:
|
||||
conn = self._initConnection()
|
||||
fieldlist = self._getFieldList(conn, tableName)
|
||||
for fieldname in fieldlist:
|
||||
propNames.append("{%s:}%s" % (tableName, fieldname))
|
||||
conn.close()
|
||||
return propNames
|
|
@ -5,21 +5,17 @@
|
|||
Implementation of a domain controller that allows users to authenticate against
|
||||
a Windows NT domain or a local computer (used by HTTPAuthenticator).
|
||||
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
+-------------------------------------------------------------------------------+
|
||||
|
||||
Purpose
|
||||
-------
|
||||
|
||||
Usage::
|
||||
|
||||
from wsgidav.addons.windowsdomaincontroller import SimpleWindowsDomainController
|
||||
domaincontroller = SimpleWindowsDomainController(presetdomain = None, presetserver = None)
|
||||
from wsgidav.addons.nt_domain_controller import NTDomainController
|
||||
domaincontroller = NTDomainController(presetdomain=None, presetserver=None)
|
||||
|
||||
where:
|
||||
|
||||
+ domaincontroller object corresponds to that in ``PyFileServer.conf`` or
|
||||
+ domaincontroller object corresponds to that in ``wsgidav.conf`` or
|
||||
as input into ``wsgidav.http_authenticator.HTTPAuthenticator``.
|
||||
|
||||
+ presetdomain allows the admin to specify a domain to be used (instead of any domain that
|
||||
|
@ -51,7 +47,7 @@ Testability and caveats
|
|||
|
||||
**Digest Authentication**
|
||||
Digest authentication requires the password to be retrieve from the system to compute
|
||||
the correct digest for comparison. This is sofar impossible (and indeed would be a
|
||||
the correct digest for comparison. This is so far impossible (and indeed would be a
|
||||
big security loophole if it was allowed), so digest authentication WILL not work
|
||||
with this class.
|
||||
|
||||
|
@ -77,22 +73,28 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
from wsgidav import util
|
||||
|
||||
import win32net
|
||||
import win32security
|
||||
import win32api
|
||||
#import win32api
|
||||
import win32netcon
|
||||
|
||||
class SimpleWindowsDomainController(object):
|
||||
__docformat__ = "reStructuredText"
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
|
||||
class NTDomainController(object):
|
||||
|
||||
def __init__(self, presetdomain = None, presetserver = None):
|
||||
self._presetdomain = presetdomain
|
||||
self._presetserver = presetserver
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
def getDomainRealm(self, inputURL, environ):
|
||||
return "Windows Domain Authentication"
|
||||
|
||||
|
@ -112,13 +114,15 @@ class SimpleWindowsDomainController(object):
|
|||
dcname = self._getDomainControllerName(domain)
|
||||
|
||||
try:
|
||||
userdata = win32net.NetUserGetInfo(dcname,user,1)
|
||||
userdata = win32net.NetUserGetInfo(dcname, user, 1)
|
||||
except:
|
||||
userdata = dict()
|
||||
if 'password' in userdata:
|
||||
if userdata['password'] != None:
|
||||
return userdata['password']
|
||||
return None
|
||||
_logger.exception("NetUserGetInfo")
|
||||
userdata = {}
|
||||
# if "password" in userdata:
|
||||
# if userdata["password"] != None:
|
||||
# return userdata["password"]
|
||||
# return None
|
||||
return userdata.get("password")
|
||||
|
||||
|
||||
def authDomainUser(self, realmname, username, password, environ):
|
||||
|
@ -141,6 +145,7 @@ class SimpleWindowsDomainController(object):
|
|||
|
||||
return (domain, username)
|
||||
|
||||
|
||||
def _getDomainControllerName(self, domain):
|
||||
if self._presetserver != None:
|
||||
return self._presetserver
|
||||
|
@ -153,22 +158,29 @@ class SimpleWindowsDomainController(object):
|
|||
|
||||
return pdc
|
||||
|
||||
|
||||
def _isUser(self, username, domain, server):
|
||||
resume = 'init'
|
||||
userslist = []
|
||||
resume = "init"
|
||||
while resume:
|
||||
if resume == 'init': resume = 0
|
||||
if resume == "init":
|
||||
resume = 0
|
||||
try:
|
||||
users, total, resume = win32net.NetUserEnum(server, 0, win32netcon.FILTER_NORMAL_ACCOUNT, 0)
|
||||
userslist += users
|
||||
users, _total, resume = win32net.NetUserEnum(server, 0, win32netcon.FILTER_NORMAL_ACCOUNT, 0)
|
||||
# Make sure, we compare unicode
|
||||
un = username.decode("utf8").lower()
|
||||
for userinfo in users:
|
||||
if username.lower() == str(userinfo['name']).lower():
|
||||
uiname = userinfo.get("name")
|
||||
assert uiname
|
||||
assert isinstance(uiname, unicode)
|
||||
if un == userinfo["name"].lower():
|
||||
return True
|
||||
except win32net.error, err:
|
||||
#print err
|
||||
except win32net.error, e:
|
||||
_logger.exception("NetUserEnum: %s" % e)
|
||||
return False
|
||||
_logger.info("User '%s' not found on server '%s'" % (username, server))
|
||||
return False
|
||||
|
||||
|
||||
def _authUser(self, username, password, domain, server):
|
||||
if not self._isUser(username, domain, server):
|
||||
return False
|
||||
|
@ -176,10 +188,12 @@ class SimpleWindowsDomainController(object):
|
|||
try:
|
||||
htoken = win32security.LogonUser(username, domain, password, win32security.LOGON32_LOGON_NETWORK, win32security.LOGON32_PROVIDER_DEFAULT)
|
||||
except win32security.error, err:
|
||||
#print err
|
||||
_logger.warning("LogonUser failed for user '%s': %s" % (username, err))
|
||||
return False
|
||||
else:
|
||||
if htoken:
|
||||
htoken.Close() #guarantee's cleanup
|
||||
_logger.debug("User '%s' logged on." % username)
|
||||
return True
|
||||
_logger.warning("Logon failed for user '%s'." % username)
|
||||
return False
|
|
@ -1,563 +0,0 @@
|
|||
"""
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
+----------------------------------------------------------------------------+
|
||||
| TODO: this module has not yet been ported from PyFileServer to WsgiDAV API |
|
||||
+----------------------------------------------------------------------------+
|
||||
|
||||
Implementation of a DAV provider that provides a very basic, read-only
|
||||
resource layer emulation of a MySQL database.
|
||||
|
||||
This module is specific to the WsgiDAV application. It provides a
|
||||
classes ``SimpleMySQLResourceAbstractionLayer``.
|
||||
|
||||
Usage::
|
||||
|
||||
(see WsgiDAV-example.conf)
|
||||
SimpleMySQLResourceAbstractionLayer(host, user, passwd, db)
|
||||
|
||||
host - host of database server
|
||||
user - username to access database
|
||||
passwd - passwd to access database
|
||||
db - name of database on database server
|
||||
|
||||
The ``SimpleMySQLResourceAbstractionLayer`` provides a very basic, read-only
|
||||
resource layer emulation of a MySQL database.
|
||||
It provides the following interface:
|
||||
|
||||
- the root collection shared consists of collections that correspond to
|
||||
table names
|
||||
|
||||
- in each table collection, there is a resource called "_ENTIRE_CONTENTS".
|
||||
This is a non-collection resource that returns a csv representation of the
|
||||
entire table
|
||||
|
||||
- if the table has a single primary key, each table record will also appear
|
||||
as a non-collection resource in the table collection using the primary key
|
||||
value as its name. This resource returns a csv representation of the record
|
||||
and will also include the record attributes as live properties with
|
||||
attribute name as property name and table name suffixed with colon as the
|
||||
property namespace
|
||||
|
||||
|
||||
This is a very basic interface and below is a by no means thorough summary of
|
||||
its limitations:
|
||||
|
||||
- Really only supports having numbers or strings as primary keys. The code uses
|
||||
a numeric or string comparison that may not hold up if the primary key is
|
||||
a date or some other datatype.
|
||||
|
||||
- There is no handling for cases like BLOBs as primary keys or such. Well, there is
|
||||
no handling for BLOBs in general.
|
||||
|
||||
- When returning contents, it buffers the entire contents! A bad way to return
|
||||
large tables. Ideally you would have a FileMixin that reads the database even
|
||||
as the application reads the file object....
|
||||
|
||||
- It takes too many database queries to return information.
|
||||
Ideally there should be some sort of caching for metadata at least, to avoid
|
||||
unnecessary queries to the database.
|
||||
|
||||
|
||||
Abstraction Layers must provide the methods as described in
|
||||
abstractionlayerinterface_
|
||||
|
||||
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
||||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
.. _abstractionlayerinterface : interfaces/abstractionlayerinterface.py
|
||||
"""
|
||||
import MySQLdb
|
||||
import MySQLdb.cursors
|
||||
import md5
|
||||
import time
|
||||
import csv
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
class SimpleMySQLResourceAbstractionLayer(object):
|
||||
|
||||
def __init__(self, host, user, passwd, db):
|
||||
self._host = host
|
||||
self._user = user
|
||||
self._passwd = passwd
|
||||
self._db = db
|
||||
|
||||
def _initConnection(self):
|
||||
return MySQLdb.connect (host = self._host,
|
||||
user = self._user,
|
||||
passwd = self._passwd,
|
||||
db = self._db)
|
||||
|
||||
def _getFieldList(self, conn, table_name):
|
||||
retlist = []
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
retlist.append(row["Field"])
|
||||
cursor.close ()
|
||||
return retlist
|
||||
|
||||
|
||||
def _isDataTypeNumeric(self, datatype):
|
||||
if datatype is None:
|
||||
return False
|
||||
#how many MySQL datatypes does it take to change a lig... I mean, store numbers
|
||||
numerictypes = ['BIGINT',
|
||||
'INTT',
|
||||
'MEDIUMINT',
|
||||
'SMALLINT',
|
||||
'TINYINT',
|
||||
'BIT',
|
||||
'DEC',
|
||||
'DECIMAL',
|
||||
'DOUBLE',
|
||||
'FLOAT',
|
||||
'REAL',
|
||||
'DOUBLE PRECISION',
|
||||
'INTEGER',
|
||||
'NUMERIC']
|
||||
datatype = datatype.upper()
|
||||
for numtype in numerictypes:
|
||||
if datatype.startswith(numtype):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _existsRecordByPrimaryKey(self, conn, table_name, pri_key_value):
|
||||
pri_key = None
|
||||
pri_field_type = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
if row["Key"] == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = row["Field"]
|
||||
pri_field_type = row["Type"]
|
||||
else:
|
||||
return False #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
|
||||
isNumType = self._isDataTypeNumeric(pri_field_type)
|
||||
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
if isNumType:
|
||||
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
|
||||
else:
|
||||
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
|
||||
row = cursor.fetchone ()
|
||||
if row is None:
|
||||
cursor.close()
|
||||
return False
|
||||
cursor.close()
|
||||
return True
|
||||
|
||||
def _getFieldByPrimaryKey(self, conn, table_name, pri_key_value, field_name):
|
||||
pri_key = None
|
||||
pri_field_type = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
if row["Key"] == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = row["Field"]
|
||||
pri_field_type = row["Type"]
|
||||
else:
|
||||
return None #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
|
||||
isNumType = self._isDataTypeNumeric(pri_field_type)
|
||||
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
if isNumType:
|
||||
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
|
||||
else:
|
||||
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
|
||||
row = cursor.fetchone ()
|
||||
if row is None:
|
||||
cursor.close()
|
||||
return None
|
||||
val = str(row[field_name])
|
||||
cursor.close()
|
||||
return val
|
||||
|
||||
|
||||
def _getRecordByPrimaryKey(self, conn, table_name, pri_key_value):
|
||||
dictRet = {}
|
||||
pri_key = None
|
||||
pri_field_type = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
if row["Key"] == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = row["Field"]
|
||||
pri_field_type = row["Type"]
|
||||
else:
|
||||
return None #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
|
||||
isNumType = self._isDataTypeNumeric(pri_field_type)
|
||||
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
if isNumType:
|
||||
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
|
||||
else:
|
||||
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
|
||||
row = cursor.fetchone ()
|
||||
if row is None:
|
||||
cursor.close()
|
||||
return None
|
||||
for fname in row.keys():
|
||||
dictRet[fname] = str(row[fname])
|
||||
cursor.close()
|
||||
return dictRet
|
||||
|
||||
def _findPrimaryKey(self, conn, table_name):
|
||||
pri_key = None
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("DESCRIBE " + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
fieldname = row["Field"]
|
||||
keyvalue = row["Key"]
|
||||
if keyvalue == "PRI":
|
||||
if pri_key is None:
|
||||
pri_key = fieldname
|
||||
else:
|
||||
return None #more than one primary key - multipart key?
|
||||
cursor.close ()
|
||||
return pri_key
|
||||
|
||||
def _listFields(self, conn, table_name, field_name):
|
||||
retlist = []
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute('SELECT ' + field_name + " FROM " + self._db + "." + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
retlist.append(str(row[field_name]))
|
||||
cursor.close()
|
||||
return retlist
|
||||
|
||||
def _listTables(self, conn):
|
||||
retlist = []
|
||||
cursor = conn.cursor ()
|
||||
cursor.execute ("SHOW TABLES")
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
retlist.append("%s" % (row[0]))
|
||||
cursor.close ()
|
||||
return retlist
|
||||
|
||||
def resolvePath(self, resheadpath, urlelementlist):
|
||||
"""
|
||||
resheadpath should always be "database"
|
||||
"""
|
||||
if len(urlelementlist) == 0:
|
||||
return resheadpath
|
||||
return resheadpath + ":" + ":".join(urlelementlist)
|
||||
|
||||
def breakPath(self, resheadpath, respath):
|
||||
residue = respath[len(resheadpath):].strip(":")
|
||||
return residue.split(":")
|
||||
|
||||
|
||||
def getResourceDescriptor(self, respath):
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 1:
|
||||
return ["Database", ""]
|
||||
elif len(resdata) == 2: # "database" and table name
|
||||
return ["Database Table", ""]
|
||||
elif len(resdata) == 3:
|
||||
if resdata[2] == "_ENTIRE_CONTENTS":
|
||||
return ["Database Table Contents", "CSV Representation of Table Contents"]
|
||||
else:
|
||||
return ["Database Record", "Attributes available as properties"]
|
||||
else:
|
||||
return ["Unknown", "Unknown"]
|
||||
|
||||
def getResourceDescription(self, respath):
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 1:
|
||||
return "Database"
|
||||
elif len(resdata) == 2: # "database" and table name
|
||||
return "Database Table"
|
||||
elif len(resdata) == 3:
|
||||
if resdata[2] == "_ENTIRE_CONTENTS":
|
||||
return "Database Table Contents"
|
||||
else:
|
||||
return "Database Record"
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
def getContentType(self, respath):
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 3:
|
||||
return "text/csv"
|
||||
else:
|
||||
return "text/html"
|
||||
|
||||
def getLastModified(self, respath):
|
||||
return time.time()
|
||||
|
||||
def supportContentLength(self, respath):
|
||||
return False
|
||||
|
||||
def getContentLength(self, respath):
|
||||
return 0
|
||||
|
||||
def getEntityTag(self, respath):
|
||||
return md5.new(respath).hexdigest()
|
||||
|
||||
def isCollection(self, respath):
|
||||
if self.exists(respath):
|
||||
resdata = respath.strip(":").split(":")
|
||||
return len(resdata) <= 2
|
||||
else:
|
||||
return False
|
||||
|
||||
def isResource(self, respath):
|
||||
if self.exists(respath):
|
||||
resdata = respath.strip(":").split(":")
|
||||
return len(resdata) == 3
|
||||
else:
|
||||
return False
|
||||
|
||||
def exists(self, respath):
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) >= 2: #database:table_name check
|
||||
conn = self._initConnection()
|
||||
tbllist = self._listTables(conn)
|
||||
conn.close()
|
||||
if resdata[1] not in tbllist:
|
||||
return False
|
||||
|
||||
if len(resdata) == 3: #database:table_name:value check
|
||||
if resdata[2] == "_ENTIRE_CONTENTS":
|
||||
return True
|
||||
else:
|
||||
conn = self._initConnection()
|
||||
val = self._existsRecordByPrimaryKey(conn, resdata[1], resdata[2])
|
||||
conn.close()
|
||||
return val
|
||||
return True
|
||||
|
||||
def createCollection(self, respath):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
|
||||
|
||||
def deleteCollection(self, respath):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
|
||||
|
||||
def supportEntityTag(self, respath):
|
||||
return False
|
||||
|
||||
def supportLastModified(self, respath):
|
||||
return False
|
||||
|
||||
def supportRanges(self, respath):
|
||||
return False
|
||||
|
||||
def openResourceForRead(self, respath):
|
||||
"""
|
||||
respath - path identifier for the resource
|
||||
|
||||
returns a file-like object / stream containing the contents of the
|
||||
resource specified.
|
||||
|
||||
The application will close() the stream.
|
||||
"""
|
||||
filestream = StringIO()
|
||||
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 3:
|
||||
table_name = resdata[1]
|
||||
conn = self._initConnection()
|
||||
listFields = self._getFieldList(conn, table_name)
|
||||
csvwriter = csv.DictWriter(filestream, listFields, extrasaction='ignore')
|
||||
dictFields = {}
|
||||
for field_name in listFields:
|
||||
dictFields[field_name] = field_name
|
||||
csvwriter.writerow(dictFields)
|
||||
|
||||
if resdata[2] == "_ENTIRE_CONTENTS":
|
||||
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
|
||||
cursor.execute ("SELECT * from " + self._db + "." + table_name)
|
||||
result_set = cursor.fetchall ()
|
||||
for row in result_set:
|
||||
csvwriter.writerow(row)
|
||||
cursor.close ()
|
||||
else:
|
||||
row = self._getRecordByPrimaryKey(conn, table_name, resdata[2])
|
||||
if row is not None:
|
||||
csvwriter.writerow(row)
|
||||
conn.close()
|
||||
|
||||
#this suffices for small dbs, but
|
||||
#for a production big database, I imagine you would have a FileMixin that
|
||||
#does the retrieving and population even as the file object is being read
|
||||
filestream.seek(0)
|
||||
return filestream
|
||||
|
||||
#filevalue = filestream.getvalue()
|
||||
#filestream.close()
|
||||
#return StringIO.StringIO(filevalue)
|
||||
|
||||
def openResourceForWrite(self, respath, contenttype=None):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
|
||||
|
||||
def deleteResource(self, respath):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
|
||||
|
||||
def copyResource(self, respath, destrespath):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
|
||||
|
||||
def getParent(self, respath):
|
||||
dsplit = respath.rsplit(":",1)
|
||||
return dsplit[0]
|
||||
|
||||
def getMemberNames(self, respath):
|
||||
"""
|
||||
respath - path identifier for the resource
|
||||
|
||||
returns a list of names of resources contained in the collection resource
|
||||
specified
|
||||
"""
|
||||
conn = self._initConnection()
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 1:
|
||||
retlist = self._listTables(conn)
|
||||
elif len(resdata) == 2:
|
||||
pri_key = self._findPrimaryKey(conn, resdata[1])
|
||||
if pri_key is not None:
|
||||
retlist = self._listFields(conn, resdata[1], pri_key)
|
||||
else:
|
||||
retlist = []
|
||||
retlist[0:0] = ["_ENTIRE_CONTENTS"]
|
||||
else:
|
||||
retlist = []
|
||||
conn.close()
|
||||
return retlist
|
||||
|
||||
def joinPath(self, rescollectionpath, resname):
|
||||
return rescollectionpath + ":" + resname
|
||||
|
||||
def splitPath(self, respath):
|
||||
dsplit = respath.rsplit(":",1)
|
||||
return (dsplit[0],dsplit[1])
|
||||
|
||||
|
||||
"""
|
||||
Properties and WsgiDAV
|
||||
---------------------------
|
||||
Properties of a resource refers to the attributes of the resource. A property
|
||||
is referenced by the property name and the property namespace. We usually
|
||||
refer to the property as ``{property namespace}property name``
|
||||
|
||||
Properties of resources as defined in webdav falls under three categories:
|
||||
|
||||
Live properties
|
||||
These properties are attributes actively maintained by the server, such as
|
||||
file size, or read permissions. if you are sharing a database record as a
|
||||
resource, for example, the attributes of the record could become the live
|
||||
properties of the resource.
|
||||
|
||||
The webdav specification defines the following properties that could be
|
||||
live properties (refer to webdav specification for details):
|
||||
{DAV:}creationdate
|
||||
{DAV:}displayname
|
||||
{DAV:}getcontentlanguage
|
||||
{DAV:}getcontentlength
|
||||
{DAV:}getcontenttype
|
||||
{DAV:}getetag
|
||||
{DAV:}getlastmodified
|
||||
{DAV:}resourcetype
|
||||
{DAV:}source
|
||||
|
||||
These properties are implemented by the abstraction layer.
|
||||
|
||||
Locking properties
|
||||
They refer to the two webdav-defined properties
|
||||
{DAV:}supportedlock and {DAV:}lockdiscovery
|
||||
|
||||
These properties are implemented by the locking library in
|
||||
``wsgidav.lock_manager`` and dead properties library in
|
||||
``wsgidav.property_manager``
|
||||
|
||||
Dead properties
|
||||
They refer to arbitrarily assigned properties not actively maintained.
|
||||
|
||||
These properties are implemented by the dead properties library in
|
||||
``wsgidav.property_manager``
|
||||
|
||||
"""
|
||||
|
||||
def writeProperty(self, respath, propertyname, propertyns, propertyvalue):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_CONFLICT)
|
||||
|
||||
def removeProperty(self, respath, propertyname, propertyns):
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_CONFLICT)
|
||||
|
||||
def getProperty(self, respath, propertyname, propertyns):
|
||||
if propertyns == 'DAV:':
|
||||
if propertyname == 'creationdate':
|
||||
return time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(0))
|
||||
elif propertyname == 'getcontenttype':
|
||||
return self.getContentType(respath)
|
||||
elif propertyname == 'resourcetype':
|
||||
if self.isCollection(respath):
|
||||
return '<D:collection />'
|
||||
else:
|
||||
return ''
|
||||
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 3:
|
||||
if propertyns == resdata[1] + ":":
|
||||
conn = self._initConnection()
|
||||
fieldlist = self._getFieldList(conn, resdata[1])
|
||||
if propertyname in fieldlist:
|
||||
val = self._getFieldByPrimaryKey(conn, resdata[1], resdata[2], propertyname)
|
||||
conn.close()
|
||||
return val
|
||||
conn.close()
|
||||
raise HTTPRequestException(processrequesterrorhandler.HTTP_NOT_FOUND)
|
||||
|
||||
def isPropertySupported(self, respath, propertyname, propertyns):
|
||||
supportedliveprops = ['creationdate', 'getcontenttype','resourcetype']
|
||||
if propertyns == "DAV:" and propertyname in supportedliveprops:
|
||||
return True
|
||||
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 3:
|
||||
conn = self._initConnection()
|
||||
fieldlist = self._getFieldList(conn, resdata[1])
|
||||
conn.close()
|
||||
ns = resdata[1] + ":"
|
||||
if propertyns == ns and propertyname in fieldlist:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getSupportedPropertyNames(self, respath):
|
||||
appProps = []
|
||||
#DAV properties for all resources
|
||||
appProps.append( ('DAV:','creationdate') )
|
||||
appProps.append( ('DAV:','getcontenttype') )
|
||||
appProps.append( ('DAV:','resourcetype') )
|
||||
|
||||
resdata = respath.strip(":").split(":")
|
||||
if len(resdata) == 3:
|
||||
conn = self._initConnection()
|
||||
fieldlist = self._getFieldList(conn, resdata[1])
|
||||
ns = resdata[1] + ":"
|
||||
for fieldname in fieldlist:
|
||||
appProps.append( (ns,fieldname) )
|
||||
conn.close()
|
||||
return appProps
|
|
@ -292,25 +292,25 @@ class VirtualResourceProvider(DAVProvider):
|
|||
return res.entryList
|
||||
|
||||
|
||||
def getSupportedInfoTypes(self, path):
|
||||
"""Return a list of supported information types.
|
||||
|
||||
See DAVProvider.getSupportedInfoTypes()
|
||||
"""
|
||||
res = self._getResByPath(path)
|
||||
infoTypes = ["isCollection",
|
||||
"displayName",
|
||||
"displayType",
|
||||
]
|
||||
if not res.isCollection():
|
||||
infoTypes.append("contentType")
|
||||
infoTypes.append("contentLength")
|
||||
infoTypes.append("etag")
|
||||
if isinstance(res, VirtualResFile):
|
||||
infoTypes.append("created")
|
||||
infoTypes.append("modified")
|
||||
|
||||
return infoTypes
|
||||
# def getSupportedInfoTypes(self, path):
|
||||
# """Return a list of supported information types.
|
||||
#
|
||||
# See DAVProvider.getSupportedInfoTypes()
|
||||
# """
|
||||
# res = self._getResByPath(path)
|
||||
# infoTypes = ["isCollection",
|
||||
# "displayName",
|
||||
# "displayType",
|
||||
# ]
|
||||
# if not res.isCollection():
|
||||
# infoTypes.append("contentType")
|
||||
# infoTypes.append("contentLength")
|
||||
# infoTypes.append("etag")
|
||||
# if isinstance(res, VirtualResFile):
|
||||
# infoTypes.append("created")
|
||||
# infoTypes.append("modified")
|
||||
#
|
||||
# return infoTypes
|
||||
|
||||
|
||||
def getInfoDict(self, path, typeList=None):
|
||||
|
@ -332,6 +332,18 @@ class VirtualResourceProvider(DAVProvider):
|
|||
else:
|
||||
displayType = "%s-File" % res.data["type"]
|
||||
|
||||
# supportedInfoTypes = ["isCollection",
|
||||
# "displayName",
|
||||
# "displayType",
|
||||
# ]
|
||||
# if not isCollection():
|
||||
# supportedInfoTypes.append("contentType")
|
||||
# supportedInfoTypes.append("contentLength")
|
||||
# supportedInfoTypes.append("etag")
|
||||
# if isinstance(res, VirtualResFile):
|
||||
# supportedInfoTypes.append("created")
|
||||
# supportedInfoTypes.append("modified")
|
||||
|
||||
dict = {"contentLength": None,
|
||||
"contentType": None,
|
||||
"name": name,
|
||||
|
@ -341,6 +353,7 @@ class VirtualResourceProvider(DAVProvider):
|
|||
"modified": res.getModifiedDate(),
|
||||
"created": res.getCreationDate(),
|
||||
"supportRanges": False,
|
||||
# "supportedInfoTypes": supportedInfoTypes,
|
||||
"isCollection": isCollection,
|
||||
}
|
||||
# fp = res.data.get("file")
|
||||
|
@ -378,6 +391,6 @@ class VirtualResourceProvider(DAVProvider):
|
|||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def openResourceForRead(self, path):
|
||||
def openResourceForRead(self, path, davres=None):
|
||||
res = self._getResByPath(path)
|
||||
return res.getContent()
|
||||
|
|
|
@ -12,7 +12,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
import sys
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
#===============================================================================
|
||||
# List of HTTP Response Codes.
|
||||
|
@ -36,7 +36,6 @@ HTTP_MOVED = 301
|
|||
HTTP_FOUND = 302
|
||||
HTTP_SEE_OTHER = 303
|
||||
HTTP_NOT_MODIFIED = 304
|
||||
|
||||
HTTP_USE_PROXY = 305
|
||||
HTTP_TEMP_REDIRECT = 307
|
||||
HTTP_BAD_REQUEST = 400
|
||||
|
@ -76,6 +75,7 @@ HTTP_NOT_EXTENDED = 510
|
|||
# sent as the error response code.
|
||||
# Otherwise only the numeric code itself is sent.
|
||||
#===============================================================================
|
||||
# TODO: paste.httpserver may raise exceptions, if a status code is not followed by a description, so should define all of them.
|
||||
ERROR_DESCRIPTIONS = {
|
||||
HTTP_OK: "200 OK",
|
||||
HTTP_CREATED: "201 Created",
|
||||
|
@ -85,7 +85,7 @@ ERROR_DESCRIPTIONS = {
|
|||
HTTP_FORBIDDEN: "403 Forbidden",
|
||||
HTTP_METHOD_NOT_ALLOWED: "405 Method Not Allowed",
|
||||
HTTP_NOT_FOUND: "404 Not Found",
|
||||
HTTP_CONFLICT: '409 Conflict',
|
||||
HTTP_CONFLICT: "409 Conflict",
|
||||
HTTP_PRECONDITION_FAILED: "412 Precondition Failed",
|
||||
HTTP_RANGE_NOT_SATISFIABLE: "416 Range Not Satisfiable",
|
||||
HTTP_MEDIATYPE_NOT_SUPPORTED: "415 Media Type Not Supported",
|
||||
|
@ -93,6 +93,7 @@ ERROR_DESCRIPTIONS = {
|
|||
HTTP_FAILED_DEPENDENCY: "424 Failed Dependency",
|
||||
HTTP_INTERNAL_ERROR: "500 Internal Server Error",
|
||||
HTTP_NOT_IMPLEMENTED: "501 Not Implemented",
|
||||
HTTP_BAD_GATEWAY: "502 Bad Gateway",
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
|
@ -105,7 +106,7 @@ ERROR_RESPONSES = {
|
|||
HTTP_BAD_REQUEST: "An invalid request was specified",
|
||||
HTTP_NOT_FOUND: "The specified resource was not found",
|
||||
HTTP_FORBIDDEN: "Access denied to the specified resource",
|
||||
HTTP_INTERNAL_ERROR: "An internal server error occured",
|
||||
HTTP_INTERNAL_ERROR: "An internal server error occurred",
|
||||
HTTP_NOT_IMPLEMENTED: "Not Implemented",
|
||||
}
|
||||
|
||||
|
@ -170,6 +171,9 @@ class DAVError(Exception):
|
|||
# return repr(self.value)
|
||||
return "DAVError(%s)" % self.getUserInfo()
|
||||
|
||||
def __str__(self): # Required for 2.4
|
||||
return self.__repr__()
|
||||
|
||||
def getUserInfo(self):
|
||||
"""Return readable string."""
|
||||
if self.value in ERROR_DESCRIPTIONS:
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
dav_provider
|
||||
============
|
||||
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
Abstract base class for DAV resource providers.
|
||||
|
@ -45,30 +45,84 @@ lockmMnager
|
|||
See lock_manager.LockManager for a sample implementation
|
||||
using shelve.
|
||||
"""
|
||||
from wsgidav import util
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
import urllib
|
||||
import time
|
||||
from lxml import etree
|
||||
import traceback
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib
|
||||
from wsgidav import util
|
||||
# Trick PyDev to do intellisense and don't produce warnings:
|
||||
from util import etree #@UnusedImport
|
||||
if False: from xml.etree import ElementTree as etree #@Reimport @UnresolvedImport
|
||||
|
||||
from dav_error import DAVError, \
|
||||
HTTP_NOT_FOUND, HTTP_FORBIDDEN,\
|
||||
PRECONDITION_CODE_ProtectedProperty, asDAVError
|
||||
|
||||
_livePropNames = ['{DAV:}creationdate',
|
||||
'{DAV:}displayname',
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}resourcetype',
|
||||
'{DAV:}getlastmodified',
|
||||
'{DAV:}getcontentlength',
|
||||
'{DAV:}getetag',
|
||||
'{DAV:}getcontentlanguage',
|
||||
'{DAV:}source',
|
||||
'{DAV:}lockdiscovery',
|
||||
'{DAV:}supportedlock']
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
_livePropNames = ["{DAV:}creationdate",
|
||||
"{DAV:}displayname",
|
||||
"{DAV:}getcontenttype",
|
||||
"{DAV:}resourcetype",
|
||||
"{DAV:}getlastmodified",
|
||||
"{DAV:}getcontentlength",
|
||||
"{DAV:}getetag",
|
||||
"{DAV:}getcontentlanguage",
|
||||
"{DAV:}source",
|
||||
"{DAV:}lockdiscovery",
|
||||
"{DAV:}supportedlock"]
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# DAVResource
|
||||
#===============================================================================
|
||||
class DAVResource(object):
|
||||
"""Helper class that represents a single DAV resource instance.
|
||||
|
||||
This class reads resource information on initialization, so querying
|
||||
multiple live properties can be handled more efficiently.
|
||||
|
||||
See also DAVProvider.getInfoDict().
|
||||
"""
|
||||
def __init__(self, davProvider, path, typeList=None):
|
||||
self.provider = davProvider
|
||||
self.path = path
|
||||
self._dict = davProvider.getInfoDict(path, typeList) or {}
|
||||
self._exists = bool(self._dict)
|
||||
def __repr__(self):
|
||||
return "%s(%s): %s" % (self.__class__.__name__, self.path, self._dict)
|
||||
def exists(self):
|
||||
return self._exists
|
||||
def isCollection(self):
|
||||
return self._exists and self._dict["isCollection"]
|
||||
def isResource(self):
|
||||
return self._exists and not self._dict["isCollection"]
|
||||
def contentLength(self):
|
||||
return self._dict.get("contentLength")
|
||||
def contentType(self):
|
||||
return self._dict.get("contentType")
|
||||
def created(self):
|
||||
return self._dict.get("created")
|
||||
def displayName(self):
|
||||
return self._dict.get("displayName")
|
||||
def displayType(self):
|
||||
return self._dict.get("displayType")
|
||||
def etag(self):
|
||||
return self._dict.get("etag")
|
||||
def modified(self):
|
||||
return self._dict.get("modified")
|
||||
def name(self):
|
||||
return self._dict.get("name")
|
||||
def supportRanges(self):
|
||||
return self._dict.get("supportRanges")
|
||||
def supportEtag(self):
|
||||
return self._dict.get("etag") is not None
|
||||
def supportModified(self):
|
||||
return self._dict.get("modified") is not None
|
||||
def supportContentLength(self):
|
||||
return self._dict.get("contentLength") is not None
|
||||
|
||||
|
||||
#===============================================================================
|
||||
|
@ -76,45 +130,44 @@ _livePropNames = ['{DAV:}creationdate',
|
|||
#===============================================================================
|
||||
|
||||
class DAVProvider(object):
|
||||
"""Abstract base class for DAV resource providers."""
|
||||
"""Abstract base class for DAV resource providers.
|
||||
|
||||
There will be only one DAVProvider instance per share (not per request).
|
||||
"""
|
||||
|
||||
|
||||
"""Available info types, that a DAV Provider MAY support.
|
||||
See DAVProvider.getInfoDict() for details."""
|
||||
INFO_TYPES = ["contentLength",
|
||||
"contentType",
|
||||
"created",
|
||||
"displayName",
|
||||
"displayType",
|
||||
"etag",
|
||||
"isCollection",
|
||||
"modified",
|
||||
"name",
|
||||
"supportRanges",
|
||||
]
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.mountPath = ""
|
||||
self.sharePath = None # TODO_ define encoding / quoting
|
||||
self.sharePath = None
|
||||
self.lockManager = None
|
||||
self.propManager = None
|
||||
self.verbose = 2
|
||||
self.caseSensitiveUrls = True
|
||||
|
||||
|
||||
def _log(self, msg):
|
||||
if self.verbose >= 2:
|
||||
print msg
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
def setMountPath(self, mountPath):
|
||||
"""Set application root for this resource provider.
|
||||
|
||||
This is the value of SCRIPT_NAME, when WsgiDAVApp is called.
|
||||
"""
|
||||
assert mountPath in ("", "/") or not mountPath.endswith("/")
|
||||
self.mountPath = mountPath
|
||||
|
||||
|
||||
def setSharePath(self, sharePath):
|
||||
"""Set application location for this resource provider.
|
||||
|
||||
@param sharePath: a ISO-8859-1 encoded byte string (unquoted).
|
||||
@param sharePath: a UTF-8 encoded, unquoted byte string.
|
||||
"""
|
||||
if isinstance(sharePath, unicode):
|
||||
sharePath = sharePath.encode("iso_8859_1")
|
||||
sharePath = sharePath.encode("utf8")
|
||||
assert sharePath=="" or sharePath.startswith("/")
|
||||
if sharePath == "/":
|
||||
sharePath = "" # This allows to code 'absPath = sharePath + path'
|
||||
|
@ -137,7 +190,10 @@ class DAVProvider(object):
|
|||
Different URLs may map to the same resource, e.g.:
|
||||
'/a/b' == '/A/b' == '/a/b/'
|
||||
getPreferredPath() returns the same value for all these variants, e.g.:
|
||||
'/a/b/'
|
||||
'/a/b/' (assuming resource names considered case insensitive)
|
||||
|
||||
@param path: a UTF-8 encoded, unquoted byte string.
|
||||
@return: a UTF-8 encoded, unquoted byte string.
|
||||
"""
|
||||
if path in ("", "/"):
|
||||
return "/"
|
||||
|
@ -153,45 +209,62 @@ class DAVProvider(object):
|
|||
|
||||
|
||||
def getRefUrl(self, path):
|
||||
"""Return the quoted, absolute, unique URL of a resource, relative to the server.
|
||||
"""Return the quoted, absolute, unique URL of a resource, relative to appRoot.
|
||||
|
||||
Byte string, ISO-8859-1 encoded.
|
||||
Byte string, UTF-8 encoded, quoted.
|
||||
Starts with a '/'. Collections also have a trailing '/'.
|
||||
|
||||
This is basically the same as normPath, but deals with 'virtual locations'
|
||||
as well.
|
||||
Since it is always unique for one resource, <refUrl> is used as key for the
|
||||
lock- and property storage.
|
||||
This is basically the same as getPreferredPath, but deals with
|
||||
'virtual locations' as well.
|
||||
|
||||
e.g. '/a/b' == '/A/b' == '/bykey/123' == '/byguid/as532'
|
||||
|
||||
getRefUrl() returns the same value for all these URLs, so it can be
|
||||
used for locking and persistence.
|
||||
used as a key for locking and persistence storage.
|
||||
|
||||
DAV providers that allow these virtual-mappings must override this
|
||||
method.
|
||||
DAV providers that allow virtual-mappings must override this method.
|
||||
|
||||
See also comments in DEVELOPERS.txt glossary.
|
||||
"""
|
||||
return urllib.quote(self.sharePath + self.getPreferredPath(path))
|
||||
|
||||
|
||||
# def getRefKey(self, path):
|
||||
# """Return an unambigous identifier string for a resource.
|
||||
#
|
||||
# Since it is always unique for one resource, <refKey> is used as key for
|
||||
# the lock- and property storage dictionaries.
|
||||
#
|
||||
# This default implementation calls getRefUrl(), and strips a possible
|
||||
# trailing '/'.
|
||||
# """
|
||||
# refKey = self.getRefUrl(path)
|
||||
# if refKey == "/":
|
||||
# return refKey
|
||||
# return refKey.rstrip("/")
|
||||
|
||||
|
||||
def getHref(self, path):
|
||||
"""Convert path to a URL that can be passed to XML responses.
|
||||
|
||||
Byte string, UTF-8 encoded, quoted.
|
||||
|
||||
@see http://www.webdav.org/specs/rfc4918.html#rfc.section.8.3
|
||||
We are using the path-absolute option. i.e. starting with '/'.
|
||||
We are using the path-absolute option. i.e. starting with '/'.
|
||||
URI ; See section 3.2.1 of [RFC2068]
|
||||
"""
|
||||
# TODO: Nautilus chokes, if href encodes '(' as '%28'
|
||||
# Nautilus chokes, if href encodes '(' as '%28'
|
||||
# So we don't encode 'extra' and 'safe' characters (see rfc2068 3.2.1)
|
||||
safe = "/" + "!*'()," + "$-_|."
|
||||
|
||||
return urllib.quote(self.sharePath + self.getPreferredPath(path), safe=safe)
|
||||
return urllib.quote(self.mountPath + self.sharePath + self.getPreferredPath(path), safe=safe)
|
||||
|
||||
|
||||
def refUrlToPath(self, refUrl):
|
||||
"""Convert a refUrl to a path, by stripping the mount prefix."""
|
||||
return "/" + urllib.unquote(refUrl.lstrip(self.sharePath).lstrip("/"))
|
||||
"""Convert a refUrl to a path, by stripping the mount prefix.
|
||||
|
||||
Used to calculate the <path> from a storage key by inverting getRefUrl().
|
||||
"""
|
||||
return "/" + urllib.unquote(util.lstripstr(refUrl, self.sharePath)).lstrip("/")
|
||||
|
||||
|
||||
def getParent(self, path):
|
||||
|
@ -206,7 +279,7 @@ class DAVProvider(object):
|
|||
|
||||
|
||||
def getMemberNames(self, path):
|
||||
"""Return list of (direct) collection member names (ISO-8859-1 byte strings).
|
||||
"""Return list of (direct) collection member names (UTF-8 byte strings).
|
||||
|
||||
Every provider must override this method.
|
||||
"""
|
||||
|
@ -221,12 +294,15 @@ class DAVProvider(object):
|
|||
"""Return iterator of child path's.
|
||||
|
||||
This default implementation calls getMemberNames() recursively.
|
||||
|
||||
@param deptFirst: use <False>, to list containers before content.
|
||||
(e.g. when moving / copying branches.)
|
||||
Use <True>, to list content before containers.
|
||||
(e.g. when deleting branches.)
|
||||
@param depth: '0' | '1' | 'infinity'
|
||||
|
||||
:Parameters:
|
||||
depthFirst : bool
|
||||
use <False>, to list containers before content.
|
||||
(e.g. when moving / copying branches.)
|
||||
Use <True>, to list content before containers.
|
||||
(e.g. when deleting branches.)
|
||||
depth : string
|
||||
'0' | '1' | 'infinity'
|
||||
"""
|
||||
assert depth in ("0", "1", "infinity")
|
||||
|
||||
|
@ -265,22 +341,24 @@ class DAVProvider(object):
|
|||
return list(self.iter(path, collections=True, resources=True, depthFirst=False, depth="1", addSelf=False))
|
||||
|
||||
|
||||
def getSupportedInfoTypes(self, path):
|
||||
"""Return a list of supported information types for a resource.
|
||||
|
||||
Return None, if <path> does not exist.
|
||||
Otherwise return a list with a subset of DAVProvider.INFO_TYPES
|
||||
|
||||
This method must be implemented.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def getInfoDict(self, path, typeList=None):
|
||||
"""Return info dictionary for path.
|
||||
"""Return an info dictionary for path.
|
||||
|
||||
This function is used to ...
|
||||
|
||||
This function is mainly used to query live properties for a resource.
|
||||
Also display information should be provided here, that can be used to
|
||||
render HTML directories.
|
||||
|
||||
The assumption is, that it is more efficient to query all infos in one
|
||||
call, rather than have single calls for every info type.
|
||||
|
||||
``getInfoDict()`` is called indirectly by the ``DAVResource`` constructor.
|
||||
It should be called only once per request and resource::
|
||||
|
||||
davres = DAVResource(davprovider, path)
|
||||
[..]
|
||||
if davres.exists():
|
||||
print davres.contentType()
|
||||
|
||||
Return None, if <path> does not exist.
|
||||
|
||||
Otherwise return a dictionary with these items:
|
||||
|
@ -296,7 +374,8 @@ class DAVProvider(object):
|
|||
(int) last modification date (in seconds, compatible with time module)
|
||||
created:
|
||||
(int) creation date (in seconds, compatible with time module)
|
||||
and for simple resources (i.e. isCollection == False):
|
||||
|
||||
and additionally for simple resources (i.e. isCollection == False):
|
||||
contentType:
|
||||
(str) MIME type of content
|
||||
contentLength:
|
||||
|
@ -310,31 +389,17 @@ class DAVProvider(object):
|
|||
not supported.
|
||||
|
||||
typeList MAY be passed, to specify a list of requested information types.
|
||||
A caller may pass an empty array, if he only wants to check for existence.
|
||||
|
||||
The implementation MAY uses this list to avoid expensive calculation of
|
||||
unwanted information types.
|
||||
|
||||
A caller may pass an empty array for typeList, if he only wants to check
|
||||
for the existence of path.
|
||||
|
||||
This method must be implemented.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def isInfoTypeSupported(self, path, infoType):
|
||||
"""Shortcut to query support of one single info type via getSupportedInfoTypes().
|
||||
|
||||
This method may be overridden with a more efficient version.
|
||||
"""
|
||||
assert infoType in DAVProvider.INFO_TYPES
|
||||
return infoType in self.getSupportedInfoTypes(path)
|
||||
|
||||
|
||||
def getInfo(self, path, infoType):
|
||||
"""Shortcut to query one single value via getInfoDict(). """
|
||||
assert infoType in DAVProvider.INFO_TYPES
|
||||
return self.getInfoDict(path, [infoType]).get(infoType)
|
||||
|
||||
|
||||
def exists(self, path):
|
||||
"""Return True, if path maps to an existing resource.
|
||||
|
||||
|
@ -361,10 +426,10 @@ class DAVProvider(object):
|
|||
|
||||
# --- Properties -----------------------------------------------------------
|
||||
|
||||
def getPropertyNames(self, path, mode="allprop"):
|
||||
def getPropertyNames(self, davres, mode="allprop"):
|
||||
"""Return list of supported property names in Clark Notation.
|
||||
|
||||
@param mode: 'allprop': common properties, that should be send on 'allprop' requests.
|
||||
@param mode: 'allprop': common properties, that should be sent on 'allprop' requests.
|
||||
'propname': all available properties.
|
||||
|
||||
This default implementation returns a combination of:
|
||||
|
@ -379,7 +444,7 @@ class DAVProvider(object):
|
|||
raise ValueError("Invalid mode '%s'." % mode)
|
||||
|
||||
# use a copy
|
||||
propNameList = self.getSupportedLivePropertyNames(path) [:]
|
||||
propNameList = self.getSupportedLivePropertyNames(davres) [:]
|
||||
|
||||
if self.lockManager:
|
||||
if not "{DAV:}lockdiscovery" in propNameList:
|
||||
|
@ -388,46 +453,47 @@ class DAVProvider(object):
|
|||
propNameList.append("{DAV:}supportedlock")
|
||||
|
||||
if self.propManager:
|
||||
refUrl = self.getRefUrl(path)
|
||||
refUrl = self.getRefUrl(davres.path)
|
||||
for deadProp in self.propManager.getProperties(refUrl):
|
||||
propNameList.append(deadProp)
|
||||
return propNameList
|
||||
|
||||
|
||||
def getProperties(self, path, mode, nameList=None, namesOnly=False):
|
||||
def getProperties(self, davres, mode, nameList=None, namesOnly=False):
|
||||
"""Return properties as list of 2-tuples (name, value).
|
||||
|
||||
<name> is the property name in Clark notation.
|
||||
<value> may have different types, depending on the status:
|
||||
name
|
||||
is the property name in Clark notation.
|
||||
value
|
||||
may have different types, depending on the status:
|
||||
- string or unicode: for standard property values.
|
||||
- lxml.etree.Element: for complex values.
|
||||
- etree.Element: for complex values.
|
||||
- DAVError in case of errors.
|
||||
- None: if namesOnly was passed.
|
||||
|
||||
@param path:
|
||||
@param mode: "allprop", "propname", or "named"
|
||||
@param nameList: list of property names in Clark Notation (only for mode 'named')
|
||||
@param namesOnly: return None for <value>
|
||||
@param namesOnly: return None for <value>
|
||||
@param davres: pass a DAVResource to access cached info
|
||||
"""
|
||||
if not mode in ("allprop", "propname", "named"):
|
||||
raise ValueError("Invalid mode '%s'." % mode)
|
||||
|
||||
if mode in ("allprop", "propname"):
|
||||
# TODO: allprop can have nameList, when <include> option is implemented
|
||||
# TODO: we create a namelist for the root path, but it should be constructed for every child individually?
|
||||
assert nameList is None
|
||||
nameList = self.getPropertyNames(path, mode)
|
||||
nameList = self.getPropertyNames(davres, mode)
|
||||
else:
|
||||
assert nameList is not None
|
||||
|
||||
|
||||
|
||||
propList = []
|
||||
for name in nameList:
|
||||
try:
|
||||
if namesOnly:
|
||||
propList.append( (name, None) )
|
||||
else:
|
||||
value = self.getPropertyValue(path, name)
|
||||
value = self.getPropertyValue(davres.path, name, davres)
|
||||
propList.append( (name, value) )
|
||||
except DAVError, e:
|
||||
propList.append( (name, e) )
|
||||
|
@ -439,11 +505,17 @@ class DAVProvider(object):
|
|||
return propList
|
||||
|
||||
|
||||
def getPropertyValue(self, path, name):
|
||||
def getPropertyValue(self, path, propname, davres=None):
|
||||
"""Return the value of a property.
|
||||
|
||||
name:
|
||||
path:
|
||||
resource path.
|
||||
propname:
|
||||
is the property name in Clark notation.
|
||||
davres:
|
||||
DAVResource instance. Contains cached resource information.
|
||||
Manadatory for live properties, but may be omitted for
|
||||
'{DAV:}lockdiscovery' or dead properties.
|
||||
return value:
|
||||
may have different types, depending on the status:
|
||||
|
||||
|
@ -455,19 +527,21 @@ class DAVProvider(object):
|
|||
This default implementation handles ``{DAV:}lockdiscovery`` and
|
||||
``{DAV:}supportedlock`` using the associated lock manager.
|
||||
|
||||
All other *live* properties (i.e. name starts with ``{DAV:}``) are
|
||||
All other *live* properties (i.e. propname starts with ``{DAV:}``) are
|
||||
delegated to self.getLivePropertyValue()
|
||||
|
||||
Finally, other properties are considered *dead*, and are handled using
|
||||
the associated property manager.
|
||||
"""
|
||||
refUrl = self.getRefUrl(path)
|
||||
# refUrl = str(refUrl)
|
||||
if self.lockManager and name == '{DAV:}lockdiscovery':
|
||||
|
||||
# print "getPropertyValue(%s, %s)" % (path, propname)
|
||||
|
||||
if self.lockManager and propname == "{DAV:}lockdiscovery":
|
||||
# TODO: we return HTTP_NOT_FOUND if no lockmanager is present. Correct?
|
||||
lm = self.lockManager
|
||||
activelocklist = lm.getUrlLockList(refUrl)
|
||||
lockdiscoveryEL = etree.Element(name)
|
||||
lockdiscoveryEL = etree.Element(propname)
|
||||
for lock in activelocklist:
|
||||
activelockEL = etree.SubElement(lockdiscoveryEL, "{DAV:}activelock")
|
||||
|
||||
|
@ -484,9 +558,9 @@ class DAVProvider(object):
|
|||
|
||||
timeout = lock["timeout"]
|
||||
if timeout < 0:
|
||||
timeout = 'Infinite'
|
||||
timeout = "Infinite"
|
||||
else:
|
||||
timeout = 'Second-' + str(long(timeout - time.time()))
|
||||
timeout = "Second-" + str(long(timeout - time.time()))
|
||||
etree.SubElement(activelockEL, "{DAV:}timeout").text = timeout
|
||||
|
||||
locktokenEL = etree.SubElement(activelockEL, "{DAV:}locktoken")
|
||||
|
@ -498,10 +572,10 @@ class DAVProvider(object):
|
|||
|
||||
return lockdiscoveryEL
|
||||
|
||||
elif self.lockManager and name == '{DAV:}supportedlock':
|
||||
elif self.lockManager and propname == "{DAV:}supportedlock":
|
||||
# TODO: we return HTTP_NOT_FOUND if no lockmanager is present. Correct?
|
||||
# TODO: the lockmanager should decide about it's features
|
||||
supportedlockEL = etree.Element(name)
|
||||
supportedlockEL = etree.Element(propname)
|
||||
|
||||
lockentryEL = etree.SubElement(supportedlockEL, "{DAV:}lockentry")
|
||||
lockscopeEL = etree.SubElement(lockentryEL, "{DAV:}lockscope")
|
||||
|
@ -517,14 +591,15 @@ class DAVProvider(object):
|
|||
|
||||
return supportedlockEL
|
||||
|
||||
elif name.startswith("{DAV:}"):
|
||||
elif propname.startswith("{DAV:}"):
|
||||
assert davres is not None, "Must pass DAVResource for querying live properties"
|
||||
# Standard live property (raises HTTP_NOT_FOUND if not supported)
|
||||
return self.getLivePropertyValue(path, name)
|
||||
return self.getLivePropertyValue(davres, propname)
|
||||
|
||||
# Dead property
|
||||
if self.propManager:
|
||||
refUrl = self.getRefUrl(path)
|
||||
value = self.propManager.getProperty(refUrl, name)
|
||||
value = self.propManager.getProperty(refUrl, propname)
|
||||
if value is not None:
|
||||
# return value
|
||||
return etree.XML(value)
|
||||
|
@ -533,7 +608,7 @@ class DAVProvider(object):
|
|||
raise DAVError(HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
def setPropertyValue(self, path, name, value, dryRun=False):
|
||||
def setPropertyValue(self, path, propname, value, dryRun=False):
|
||||
"""Set or remove property value.
|
||||
|
||||
value == None means 'remove property'.
|
||||
|
@ -544,29 +619,29 @@ class DAVProvider(object):
|
|||
run, but MUST NOT change any data.
|
||||
|
||||
@param path:
|
||||
@param name: property name in Clark Notation
|
||||
@param propname: property name in Clark Notation
|
||||
@param value: value == None means 'remove property'.
|
||||
@param dryRun: boolean
|
||||
"""
|
||||
# if value is not None and not isinstance(value, (unicode, str, etree._Element)):
|
||||
if value is not None and not isinstance(value, (etree._Element)):
|
||||
raise ValueError()
|
||||
if self.lockManager and name in ("{DAV:}lockdiscovery", "{DAV:}supportedlock"):
|
||||
if self.lockManager and propname in ("{DAV:}lockdiscovery", "{DAV:}supportedlock"):
|
||||
raise DAVError(HTTP_FORBIDDEN, # TODO: Chun used HTTP_CONFLICT
|
||||
preconditionCode=PRECONDITION_CODE_ProtectedProperty)
|
||||
if name.startswith("{DAV:}"):
|
||||
if propname.startswith("{DAV:}"):
|
||||
# raises DAVError(HTTP_FORBIDDEN) if read-only, or not supported
|
||||
return self.setLivePropertyValue(path, name, value, dryRun)
|
||||
return self.setLivePropertyValue(path, propname, value, dryRun)
|
||||
# Dead property
|
||||
if self.propManager:
|
||||
# TODO: do we write all proprties?
|
||||
# TODO: accept etree._Element
|
||||
refUrl = self.getRefUrl(path)
|
||||
if value is None:
|
||||
return self.propManager.removeProperty(refUrl, name)
|
||||
return self.propManager.removeProperty(refUrl, propname)
|
||||
else:
|
||||
value = etree.tostring(value, pretty_print=False)
|
||||
return self.propManager.writeProperty(refUrl, name, value, dryRun)
|
||||
value = etree.tostring(value)
|
||||
return self.propManager.writeProperty(refUrl, propname, value, dryRun)
|
||||
|
||||
raise DAVError(HTTP_FORBIDDEN) # TODO: Chun used HTTP_CONFLICT
|
||||
|
||||
|
@ -577,77 +652,71 @@ class DAVProvider(object):
|
|||
self.propManager.removeProperties(self.getRefUrl(path))
|
||||
|
||||
|
||||
def getSupportedLivePropertyNames(self, path):
|
||||
"""Return list of supported live properties in Clark Notation.
|
||||
|
||||
SHOULD NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
|
||||
|
||||
This default implementation uses self.getSupportedInfoTypes() to figure
|
||||
it out.
|
||||
"""
|
||||
types = self.getSupportedInfoTypes(path)
|
||||
appProps = []
|
||||
if "created" in types:
|
||||
appProps.append("{DAV:}creationdate")
|
||||
if "contentType" in types:
|
||||
appProps.append("{DAV:}getcontenttype")
|
||||
if "isCollection" in types:
|
||||
appProps.append("{DAV:}resourcetype")
|
||||
if "modified" in types:
|
||||
appProps.append("{DAV:}getlastmodified")
|
||||
if "displayName" in types:
|
||||
appProps.append("{DAV:}displayname")
|
||||
if "etag" in types:
|
||||
appProps.append("{DAV:}getetag")
|
||||
# for non-collections:
|
||||
if "contentLength" in types:
|
||||
appProps.append("{DAV:}getcontentlength")
|
||||
return appProps
|
||||
|
||||
|
||||
def getLivePropertyValue(self, path, name):
|
||||
def getSupportedLivePropertyNames(self, davres):
|
||||
"""Return list of supported live properties in Clark Notation.
|
||||
|
||||
SHOULD NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
|
||||
|
||||
This default implementation uses self.getInfoDict() to figure it out.
|
||||
"""
|
||||
# TODO: this could be more efficient if we could query a list of
|
||||
# livre props with a single call. Currently getInfoDict is called
|
||||
# once per prop!
|
||||
infos = self.getInfoDict(path)
|
||||
propmap = {"created": "{DAV:}creationdate",
|
||||
"contentType": "{DAV:}getcontenttype",
|
||||
"isCollection": "{DAV:}resourcetype",
|
||||
"modified": "{DAV:}getlastmodified",
|
||||
"displayName": "{DAV:}displayname",
|
||||
"etag": "{DAV:}getetag",
|
||||
}
|
||||
if not davres.isCollection():
|
||||
propmap["contentLength"] = "{DAV:}getcontentlength"
|
||||
|
||||
appProps = []
|
||||
for k, v in propmap.items():
|
||||
if k in davres._dict:
|
||||
appProps.append(v)
|
||||
return appProps
|
||||
|
||||
if name == '{DAV:}creationdate':
|
||||
return util.getRfc1123Time(infos["created"])
|
||||
|
||||
elif name == '{DAV:}getcontenttype':
|
||||
return infos["contentType"]
|
||||
def getLivePropertyValue(self, davres, propname):
|
||||
"""Return list of supported live properties in Clark Notation.
|
||||
|
||||
SHOULD NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
|
||||
|
||||
elif name == '{DAV:}resourcetype':
|
||||
if infos["isCollection"]:
|
||||
resourcetypeEL = etree.Element(name)
|
||||
This default implementation uses self.getInfoDict() to figure it out.
|
||||
"""
|
||||
if not davres.exists():
|
||||
raise DAVError(HTTP_NOT_FOUND)
|
||||
|
||||
if propname == "{DAV:}creationdate":
|
||||
return util.getRfc1123Time(davres.created())
|
||||
|
||||
elif propname == "{DAV:}getcontenttype":
|
||||
return davres.contentType()
|
||||
|
||||
elif propname == "{DAV:}resourcetype":
|
||||
if davres.isCollection():
|
||||
resourcetypeEL = etree.Element(propname)
|
||||
etree.SubElement(resourcetypeEL, "{DAV:}collection")
|
||||
return resourcetypeEL
|
||||
return ""
|
||||
|
||||
elif name == '{DAV:}getlastmodified':
|
||||
return util.getRfc1123Time(infos["modified"])
|
||||
elif propname == "{DAV:}getlastmodified":
|
||||
return util.getRfc1123Time(davres.modified())
|
||||
|
||||
elif name == '{DAV:}getcontentlength':
|
||||
if infos["contentLength"] is not None:
|
||||
return str(infos["contentLength"])
|
||||
elif propname == "{DAV:}getcontentlength":
|
||||
if davres.contentLength() is not None:
|
||||
return str(davres.contentLength())
|
||||
|
||||
elif name == '{DAV:}getetag':
|
||||
return infos["etag"]
|
||||
elif propname == "{DAV:}getetag":
|
||||
return davres.etag()
|
||||
|
||||
elif name == '{DAV:}displayname':
|
||||
return infos["displayName"]
|
||||
elif propname == "{DAV:}displayname":
|
||||
return davres.displayName()
|
||||
|
||||
# No persistence available, or property not found
|
||||
raise DAVError(HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
def setLivePropertyValue(self, path, name, value, dryRun=False):
|
||||
def setLivePropertyValue(self, path, propname, value, dryRun=False):
|
||||
"""Set or remove a live property value.
|
||||
|
||||
value == None means 'remove property'.
|
||||
|
@ -663,7 +732,7 @@ class DAVProvider(object):
|
|||
removed.
|
||||
|
||||
@param path:
|
||||
@param name: property name in Clark Notation
|
||||
@param propname: property name in Clark Notation
|
||||
@param value: value == None means 'remove property'.
|
||||
@param dryRun: boolean
|
||||
|
||||
|
@ -716,7 +785,7 @@ class DAVProvider(object):
|
|||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def openResourceForRead(self, path):
|
||||
def openResourceForRead(self, path, davres=None):
|
||||
"""Every provider must override this method."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ from wsgidav import util
|
|||
import sys
|
||||
import threading
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
|
||||
class WsgiDavDebugFilter(object):
|
||||
|
@ -25,7 +25,7 @@ class WsgiDavDebugFilter(object):
|
|||
def __init__(self, application):
|
||||
|
||||
self._application = application
|
||||
|
||||
self.out = sys.stderr
|
||||
self.passedLitmus = {}
|
||||
|
||||
self._dumpheaders = [
|
||||
|
@ -63,6 +63,7 @@ class WsgiDavDebugFilter(object):
|
|||
# "basic: 9",
|
||||
# "basic: 14",
|
||||
# "props: 16",
|
||||
"props: 18",
|
||||
# "locks: 9",
|
||||
# "locks: 12",
|
||||
# "locks: 13",
|
||||
|
@ -85,8 +86,8 @@ class WsgiDavDebugFilter(object):
|
|||
def __call__(self, environ, start_response):
|
||||
""""""
|
||||
# TODO: pass srvcfg with constructor instead?
|
||||
srvcfg = environ['wsgidav.config']
|
||||
verbose = srvcfg.get('verbose', 2)
|
||||
srvcfg = environ["wsgidav.config"]
|
||||
verbose = srvcfg.get("verbose", 2)
|
||||
debugBreak = False
|
||||
dumpRequest = False
|
||||
dumpResponse = False
|
||||
|
@ -105,7 +106,7 @@ class WsgiDavDebugFilter(object):
|
|||
# Turn on max. debugging for selected litmus tests
|
||||
litmusTag = environ.get("HTTP_X_LITMUS", environ.get("HTTP_X_LITMUS_SECOND"))
|
||||
if litmusTag and verbose >= 2:
|
||||
print >> environ['wsgi.errors'], "----\nRunning litmus test '%s'..." % litmusTag
|
||||
print >> self.out, "----\nRunning litmus test '%s'..." % litmusTag
|
||||
for litmusSubstring in self._debuglitmus:
|
||||
if litmusSubstring in litmusTag:
|
||||
verbose = 3
|
||||
|
@ -115,7 +116,7 @@ class WsgiDavDebugFilter(object):
|
|||
break
|
||||
for litmusSubstring in self._break_after_litmus:
|
||||
if litmusSubstring in self.passedLitmus and litmusSubstring not in litmusTag:
|
||||
print >> environ['wsgi.errors'], " *** break after litmus %s" % litmusTag
|
||||
print >> self.out, " *** break after litmus %s" % litmusTag
|
||||
sys.exit(-1)
|
||||
if litmusSubstring in litmusTag:
|
||||
self.passedLitmus[litmusSubstring] = True
|
||||
|
@ -128,11 +129,11 @@ class WsgiDavDebugFilter(object):
|
|||
dumpResponse = True
|
||||
|
||||
if dumpRequest:
|
||||
print >> environ['wsgi.errors'], "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
|
||||
print >> self.out, "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
|
||||
for k, v in environ.items():
|
||||
if k == k.upper():
|
||||
print >> environ['wsgi.errors'], "%20s: »%s«" % (k, v)
|
||||
print >> environ['wsgi.errors'], "\n"
|
||||
print >> self.out, "%20s: »%s«" % (k, v)
|
||||
print >> self.out, "\n"
|
||||
elif verbose >= 2:
|
||||
# Dump selected headers
|
||||
printedHeader = False
|
||||
|
@ -140,13 +141,13 @@ class WsgiDavDebugFilter(object):
|
|||
if k in self._dumpheaders:
|
||||
if not printedHeader:
|
||||
printedHeader = True
|
||||
print >> environ['wsgi.errors'], "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
|
||||
print >> environ['wsgi.errors'], "%20s: »%s«" % (k, v)
|
||||
print >> self.out, "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
|
||||
print >> self.out, "%20s: »%s«" % (k, v)
|
||||
|
||||
# Set debug options to environment
|
||||
environ['wsgidav.verbose'] = verbose
|
||||
environ['wsgidav.debug_methods'] = self._debugmethods
|
||||
environ['wsgidav.debug_break'] = debugBreak
|
||||
environ["wsgidav.verbose"] = verbose
|
||||
environ["wsgidav.debug_methods"] = self._debugmethods
|
||||
environ["wsgidav.debug_break"] = debugBreak
|
||||
|
||||
# TODO: add timings and byte/request conters
|
||||
|
||||
|
@ -158,19 +159,19 @@ class WsgiDavDebugFilter(object):
|
|||
util.log("DebugFilter got exception arg", exc_info)
|
||||
# raise exc_info
|
||||
if dumpResponse:
|
||||
print >> environ['wsgi.errors'], "=========> Response from <%s> %s" % (threading._get_ident(), threading.currentThread())
|
||||
print >> self.out, "=========> Response from <%s> %s" % (threading._get_ident(), threading.currentThread())
|
||||
|
||||
print >> environ['wsgi.errors'], 'Response code:', respcode
|
||||
print >> self.out, "Response code:", respcode
|
||||
headersdict = dict(headers)
|
||||
for envitem in headersdict.keys():
|
||||
print >> environ['wsgi.errors'], "\t", envitem, ":\t", repr(headersdict[envitem])
|
||||
print >> environ['wsgi.errors'], "\n"
|
||||
print >> self.out, "\t", envitem, ":\t", repr(headersdict[envitem])
|
||||
print >> self.out, "\n"
|
||||
return start_response(respcode, headers, exc_info)
|
||||
|
||||
for v in iter(self._application(environ, start_response_wrapper)):
|
||||
util.debug("sc", "debug_filter: yield response chunk (%s bytes)" % len(v))
|
||||
if dumpResponse and environ['REQUEST_METHOD'] != 'GET':
|
||||
print >> environ['wsgi.errors'], v
|
||||
# util.log("debug_filter: yield response chunk (%s bytes)" % len(v))
|
||||
if dumpResponse and environ["REQUEST_METHOD"] != "GET":
|
||||
print >> self.out, v
|
||||
yield v
|
||||
|
||||
return
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
request_server
|
||||
=============
|
||||
==============
|
||||
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
|
@ -15,6 +15,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
from wsgidav.dav_error import DAVError, HTTP_OK, HTTP_MEDIATYPE_NOT_SUPPORTED
|
||||
from wsgidav.dav_provider import DAVResource
|
||||
import sys
|
||||
import urllib
|
||||
import util
|
||||
|
@ -35,10 +36,17 @@ class WsgiDavDirBrowser(object):
|
|||
dav = environ["wsgidav.provider"]
|
||||
|
||||
if environ["REQUEST_METHOD"] in ("GET", "HEAD" ) and dav and dav.isCollection(path):
|
||||
# TODO: do we nee to handle IF headers?
|
||||
# TODO: do we need to handle IF headers?
|
||||
# self._evaluateIfHeaders(path, environ)
|
||||
if environ["REQUEST_METHOD"] =="HEAD":
|
||||
return util.sendSimpleResponse(environ, start_response, HTTP_OK)
|
||||
|
||||
# if True:
|
||||
# from cProfile import Profile
|
||||
# profile = Profile()
|
||||
# profile.runcall(self._listDirectory, environ, start_response)
|
||||
# # sort: 0:"calls",1:"time", 2: "cumulative"
|
||||
# profile.print_stats(sort=2)
|
||||
return self._listDirectory(environ, start_response)
|
||||
|
||||
return self._application(environ, start_response)
|
||||
|
@ -67,8 +75,10 @@ class WsgiDavDirBrowser(object):
|
|||
displaypath = urllib.unquote(dav.getHref(path))
|
||||
trailer = environ.get("wsgidav.config", {}).get("response_trailer")
|
||||
|
||||
o_list = []
|
||||
o_list.append('<html><head><title>WsgiDAV - Index of %s </title>' % displaypath)
|
||||
o_list = []
|
||||
o_list.append("<html><head>")
|
||||
o_list.append("<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />")
|
||||
o_list.append("<title>WsgiDAV - Index of %s </title>" % displaypath)
|
||||
o_list.append("""\
|
||||
<style type="text/css">
|
||||
img { border: 0; padding: 0 2px; vertical-align: text-bottom; }
|
||||
|
@ -80,22 +90,33 @@ class WsgiDavDirBrowser(object):
|
|||
</head>
|
||||
<body>
|
||||
""")
|
||||
o_list.append('<H1>%s</H1>' % (displaypath,))
|
||||
o_list.append("<H1>%s</H1>" % displaypath)
|
||||
o_list.append("<hr/><table>")
|
||||
|
||||
if path in ("", "/"):
|
||||
o_list.append('<tr><td colspan="4">Top level share</td></tr>')
|
||||
o_list.append("<tr><td colspan='4'>Top level share</td></tr>")
|
||||
else:
|
||||
o_list.append('<tr><td colspan="4"><a href="' + dav.getHref(dav.getParent(path)) + '">Up to higher level</a></td></tr>')
|
||||
o_list.append("<tr><td colspan='4'><a href='" + dav.getHref(dav.getParent(path)) + "'>Up to higher level</a></td></tr>")
|
||||
|
||||
for name in dav.getMemberNames(path):
|
||||
childPath = path.rstrip("/") + "/" + name
|
||||
infoDict = dav.getInfoDict(childPath)
|
||||
infoDict["strModified"] = util.getRfc1123Time(infoDict["modified"])
|
||||
if infoDict["contentLength"] or not infoDict["isCollection"]:
|
||||
infoDict["strSize"] = "%s B" % infoDict["contentLength"]
|
||||
res = DAVResource(dav, childPath)
|
||||
infoDict = res._dict
|
||||
if not infoDict:
|
||||
print >>sys.stderr, "WARNING: WsgiDavDirBrowser could not getInfoDict for '%s'" % childPath
|
||||
continue
|
||||
|
||||
if infoDict["modified"] is not None:
|
||||
infoDict["strModified"] = util.getRfc1123Time(infoDict["modified"])
|
||||
else:
|
||||
infoDict["strModified"] = ""
|
||||
|
||||
if infoDict["contentLength"] is not None and not infoDict["isCollection"]:
|
||||
# infoDict["strSize"] = "%s Bytes" % infoDict["contentLength"]
|
||||
infoDict["strSize"] = util.byteNumberString(infoDict["contentLength"])
|
||||
else:
|
||||
infoDict["strSize"] = ""
|
||||
|
||||
infoDict["url"] = dav.getHref(path).rstrip("/") + "/" + urllib.quote(name)
|
||||
if infoDict["isCollection"]:
|
||||
infoDict["url"] = infoDict["url"] + "/"
|
||||
|
@ -105,25 +126,13 @@ class WsgiDavDirBrowser(object):
|
|||
<td>%(displayType)s</td>
|
||||
<td>%(strSize)s</td>
|
||||
<td>%(strModified)s</td></tr>\n""" % infoDict)
|
||||
# for name in dav.getMemberNames(path):
|
||||
# childPath = path.rstrip("/") + "/" + name
|
||||
# infoDict = dav.getResourceInfo(childPath)
|
||||
# infoDict["strModified"] = util.getRfc1123Time(infoDict["modified"])
|
||||
# if infoDict["size"] or not infoDict["isCollection"]:
|
||||
# infoDict["strSize"] = "%s B" % infoDict["size"]
|
||||
# else:
|
||||
# infoDict["strSize"] = ""
|
||||
# infoDict["url"] = dav.getHref(path).rstrip("/") + "/" + urllib.quote(name)
|
||||
# if infoDict["isCollection"]:
|
||||
# infoDict["url"] = infoDict["url"] + "/"
|
||||
#
|
||||
# o_list.append("""\
|
||||
# <tr><td><a href="%(url)s">%(displayName)s</a></td>
|
||||
# <td>%(resourceType)s</td>
|
||||
# <td>%(strSize)s</td>
|
||||
# <td>%(strModified)s</td></tr>\n""" % infoDict)
|
||||
|
||||
o_list.append("</table>\n")
|
||||
|
||||
if "http_authenticator.username" in environ:
|
||||
o_list.append("<p>Authenticated user: '%s', realm: '%s'.</p>" % (environ.get("http_authenticator.username"),
|
||||
environ.get("http_authenticator.realm")))
|
||||
|
||||
if trailer:
|
||||
o_list.append("%s\n" % trailer)
|
||||
o_list.append("<hr/>\n<a href='http://wsgidav.googlecode.com/'>WsgiDAV server</a> - %s\n" % util.getRfc1123Time())
|
||||
|
@ -133,4 +142,4 @@ class WsgiDavDirBrowser(object):
|
|||
start_response("200 OK", [("Content-Type", "text/html"),
|
||||
("Date", util.getRfc1123Time()),
|
||||
])
|
||||
return [ "\n".join(o_list) ] # TODO: no need to join?
|
||||
return [ "\n".join(o_list) ]
|
||||
|
|
|
@ -37,7 +37,8 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
__docformat__ = 'reStructuredText'
|
||||
import sys
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
class WsgiDAVDomainController(object):
|
||||
|
||||
|
@ -46,33 +47,51 @@ class WsgiDAVDomainController(object):
|
|||
# self.allowAnonymous = allowAnonymous
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
def getDomainRealm(self, inputURL, environ):
|
||||
"""Resolve a relative url to the appropriate realm name."""
|
||||
# we don't get the realm here, its already been resolved in request_resolver
|
||||
davProvider = environ["wsgidav.provider"]
|
||||
if not davProvider:
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print >> environ['wsgi.errors'], "getDomainRealm(%s): '%s'" %(inputURL, None)
|
||||
if environ["wsgidav.verbose"] >= 2:
|
||||
print >>sys.stderr, "getDomainRealm(%s): '%s'" %(inputURL, None)
|
||||
return None
|
||||
# if environ['wsgidav.verbose'] >= 2:
|
||||
# print >> environ['wsgi.errors'], "getDomainRealm(%s): '%s'" %(inputURL, davProvider.sharePath)
|
||||
return davProvider.sharePath
|
||||
realm = davProvider.sharePath
|
||||
if realm == "":
|
||||
realm = "/"
|
||||
# if environ["wsgidav.verbose"] >= 2:
|
||||
# print >>sys.stderr, "getDomainRealm(%s): '%s'" %(inputURL, realm)
|
||||
return realm
|
||||
|
||||
|
||||
def requireAuthentication(self, realmname, environ):
|
||||
"""Return True if this realm requires authentication or False if it is
|
||||
available for general access."""
|
||||
# TODO: Should check for --allow_anonymous?
|
||||
# assert realmname in environ['wsgidav.config']['user_mapping'], "Currently there must be at least on user mapping for this realm"
|
||||
# assert realmname in environ["wsgidav.config"]["user_mapping"], "Currently there must be at least on user mapping for this realm"
|
||||
return realmname in self.userMap
|
||||
|
||||
|
||||
def isRealmUser(self, realmname, username, environ):
|
||||
# if environ['wsgidav.verbose'] >= 2:
|
||||
# print >> environ['wsgi.errors'], "isRealmUser('%s', '%s'): %s" %(realmname, username, realmname in self.userMap and username in self.userMap[realmname])
|
||||
"""Returns True if this username is valid for the realm, False otherwise."""
|
||||
# if environ["wsgidav.verbose"] >= 2:
|
||||
# print >>sys.stderr, "isRealmUser('%s', '%s'): %s" %(realmname, username, realmname in self.userMap and username in self.userMap[realmname])
|
||||
return realmname in self.userMap and username in self.userMap[realmname]
|
||||
|
||||
|
||||
def getRealmUserPassword(self, realmname, username, environ):
|
||||
"""Return the password for the given username for the realm.
|
||||
|
||||
Used for digest authentication.
|
||||
"""
|
||||
return self.userMap.get(realmname, {}).get(username, {}).get("password")
|
||||
|
||||
|
||||
def authDomainUser(self, realmname, username, password, environ):
|
||||
return password == self.getRealmUserPassword(realmname, username, environ)
|
||||
"""Returns True if this username/password pair is valid for the realm,
|
||||
False otherwise. Used for basic authentication."""
|
||||
user = self.userMap.get(realmname, {}).get(username)
|
||||
return user is not None and password == user.get("password")
|
||||
|
|
|
@ -82,7 +82,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
import util
|
||||
from dav_error import DAVError, getHttpStatusString, asDAVError,\
|
||||
|
@ -111,10 +111,10 @@ class ErrorPrinter(object):
|
|||
util.debug("sc", "ErrorPrinter: yield start")
|
||||
yield v
|
||||
util.debug("sc", "ErrorPrinter: yield end")
|
||||
except GeneratorExit:
|
||||
# TODO: required?
|
||||
util.debug("sc", "GeneratorExit")
|
||||
raise
|
||||
# except GeneratorExit:
|
||||
# # TODO: required?
|
||||
# util.debug("sc", "GeneratorExit")
|
||||
# raise
|
||||
except DAVError, e:
|
||||
util.debug("sc", "re-raising %s" % e)
|
||||
raise
|
||||
|
@ -122,7 +122,8 @@ class ErrorPrinter(object):
|
|||
util.debug("sc", "re-raising 2 - %s" % e)
|
||||
if self._catch_all_exceptions:
|
||||
# Catch all exceptions to return as 500 Internal Error
|
||||
traceback.print_exc(10, sys.stderr)
|
||||
# traceback.print_exc(10, sys.stderr)
|
||||
traceback.print_exc(10, environ.get("wsgi.errors") or sys.stderr)
|
||||
raise asDAVError(e)
|
||||
else:
|
||||
util.log("ErrorPrinter: caught Exception")
|
||||
|
@ -136,26 +137,26 @@ class ErrorPrinter(object):
|
|||
|
||||
if evalue == HTTP_INTERNAL_ERROR:# and e.srcexception:
|
||||
print >>sys.stderr, "ErrorPrinter: caught HTTPRequestException(HTTP_INTERNAL_ERROR)"
|
||||
traceback.print_exc(10, sys.stderr) # TODO: inserted this for debugging
|
||||
traceback.print_exc(10, environ.get("wsgi.errors") or sys.stderr) # TODO: inserted this for debugging
|
||||
print >>sys.stderr, "e.srcexception:\n%s" % e.srcexception
|
||||
|
||||
if evalue in ERROR_RESPONSES:
|
||||
respbody = '<html><head><title>' + respcode + '</title></head><body><h1>' + respcode + '</h1>'
|
||||
respbody = respbody + '<p>' + ERROR_RESPONSES[evalue] + '</p>'
|
||||
respbody = "<html><head><title>" + respcode + "</title></head><body><h1>" + respcode + "</h1>"
|
||||
respbody = respbody + "<p>" + ERROR_RESPONSES[evalue] + "</p>"
|
||||
if e.contextinfo:
|
||||
respbody += "%s\n" % e.contextinfo
|
||||
respbody += '<hr>\n'
|
||||
respbody += "<hr>\n"
|
||||
if self._server_descriptor:
|
||||
respbody = respbody + self._server_descriptor + '<hr>'
|
||||
respbody = respbody + datestr + '</body></html>'
|
||||
respbody = respbody + self._server_descriptor + "<hr>"
|
||||
respbody = respbody + datestr + "</body></html>"
|
||||
else:
|
||||
# TODO: added html body, to see if this fixes 'connection closed' bug
|
||||
respbody = '<html><head><title>' + respcode + '</title></head><body><h1>' + respcode + '</h1></body></html>'
|
||||
respbody = "<html><head><title>" + respcode + "</title></head><body><h1>" + respcode + "</h1></body></html>"
|
||||
|
||||
util.debug("sc", "Return error html %s: %s" % (respcode, respbody))
|
||||
start_response(respcode,
|
||||
[('Content-Type', 'text/html'),
|
||||
('Date', datestr)
|
||||
[("Content-Type", "text/html"),
|
||||
("Date", datestr)
|
||||
],
|
||||
# sys.exc_info() # TODO: Always provide exc_info when beginning an error response?
|
||||
)
|
||||
|
|
|
@ -19,7 +19,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
from dav_provider import DAVProvider
|
||||
|
||||
|
@ -32,6 +32,8 @@ import stat
|
|||
from dav_error import DAVError, HTTP_FORBIDDEN
|
||||
|
||||
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
BUFFER_SIZE = 8192
|
||||
|
||||
|
||||
|
@ -41,75 +43,65 @@ BUFFER_SIZE = 8192
|
|||
class ReadOnlyFilesystemProvider(DAVProvider):
|
||||
|
||||
def __init__(self, rootFolderPath):
|
||||
if not rootFolderPath or not os.path.exists(rootFolderPath):
|
||||
raise ValueError("Invalid root path: %s" % rootFolderPath)
|
||||
super(ReadOnlyFilesystemProvider, self).__init__()
|
||||
self.rootFolderPath = rootFolderPath
|
||||
self.rootFolderPath = os.path.abspath(rootFolderPath)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
# return "%s for '%s', path '%s'" % (self.__class__.__name__, self., self.rootFolderPath)
|
||||
return "%s for path '%s'" % (self.__class__.__name__, self.rootFolderPath)
|
||||
|
||||
|
||||
def _locToFilePath(self, path):
|
||||
"""Convert resource path to an absolute file path."""
|
||||
"""Convert resource path to a unicode absolute file path."""
|
||||
# TODO: cache results
|
||||
# print "_locToFilePath(%s)..." % (path)
|
||||
assert self.rootFolderPath is not None
|
||||
pathInfoParts = path.strip("/").split("/")
|
||||
|
||||
r = os.path.join(self.rootFolderPath, *pathInfoParts)
|
||||
r = os.path.abspath(r)
|
||||
r = os.path.abspath(os.path.join(self.rootFolderPath, *pathInfoParts))
|
||||
if not r.startswith(self.rootFolderPath):
|
||||
raise RuntimeError("Security exception: tried to access file outside root.")
|
||||
# if not isinstance(r, unicode):
|
||||
# util.log("_locToFilePath(%s): %r" % (path, r))
|
||||
# r = r.decode("utf8")
|
||||
r = util.toUnicode(r)
|
||||
# print "_locToFilePath(%s): %s" % (path, r)
|
||||
return r
|
||||
|
||||
|
||||
def getMemberNames(self, path):
|
||||
"""Return list of (direct) collection member names (ISO-8859-1 byte strings)."""
|
||||
"""Return list of (direct) collection member names (UTF-8 byte strings)."""
|
||||
fp = self._locToFilePath(path)
|
||||
|
||||
# TODO: iso_8859_1 doesn't know EURO sign
|
||||
# On Windows NT/2k/XP and Unix, if path is a Unicode object, the result
|
||||
# will be a list of Unicode objects.
|
||||
# Undecodable filenames will still be returned as string objects
|
||||
|
||||
# If we don't request unicode, for example Vista may return a '?'
|
||||
# instead of a special character. The name would then be unusable to
|
||||
# build a URL that references this resource.
|
||||
# fp = unicode(fp)
|
||||
nameList = []
|
||||
for name in os.listdir(fp):
|
||||
# print "%r" % name
|
||||
assert isinstance(name, unicode)
|
||||
name = name.encode("utf8")
|
||||
# print "-> %r" % name
|
||||
nameList.append(name)
|
||||
# if isinstance(name, unicode):
|
||||
# print "->%r" % name.encode("iso_8859_1")
|
||||
# nameList.append(name.encode("iso_8859_1"))
|
||||
# else:
|
||||
# nameList.append(name)
|
||||
return nameList
|
||||
|
||||
|
||||
def getSupportedInfoTypes(self, path):
|
||||
"""Return a list of supported information types.
|
||||
|
||||
See DAVProvider.getSupportedInfoTypes()
|
||||
"""
|
||||
infoTypes = ["created",
|
||||
"contentType",
|
||||
"isCollection",
|
||||
"modified",
|
||||
"displayName",
|
||||
]
|
||||
if not self.isCollection(path):
|
||||
infoTypes.append("contentLength")
|
||||
infoTypes.append("etag")
|
||||
|
||||
return infoTypes
|
||||
|
||||
|
||||
def getInfoDict(self, path, typeList=None):
|
||||
"""Return info dictionary for path.
|
||||
|
||||
See DAVProvider.getInfoDict()
|
||||
"""
|
||||
fp = self._locToFilePath(path)
|
||||
# print >>sys.stderr, "getInfoDict(%s)" % (path, )
|
||||
|
||||
if not os.path.exists(fp):
|
||||
return None
|
||||
# Early out,if typeList is [] (i.e. test for resource existence only)
|
||||
|
@ -120,7 +112,10 @@ class ReadOnlyFilesystemProvider(DAVProvider):
|
|||
# The file system may have non-files (e.g. links)
|
||||
isFile = os.path.isfile(fp)
|
||||
name = util.getUriName(self.getPreferredPath(path))
|
||||
# print "name(%s)=%s" % (path, name)
|
||||
|
||||
# TODO: this line in: browser doesn't work, but DAVEx does
|
||||
# name = name.decode("utf8")
|
||||
# util.log("getInfoDict(%s): name='%s'" % (path, name))
|
||||
|
||||
displayType = "File"
|
||||
if isCollection:
|
||||
|
@ -166,6 +161,9 @@ class ReadOnlyFilesystemProvider(DAVProvider):
|
|||
|
||||
|
||||
def isResource(self, path):
|
||||
# TODO: does it make sense to treat non-files/non-collections as
|
||||
# existing, but neither isCollection nor isResource?
|
||||
# Maybe we should define existing as 'isCollection or isResource'
|
||||
fp = self._locToFilePath(path)
|
||||
return os.path.isfile(fp)
|
||||
|
||||
|
@ -182,14 +180,17 @@ class ReadOnlyFilesystemProvider(DAVProvider):
|
|||
raise DAVError(HTTP_FORBIDDEN)
|
||||
|
||||
|
||||
def openResourceForRead(self, path):
|
||||
def openResourceForRead(self, path, davres=None):
|
||||
fp = self._locToFilePath(path)
|
||||
# mime = self.getContentType(path)
|
||||
mime = self.getInfo(path, "contentType")
|
||||
if mime.startswith("text"):
|
||||
return file(fp, 'r', BUFFER_SIZE)
|
||||
if davres:
|
||||
mime = davres.contentType()
|
||||
else:
|
||||
return file(fp, 'rb', BUFFER_SIZE)
|
||||
mime = self.getInfoDict(path, ["contentType"]).get("contentType")
|
||||
|
||||
if mime.startswith("text"):
|
||||
return file(fp, "r", BUFFER_SIZE)
|
||||
else:
|
||||
return file(fp, "rb", BUFFER_SIZE)
|
||||
|
||||
|
||||
def openResourceForWrite(self, path, contenttype=None):
|
||||
|
@ -230,14 +231,19 @@ class FilesystemProvider(ReadOnlyFilesystemProvider):
|
|||
|
||||
def openResourceForWrite(self, path, contenttype=None):
|
||||
fp = self._locToFilePath(path)
|
||||
if contenttype is None:
|
||||
istext = False
|
||||
else:
|
||||
istext = contenttype.startswith("text")
|
||||
if istext:
|
||||
return file(fp, 'w', BUFFER_SIZE)
|
||||
else:
|
||||
return file(fp, 'wb', BUFFER_SIZE)
|
||||
# if contenttype is None:
|
||||
# istext = False
|
||||
# else:
|
||||
# istext = contenttype.startswith("text")
|
||||
# if istext:
|
||||
# return file(fp, "w", BUFFER_SIZE)
|
||||
# else:
|
||||
# return file(fp, "wb", BUFFER_SIZE)
|
||||
mode = "wb"
|
||||
if contenttype and contenttype.startswith("text"):
|
||||
mode = "w"
|
||||
_logger.debug("openResourceForWrite: %s, %s" % (fp, mode))
|
||||
return file(fp, mode, BUFFER_SIZE)
|
||||
|
||||
def deleteResource(self, path):
|
||||
fp = self._locToFilePath(path)
|
||||
|
|
|
@ -3,11 +3,13 @@ http_authenticator
|
|||
==================
|
||||
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
WSGI middleware for HTTP basic and digest authentication.
|
||||
|
||||
Usage::
|
||||
|
||||
from http_authenticator import HTTPAuthenticator
|
||||
|
||||
WSGIApp = HTTPAuthenticator(ProtectedWSGIApp, domain_controller, acceptbasic,
|
||||
|
@ -33,8 +35,8 @@ Usage::
|
|||
The HTTPAuthenticator will put the following authenticated information in the
|
||||
environ dictionary::
|
||||
|
||||
environ['http_authenticator.realm'] = realm name
|
||||
environ['http_authenticator.username'] = username
|
||||
environ["http_authenticator.realm"] = realm name
|
||||
environ["http_authenticator.username"] = username
|
||||
|
||||
|
||||
Domain Controllers
|
||||
|
@ -60,6 +62,7 @@ in a single realm name (for display) and a single dictionary of username (key)
|
|||
and password (value) string pairs
|
||||
|
||||
Usage::
|
||||
|
||||
from http_authenticator import SimpleDomainController
|
||||
users = dict(({'John Smith': 'YouNeverGuessMe', 'Dan Brown': 'DontGuessMeEither'})
|
||||
realm = 'Sample Realm'
|
||||
|
@ -79,19 +82,26 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
import random
|
||||
import base64
|
||||
import md5
|
||||
try:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import md5
|
||||
import time
|
||||
import re
|
||||
import util
|
||||
|
||||
_logger = util.getModuleLogger(__name__, True)
|
||||
|
||||
|
||||
class SimpleDomainController(object):
|
||||
"""SimpleDomainController : Simple domain controller for HTTPAuthenticator."""
|
||||
def __init__(self, dictusers = None, realmname = 'SimpleDomain'):
|
||||
def __init__(self, dictusers = None, realmname = "SimpleDomain"):
|
||||
if dictusers is None:
|
||||
self._users = dict({'John Smith': 'YouNeverGuessMe'})
|
||||
self._users = dict({"John Smith": "YouNeverGuessMe"})
|
||||
else:
|
||||
self._users = dictusers
|
||||
self._realmname = realmname
|
||||
|
@ -108,16 +118,17 @@ class SimpleDomainController(object):
|
|||
def getRealmUserPassword(self, realmname, username, environ):
|
||||
if username in self._users:
|
||||
return self._users[username]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def authRealmUser(self, realmname, username, password, environ):
|
||||
def authDomainUser(self, realmname, username, password, environ):
|
||||
if username in self._users:
|
||||
return self._users[username] == password
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# HTTPAuthenticator
|
||||
#===============================================================================
|
||||
class HTTPAuthenticator(object):
|
||||
"""WSGI Middleware for basic and digest authenticator."""
|
||||
def __init__(self, application, domaincontroller, acceptbasic=True, acceptdigest=True, defaultdigest=True):
|
||||
|
@ -131,86 +142,100 @@ class HTTPAuthenticator(object):
|
|||
self._acceptbasic = acceptbasic
|
||||
self._acceptdigest = acceptdigest
|
||||
self._defaultdigest = defaultdigest
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'], environ)
|
||||
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
|
||||
|
||||
if environ.get("REQUEST_METHOD") == "PUT":
|
||||
pass
|
||||
|
||||
if not self._domaincontroller.requireAuthentication(realmname, environ):
|
||||
# no authentication needed
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print "No authorization required for realm '%s'" % realmname
|
||||
environ['http_authenticator.realm'] = realmname
|
||||
environ['http_authenticator.username'] = ''
|
||||
_logger.debug("No authorization required for realm '%s'" % realmname)
|
||||
environ["http_authenticator.realm"] = realmname
|
||||
environ["http_authenticator.username"] = ""
|
||||
return self._application(environ, start_response)
|
||||
|
||||
if 'HTTP_AUTHORIZATION' in environ:
|
||||
authheader = environ['HTTP_AUTHORIZATION']
|
||||
if "HTTP_AUTHORIZATION" in environ:
|
||||
authheader = environ["HTTP_AUTHORIZATION"]
|
||||
authmatch = self._headermethod.search(authheader)
|
||||
authmethod = "None"
|
||||
if authmatch:
|
||||
authmethod = authmatch.group(1).lower()
|
||||
if authmethod == 'digest' and self._acceptdigest:
|
||||
|
||||
if authmethod == "digest" and self._acceptdigest:
|
||||
return self.authDigestAuthRequest(environ, start_response)
|
||||
elif authmethod == 'basic' and self._acceptbasic:
|
||||
return self.authBasicAuthRequest(environ, start_response)
|
||||
else:
|
||||
start_response("400 Bad Request", [('Content-Length', '0')])
|
||||
return ['']
|
||||
else:
|
||||
if self._defaultdigest:
|
||||
return self.sendDigestAuthResponse(environ, start_response)
|
||||
else:
|
||||
elif authmethod == "digest" and self._acceptbasic:
|
||||
return self.sendBasicAuthResponse(environ, start_response)
|
||||
elif authmethod == "basic" and self._acceptbasic:
|
||||
return self.authBasicAuthRequest(environ, start_response)
|
||||
|
||||
util.log("HTTPAuthenticator: respond with 400 Bad request; Auth-Method: %s" % authmethod)
|
||||
start_response("400 Bad Request", [("Content-Length", "0"),
|
||||
("Date", util.getRfc1123Time()),
|
||||
])
|
||||
return [""]
|
||||
|
||||
if self._defaultdigest:
|
||||
return self.sendDigestAuthResponse(environ, start_response)
|
||||
return self.sendBasicAuthResponse(environ, start_response)
|
||||
|
||||
return ['']
|
||||
|
||||
def sendBasicAuthResponse(self, environ, start_response):
|
||||
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print >> environ['wsgi.errors'], "401 Not Authorized for realm '%s'" % realmname
|
||||
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
|
||||
_logger.info("401 Not Authorized for realm '%s' (basic)" % realmname)
|
||||
wwwauthheaders = "Basic realm=\"" + realmname + "\""
|
||||
start_response("401 Not Authorized", [('WWW-Authenticate', wwwauthheaders)])
|
||||
return [self.getErrorMessage()]
|
||||
start_response("401 Not Authorized", [("WWW-Authenticate", wwwauthheaders),
|
||||
("Content-Type", "text/html"),
|
||||
("Date", util.getRfc1123Time()),
|
||||
])
|
||||
return [ self.getErrorMessage() ]
|
||||
|
||||
|
||||
def authBasicAuthRequest(self, environ, start_response):
|
||||
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
|
||||
authheader = environ['HTTP_AUTHORIZATION']
|
||||
authvalue = ''
|
||||
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
|
||||
authheader = environ["HTTP_AUTHORIZATION"]
|
||||
authvalue = ""
|
||||
try:
|
||||
authvalue = authheader[len("Basic "):]
|
||||
except:
|
||||
authvalue = ''
|
||||
authvalue = authvalue.strip().decode('base64')
|
||||
username, password = authvalue.split(':',1)
|
||||
authvalue = ""
|
||||
authvalue = authvalue.strip().decode("base64")
|
||||
username, password = authvalue.split(":",1)
|
||||
|
||||
if self._domaincontroller.authRealmUser(realmname, username, password, environ):
|
||||
environ['http_authenticator.realm'] = realmname
|
||||
environ['http_authenticator.username'] = username
|
||||
if self._domaincontroller.authDomainUser(realmname, username, password, environ):
|
||||
environ["http_authenticator.realm"] = realmname
|
||||
environ["http_authenticator.username"] = username
|
||||
return self._application(environ, start_response)
|
||||
else:
|
||||
return self.sendBasicAuthResponse(environ, start_response)
|
||||
return self.sendBasicAuthResponse(environ, start_response)
|
||||
|
||||
|
||||
def sendDigestAuthResponse(self, environ, start_response):
|
||||
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
|
||||
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
|
||||
random.seed()
|
||||
serverkey = hex(random.getrandbits(32))[2:]
|
||||
etagkey = md5.new(environ['PATH_INFO']).hexdigest()
|
||||
etagkey = md5(environ["PATH_INFO"]).hexdigest()
|
||||
timekey = str(time.time())
|
||||
nonce = base64.b64encode(timekey + md5.new(timekey + ":" + etagkey + ":" + serverkey).hexdigest())
|
||||
nonce = base64.b64encode(timekey + md5(timekey + ":" + etagkey + ":" + serverkey).hexdigest())
|
||||
wwwauthheaders = "Digest realm=\"" + realmname + "\", nonce=\"" + nonce + \
|
||||
"\", algorithm=\"MD5\", qop=\"auth\""
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print >> environ['wsgi.errors'], "401 Not Authorized for realm '%s': %s" % (realmname, wwwauthheaders)
|
||||
start_response("401 Not Authorized", [('WWW-Authenticate', wwwauthheaders)])
|
||||
return [self.getErrorMessage()]
|
||||
_logger.info("401 Not Authorized for realm '%s' (digest): %s" % (realmname, wwwauthheaders))
|
||||
start_response("401 Not Authorized", [("WWW-Authenticate", wwwauthheaders),
|
||||
("Content-Type", "text/html"),
|
||||
("Date", util.getRfc1123Time()),
|
||||
])
|
||||
return [ self.getErrorMessage() ]
|
||||
|
||||
|
||||
def authDigestAuthRequest(self, environ, start_response):
|
||||
|
||||
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
|
||||
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
|
||||
|
||||
isinvalidreq = False
|
||||
|
||||
authheaderdict = dict([])
|
||||
authheaders = environ['HTTP_AUTHORIZATION'] + ','
|
||||
authheaders = environ["HTTP_AUTHORIZATION"] + ","
|
||||
if not authheaders.lower().strip().startswith("digest"):
|
||||
isinvalidreq = True
|
||||
authheaderlist = self._headerparser.findall(authheaders)
|
||||
|
@ -218,9 +243,11 @@ class HTTPAuthenticator(object):
|
|||
authheaderkey = authheader[0]
|
||||
authheadervalue = authheader[1].strip().strip("\"")
|
||||
authheaderdict[authheaderkey] = authheadervalue
|
||||
|
||||
_logger.info("authDigestAuthRequest: %s" % authheaderdict)
|
||||
|
||||
if 'username' in authheaderdict:
|
||||
req_username = authheaderdict['username']
|
||||
if "username" in authheaderdict:
|
||||
req_username = authheaderdict["username"]
|
||||
req_username_org = req_username
|
||||
# Fix for Windows XP:
|
||||
# net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
|
||||
|
@ -228,87 +255,85 @@ class HTTPAuthenticator(object):
|
|||
# but send the digest for the simple name ('DOMAIN\tester').
|
||||
if r"\\" in req_username:
|
||||
req_username = req_username.replace("\\\\", "\\")
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print >> environ['wsgi.errors'], "Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username)
|
||||
_logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
|
||||
|
||||
if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
|
||||
isinvalidreq = True
|
||||
else:
|
||||
isinvalidreq = True
|
||||
|
||||
# TODO: ??
|
||||
# TODO: Chun added this comments
|
||||
# Do not do realm checking - a hotfix for WinXP using some other realm's
|
||||
# auth details for this realm - if user/password match
|
||||
#if 'realm' in authheaderdict:
|
||||
# if authheaderdict['realm'].upper() != realmname.upper():
|
||||
# if authheaderdict["realm"].upper() != realmname.upper():
|
||||
# isinvalidreq = True
|
||||
|
||||
if 'algorithm' in authheaderdict:
|
||||
if authheaderdict['algorithm'].upper() != "MD5":
|
||||
if "algorithm" in authheaderdict:
|
||||
if authheaderdict["algorithm"].upper() != "MD5":
|
||||
isinvalidreq = True # only MD5 supported
|
||||
|
||||
if 'uri' in authheaderdict:
|
||||
req_uri = authheaderdict['uri']
|
||||
if "uri" in authheaderdict:
|
||||
req_uri = authheaderdict["uri"]
|
||||
|
||||
if 'nonce' in authheaderdict:
|
||||
req_nonce = authheaderdict['nonce']
|
||||
if "nonce" in authheaderdict:
|
||||
req_nonce = authheaderdict["nonce"]
|
||||
else:
|
||||
isinvalidreq = True
|
||||
|
||||
req_hasqop = False
|
||||
if 'qop' in authheaderdict:
|
||||
if "qop" in authheaderdict:
|
||||
req_hasqop = True
|
||||
req_qop = authheaderdict['qop']
|
||||
req_qop = authheaderdict["qop"]
|
||||
if req_qop.lower() != "auth":
|
||||
isinvalidreq = True # only auth supported, auth-int not supported
|
||||
else:
|
||||
req_qop = None
|
||||
|
||||
if 'cnonce' in authheaderdict:
|
||||
req_cnonce = authheaderdict['cnonce']
|
||||
if "cnonce" in authheaderdict:
|
||||
req_cnonce = authheaderdict["cnonce"]
|
||||
else:
|
||||
req_cnonce = None
|
||||
if req_hasqop:
|
||||
isinvalidreq = True
|
||||
|
||||
if 'nc' in authheaderdict: # is read but nonce-count checking not implemented
|
||||
req_nc = authheaderdict['nc']
|
||||
if "nc" in authheaderdict: # is read but nonce-count checking not implemented
|
||||
req_nc = authheaderdict["nc"]
|
||||
else:
|
||||
req_nc = None
|
||||
if req_hasqop:
|
||||
isinvalidreq = True
|
||||
|
||||
if 'response' in authheaderdict:
|
||||
req_response = authheaderdict['response']
|
||||
if "response" in authheaderdict:
|
||||
req_response = authheaderdict["response"]
|
||||
else:
|
||||
isinvalidreq = True
|
||||
|
||||
if not isinvalidreq:
|
||||
req_password = self._domaincontroller.getRealmUserPassword(realmname, req_username, environ)
|
||||
req_method = environ['REQUEST_METHOD']
|
||||
|
||||
req_method = environ["REQUEST_METHOD"]
|
||||
|
||||
required_digest = self.computeDigestResponse(req_username, realmname, req_password, req_method, req_uri, req_nonce, req_cnonce, req_qop, req_nc)
|
||||
|
||||
if required_digest != req_response:
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print >> environ['wsgi.errors'], "computeDigestResponse('%s', '%s', ...): %s != %s" % (realmname, req_username, required_digest, req_response)
|
||||
_logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (realmname, req_username, required_digest, req_response))
|
||||
isinvalidreq = True
|
||||
else:
|
||||
# if environ['wsgidav.verbose'] >= 2:
|
||||
# print >> environ['wsgi.errors'], "digest suceeded for realm '%s', user '%s'" % (realmname, req_username)
|
||||
# _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
|
||||
pass
|
||||
|
||||
if isinvalidreq:
|
||||
if environ['wsgidav.verbose'] >= 2:
|
||||
print >> environ['wsgi.errors'], "Authentication failed for user '%s', realm '%s'" % (req_username, realmname)
|
||||
_logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
|
||||
return self.sendDigestAuthResponse(environ, start_response)
|
||||
|
||||
environ['http_authenticator.realm'] = realmname
|
||||
environ['http_authenticator.username'] = req_username
|
||||
environ["http_authenticator.realm"] = realmname
|
||||
environ["http_authenticator.username"] = req_username
|
||||
return self._application(environ, start_response)
|
||||
|
||||
return self.sendDigestAuthResponse(environ, start_response)
|
||||
|
||||
|
||||
def computeDigestResponse(self, username, realm, password, method, uri, nonce, cnonce, qop, nc):
|
||||
A1 = username + ":" + realm + ":" + password
|
||||
A2 = method + ":" + uri
|
||||
|
@ -318,12 +343,15 @@ class HTTPAuthenticator(object):
|
|||
digestresp = self.md5kd( self.md5h(A1), nonce + ":" + self.md5h(A2))
|
||||
return digestresp
|
||||
|
||||
|
||||
def md5h(self, data):
|
||||
return md5.new(data).hexdigest()
|
||||
return md5(data).hexdigest()
|
||||
|
||||
|
||||
def md5kd(self, secret, data):
|
||||
return self.md5h(secret + ':' + data)
|
||||
return self.md5h(secret + ":" + data)
|
||||
|
||||
|
||||
def getErrorMessage(self):
|
||||
message = """\
|
||||
<html><head><title>401 Access not authorized</title></head>
|
||||
|
@ -332,4 +360,4 @@ class HTTPAuthenticator(object):
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
return message
|
||||
return message
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
class IDAVProvider(object):
|
||||
"""
|
||||
TODO: not sure, if we really need interfaces if we have an abstract base class.
|
||||
|
||||
For now, see wsgidav.DAVProvider.
|
||||
+----------------------------------------------------------------------+
|
||||
| TODO: document this interface |
|
||||
| For now, see wsgidav.DAVProvider instead. |
|
||||
+----------------------------------------------------------------------+
|
||||
|
||||
This class is an interface for a WebDAV provider.
|
||||
Implementations in WsgiDAV include::
|
||||
|
||||
wsgidav.DAVProvider (abstract base class)
|
||||
wsgidav.fs_dav_provider.ReadOnlyFilesystemProvider
|
||||
wsgidav.fs_dav_provider.FilesystemProvider
|
||||
wsgidav.addons.mysql_dav_provider.MySQLBrowserProvider
|
||||
wsgidav.addons.VirtualResourceProvider
|
||||
|
||||
All methods must be implemented.
|
||||
|
||||
"""
|
||||
|
|
|
@ -1,64 +1,36 @@
|
|||
class IDomainController(object):
|
||||
"""
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
+-------------------------------------------------------------------------------+
|
||||
This class is an interface for a PropertyManager. Implementations for the
|
||||
property manager in WsgiDAV include::
|
||||
"""
|
||||
+----------------------------------------------------------------------+
|
||||
| TODO: document this interface |
|
||||
| For now, see wsgidav.domain_controller instead |
|
||||
+----------------------------------------------------------------------+
|
||||
|
||||
This class is an interface for a domain controller.
|
||||
Implementations in WsgiDAV include::
|
||||
|
||||
wsgidav.domain_controller.WsgiDAVDomainController
|
||||
wsgidav.addons.windowsdomaincontroller.SimpleWindowsDomainController
|
||||
wsgidav.domain_controller.WsgiDAVDomainController
|
||||
wsgidav.addons.nt_domain_controller.NTDomainController
|
||||
|
||||
All methods must be implemented.
|
||||
All methods must be implemented.
|
||||
|
||||
The environ variable here is the WSGI 'environ' dictionary. It is passed to
|
||||
all methods of the domain controller as a means for developers to pass information
|
||||
from previous middleware or server config (if required).
|
||||
"""
|
||||
The environ variable here is the WSGI 'environ' dictionary. It is passed to
|
||||
all methods of the domain controller as a means for developers to pass information
|
||||
from previous middleware or server config (if required).
|
||||
|
||||
|
||||
"""
|
||||
Domain Controllers
|
||||
------------------
|
||||
Domain Controllers
|
||||
------------------
|
||||
|
||||
The HTTP basic and digest authentication schemes are based on the following
|
||||
concept:
|
||||
The HTTP basic and digest authentication schemes are based on the following
|
||||
concept:
|
||||
|
||||
Each requested relative URI can be resolved to a realm for authentication,
|
||||
for example:
|
||||
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
|
||||
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
|
||||
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
|
||||
and each realm would have a set of username and password pairs that would
|
||||
allow access to the resource.
|
||||
Each requested relative URI can be resolved to a realm for authentication,
|
||||
for example:
|
||||
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
|
||||
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
|
||||
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
|
||||
and each realm would have a set of username and password pairs that would
|
||||
allow access to the resource.
|
||||
|
||||
A domain controller provides this information to the HTTPAuthenticator.
|
||||
"""
|
||||
|
||||
def getDomainRealm(self, inputURL, environ):
|
||||
"""
|
||||
resolves a relative url to the appropriate realm name
|
||||
"""
|
||||
|
||||
def requireAuthentication(self, realmname, environ):
|
||||
"""
|
||||
returns True if this realm requires authentication
|
||||
or False if it is available for general access
|
||||
"""
|
||||
|
||||
def isRealmUser(self, realmname, username, environ):
|
||||
"""
|
||||
returns True if this username is valid for the realm, False otherwise
|
||||
"""
|
||||
|
||||
def getRealmUserPassword(self, realmname, username, environ):
|
||||
"""
|
||||
returns the password for the given username for the realm.
|
||||
Used for digest authentication.
|
||||
"""
|
||||
|
||||
def authDomainUser(self, realmname, username, password, environ):
|
||||
"""
|
||||
returns True if this username/password pair is valid for the realm,
|
||||
False otherwise. Used for basic authentication.
|
||||
"""
|
||||
A domain controller provides this information to the HTTPAuthenticator.
|
||||
"""
|
||||
|
|
|
@ -1,97 +1,19 @@
|
|||
|
||||
class LockManagerInterface(object):
|
||||
"""
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
| See wsgidav.lock_manager instead |
|
||||
+-------------------------------------------------------------------------------+
|
||||
This class is an interface for a LockManager. Implementations for the lock manager
|
||||
in WsgiDAV include::
|
||||
"""
|
||||
+----------------------------------------------------------------------+
|
||||
| TODO: document this interface |
|
||||
| For now, see wsgidav.lock_manager instead |
|
||||
+----------------------------------------------------------------------+
|
||||
|
||||
This class is an interface for a LockManager.
|
||||
Implementations for the lock manager in WsgiDAV include::
|
||||
|
||||
wsgidav.lock_manager.LockManager
|
||||
wsgidav.lock_manager.LockManager
|
||||
wsgidav.lock_manager.ShelveLockManager
|
||||
|
||||
All methods must be implemented.
|
||||
All methods must be implemented.
|
||||
|
||||
The url variable in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
"""
|
||||
|
||||
def generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockheadurl, timeout):
|
||||
"""
|
||||
returns a new locktoken for the following lock:
|
||||
username - username of user performing the lock
|
||||
locktype - only the locktype "write" is defined in the webdav specification
|
||||
lockscope - "shared" or "exclusive"
|
||||
lockdepth - depth of lock. "0" or "infinity"
|
||||
lockowner - a arbitrary field provided by the client at lock time
|
||||
lockheadurl - the url the lock is being performed on
|
||||
timeout - -1 for infinite, positive value for number of seconds.
|
||||
Could be None, fall back to a default.
|
||||
"""
|
||||
|
||||
def deleteLock(self, locktoken):
|
||||
"""
|
||||
deletes a lock specified by locktoken
|
||||
"""
|
||||
|
||||
def isTokenLockedByUser(self, locktoken, username):
|
||||
"""
|
||||
returns True if locktoken corresponds to a lock locked by username
|
||||
"""
|
||||
|
||||
def isUrlLocked(self, url):
|
||||
"""
|
||||
returns True if the resource at url is locked
|
||||
"""
|
||||
|
||||
def getUrlLockScope(self, url):
|
||||
"""
|
||||
returns the lockscope of all locks on url. 'shared' or 'exclusive'
|
||||
"""
|
||||
|
||||
def getLockProperty(self, locktoken, lockproperty):
|
||||
"""
|
||||
returns the value for the following properties for the lock specified by
|
||||
locktoken:
|
||||
'LOCKUSER', 'LOCKTYPE', 'LOCKSCOPE', 'LOCKDEPTH', 'LOCKOWNER', 'LOCKHEADURL'
|
||||
and
|
||||
'LOCKTIME' - number of seconds left on the lock.
|
||||
"""
|
||||
|
||||
def isUrlLockedByToken(self, url, locktoken):
|
||||
"""
|
||||
returns True if the resource at url is locked by lock specified by locktoken
|
||||
"""
|
||||
|
||||
def getTokenListForUrl(self, url):
|
||||
"""
|
||||
returns a list of locktokens corresponding to locks on url.
|
||||
"""
|
||||
|
||||
def getTokenListForUrlByUser(self, url, username):
|
||||
"""
|
||||
returns a list of locktokens corresponding to locks on url by user username.
|
||||
"""
|
||||
|
||||
def addUrlToLock(self, url, locktoken):
|
||||
"""
|
||||
adds url to be locked by lock specified by locktoken.
|
||||
|
||||
more than one url can be locked by a lock - depth infinity locks.
|
||||
"""
|
||||
|
||||
def removeAllLocksFromUrl(self, url):
|
||||
"""
|
||||
removes all locks from a url.
|
||||
|
||||
This usually happens when the resource specified by url is being deleted.
|
||||
"""
|
||||
|
||||
def refreshLock(self, locktoken, timeout):
|
||||
"""
|
||||
refreshes the lock specified by locktoken.
|
||||
|
||||
timeout : -1 for infinite, positive value for number of seconds.
|
||||
Could be None, fall back to a default.
|
||||
"""
|
||||
The url variable in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
"""
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
|
||||
class PropertyManagerInterface(object):
|
||||
"""
|
||||
+----------------------------------------------------------------------+
|
||||
| TODO: document this interface |
|
||||
| For now, see wsgidav.lock_manager instead |
|
||||
+----------------------------------------------------------------------+
|
||||
|
||||
"""
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
| See wsgidav.property_manager instead |
|
||||
+-------------------------------------------------------------------------------+
|
||||
This class is an interface for a PropertyManager. Implementations for the
|
||||
property manager in WsgiDAV include::
|
||||
This class is an interface for a PropertyManager.
|
||||
Implementations of a property manager in WsgiDAV include::
|
||||
|
||||
wsgidav.property_manager.PropertyManager
|
||||
<wsgidav.property_manager.PropertyManager>_
|
||||
wsgidav.property_manager.ShelvePropertyManager
|
||||
|
||||
All methods must be implemented.
|
||||
All methods must be implemented.
|
||||
|
||||
The url variables in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
The url variable in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
|
||||
|
||||
All methods must be implemented.
|
||||
|
||||
The url variables in methods refers to the relative URL of a resource. e.g. the
|
||||
resource http://server/share1/dir1/dir2/file3.txt would have a url of
|
||||
'/share1/dir1/dir2/file3.txt'
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Properties and WsgiDAV
|
||||
---------------------------
|
||||
----------------------
|
||||
Properties of a resource refers to the attributes of the resource. A property
|
||||
is referenced by the property name and the property namespace. We usually
|
||||
refer to the property as ``{property namespace}property name``
|
||||
|
@ -63,40 +68,3 @@ class PropertyManagerInterface(object):
|
|||
``wsgidav.property_manager``
|
||||
|
||||
"""
|
||||
|
||||
def getProperties(self, normurl):
|
||||
"""
|
||||
return a list of properties for url specified by normurl
|
||||
|
||||
return list is a list of tuples (a, b) where a is the property namespace
|
||||
and b the property name
|
||||
"""
|
||||
|
||||
def getProperty(self, normurl, propname, propns):
|
||||
"""
|
||||
return the value of the property for url specified by normurl where
|
||||
propertyname is propname and property namespace is propns
|
||||
"""
|
||||
|
||||
def writeProperty(self, normurl, propname, propns, propertyvalue):
|
||||
"""
|
||||
write propertyvalue as value of the property for url specified by
|
||||
normurl where propertyname is propname and property namespace is propns
|
||||
"""
|
||||
|
||||
def removeProperty(self, normurl, propname, propns):
|
||||
"""
|
||||
delete the property for url specified by normurl where
|
||||
propertyname is propname and property namespace is propns
|
||||
"""
|
||||
|
||||
def removeProperties(self, normurl):
|
||||
"""
|
||||
delete all properties from url specified by normurl
|
||||
"""
|
||||
|
||||
def copyProperties(self, origurl, desturl):
|
||||
"""
|
||||
copy all properties from url specified by origurl to url specified by desturl
|
||||
"""
|
||||
|
|
@ -8,14 +8,9 @@ lock_manager
|
|||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
A low performance lock manager implementation using shelve.
|
||||
Implements two lock managers: one in-memory (dict-based), and one persistent low
|
||||
performance variant using shelve.
|
||||
|
||||
This module consists of a number of miscellaneous functions for the locks
|
||||
features of WebDAV.
|
||||
|
||||
It also includes an implementation of a LockManager for
|
||||
storage of locks. This implementation use
|
||||
shelve for file storage. See request_server.py for details.
|
||||
|
||||
LockManagers must provide the methods as described in
|
||||
lockmanagerinterface_
|
||||
|
@ -29,15 +24,18 @@ from pprint import pprint
|
|||
from dav_error import DAVError, \
|
||||
HTTP_LOCKED, PRECONDITION_CODE_LockConflict, HTTP_FORBIDDEN,\
|
||||
HTTP_PRECONDITION_FAILED
|
||||
import os
|
||||
import sys
|
||||
import util
|
||||
import shelve
|
||||
import threading
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from rw_lock import ReadWriteLock
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
# TODO: comment's from Ian Bicking (2005)
|
||||
#@@: Use of shelve means this is only really useful in a threaded environment.
|
||||
|
@ -54,59 +52,64 @@ __docformat__ = 'reStructuredText'
|
|||
|
||||
class LockManager(object):
|
||||
"""
|
||||
A low performance lock manager implementation using shelve.
|
||||
An in-memory lock manager implementation using a dictionary.
|
||||
|
||||
This is obviously not persistent, but should be enough in some cases.
|
||||
For a persistent implementation, see lock_manager.ShelveLockManager().
|
||||
"""
|
||||
LOCK_TIME_OUT_DEFAULT = 604800 # 1 week, in seconds
|
||||
|
||||
# any numofsecs above the following limit is regarded as infinite
|
||||
MAX_FINITE_TIMEOUT_LIMIT = 10*365*24*60*60 #approx 10 years
|
||||
|
||||
def __init__(self, persiststore):
|
||||
def __init__(self):
|
||||
self._loaded = False
|
||||
self._dict = None
|
||||
self._init_lock = threading.RLock()
|
||||
self._write_lock = threading.RLock()
|
||||
self._persiststorepath = persiststore
|
||||
self._lock = ReadWriteLock()
|
||||
self._verbose = 2
|
||||
|
||||
|
||||
def _performInitialization(self):
|
||||
self._init_lock.acquire(True)
|
||||
try:
|
||||
if self._loaded: # test again within the critical section
|
||||
# self._lock.release()
|
||||
return True
|
||||
self._dict = shelve.open(self._persiststorepath)
|
||||
self._loaded = True
|
||||
self._cleanup()
|
||||
if self._verbose >= 2:
|
||||
self._dump("After shelve.open()")
|
||||
finally:
|
||||
self._init_lock.release()
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._dict)
|
||||
return "LockManager"
|
||||
|
||||
|
||||
def __del__(self):
|
||||
if self._loaded:
|
||||
self._dict.close()
|
||||
# if self._loaded and hasattr(self._dict, "close"):
|
||||
self._close()
|
||||
|
||||
|
||||
def _lazyOpen(self):
|
||||
_logger.debug("_lazyOpen()")
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
self._dict = {}
|
||||
self._loaded = True
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _sync(self):
|
||||
pass
|
||||
|
||||
|
||||
def _close(self):
|
||||
_logger.debug("_close()")
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
self._dict = None
|
||||
self._loaded = False
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _cleanup(self):
|
||||
"""TODO: Purge expired locks."""
|
||||
pass
|
||||
|
||||
|
||||
def _log(self, msg):
|
||||
if self._verbose >= 2:
|
||||
util.log(msg)
|
||||
|
||||
|
||||
def _dump(self, msg="", out=None):
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
if not out and self._verbose >= 2:
|
||||
return # Already dumped on init
|
||||
if out is None:
|
||||
|
@ -120,7 +123,7 @@ class LockManager(object):
|
|||
def _splitToken(key):
|
||||
return key.split(":", 1)[1]
|
||||
|
||||
print >>out, "LockManager(%s): %s" % (self._persiststorepath, msg)
|
||||
print >>out, "%s: %s" % (self, msg)
|
||||
|
||||
for k, v in self._dict.items():
|
||||
if k.startswith("URL2TOKEN:"):
|
||||
|
@ -155,15 +158,32 @@ class LockManager(object):
|
|||
pprint(ownerDict, indent=4, width=255, stream=out)
|
||||
|
||||
|
||||
def generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockroot, timeout):
|
||||
def _generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockroot, timeout):
|
||||
"""Acquire lock and return lockDict.
|
||||
|
||||
|
||||
username
|
||||
Name of the principal.
|
||||
locktype
|
||||
Must be 'write'.
|
||||
lockscope
|
||||
Must be 'shared' or 'exclusive'.
|
||||
lockdepth
|
||||
Must be '0' or 'infinity'.
|
||||
lockowner
|
||||
String identifying the owner.
|
||||
lockroot
|
||||
Resource URL.
|
||||
timeout
|
||||
Seconds to live
|
||||
|
||||
This function does NOT check, if the new lock creates a conflict!
|
||||
"""
|
||||
# self._log("generateLock(%s, %s, %s, %s, %s, %s, %s)" % (username, lockscope, lockdepth, lockowner, lockroot, timeout))
|
||||
assert locktype == "write"
|
||||
assert lockscope in ("shared", "exclusive")
|
||||
assert lockdepth in ("0", "infinity")
|
||||
assert isinstance(lockowner, str)
|
||||
assert isinstance(lockroot, str)
|
||||
# assert not lockroot.endswith("/")
|
||||
|
||||
if timeout is None:
|
||||
timeout = LockManager.LOCK_TIME_OUT_DEFAULT
|
||||
|
@ -173,8 +193,6 @@ class LockManager(object):
|
|||
timeout = time.time() + timeout
|
||||
|
||||
randtoken = "opaquelocktoken:" + str(hex(random.getrandbits(256)))
|
||||
while randtoken in self._dict:
|
||||
randtoken = "opaquelocktoken:" + str(hex(random.getrandbits(256)))
|
||||
|
||||
lockDict = {"root": lockroot,
|
||||
"type": locktype,
|
||||
|
@ -185,12 +203,12 @@ class LockManager(object):
|
|||
"principal": username,
|
||||
"token": randtoken,
|
||||
}
|
||||
self._log("generateLock %s" % _lockString(lockDict))
|
||||
_logger.debug("_generateLock %s" % _lockString(lockDict))
|
||||
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
#
|
||||
self._dict[randtoken] = lockDict
|
||||
|
||||
|
@ -203,50 +221,49 @@ class LockManager(object):
|
|||
tokList.append(randtoken)
|
||||
self._dict[key] = tokList
|
||||
|
||||
self._dict.sync()
|
||||
# if self._verbose:
|
||||
# self._dump("After generateLock(%s)" % lockroot)
|
||||
self._sync()
|
||||
# if self._verbose >= 2:
|
||||
# self._dump("After _generateLock(%s)" % lockroot)
|
||||
return lockDict
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def getCheckedLock(self, resourceAL,
|
||||
lockroot, locktype, lockscope, lockdepth, lockowner, timeout,
|
||||
user, tokenList):
|
||||
def acquire(self, lockroot, locktype, lockscope, lockdepth,
|
||||
lockowner, timeout, user, tokenList):
|
||||
"""Check for permissions and acquire a lock.
|
||||
|
||||
On success, return length-1 list: [ {newLockDict, None} ]
|
||||
On success, return a one-element list with a tuple: [ (newLockDict, None) ]
|
||||
On error return a list of conflicts (@see self.checkLockPermission)
|
||||
"""
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
conflictList = self.checkLockPermission(resourceAL, lockroot, locktype, lockscope, lockdepth, tokenList, user)
|
||||
self._lazyOpen()
|
||||
conflictList = self.checkLockPermission(lockroot, locktype, lockscope, lockdepth, tokenList, user)
|
||||
if len(conflictList) > 0:
|
||||
return conflictList
|
||||
lockDict = self.generateLock(user, locktype, lockscope, lockdepth, lockowner, lockroot, timeout)
|
||||
lockDict = self._generateLock(user, locktype, lockscope, lockdepth, lockowner, lockroot, timeout)
|
||||
return [ (lockDict, None) ]
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def refreshLock(self, locktoken, timeout=None):
|
||||
def refresh(self, locktoken, timeout=None):
|
||||
"""Set new timeout for lock, if existing and valid."""
|
||||
if timeout is None:
|
||||
timeout = LockManager.LOCK_TIME_OUT_DEFAULT
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
lock = self.getLock(locktoken)
|
||||
self._log("refreshLock %s" % _lockString(lock))
|
||||
_logger.debug("refresh %s" % _lockString(lock))
|
||||
if lock:
|
||||
lock["timeout"] = time.time() + timeout
|
||||
self._dict[locktoken] = lock
|
||||
self._dict.sync()
|
||||
self._sync()
|
||||
return lock
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def getLock(self, locktoken, key=None):
|
||||
|
@ -256,36 +273,40 @@ class LockManager(object):
|
|||
Side effect: if lock is expired, it will be purged and None is returned.
|
||||
"""
|
||||
assert key in (None, "type", "scope", "depth", "owner", "root", "timeout", "principal", "token")
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
lock = self._dict.get(locktoken)
|
||||
if lock is None: # Lock not found: purge dangling URL2TOKEN entries
|
||||
self.deleteLock(locktoken)
|
||||
return None
|
||||
timeout = lock["timeout"]
|
||||
if timeout >= 0 and timeout < time.time():
|
||||
self.deleteLock(locktoken)
|
||||
return None
|
||||
if key is None:
|
||||
return lock
|
||||
else:
|
||||
return lock[key]
|
||||
|
||||
|
||||
def deleteLock(self, locktoken):
|
||||
"""Delete lock and url2token mapping."""
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
lock = self._dict.get(locktoken)
|
||||
self._log("deleteLock %s" % _lockString(lock))
|
||||
if lock is None: # Lock not found: purge dangling URL2TOKEN entries
|
||||
self.release(locktoken)
|
||||
return None
|
||||
timeout = lock["timeout"]
|
||||
if timeout >= 0 and timeout < time.time():
|
||||
self.release(locktoken)
|
||||
return None
|
||||
if key is None:
|
||||
return lock
|
||||
else:
|
||||
return lock[key]
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def release(self, locktoken):
|
||||
"""Delete lock and url2token mapping."""
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._lazyOpen()
|
||||
lock = self._dict.get(locktoken)
|
||||
_logger.debug("release %s" % _lockString(lock))
|
||||
if lock is None:
|
||||
return False
|
||||
# Remove url to lock mapping
|
||||
key = "URL2TOKEN:%s" % lock.get("root")
|
||||
if key in self._dict:
|
||||
# self._log(" delete token %s from url %s" % (locktoken, lock.get("root")))
|
||||
# _logger.debug(" delete token %s from url %s" % (locktoken, lock.get("root")))
|
||||
tokList = self._dict[key]
|
||||
if len(tokList) > 1:
|
||||
# Note: shelve dictionary returns copies, so we must reassign values:
|
||||
|
@ -296,11 +317,11 @@ class LockManager(object):
|
|||
# Remove the lock
|
||||
del self._dict[locktoken]
|
||||
|
||||
# if self._verbose:
|
||||
# self._dump("After deleteLock(%s)" % locktoken)
|
||||
self._dict.sync()
|
||||
# if self._verbose >= 2:
|
||||
# self._dump("After release(%s)" % locktoken)
|
||||
self._sync()
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def isTokenLockedByUser(self, token, username):
|
||||
|
@ -312,16 +333,21 @@ class LockManager(object):
|
|||
"""Return list of lockDict, if <url> is protected by at least one direct, valid lock.
|
||||
|
||||
Side effect: expired locks for this url are purged.
|
||||
"""
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
key = "URL2TOKEN:%s" % url
|
||||
lockList = []
|
||||
for tok in self._dict.get(key, []):
|
||||
lock = self.getLock(tok)
|
||||
if lock and (username is None or username == lock["principal"]):
|
||||
lockList.append(lock)
|
||||
return lockList
|
||||
"""
|
||||
# assert url and not url.endswith("/")
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._lazyOpen()
|
||||
key = "URL2TOKEN:%s" % url
|
||||
lockList = []
|
||||
for tok in self._dict.get(key, []):
|
||||
lock = self.getLock(tok)
|
||||
if lock and (username is None or username == lock["principal"]):
|
||||
lockList.append(lock)
|
||||
return lockList
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def getIndirectUrlLockList(self, url, username=None):
|
||||
|
@ -330,22 +356,26 @@ class LockManager(object):
|
|||
If a username is given, only locks owned by this principal are returned.
|
||||
Side effect: expired locks for this url and all parents are purged.
|
||||
"""
|
||||
lockList = []
|
||||
u = url
|
||||
while u:
|
||||
# TODO: check, if expired
|
||||
ll = self.getUrlLockList(u)
|
||||
for l in ll:
|
||||
if u != url and l["depth"] != "infinity":
|
||||
continue # We only consider parents with Depth: inifinity
|
||||
# TODO: handle shared locks in some way?
|
||||
# if l["scope"] == "shared" and lockscope == "shared" and username != l["principal"]:
|
||||
# continue # Only compatible with shared locks by other users
|
||||
if username == l["principal"]:
|
||||
lockList.append(l)
|
||||
u = util.getUriParent(u)
|
||||
# self._log("getIndirectUrlLockList(%s, %s): %s" % (url, username, lockList))
|
||||
return lockList
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
lockList = []
|
||||
u = url
|
||||
while u:
|
||||
# TODO: check, if expired
|
||||
ll = self.getUrlLockList(u)
|
||||
for l in ll:
|
||||
if u != url and l["depth"] != "infinity":
|
||||
continue # We only consider parents with Depth: infinity
|
||||
# TODO: handle shared locks in some way?
|
||||
# if l["scope"] == "shared" and lockscope == "shared" and username != l["principal"]:
|
||||
# continue # Only compatible with shared locks by other users
|
||||
if username == l["principal"]:
|
||||
lockList.append(l)
|
||||
u = util.getUriParent(u)
|
||||
# _logger.debug("getIndirectUrlLockList(%s, %s): %s" % (url, username, lockList))
|
||||
return lockList
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def isUrlLocked(self, url):
|
||||
|
@ -354,32 +384,31 @@ class LockManager(object):
|
|||
return len(lockList) > 0
|
||||
|
||||
|
||||
def getUrlLockScope(self, url):
|
||||
lockList = self.getUrlLockList(url)
|
||||
# either one exclusive lock, or many shared locks - first lock will give lock scope
|
||||
if len(lockList) > 0:
|
||||
return lockList[0].get("scope")
|
||||
return None
|
||||
# def getUrlLockScope(self, url):
|
||||
# lockList = self.getUrlLockList(url)
|
||||
# # either one exclusive lock, or many shared locks - first lock will give lock scope
|
||||
# if len(lockList) > 0:
|
||||
# return lockList[0].get("scope")
|
||||
# return None
|
||||
|
||||
|
||||
def isUrlLockedByToken(self, url, locktoken):
|
||||
"""Check, if url is directly or indirectly locked by locktoken."""
|
||||
"""Check, if url (or any of it's parents) is locked by locktoken."""
|
||||
lockUrl = self.getLock(locktoken, "root")
|
||||
# FIXME: make sure '/a/b' doesn't match '/a/bb'
|
||||
return ( lockUrl and url.startswith(lockUrl) )
|
||||
return lockUrl and util.isEqualOrChildUri(lockUrl, url)
|
||||
|
||||
|
||||
def removeAllLocksFromUrl(self, url):
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
lockList = self.getUrlLockList(url)
|
||||
for lock in lockList:
|
||||
self.deleteLock(lock["token"])
|
||||
self.release(lock["token"])
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def checkLockPermission(self, resourceAL, lockroot, locktype, lockscope, lockdepth, tokenList, user):
|
||||
def checkLockPermission(self, lockroot, locktype, lockscope, lockdepth, tokenList, user):
|
||||
"""Check, if <user> can lock <lockroot>, otherwise return a list of conflicting locks.
|
||||
|
||||
An empty list is returned, if the lock would be granted.
|
||||
|
@ -394,6 +423,7 @@ class LockManager(object):
|
|||
@see http://www.webdav.org/specs/rfc4918.html#lock-model
|
||||
|
||||
TODO: verify assumptions:
|
||||
|
||||
- Parent locks WILL NOT be conflicting, if they are depth-0.
|
||||
- Exclusive depth-infinity parent locks WILL be conflicting, even if they
|
||||
are owned by <user>.
|
||||
|
@ -403,7 +433,7 @@ class LockManager(object):
|
|||
different applications on his client.)
|
||||
- TODO: Can <user> lock-exclusive, if she holds a parent shared-lock?
|
||||
(currently NO; it would only make sense, if he was the only shared-lock holder)
|
||||
- TODO: litmus tries to acquire a shared lock one resource twice (locks: 27 'double_sharedlock')
|
||||
- TODO: litmus tries to acquire a shared lock on one resource twice (locks: 27 'double_sharedlock')
|
||||
and fails, when we return HTTP_LOCKED. So we allow multi shared locks
|
||||
on a resource even for the same principal
|
||||
|
||||
|
@ -424,52 +454,56 @@ class LockManager(object):
|
|||
assert lockscope in ("shared", "exclusive")
|
||||
assert lockdepth in ("0", "infinity")
|
||||
|
||||
self._log("checkLockPermission(%s, %s, %s, %s)" % (lockroot, lockscope, lockdepth, user))
|
||||
_logger.debug("checkLockPermission(%s, %s, %s, %s)" % (lockroot, lockscope, lockdepth, user))
|
||||
|
||||
conflictLockList = []
|
||||
|
||||
# Check lockroot and all parents for conflicting locks
|
||||
u = lockroot
|
||||
while u:
|
||||
# TODO: check, if expired
|
||||
ll = self.getUrlLockList(u)
|
||||
for l in ll:
|
||||
self._log(" check parent %s, %s" % (u, l))
|
||||
if u != lockroot and l["depth"] != "infinity":
|
||||
continue # We only consider parents with Depth: infinity
|
||||
elif l["scope"] == "shared" and lockscope == "shared": # and user != l["principal"]:
|
||||
continue # Only compatible with shared locks (even by same principal)
|
||||
# Return first lock as a list (in a sane system there can be max. one anyway)
|
||||
self._log(" -> DENIED due to locked parent %s" % _lockString(l))
|
||||
return [ (l, DAVError(HTTP_LOCKED)) ]
|
||||
# TODO: this also will check the realm level itself
|
||||
u = util.getUriParent(u)
|
||||
|
||||
if lockdepth == "0":
|
||||
return conflictLockList
|
||||
|
||||
# TODO: we could exit also, if lockroot is not a collection or assert that depth=0 in this case
|
||||
|
||||
# Check child urls for conflicting locks
|
||||
prefix = "URL2TOKEN:" + lockroot
|
||||
if not prefix.endswith("/"):
|
||||
prefix += "/"
|
||||
|
||||
for u, ll in self._dict.items():
|
||||
if not u.startswith(prefix):
|
||||
continue # Not a child
|
||||
# TODO: check, if expired
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
conflictLockList = []
|
||||
|
||||
# Check lockroot and all parents for conflicting locks
|
||||
u = lockroot
|
||||
while u:
|
||||
# TODO: check, if expired
|
||||
ll = self.getUrlLockList(u)
|
||||
for l in ll:
|
||||
_logger.debug(" check parent %s, %s" % (u, l))
|
||||
if u != lockroot and l["depth"] != "infinity":
|
||||
continue # We only consider parents with Depth: infinity
|
||||
elif l["scope"] == "shared" and lockscope == "shared": # and user != l["principal"]:
|
||||
continue # Only compatible with shared locks (even by same principal)
|
||||
# Return first lock as a list (in a sane system there can be max. one anyway)
|
||||
_logger.debug(" -> DENIED due to locked parent %s" % _lockString(l))
|
||||
return [ (l, DAVError(HTTP_LOCKED)) ]
|
||||
# TODO: this also will check the realm level itself
|
||||
u = util.getUriParent(u)
|
||||
|
||||
if lockdepth == "0":
|
||||
return conflictLockList
|
||||
|
||||
for l in ll:
|
||||
lockDict = self.getLock(l)
|
||||
# self._log(" check child %s, %s" % (u, l))
|
||||
# TODO: no-conflicting-lock can pass a list of href elements too:
|
||||
conflictLockList.append((lockDict,
|
||||
DAVError(HTTP_PRECONDITION_FAILED,
|
||||
preconditionCode=PRECONDITION_CODE_LockConflict)
|
||||
))
|
||||
self._log(" -> DENIED due to locked child %s" % _lockString(lockDict))
|
||||
return conflictLockList
|
||||
# TODO: we could exit also, if lockroot is not a collection or assert that depth=0 in this case
|
||||
|
||||
# Check child urls for conflicting locks
|
||||
prefix = "URL2TOKEN:" + lockroot
|
||||
if not prefix.endswith("/"):
|
||||
prefix += "/"
|
||||
|
||||
for u, ll in self._dict.items():
|
||||
if not u.startswith(prefix):
|
||||
continue # Not a child
|
||||
# TODO: check, if expired
|
||||
|
||||
for l in ll:
|
||||
lockDict = self.getLock(l)
|
||||
_logger.debug(" check child %s, %s" % (u, l))
|
||||
# TODO: no-conflicting-lock can pass a list of href elements too:
|
||||
conflictLockList.append((lockDict,
|
||||
DAVError(HTTP_PRECONDITION_FAILED,
|
||||
preconditionCode=PRECONDITION_CODE_LockConflict)
|
||||
))
|
||||
_logger.debug(" -> DENIED due to locked child %s" % _lockString(lockDict))
|
||||
return conflictLockList
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def checkAccessPermission(self, resourceAL, url, tokenList, accesstype, accessdepth, user):
|
||||
|
@ -504,71 +538,139 @@ class LockManager(object):
|
|||
|
||||
assert accesstype == "write"
|
||||
assert accessdepth in ("0", "infinity")
|
||||
conflictLockList = []
|
||||
self._log("checkAccessPermission(%s, %s, %s, %s)" % (url, tokenList, accessdepth, user))
|
||||
_logger.debug("checkAccessPermission(%s, %s, %s, %s)" % (url, tokenList, accessdepth, user))
|
||||
|
||||
# Check url and all parents for conflicting locks
|
||||
u = url
|
||||
while u:
|
||||
# TODO: check, if expired
|
||||
ll = self.getUrlLockList(u)
|
||||
# self._log(" checking %s" % u)
|
||||
for l in ll:
|
||||
self._log(" l=%s" % l)
|
||||
if u != url and l["depth"] != "infinity":
|
||||
continue # We only consider parents with Depth: inifinity
|
||||
elif user == l["principal"] and l["token"] in tokenList:
|
||||
continue # User owns this lock
|
||||
elif l["token"] in tokenList:
|
||||
# Token is owned by another user
|
||||
conflictLockList.append((l, DAVError(HTTP_FORBIDDEN)))
|
||||
self._log(" -> DENIED due to locked parent %s" % _lockString(l))
|
||||
else:
|
||||
# Token is owned by user, but not passed with lock list
|
||||
# TODO: no-conflicting-lock can pass a list of href elements too:
|
||||
conflictLockList.append((l, DAVError(HTTP_LOCKED,
|
||||
preconditionCode=PRECONDITION_CODE_LockConflict)))
|
||||
self._log(" -> DENIED due to locked parent %s" % _lockString(l))
|
||||
u = util.getUriParent(u)
|
||||
|
||||
if accessdepth == "0":
|
||||
# We only request flat access, so no need to check for child locks
|
||||
return conflictLockList
|
||||
|
||||
# TODO: we could exit also, if <url> is not a collection
|
||||
|
||||
# Check child urls for conflicting locks
|
||||
prefix = "URL2TOKEN:" + url
|
||||
if not prefix.endswith("/"):
|
||||
prefix += "/"
|
||||
|
||||
for u, ll in self._dict.items():
|
||||
if not u.startswith(prefix):
|
||||
continue # Not a child
|
||||
# TODO: check, if expired
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
conflictLockList = []
|
||||
# Check url and all parents for conflicting locks
|
||||
u = url
|
||||
while u:
|
||||
# print "checking ", u
|
||||
# if u != "/":
|
||||
# u = u.rstrip("/")
|
||||
# TODO: check, if expired
|
||||
ll = self.getUrlLockList(u)
|
||||
# _logger.debug(" checking %s" % u)
|
||||
for l in ll:
|
||||
_logger.debug(" l=%s" % l)
|
||||
if u != url and l["depth"] != "infinity":
|
||||
continue # We only consider parents with Depth: inifinity
|
||||
elif user == l["principal"] and l["token"] in tokenList:
|
||||
continue # User owns this lock
|
||||
elif l["token"] in tokenList:
|
||||
# Token is owned by another user
|
||||
conflictLockList.append((l, DAVError(HTTP_FORBIDDEN)))
|
||||
_logger.debug(" -> DENIED due to locked parent %s" % _lockString(l))
|
||||
else:
|
||||
# Token is owned by user, but not passed with lock list
|
||||
# TODO: no-conflicting-lock can pass a list of href elements too:
|
||||
conflictLockList.append((l, DAVError(HTTP_LOCKED,
|
||||
preconditionCode=PRECONDITION_CODE_LockConflict)))
|
||||
_logger.debug(" -> DENIED due to locked parent %s" % _lockString(l))
|
||||
u = util.getUriParent(u)
|
||||
|
||||
if accessdepth == "0":
|
||||
# We only request flat access, so no need to check for child locks
|
||||
return conflictLockList
|
||||
|
||||
for tok in ll:
|
||||
l = self.getLock(tok)
|
||||
# TODO: no-conflicting-lock can pass a list of href elements too:
|
||||
conflictLockList.append((l,
|
||||
DAVError(HTTP_LOCKED,
|
||||
preconditionCode=PRECONDITION_CODE_LockConflict)
|
||||
))
|
||||
self._log(" -> DENIED due to locked child %s" % _lockString(l))
|
||||
|
||||
return conflictLockList
|
||||
# TODO: we could exit also, if <url> is not a collection
|
||||
|
||||
# Check child urls for conflicting locks
|
||||
prefix = "URL2TOKEN:" + url
|
||||
if not prefix.endswith("/"):
|
||||
prefix += "/"
|
||||
|
||||
for u, ll in self._dict.items():
|
||||
if not u.startswith(prefix):
|
||||
continue # Not a child
|
||||
# TODO: check, if expired
|
||||
|
||||
for tok in ll:
|
||||
l = self.getLock(tok)
|
||||
# TODO: no-conflicting-lock can pass a list of href elements too:
|
||||
conflictLockList.append((l,
|
||||
DAVError(HTTP_LOCKED,
|
||||
preconditionCode=PRECONDITION_CODE_LockConflict)
|
||||
))
|
||||
_logger.debug(" -> DENIED due to locked child %s" % _lockString(l))
|
||||
|
||||
return conflictLockList
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# ShelveLockManager
|
||||
#===============================================================================
|
||||
class ShelveLockManager(LockManager):
|
||||
"""
|
||||
A low performance lock manager implementation using shelve.
|
||||
"""
|
||||
def __init__(self, storagePath):
|
||||
self._storagePath = os.path.abspath(storagePath)
|
||||
super(ShelveLockManager, self).__init__()
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "ShelveLockManager(%s)" % self._storagePath
|
||||
|
||||
|
||||
def _lazyOpen(self):
|
||||
_logger.debug("_lazyOpen(%s)" % self._storagePath)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
# Test again within the critical section
|
||||
if self._loaded:
|
||||
return True
|
||||
# Open with writeback=False, which is faster, but we have to be
|
||||
# careful to re-assign values to _dict after modifying them
|
||||
self._dict = shelve.open(self._storagePath,
|
||||
writeback=False)
|
||||
self._loaded = True
|
||||
if __debug__ and self._verbose >= 2:
|
||||
# self._check("After shelve.open()")
|
||||
self._dump("After shelve.open()")
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _sync(self):
|
||||
"""Write persistent dictionary to disc."""
|
||||
_logger.debug("_sync()")
|
||||
self._lock.acquireWrite() # TODO: read access is enough?
|
||||
try:
|
||||
if self._loaded:
|
||||
self._dict.sync()
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _close(self):
|
||||
_logger.debug("_close()")
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if self._loaded:
|
||||
self._dict.close()
|
||||
self._dict = None
|
||||
self._loaded = False
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Tool functions
|
||||
#===============================================================================
|
||||
|
||||
reSecondsReader = re.compile(r'second\-([0-9]+)', re.I)
|
||||
|
||||
def readTimeoutValueHeader(timeoutvalue):
|
||||
"""Return -1 if infinite, else return numofsecs."""
|
||||
timeoutsecs = 0
|
||||
timeoutvaluelist = timeoutvalue.split(',')
|
||||
timeoutvaluelist = timeoutvalue.split(",")
|
||||
for timeoutspec in timeoutvaluelist:
|
||||
timeoutspec = timeoutspec.strip()
|
||||
if timeoutspec.lower() == 'infinite':
|
||||
if timeoutspec.lower() == "infinite":
|
||||
return -1
|
||||
else:
|
||||
listSR = reSecondsReader.findall(timeoutspec)
|
||||
|
@ -595,8 +697,8 @@ def _lockString(lockDict):
|
|||
|
||||
|
||||
def test():
|
||||
l = LockManager("wsgidav-locks.shelve")
|
||||
l._performInitialization()
|
||||
l = ShelveLockManager("wsgidav-locks.shelve")
|
||||
l._lazyOpen()
|
||||
l._dump()
|
||||
# l.generateLock("martin", "", lockscope, lockdepth, lockowner, lockroot, timeout)
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ property_manager
|
|||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
A low performance property manager implementation using shelve.
|
||||
Implements two property managers: one in-memory (dict-based), and one
|
||||
persistent low performance variant using shelve.
|
||||
|
||||
This module consists of a number of miscellaneous functions for the dead
|
||||
properties features of WebDAV.
|
||||
|
@ -67,7 +68,10 @@ Dead properties
|
|||
``wsgidav.property_manager``.
|
||||
"""
|
||||
from wsgidav import util
|
||||
import traceback
|
||||
import os
|
||||
import sys
|
||||
import shelve
|
||||
from rw_lock import ReadWriteLock
|
||||
|
||||
# TODO: comment's from Ian Bicking (2005)
|
||||
#@@: Use of shelve means this is only really useful in a threaded environment.
|
||||
|
@ -80,41 +84,64 @@ import traceback
|
|||
# in a parallel directory structure to the files you are describing.
|
||||
# Pickle is expedient, but later you could use something more readable
|
||||
# (pickles aren't particularly readable)
|
||||
import sys
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
import shelve
|
||||
import threading
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# PropertyManager
|
||||
#===============================================================================
|
||||
class PropertyManager(object):
|
||||
"""
|
||||
A low performance property manager implementation using shelve
|
||||
An in-memory property manager implementation using a dictionary.
|
||||
|
||||
This is obviously not persistent, but should be enough in some cases.
|
||||
For a persistent implementation, see property_manager.ShelvePropertyManager().
|
||||
"""
|
||||
def __init__(self, persiststore):
|
||||
self._loaded = False
|
||||
def __init__(self):
|
||||
self._dict = None
|
||||
self._init_lock = threading.RLock()
|
||||
self._write_lock = threading.RLock()
|
||||
self._persiststorepath = persiststore
|
||||
self._loaded = False
|
||||
self._lock = ReadWriteLock()
|
||||
self._verbose = 2
|
||||
|
||||
|
||||
def _performInitialization(self):
|
||||
self._init_lock.acquire(True)
|
||||
def __repr__(self):
|
||||
return "PropertyManager"
|
||||
|
||||
|
||||
def __del__(self):
|
||||
if __debug__ and self._verbose >= 2:
|
||||
self._check()
|
||||
self._close()
|
||||
|
||||
|
||||
def _lazyOpen(self):
|
||||
_logger.debug("_lazyOpen()")
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if self._loaded: # test again within the critical section
|
||||
return True
|
||||
self._dict = shelve.open(self._persiststorepath)
|
||||
self._dict = {}
|
||||
self._loaded = True
|
||||
if self._verbose >= 2:
|
||||
self._dump("After shelve.open()")
|
||||
finally:
|
||||
self._init_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _sync(self):
|
||||
pass
|
||||
|
||||
|
||||
def _close(self):
|
||||
_logger.debug("_close()")
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
self._dict = None
|
||||
self._loaded = False
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _check(self, msg=""):
|
||||
try:
|
||||
if not self._loaded:
|
||||
|
@ -124,27 +151,24 @@ class PropertyManager(object):
|
|||
# print " -> %s" % self._dict[k]
|
||||
for k, v in self._dict.items():
|
||||
_ = "%s, %s" % (k, v)
|
||||
self._log("PropertyManager checks ok " + msg)
|
||||
_logger.debug("%s checks ok %s" % (self.__class__.__name__, msg))
|
||||
return True
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
_logger.exception("%s _check: ERROR %s" % (self.__class__.__name__, msg))
|
||||
# traceback.print_exc()
|
||||
# raise
|
||||
# sys.exit(-1)
|
||||
return False
|
||||
|
||||
def _log(self, msg):
|
||||
if self._verbose >= 2:
|
||||
util.log(msg)
|
||||
|
||||
|
||||
def _dump(self, msg="", out=None):
|
||||
if out is None:
|
||||
out = sys.stdout
|
||||
print >>out, "PropertyManager(%s): %s" % (self._persiststorepath, msg)
|
||||
print >>out, "%s(%s): %s" % (self.__class__.__name__, self.__repr__(), msg)
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
if self._verbose >= 2:
|
||||
return # Already dumped in _performInitialization
|
||||
return # Already dumped in _lazyOpen
|
||||
try:
|
||||
for k, v in self._dict.items():
|
||||
print >>out, " ", k
|
||||
|
@ -154,107 +178,180 @@ class PropertyManager(object):
|
|||
except Exception, e:
|
||||
print >>out, " %s: ERROR %s" % (k2, e)
|
||||
except Exception, e:
|
||||
print >>sys.stderr, "PropertyManager._dump() ERROR: %s" % e
|
||||
|
||||
print >>sys.stderr, "PropertyManager._dump() ERROR: %s" % e
|
||||
|
||||
|
||||
def getProperties(self, normurl):
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
returnlist = []
|
||||
if normurl in self._dict:
|
||||
for propdata in self._dict[normurl].keys():
|
||||
returnlist.append(propdata)
|
||||
return returnlist
|
||||
_logger.debug("getProperties(%s)" % normurl)
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._lazyOpen()
|
||||
returnlist = []
|
||||
if normurl in self._dict:
|
||||
for propdata in self._dict[normurl].keys():
|
||||
returnlist.append(propdata)
|
||||
return returnlist
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def getProperty(self, normurl, propname):
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
if normurl not in self._dict:
|
||||
return None
|
||||
# TODO: sometimes we get exceptions here: (catch or otherwise make more robust?)
|
||||
_logger.debug("getProperty(%s, %s)" % (normurl, propname))
|
||||
self._lock.acquireRead()
|
||||
try:
|
||||
resourceprops = self._dict[normurl]
|
||||
except Exception, e:
|
||||
util.log("getProperty(%s, %s) failed : %s" % (normurl, propname, e))
|
||||
raise
|
||||
return resourceprops.get(propname)
|
||||
if not self._loaded:
|
||||
self._lazyOpen()
|
||||
if normurl not in self._dict:
|
||||
return None
|
||||
# TODO: sometimes we get exceptions here: (catch or otherwise make more robust?)
|
||||
try:
|
||||
resourceprops = self._dict[normurl]
|
||||
except Exception, e:
|
||||
_logger.exception("getProperty(%s, %s) failed : %s" % (normurl, propname, e))
|
||||
raise
|
||||
return resourceprops.get(propname)
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def writeProperty(self, normurl, propname, propertyvalue, dryRun=False):
|
||||
self._log("writeProperty(%s, %s, dryRun=%s):\n\t%s" % (normurl, propname, dryRun, propertyvalue))
|
||||
# self._log("writeProperty(%s, %s, dryRun=%s):\n\t%s" % (normurl, propname, dryRun, propertyvalue))
|
||||
assert normurl and normurl.startswith("/")
|
||||
assert propname #and propname.startswith("{")
|
||||
assert propertyvalue is not None
|
||||
|
||||
_logger.debug("writeProperty(%s, %s, dryRun=%s):\n\t%s" % (normurl, propname, dryRun, propertyvalue))
|
||||
if dryRun:
|
||||
return # TODO: can we check anything here?
|
||||
|
||||
propertyname = propname
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
if normurl in self._dict:
|
||||
locatordict = self._dict[normurl]
|
||||
else:
|
||||
locatordict = dict([])
|
||||
locatordict[propertyname] = propertyvalue
|
||||
locatordict = {} #dict([])
|
||||
locatordict[propname] = propertyvalue
|
||||
# This re-assignment is important, so Shelve realizes the change:
|
||||
self._dict[normurl] = locatordict
|
||||
self._dict.sync()
|
||||
self._sync()
|
||||
if __debug__ and self._verbose >= 2:
|
||||
self._check()
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._check()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def removeProperty(self, normurl, propname, dryRun=False):
|
||||
"""
|
||||
Specifying the removal of a property that does not exist is NOT an error.
|
||||
"""
|
||||
self._log("removeProperty(%s, %s, dryRun=%s)" % (normurl, propname, dryRun))
|
||||
_logger.debug("removeProperty(%s, %s, dryRun=%s)" % (normurl, propname, dryRun))
|
||||
if dryRun:
|
||||
# TODO: can we check anything here?
|
||||
return
|
||||
propertyname = propname
|
||||
self._write_lock.acquire(True)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
if normurl in self._dict:
|
||||
locatordict = self._dict[normurl]
|
||||
if propertyname in locatordict:
|
||||
del locatordict[propertyname]
|
||||
if propname in locatordict:
|
||||
del locatordict[propname]
|
||||
# This re-assignment is important, so Shelve realizes the change:
|
||||
self._dict[normurl] = locatordict
|
||||
self._dict.sync()
|
||||
self._sync()
|
||||
if __debug__ and self._verbose >= 2:
|
||||
self._check()
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._check()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def removeProperties(self, normurl):
|
||||
self._write_lock.acquire(True)
|
||||
self._log("removeProperties(%s)" % normurl)
|
||||
_logger.debug("removeProperties(%s)" % normurl)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
self._lazyOpen()
|
||||
if normurl in self._dict:
|
||||
del self._dict[normurl]
|
||||
self._sync()
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._lock.release()
|
||||
|
||||
def copyProperties(self, origurl, desturl):
|
||||
self._write_lock.acquire(True)
|
||||
self._log("copyProperties(%s, %s)" % (origurl, desturl))
|
||||
self._check()
|
||||
|
||||
def copyProperties(self, srcurl, desturl):
|
||||
_logger.debug("copyProperties(%s, %s)" % (srcurl, desturl))
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if __debug__ and self._verbose >= 2:
|
||||
self._check()
|
||||
if not self._loaded:
|
||||
self._performInitialization()
|
||||
if origurl in self._dict:
|
||||
self._dict[desturl] = self._dict[origurl].copy()
|
||||
self._lazyOpen()
|
||||
if srcurl in self._dict:
|
||||
self._dict[desturl] = self._dict[srcurl].copy()
|
||||
self._sync()
|
||||
if __debug__ and self._verbose >= 2:
|
||||
self._check("after copy")
|
||||
finally:
|
||||
self._write_lock.release()
|
||||
self._check("after copy")
|
||||
self._lock.release()
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# ShelvePropertyManager
|
||||
#===============================================================================
|
||||
|
||||
class ShelvePropertyManager(PropertyManager):
|
||||
"""
|
||||
A low performance property manager implementation using shelve
|
||||
"""
|
||||
def __init__(self, storagePath):
|
||||
self._storagePath = os.path.abspath(storagePath)
|
||||
super(ShelvePropertyManager, self).__init__()
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._dict)
|
||||
return "ShelvePropertyManager(%s)" % self._storagePath
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self._check()
|
||||
if self._loaded:
|
||||
self._dict.close()
|
||||
self._check()
|
||||
def _lazyOpen(self):
|
||||
_logger.debug("_lazyOpen(%s)" % self._storagePath)
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
# Test again within the critical section
|
||||
if self._loaded:
|
||||
return True
|
||||
# Open with writeback=False, which is faster, but we have to be
|
||||
# careful to re-assign values to _dict after modifying them
|
||||
self._dict = shelve.open(self._storagePath,
|
||||
writeback=False)
|
||||
self._loaded = True
|
||||
if __debug__ and self._verbose >= 2:
|
||||
self._check("After shelve.open()")
|
||||
self._dump("After shelve.open()")
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _sync(self):
|
||||
"""Write persistent dictionary to disc."""
|
||||
_logger.debug("_sync()")
|
||||
self._lock.acquireWrite() # TODO: read access is enough?
|
||||
try:
|
||||
if self._loaded:
|
||||
self._dict.sync()
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _close(self):
|
||||
_logger.debug("_close()")
|
||||
self._lock.acquireWrite()
|
||||
try:
|
||||
if self._loaded:
|
||||
self._dict.close()
|
||||
self._dict = None
|
||||
self._loaded = False
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
request_resolver
|
||||
===============
|
||||
================
|
||||
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
|
@ -9,6 +9,8 @@ request_resolver
|
|||
WSGI middleware that finds the registered mapped DAV-Provider, creates a new
|
||||
RequestServer instance, and dispatches the request.
|
||||
|
||||
|
||||
|
||||
+-------------------------------------------------------------------------------+
|
||||
| The following documentation was taken over from PyFileServer and is outdated! |
|
||||
+-------------------------------------------------------------------------------+
|
||||
|
@ -95,13 +97,10 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
import util
|
||||
#import urllib
|
||||
from dav_error import DAVError, HTTP_NOT_FOUND
|
||||
from request_server import RequestServer
|
||||
#from threading import local
|
||||
#_tls = local() # Thread local storage dict
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
# NOTE (Martin Wendt, 2009-05):
|
||||
# The following remarks were made by Ian Bicking when reviewing PyFileServer in 2005.
|
||||
|
@ -168,7 +167,7 @@ class RequestResolver(object):
|
|||
# Hotfix for WinXP / Vista: check for '/' also
|
||||
if environ["REQUEST_METHOD"] == "OPTIONS" and path in ("/", "*"):
|
||||
# Answer HTTP 'OPTIONS' method on server-level.
|
||||
# From RFC 2616
|
||||
# From RFC 2616:
|
||||
# If the Request-URI is an asterisk ("*"), the OPTIONS request is
|
||||
# intended to apply to the server in general rather than to a specific
|
||||
# resource. Since a server's communication options typically depend on
|
||||
|
@ -176,13 +175,13 @@ class RequestResolver(object):
|
|||
# type of method; it does nothing beyond allowing the client to test the
|
||||
# capabilities of the server. For example, this can be used to test a
|
||||
# proxy for HTTP/1.1 compliance (or lack thereof).
|
||||
start_response('200 OK', [('Content-Type', 'text/html'),
|
||||
('Content-Length', '0'),
|
||||
('DAV', '1,2'),
|
||||
('Server', 'DAV/2'),
|
||||
('Date', util.getRfc1123Time()),
|
||||
start_response("200 OK", [("Content-Type", "text/html"),
|
||||
("Content-Length", "0"),
|
||||
("DAV", "1,2"),
|
||||
("Server", "DAV/2"),
|
||||
("Date", util.getRfc1123Time()),
|
||||
])
|
||||
# return ['']
|
||||
# return [""]
|
||||
yield [ "" ]
|
||||
return
|
||||
|
||||
|
@ -192,16 +191,13 @@ class RequestResolver(object):
|
|||
"Could not find resource provider for '%s'" % path)
|
||||
# Let the appropriate resource provider for the realm handle the request
|
||||
|
||||
# **************************
|
||||
# ************************** only to make sure we have no __del__
|
||||
# **************************
|
||||
# **************************
|
||||
app = RequestServer(provider)
|
||||
environ["wsgidav.request_server"] = app # Keep it alive for the whole request lifetime
|
||||
# environ["wsgidav.request_server"] = app # Keep it alive for the whole request lifetime
|
||||
# _tls.app = app # TODO: try to avoid Server socket closed
|
||||
for v in app(environ, start_response):
|
||||
util.debug("sc", "RequestResolver: yield start")
|
||||
yield v
|
||||
util.log("Response", v)
|
||||
util.debug("sc", "RequestResolver: yield end")
|
||||
return
|
||||
# return app(environ, start_response)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Running WsgiDAV
|
||||
====================
|
||||
===============
|
||||
|
||||
WsgiDAV comes bundled with a simple WSGI webserver.
|
||||
|
||||
|
@ -57,6 +57,7 @@ It includes code from the following sources:
|
|||
flexible handler method <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/307618> under public domain.
|
||||
|
||||
"""
|
||||
from wsgidav.version import __version__
|
||||
import socket
|
||||
|
||||
|
||||
|
@ -69,6 +70,7 @@ try:
|
|||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
|
||||
SERVER_ERROR = """\
|
||||
<html>
|
||||
|
@ -85,7 +87,7 @@ SERVER_ERROR = """\
|
|||
|
||||
class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
|
||||
_SUPPORTED_METHODS = ['HEAD','GET','PUT','POST','OPTIONS','TRACE','DELETE','PROPFIND','PROPPATCH','MKCOL','COPY','MOVE','LOCK','UNLOCK']
|
||||
_SUPPORTED_METHODS = ["HEAD","GET","PUT","POST","OPTIONS","TRACE","DELETE","PROPFIND","PROPPATCH","MKCOL","COPY","MOVE","LOCK","UNLOCK"]
|
||||
|
||||
def log_message (self, *args):
|
||||
pass
|
||||
|
@ -95,7 +97,7 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
|
||||
def getApp (self):
|
||||
# We want fragments to be returned as part of <path>
|
||||
_protocol, _host, path, _parameters, query, _fragment = urlparse.urlparse ('http://dummyhost%s' % self.path,
|
||||
_protocol, _host, path, _parameters, query, _fragment = urlparse.urlparse ("http://dummyhost%s" % self.path,
|
||||
allow_fragments=False)
|
||||
# Find any application we might have
|
||||
for appPath, app in self.server.wsgiApplications:
|
||||
|
@ -103,9 +105,9 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
# We found the application to use - work out the scriptName and pathInfo
|
||||
pathInfo = path [len (appPath):]
|
||||
if (len (pathInfo) > 0):
|
||||
if (not pathInfo.startswith ('/')):
|
||||
pathInfo = '/' + pathInfo
|
||||
if (appPath.endswith ('/')):
|
||||
if (not pathInfo.startswith ("/")):
|
||||
pathInfo = "/" + pathInfo
|
||||
if (appPath.endswith ("/")):
|
||||
scriptName = appPath[:-1]
|
||||
else:
|
||||
scriptName = appPath
|
||||
|
@ -121,40 +123,45 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
def do_method(self):
|
||||
app, scriptName, pathInfo, query = self.getApp ()
|
||||
if (not app):
|
||||
self.send_error (404, 'Application not found.')
|
||||
self.send_error (404, "Application not found.")
|
||||
return
|
||||
self.runWSGIApp (app, scriptName, pathInfo, query)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if len(name)>3 and name[0:3] == 'do_' and name[3:] in self._SUPPORTED_METHODS:
|
||||
if len(name)>3 and name[0:3] == "do_" and name[3:] in self._SUPPORTED_METHODS:
|
||||
return self.handlerFunctionClosure(name)
|
||||
else:
|
||||
self.send_error (501, 'Method Not Implemented.')
|
||||
self.send_error (501, "Method Not Implemented.")
|
||||
return
|
||||
|
||||
def runWSGIApp (self, application, scriptName, pathInfo, query):
|
||||
logging.info ("Running application with SCRIPT_NAME %s PATH_INFO %s" % (scriptName, pathInfo))
|
||||
|
||||
env = {'wsgi.version': (1,0)
|
||||
,'wsgi.url_scheme': 'http'
|
||||
,'wsgi.input': self.rfile
|
||||
,'wsgi.errors': sys.stderr
|
||||
,'wsgi.multithread': 1
|
||||
,'wsgi.multiprocess': 0
|
||||
,'wsgi.run_once': 0
|
||||
,'REQUEST_METHOD': self.command
|
||||
,'SCRIPT_NAME': scriptName
|
||||
,'PATH_INFO': pathInfo
|
||||
,'QUERY_STRING': query
|
||||
,'CONTENT_TYPE': self.headers.get ('Content-Type', '')
|
||||
,'CONTENT_LENGTH': self.headers.get ('Content-Length', '')
|
||||
,'REMOTE_ADDR': self.client_address[0]
|
||||
,'SERVER_NAME': self.server.server_address [0]
|
||||
,'SERVER_PORT': str (self.server.server_address [1])
|
||||
,'SERVER_PROTOCOL': self.request_version
|
||||
}
|
||||
if self.command == "PUT":
|
||||
pass # breakpoint
|
||||
|
||||
env = {"wsgi.version": (1, 0),
|
||||
"wsgi.url_scheme": "http",
|
||||
"wsgi.input": self.rfile,
|
||||
"wsgi.errors": sys.stderr,
|
||||
"wsgi.multithread": 1,
|
||||
"wsgi.multiprocess": 0,
|
||||
"wsgi.run_once": 0,
|
||||
"REQUEST_METHOD": self.command,
|
||||
"SCRIPT_NAME": scriptName,
|
||||
"PATH_INFO": pathInfo,
|
||||
"QUERY_STRING": query,
|
||||
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
|
||||
"CONTENT_LENGTH": self.headers.get("Content-Length", ""),
|
||||
"REMOTE_ADDR": self.client_address[0],
|
||||
"SERVER_NAME": self.server.server_address[0],
|
||||
"SERVER_PORT": str(self.server.server_address[1]),
|
||||
"SERVER_PROTOCOL": self.request_version,
|
||||
}
|
||||
for httpHeader, httpValue in self.headers.items():
|
||||
env ['HTTP_%s' % httpHeader.replace ('-', '_').upper()] = httpValue
|
||||
if not httpHeader in ("Content-Type", "Content-Length"):
|
||||
env ["HTTP_%s" % httpHeader.replace ("-", "_").upper()] = httpValue
|
||||
# print env["REQUEST_METHOD"], env.get("HTTP_AUTHORIZATION")
|
||||
|
||||
# Setup the state
|
||||
self.wsgiSentHeaders = 0
|
||||
|
@ -162,25 +169,25 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
|
||||
try:
|
||||
# We have there environment, now invoke the application
|
||||
util.debug("sc", "runWSGIApp application()...")
|
||||
_logger.debug("runWSGIApp application()...")
|
||||
result = application (env, self.wsgiStartResponse)
|
||||
try:
|
||||
for data in result:
|
||||
if data:
|
||||
self.wsgiWriteData (data)
|
||||
else:
|
||||
util.debug("sc", "runWSGIApp empty data")
|
||||
_logger.debug("runWSGIApp empty data")
|
||||
finally:
|
||||
util.debug("sc", "runWSGIApp finally.")
|
||||
if hasattr(result, 'close'):
|
||||
_logger.debug("runWSGIApp finally.")
|
||||
if hasattr(result, "close"):
|
||||
result.close()
|
||||
except:
|
||||
util.debug("sc", "runWSGIApp caught exception...")
|
||||
_logger.debug("runWSGIApp caught exception...")
|
||||
errorMsg = StringIO()
|
||||
traceback.print_exc(file=errorMsg)
|
||||
logging.error (errorMsg.getvalue())
|
||||
if not self.wsgiSentHeaders:
|
||||
self.wsgiStartResponse('500 Server Error', [('Content-type', 'text/html')])
|
||||
self.wsgiStartResponse("500 Server Error", [("Content-type", "text/html")])
|
||||
self.wsgiWriteData(SERVER_ERROR)
|
||||
|
||||
if (not self.wsgiSentHeaders):
|
||||
|
@ -189,7 +196,7 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
def wsgiStartResponse (self, response_status, response_headers, exc_info=None):
|
||||
util.debug("sc", "wsgiStartResponse(%s, %s, %s)" % (response_status, response_headers, exc_info))
|
||||
_logger.debug("wsgiStartResponse(%s, %s, %s)" % (response_status, response_headers, exc_info))
|
||||
if (self.wsgiSentHeaders):
|
||||
raise Exception ("Headers already sent and start_response called again!")
|
||||
# Should really take a copy to avoid changes in the application....
|
||||
|
@ -200,16 +207,16 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
if (not self.wsgiSentHeaders):
|
||||
status, headers = self.wsgiHeaders
|
||||
# Need to send header prior to data
|
||||
statusCode = status [:status.find (' ')]
|
||||
statusMsg = status [status.find (' ') + 1:]
|
||||
util.debug("sc", "wsgiWriteData: send headers '%s', %s" % (status, headers))
|
||||
statusCode = status [:status.find (" ")]
|
||||
statusMsg = status [status.find (" ") + 1:]
|
||||
_logger.debug("wsgiWriteData: send headers '%s', %s" % (status, headers))
|
||||
self.send_response (int (statusCode), statusMsg)
|
||||
for header, value in headers:
|
||||
self.send_header (header, value)
|
||||
self.end_headers()
|
||||
self.wsgiSentHeaders = 1
|
||||
# Send the data
|
||||
util.debug("sc", "wsgiWriteData: '%s...', len=%s" % (data[:10], len(data)))
|
||||
_logger.debug("wsgiWriteData: '%s...', len=%s" % (data[:10], len(data)))
|
||||
self.wfile.write (data)
|
||||
|
||||
class ExtServer (SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
|
@ -224,16 +231,16 @@ class ExtServer (SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
|||
|
||||
|
||||
def serve(conf, app):
|
||||
host = conf.get('host', 'localhost')
|
||||
port = int(conf.get('port', 8080))
|
||||
server = ExtServer((host, port), {'': app})
|
||||
if conf.get('verbose') >= 1:
|
||||
host = conf.get("host", "localhost")
|
||||
port = int(conf.get("port", 8080))
|
||||
server = ExtServer((host, port), {"": app})
|
||||
if conf.get("verbose") >= 1:
|
||||
if host in ("", "0.0.0.0"):
|
||||
print "WsgiDAV serving at %s, port %s (local IP is %s)..." % (host, port, socket.gethostbyname(socket.gethostname()))
|
||||
print "WsgiDAV %s serving at %s, port %s (local IP is %s)..." % (__version__, host, port, socket.gethostbyname(socket.gethostname()))
|
||||
else:
|
||||
print "WsgiDAV serving at %s, port %s..." % (host, port)
|
||||
print "WsgiDAV %s serving at %s, port %s..." % (__version__, host, port)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
raise RuntimeError("Use run_server.py")
|
||||
|
|
|
@ -40,6 +40,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
from optparse import OptionParser
|
||||
from pprint import pprint
|
||||
from inspect import isfunction
|
||||
from wsgidav.wsgidav_app import DEFAULT_CONFIG
|
||||
import traceback
|
||||
import sys
|
||||
import os
|
||||
|
@ -55,36 +56,6 @@ __docformat__ = "reStructuredText"
|
|||
# Use this config file, if no --config_file option is specified
|
||||
DEFAULT_CONFIG_FILE = "wsgidav.conf"
|
||||
|
||||
# Use these settings, if config file does not define them (or is totally missing)
|
||||
DEFAULT_CONFIG = {
|
||||
"provider_mapping": {},
|
||||
"user_mapping": {},
|
||||
# "host": "127.0.0.1",
|
||||
# "port": 80,
|
||||
"propsmanager": None,
|
||||
"propsfile": None,
|
||||
"locksmanager": None, # None: use lock_manager.LockManager
|
||||
"locksfile": None, # Used as default for
|
||||
"domaincontroller": None,
|
||||
|
||||
# HTTP Authentication Options
|
||||
"acceptbasic": True, # Allow basic authentication, True or False
|
||||
"acceptdigest": True, # Allow digest authentication, True or False
|
||||
"defaultdigest": True, # True (default digest) or False (default basic)
|
||||
|
||||
# Verbose Output
|
||||
"verbose": 2, # 0 - no output (excepting application exceptions)
|
||||
# 1 - show single line request summaries (for HTTP logging)
|
||||
# 2 - show additional events
|
||||
# 3 - show full request/response header info (HTTP Logging)
|
||||
# request body and GET response bodies not shown
|
||||
|
||||
|
||||
# Organizational Information - printed as a footer on html output
|
||||
"response_trailer": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _initCommandLineOptions():
|
||||
"""Parse command line options into a dictionary."""
|
||||
|
@ -118,14 +89,14 @@ See http://wsgidav.googlecode.com for additional information."""
|
|||
description=None, #description,
|
||||
add_help_option=True,
|
||||
# prog="wsgidav",
|
||||
epilog=epilog
|
||||
# epilog=epilog # TODO: Not available on Python 2.4?
|
||||
)
|
||||
|
||||
parser.add_option("-p", "--port",
|
||||
dest="port",
|
||||
type="int",
|
||||
default=8080,
|
||||
help='port to serve on (default: %default)')
|
||||
help="port to serve on (default: %default)")
|
||||
parser.add_option("-H", "--host", # '-h' conflicts with --help
|
||||
dest="host",
|
||||
default="localhost",
|
||||
|
@ -146,7 +117,7 @@ See http://wsgidav.googlecode.com for additional information."""
|
|||
|
||||
parser.add_option("-c", "--config",
|
||||
dest="config_file",
|
||||
help="Configuration file (default: %default).")
|
||||
help="Configuration file (default: %s in current directory)." % DEFAULT_CONFIG_FILE)
|
||||
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
@ -159,13 +130,13 @@ See http://wsgidav.googlecode.com for additional information."""
|
|||
defPath = os.path.abspath(DEFAULT_CONFIG_FILE)
|
||||
if os.path.exists(defPath):
|
||||
if options.verbose >= 2:
|
||||
print "Using default config file: %s" % defPath
|
||||
print "Using default configuration file: %s" % defPath
|
||||
options.config_file = defPath
|
||||
else:
|
||||
# If --config was specified convert to absolute path and assert it exists
|
||||
options.config_file = os.path.abspath(options.config_file)
|
||||
if not os.path.exists(options.config_file):
|
||||
parser.error("Invalid config file specified: %s" % options.config_file)
|
||||
parser.error("Could not open specified configuration file: %s" % options.config_file)
|
||||
|
||||
# Convert options object to dictionary
|
||||
cmdLineOpts = options.__dict__.copy()
|
||||
|
@ -199,9 +170,9 @@ def _readConfigFile(config_file, verbose):
|
|||
if verbose >= 1:
|
||||
traceback.print_exc()
|
||||
exceptioninfo = traceback.format_exception_only(sys.exc_type, sys.exc_value) #@UndefinedVariable
|
||||
exceptiontext = ''
|
||||
exceptiontext = ""
|
||||
for einfo in exceptioninfo:
|
||||
exceptiontext += einfo + '\n'
|
||||
exceptiontext += einfo + "\n"
|
||||
raise RuntimeError("Failed to read configuration file: " + config_file + "\nDue to " + exceptiontext)
|
||||
|
||||
return conf
|
||||
|
@ -231,11 +202,12 @@ def _initConfig():
|
|||
config["port"] = cmdLineOpts.get("port")
|
||||
if cmdLineOpts.get("host"):
|
||||
config["host"] = cmdLineOpts.get("host")
|
||||
if cmdLineOpts.get("verbose"):
|
||||
if cmdLineOpts.get("verbose") is not None:
|
||||
config["verbose"] = cmdLineOpts.get("verbose")
|
||||
|
||||
if cmdLineOpts.get("root_path"):
|
||||
config["provider_mapping"]["/"] = FilesystemProvider(cmdLineOpts.get("root_path"))
|
||||
root_path = os.path.abspath(cmdLineOpts.get("root_path"))
|
||||
config["provider_mapping"]["/"] = FilesystemProvider(root_path)
|
||||
|
||||
if cmdLineOpts["verbose"] >= 3:
|
||||
print "Configuration(%s):" % cmdLineOpts["config_file"]
|
||||
|
@ -244,7 +216,7 @@ def _initConfig():
|
|||
if not config["provider_mapping"]:
|
||||
print >>sys.stderr, "ERROR: No DAV provider defined. Try --help option."
|
||||
sys.exit(-1)
|
||||
# raise RuntimeWarning("No At least one DAV provider must be specified by a --root option, or in a configuration file.")
|
||||
# raise RuntimeWarning("At least one DAV provider must be specified by a --root option, or in a configuration file.")
|
||||
return config
|
||||
|
||||
|
||||
|
@ -258,7 +230,7 @@ def _runPaste(app, config):
|
|||
try:
|
||||
from paste import httpserver
|
||||
if config["verbose"] >= 2:
|
||||
print "Running paste.httpserver..."
|
||||
print "Running WsgiDAV %s on paste.httpserver..." % __version__
|
||||
# See http://pythonpaste.org/modules/httpserver.html for more options
|
||||
httpserver.serve(app,
|
||||
host=config["host"],
|
||||
|
@ -282,7 +254,7 @@ def _runCherryPy(app, config):
|
|||
# http://cherrypy.org/apidocs/3.0.2/cherrypy.wsgiserver-module.html
|
||||
from cherrypy import wsgiserver
|
||||
if config["verbose"] >= 2:
|
||||
print "wsgiserver.CherryPyWSGIServer..."
|
||||
print "Running WsgiDAV %s on wsgiserver.CherryPyWSGIServer..." % __version__
|
||||
server = wsgiserver.CherryPyWSGIServer(
|
||||
(config["host"], config["port"]),
|
||||
app,
|
||||
|
@ -303,7 +275,7 @@ def _runSimpleServer(app, config):
|
|||
# http://www.python.org/doc/2.5.2/lib/module-wsgiref.html
|
||||
from wsgiref.simple_server import make_server
|
||||
if config["verbose"] >= 2:
|
||||
print "Running wsgiref.simple_server (single threaded)..."
|
||||
print "Running WsgiDAV %s on wsgiref.simple_server (single threaded)..." % __version__
|
||||
httpd = make_server(config["host"], config["port"], app)
|
||||
# print "Serving HTTP on port 8000..."
|
||||
httpd.serve_forever()
|
||||
|
@ -317,11 +289,11 @@ def _runSimpleServer(app, config):
|
|||
|
||||
|
||||
def _runBuiltIn(app, config):
|
||||
"""Run WsgiDAV using ext_wsgiutils_server from the WsgiDAV package."""
|
||||
"""Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
|
||||
try:
|
||||
import ext_wsgiutils_server
|
||||
if config["verbose"] >= 2:
|
||||
print "Running wsgidav.ext_wsgiutils_server..."
|
||||
print "Running WsgiDAV %s on wsgidav.ext_wsgiutils_server..." % __version__
|
||||
ext_wsgiutils_server.serve(config, app)
|
||||
except ImportError, e:
|
||||
if config["verbose"] >= 1:
|
||||
|
@ -330,40 +302,37 @@ def _runBuiltIn(app, config):
|
|||
return True
|
||||
|
||||
|
||||
SUPPORTED_SERVERS = {"paste": _runPaste,
|
||||
"cherrypy": _runCherryPy,
|
||||
"wsgiref": _runSimpleServer,
|
||||
"wsgidav": _runBuiltIn,
|
||||
}
|
||||
|
||||
|
||||
def run():
|
||||
def run():
|
||||
config = _initConfig()
|
||||
|
||||
|
||||
# from paste import pyconfig
|
||||
# config = pyconfig.Config()
|
||||
# config.load(opts.config_file)
|
||||
|
||||
# from paste.deploy import loadapp
|
||||
# app = loadapp('config:/path/to/config.ini')
|
||||
# app = loadapp("config:wsgidav.conf")
|
||||
|
||||
app = WsgiDAVApp(config)
|
||||
|
||||
# from wsgidav.wsgiapp import make_app
|
||||
# global_conf = {}
|
||||
# app = make_app(global_conf)
|
||||
|
||||
app = WsgiDAVApp(config)
|
||||
|
||||
# Try running WsgiDAV inside the following external servers:
|
||||
res = False
|
||||
|
||||
# if not res:
|
||||
# res = _runCherryPy(app, config)
|
||||
|
||||
# if not res:
|
||||
# res = _runPaste(app, config)
|
||||
|
||||
# wsgiref.simple_server is single threaded
|
||||
# if not res:
|
||||
# res = _runSimpleServer(app, config)
|
||||
|
||||
if not res:
|
||||
res = _runBuiltIn(app, config)
|
||||
for e in config["ext_servers"]:
|
||||
fn = SUPPORTED_SERVERS.get(e)
|
||||
if fn is None:
|
||||
print "Invalid external server '%s'. (expected: '%s')" % (e, "', '".join(SUPPORTED_SERVERS.keys()))
|
||||
elif fn(app, config):
|
||||
res = True
|
||||
break
|
||||
|
||||
if not res:
|
||||
print "No supported WSGI server installed."
|
||||
|
|
|
@ -25,41 +25,19 @@ def addUser(realmName, user, password, description, roles=[]):
|
|||
|
||||
################################################################################
|
||||
# SERVER OPTIONS
|
||||
|
||||
# Property Options
|
||||
|
||||
#propsmanager = # uncomment this line to specify your own property manager
|
||||
# default: wsgidav.property_manager.PropertyManager
|
||||
|
||||
#propsfile = # uncomment this line to specify a storage file location
|
||||
# for wsgidav.property_manager.PropertyManager
|
||||
# default: 'wsgidav-props.shelve' in the current directory
|
||||
#===============================================================================
|
||||
# 3rd party servers
|
||||
# Try to run WsgiDAV inside these WSGI servers, in that order
|
||||
ext_servers = (
|
||||
# "paste",
|
||||
# "cherrypy",
|
||||
# "wsgiref",
|
||||
"wsgidav",
|
||||
)
|
||||
|
||||
|
||||
# Locks Options
|
||||
|
||||
#locksmanager = # uncomment this line to specify your own locks manager
|
||||
# default: wsgidav.lock_manager.LockManager
|
||||
|
||||
#locksfile = # uncomment this line to specify a storage file location
|
||||
# for wsgidav.lock_manager.LockManager
|
||||
# default: 'wsgidav-locks.shelve' in current directory
|
||||
|
||||
|
||||
# Domain Controller
|
||||
|
||||
#domaincontroller = # uncomment this line to specify your own domain controller
|
||||
# default: wsgidav.domain_controller
|
||||
# uses USERS section below
|
||||
|
||||
|
||||
# HTTP Authentication Options
|
||||
|
||||
acceptbasic = True # Allow basic authentication, True or False
|
||||
acceptdigest = True # Allow digest authentication, True or False
|
||||
defaultdigest = True # True (default digest) or False (default basic)
|
||||
|
||||
# Verbose Output
|
||||
#===============================================================================
|
||||
# Debugging
|
||||
|
||||
verbose = 2 # 0 - no output (excepting application exceptions)
|
||||
# 1 - show single line request summaries (HTTP logging)
|
||||
|
@ -68,6 +46,13 @@ verbose = 2 # 0 - no output (excepting application exceptions)
|
|||
# request body and GET response bodies not shown
|
||||
|
||||
|
||||
# Enable specific module loggers
|
||||
# E.g. ["lock_manager", "property_manager", "http_authenticator", ...]
|
||||
|
||||
enable_loggers = []
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Organizational Information - printed as a footer on html output
|
||||
|
||||
admin_email = "admin@example.com"
|
||||
|
@ -78,21 +63,66 @@ Support contact: <a href='mailto:%s'>Administrator</a> at %s.
|
|||
""" % (admin_email, organization)
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# DAV Provider
|
||||
|
||||
#===============================================================================
|
||||
# Property Manager
|
||||
#
|
||||
# Uncomment this lines to specify your own property manager.
|
||||
# Default: wsgidav.property_manager.PropertyManager
|
||||
# Also available: wsgidav.property_manager.ShelvePropertyManager
|
||||
#
|
||||
# Check the documentation on how to develop custom property managers.
|
||||
# Note that the default PropertyManager works in-memory, and thus is NOT
|
||||
# persistent.
|
||||
|
||||
# Example: Use in-memory property manager (this is also the default)
|
||||
#from wsgidav.property_manager import PropertyManager
|
||||
#propsmanager = PropertyManager()
|
||||
|
||||
# Example: Use PERSISTENT shelve based property manager
|
||||
#from wsgidav.property_manager import ShelvePropertyManager
|
||||
#propsmanager = ShelvePropertyManager("wsgidav-props.shelve")
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Lock Manager
|
||||
#
|
||||
# Uncomment this lines to specify your own locks manager.
|
||||
# Default: wsgidav.lock_manager.LockManager
|
||||
# Also available: wsgidav.lock_manager.ShelveLockManager
|
||||
#
|
||||
# Check the documentation on how to develop custom lock managers.
|
||||
# Note that the default LockManager works in-memory, and thus is NOT persistent.
|
||||
|
||||
# Example: Use in-memory lock manager (this is also the default)
|
||||
#from wsgidav.lock_manager import LockManager
|
||||
#locksmanager = LockManager()
|
||||
|
||||
# Example: Use PERSISTENT shelve based lock manager
|
||||
#from wsgidav.lock_manager import ShelveLockManager
|
||||
#locksmanager = ShelveLockManager("wsgidav-locks.shelve")
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# SHARES
|
||||
#
|
||||
# If you would like to publish files in the location '/v_root' through a
|
||||
# WsgiDAV share 'files', so that it can be accessed by this URL:
|
||||
# http://server:port/files
|
||||
# insert the following line:
|
||||
# addShare('files', '/v_root')
|
||||
# addShare("files", "/v_root")
|
||||
# or, on a Windows box:
|
||||
# addShare('files', 'c:\v_root')
|
||||
# addShare("files", "c:\\v_root")
|
||||
#
|
||||
# To access the same directory using a root level share
|
||||
# http://server:port/
|
||||
# insert this line:
|
||||
# addShare('', 'c:\v_root')
|
||||
# addShare("", "/v_root")
|
||||
#
|
||||
# The above examples use wsgidav.fs_dav_provider.FilesystemProvider, which is
|
||||
# the default provider implementation.
|
||||
|
@ -100,29 +130,54 @@ Support contact: <a href='mailto:%s'>Administrator</a> at %s.
|
|||
# If you wish to use a custom provider, an object must be passed as second
|
||||
# parameter. See the examples below.
|
||||
|
||||
addShare("test", r"C:\temp")
|
||||
addShare("temp", "C:\\temp")
|
||||
|
||||
### Add a read-only file share:
|
||||
#from wsgidav.fs_dav_provider import ReadOnlyFilesystemProvider
|
||||
#addShare("tmp", ReadOnlyFilesystemProvider(r"C:\tmp"))
|
||||
#addShare("tmp", ReadOnlyFilesystemProvider("/tmp"))
|
||||
|
||||
|
||||
### Publish an MySQL 'world' database as share '/world-db'
|
||||
#from wsgidav.addons.mysql_dav_provider import MySQLBrowserProvider
|
||||
#addShare("world-db", MySQLBrowserProvider("localhost", "root", "test", "world"))
|
||||
|
||||
# Publish an SQL table
|
||||
#from wsgidav.addons.simplemysqlabstractionlayer import SimpleMySQLResourceAbstractionLayer
|
||||
#addrealm('testdb', 'database', 'testdb')
|
||||
#addrealm('mysqldb', 'database', 'mysqldb')
|
||||
#addAL("testdb", SimpleMySQLResourceAbstractionLayer("localhost", "", "anon", "test"))
|
||||
#addAL("mysqldb", SimpleMySQLResourceAbstractionLayer("localhost", "", "anon", "mysql"))
|
||||
|
||||
# Publish a virtual structure
|
||||
from wsgidav.addons.virtual_dav_provider import VirtualResourceProvider
|
||||
addShare("virtres", VirtualResourceProvider())
|
||||
#addShare("", VirtualResourceProvider())
|
||||
#from wsgidav.addons.virtual_dav_provider import VirtualResourceProvider
|
||||
#addShare("virtres", VirtualResourceProvider())
|
||||
|
||||
|
||||
################################################################################
|
||||
# AUTHENTICATION
|
||||
#===============================================================================
|
||||
# HTTP Authentication Options
|
||||
|
||||
acceptbasic = True # Allow basic authentication, True or False
|
||||
acceptdigest = True # Allow digest authentication, True or False
|
||||
defaultdigest = True # True (default digest) or False (default basic)
|
||||
|
||||
|
||||
#domaincontroller = # Uncomment this line to specify your own domain controller
|
||||
# Default: wsgidav.domain_controller, which uses the USERS
|
||||
# section below
|
||||
|
||||
|
||||
# Example: use a domain controller that allows users to authenticate against
|
||||
# a Windows NT domain or a local computer.
|
||||
# Note: NTDomainController requires basic authentication:
|
||||
# Set acceptbasic=True, acceptdigest=False, defaultdigest=False
|
||||
|
||||
#from wsgidav.addons.nt_domain_controller import NTDomainController
|
||||
#domaincontroller = NTDomainController(presetdomain=None, presetserver=None)
|
||||
#acceptbasic = True
|
||||
#acceptdigest = False
|
||||
#defaultdigest = False
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# USERS
|
||||
#
|
||||
# This section is used by for authentication by the default Domain Controller.
|
||||
# This section is ONLY used by the DEFAULT Domain Controller.
|
||||
#
|
||||
# Users are defined per realm:
|
||||
# addUser(<realm>, <user>, <password>, <description>)
|
||||
|
@ -132,15 +187,15 @@ addShare("virtres", VirtualResourceProvider())
|
|||
# If no users are specified for a realm, no authentication is required.
|
||||
# Thus granting read-write access to anonymous!
|
||||
#
|
||||
# Note: If you wish to use Windows WebDAV support (such as Windows XP's My Network Places),
|
||||
# you need to include the domain of the user as part of the username (note the DOUBLE slash),
|
||||
# such as:
|
||||
# addUser('v_root', 'domain\\user', 'password', 'description')
|
||||
# Note: If you wish to use Windows WebDAV support (such as Windows XP's My
|
||||
# Network Places), you need to include the domain of the user as part of the
|
||||
# username (note the DOUBLE slash), such as:
|
||||
# addUser("v_root", "domain\\user", "password", "description")
|
||||
|
||||
addUser('', 'tester', 'tester', '')
|
||||
addUser('', 'tester2', 'tester2', '')
|
||||
addUser("", "tester", "tester", "")
|
||||
addUser("", "tester2", "tester2", "")
|
||||
|
||||
#addUser('temp', 'tester', 'tester', '')
|
||||
#addUser('temp', 'tester2', 'tester2', '')
|
||||
#addUser("temp", "tester", "tester", "")
|
||||
#addUser("temp", "tester2", "tester2", "")
|
||||
|
||||
addUser('virtres', 'tester', 'tester', '')
|
||||
#addUser("virtres", "tester", "tester", "")
|
||||
|
|
49
wsgidav/server/server_sample.py
Normal file
49
wsgidav/server/server_sample.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
server_sample
|
||||
=============
|
||||
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
Simple example how to a run WsgiDAV in a 3rd-party WSGI server.
|
||||
|
||||
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
||||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
from tempfile import gettempdir
|
||||
from wsgidav.fs_dav_provider import FilesystemProvider
|
||||
from wsgidav.version import __version__
|
||||
from wsgidav.wsgidav_app import DEFAULT_CONFIG, WsgiDAVApp
|
||||
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
|
||||
rootpath = gettempdir()
|
||||
provider = FilesystemProvider(rootpath)
|
||||
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config.update({
|
||||
"provider_mapping": {"/": provider},
|
||||
"user_mapping": {},
|
||||
"verbose": 1,
|
||||
"enable_loggers": [],
|
||||
"propsmanager": None, # None: use property_manager.PropertyManager
|
||||
"locksmanager": None, # None: use lock_manager.LockManager
|
||||
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
|
||||
})
|
||||
app = WsgiDAVApp(config)
|
||||
|
||||
# For an example. use paste.httpserver
|
||||
# (See http://pythonpaste.org/modules/httpserver.html for more options)
|
||||
from paste import httpserver
|
||||
httpserver.serve(app,
|
||||
host="localhost",
|
||||
port=8080,
|
||||
server_version="WsgiDAV/%s" % __version__,
|
||||
)
|
||||
|
||||
# Or use default the server that is part of the WsgiDAV package:
|
||||
#from wsgidav.server import ext_wsgiutils_server
|
||||
#ext_wsgiutils_server.serve(config, app)
|
697
wsgidav/util.py
697
wsgidav/util.py
|
@ -4,8 +4,8 @@
|
|||
util
|
||||
====
|
||||
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Author: Martin Wendt, moogle(at)wwwendt.de
|
||||
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
|
||||
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
|
||||
|
||||
Miscellaneous support functions for WsgiDAV.
|
||||
|
@ -15,16 +15,19 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
"""
|
||||
from urllib import quote
|
||||
from lxml import etree
|
||||
from lxml.etree import SubElement
|
||||
from dav_error import DAVError, getHttpStatusString, HTTP_BAD_REQUEST
|
||||
from pprint import pprint
|
||||
from wsgidav.dav_error import HTTP_PRECONDITION_FAILED, HTTP_NOT_MODIFIED
|
||||
import locale
|
||||
import urllib
|
||||
import logging
|
||||
import re
|
||||
import md5
|
||||
try:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import md5
|
||||
import os
|
||||
import calendar
|
||||
import threading
|
||||
import sys
|
||||
import time
|
||||
import stat
|
||||
|
@ -34,30 +37,42 @@ try:
|
|||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
# Import XML support
|
||||
useLxml = False
|
||||
try:
|
||||
from lxml import etree
|
||||
useLxml = True
|
||||
except ImportError:
|
||||
try:
|
||||
# Try xml module (Python 2.5 or later)
|
||||
from xml.etree import ElementTree as etree
|
||||
print "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/."
|
||||
except ImportError:
|
||||
try:
|
||||
# Try elementtree (http://effbot.org/zone/element-index.htm)
|
||||
from elementtree import ElementTree as etree
|
||||
except ImportError:
|
||||
print "ERROR: Could not import lxml, xml, nor elementtree. Consider installing lxml from http://codespeak.net/lxml/ or update to Python 2.5 or later."
|
||||
raise
|
||||
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
BASE_LOGGER_NAME = "wsgidav"
|
||||
_logger = logging.getLogger(BASE_LOGGER_NAME)
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Debugging
|
||||
# String handling
|
||||
#===============================================================================
|
||||
def traceCall(msg=None):
|
||||
"""Return name of calling function."""
|
||||
if __debug__:
|
||||
f_code = sys._getframe(2).f_code
|
||||
if msg is None:
|
||||
msg = ": %s"
|
||||
else: msg = ""
|
||||
print "%s.%s #%s%s" % (f_code.co_filename, f_code.co_name, f_code.co_lineno, msg)
|
||||
|
||||
|
||||
def isVerboseMode(environ):
|
||||
if environ['wsgidav.verbose'] >= 1 and environ["REQUEST_METHOD"] in environ.get('wsgidav.debug_methods', []):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def getRfc1123Time(secs=None):
|
||||
"""Return <secs> in rfc 1123 date/time format (pass secs=None for current date)."""
|
||||
return time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(secs))
|
||||
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(secs))
|
||||
|
||||
|
||||
def getLogTime(secs=None):
|
||||
"""Return <secs> in log time format (pass secs=None for current date)."""
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
|
||||
|
||||
|
||||
def parseTimeString(timestring):
|
||||
|
@ -101,31 +116,188 @@ def _parsegmtime(timestring):
|
|||
return None
|
||||
|
||||
|
||||
def log(msg, var=None):
|
||||
out = sys.stderr
|
||||
tid = threading._get_ident() #threading.currentThread()
|
||||
print >>out, "<%s> %s" % (tid, msg)
|
||||
if var:
|
||||
pprint(var, out, indent=4)
|
||||
#===============================================================================
|
||||
# Logging
|
||||
#===============================================================================
|
||||
|
||||
def initLogging(verbose=2, enable_loggers=[]):
|
||||
"""Initialize base logger named 'wsgidav'.
|
||||
|
||||
The base logger is filtered by the *verbose* configuration option.
|
||||
Log entries will have a time stamp and thread id.
|
||||
|
||||
:Parameters:
|
||||
verbose : int
|
||||
Verbosity configuration (0..3)
|
||||
enable_loggers : string list
|
||||
List of module logger names, that will be switched to DEBUG level.
|
||||
|
||||
Module loggers
|
||||
~~~~~~~~~~~~~~
|
||||
Module loggers (e.g 'wsgidav.lock_manager') are named loggers, that can be
|
||||
independently switched to DEBUG mode.
|
||||
|
||||
def debug(cat, msg):
|
||||
"""Print debug msessage to console (type filtered).
|
||||
Except for verbosity, they will will inherit settings from the base logger.
|
||||
|
||||
They will suppress DEBUG level messages, unless they are enabled by passing
|
||||
their name to util.initLogging().
|
||||
|
||||
If enabled, module loggers will print DEBUG messages, even if verbose == 2.
|
||||
|
||||
This is only a hack during early develpment: we can spread log calls and
|
||||
use temporarily disable it by hard coding the filter condition here.
|
||||
cat: 'pp' Paste Prune
|
||||
'sc' "Socket closed" exception
|
||||
Example initialize and use a module logger, that will generate output,
|
||||
if enabled (and verbose >= 2):
|
||||
|
||||
.. python::
|
||||
_logger = util.getModuleLogger(__name__)
|
||||
[..]
|
||||
_logger.debug("foo: '%s'" % s)
|
||||
|
||||
This logger would be enabled by passing its name to initLoggiong():
|
||||
|
||||
.. python::
|
||||
enable_loggers = ["lock_manager",
|
||||
"property_manager",
|
||||
]
|
||||
util.initLogging(2, enable_loggers)
|
||||
|
||||
|
||||
Log level matrix
|
||||
~~~~~~~~~~~~~~~~
|
||||
======= =========== ====================== =======================
|
||||
verbose Log level
|
||||
------- -----------------------------------------------------------
|
||||
n base logger module logger(enabled) module logger(disabled)
|
||||
======= =========== ====================== =======================
|
||||
0 ERROR ERROR ERROR
|
||||
1 WARN WARN WARN
|
||||
2 INFO DEBUG INFO
|
||||
3 DEBUG DEBUG INFO
|
||||
======= =========== ====================== =======================
|
||||
"""
|
||||
assert cat in ("pp", "sc")
|
||||
tid = threading._get_ident() #threading.currentThread()
|
||||
if cat in ("pp", "NOTsc"):
|
||||
print >>sys.stderr, "<%s>[%s] %s" % (tid, cat, msg)
|
||||
|
||||
formatter = logging.Formatter("<%(thread)d> [%(asctime)s.%(msecs)d] %(name)s: %(message)s",
|
||||
"%H:%M:%S")
|
||||
|
||||
# Define handlers
|
||||
consoleHandler = logging.StreamHandler(sys.stderr)
|
||||
consoleHandler.setFormatter(formatter)
|
||||
consoleHandler.setLevel(logging.DEBUG)
|
||||
|
||||
# Add the handlers to the base logger
|
||||
logger = logging.getLogger(BASE_LOGGER_NAME)
|
||||
|
||||
if verbose >= 3: # --debug
|
||||
logger.setLevel(logging.DEBUG)
|
||||
elif verbose >= 2: # --verbose
|
||||
logger.setLevel(logging.INFO)
|
||||
elif verbose >= 1: # standard
|
||||
logger.setLevel(logging.WARN)
|
||||
consoleHandler.setLevel(logging.WARN)
|
||||
else: # --quiet
|
||||
logger.setLevel(logging.ERROR)
|
||||
consoleHandler.setLevel(logging.ERROR)
|
||||
|
||||
# Don't call the root's handlers after our custom handlers
|
||||
logger.propagate = False
|
||||
|
||||
# Remove previous handlers
|
||||
for hdlr in logger.handlers[:]: # Must iterate an array copy
|
||||
try:
|
||||
hdlr.flush()
|
||||
hdlr.close()
|
||||
except:
|
||||
pass
|
||||
logger.removeHandler(hdlr)
|
||||
|
||||
logger.addHandler(consoleHandler)
|
||||
|
||||
if verbose >= 2:
|
||||
for e in enable_loggers:
|
||||
if not e.startswith(BASE_LOGGER_NAME + "."):
|
||||
e = BASE_LOGGER_NAME + "." + e
|
||||
l = logging.getLogger(e.strip())
|
||||
# if verbose >= 2:
|
||||
# log("Logger(%s).setLevel(DEBUG)" % e.strip())
|
||||
l.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
|
||||
def getModuleLogger(moduleName, defaultToVerbose=False):
|
||||
"""Create a module logger, that can be en/disabled by configuration.
|
||||
|
||||
@see: unit.initLogging
|
||||
"""
|
||||
# _logger.debug("getModuleLogger(%s)" % moduleName)
|
||||
if not moduleName.startswith(BASE_LOGGER_NAME + "."):
|
||||
moduleName = BASE_LOGGER_NAME + "." + moduleName
|
||||
# assert not "." in moduleName, "Only pass the module name, without leading '%s.'." % BASE_LOGGER_NAME
|
||||
# logger = logging.getLogger("%s.%s" % (BASE_LOGGER_NAME, moduleName))
|
||||
logger = logging.getLogger(moduleName)
|
||||
if logger.level == logging.NOTSET and not defaultToVerbose:
|
||||
logger.setLevel(logging.INFO) # Disable debug messages by default
|
||||
return logger
|
||||
|
||||
|
||||
def log(msg, var=None):
|
||||
"""Shortcut for logging.getLogger('wsgidav').info(msg)
|
||||
|
||||
This message will only display, if verbose >= 2.
|
||||
"""
|
||||
_logger.info(msg)
|
||||
if var and logging.INFO >= _logger.getEffectiveLevel():
|
||||
pprint(var, sys.stderr, indent=4)
|
||||
|
||||
|
||||
def debug(module, msg, var=None):
|
||||
"""Shortcut for logging.getLogger('wsgidav.MODULE').debug(msg)
|
||||
|
||||
This message will only display, if the module logger was enabled and
|
||||
verbose >= 2.
|
||||
If module is None, the base logger is used, so the message is only displayed
|
||||
if verbose >= 3.
|
||||
"""
|
||||
if module:
|
||||
logger = logging.getLogger(BASE_LOGGER_NAME+"."+module)
|
||||
# Disable debug messages for module loggers by default
|
||||
if logger.level == logging.NOTSET:
|
||||
logger.setLevel(logging.INFO)
|
||||
else:
|
||||
logger = _logger
|
||||
logger.debug(msg)
|
||||
if var and logging.DEBUG >= logger.getEffectiveLevel():
|
||||
pprint(var, sys.stderr, indent=4)
|
||||
|
||||
|
||||
def traceCall(msg=None):
|
||||
"""Return name of calling function."""
|
||||
if __debug__:
|
||||
f_code = sys._getframe(2).f_code
|
||||
if msg is None:
|
||||
msg = ": %s"
|
||||
else: msg = ""
|
||||
print "%s.%s #%s%s" % (f_code.co_filename, f_code.co_name, f_code.co_lineno, msg)
|
||||
|
||||
|
||||
def isVerboseMode(environ):
|
||||
if environ["wsgidav.verbose"] >= 1 and environ["REQUEST_METHOD"] in environ.get("wsgidav.debug_methods", []):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# WSGI, strings and URLs
|
||||
# Strings
|
||||
#===============================================================================
|
||||
|
||||
def lstripstr(s, prefix, ignoreCase=False):
|
||||
if ignoreCase:
|
||||
if not s.lower().startswith(prefix.lower()):
|
||||
return s
|
||||
else:
|
||||
if not s.startswith(prefix):
|
||||
return s
|
||||
return s[len(prefix):]
|
||||
|
||||
|
||||
def saveSplit(s, sep, maxsplit):
|
||||
"""Split string, always returning n-tuple (filled with None if necessary)."""
|
||||
tok = s.split(sep, maxsplit)
|
||||
|
@ -148,65 +320,149 @@ def splitNamespace(clarkName):
|
|||
return ("", clarkName)
|
||||
|
||||
|
||||
def getContentLength(environ):
|
||||
"""Return CONTENT_LENGTH in a safe way (defaults to 0)."""
|
||||
def toUnicode(s):
|
||||
"""Convert a binary string to Unicode using UTF-8 (fallback to latin-1)."""
|
||||
if not isinstance(s, str):
|
||||
return s
|
||||
try:
|
||||
return max(0, long(environ.get('CONTENT_LENGTH', 0)))
|
||||
u = s.decode("utf8")
|
||||
# log("toUnicode(%r) = '%r'" % (s, u))
|
||||
except:
|
||||
log("toUnicode(%r) *** UTF-8 failed. Trying latin-1 " % s)
|
||||
u = s.decode("latin-1")
|
||||
return u
|
||||
|
||||
|
||||
def stringRepr(s):
|
||||
"""Return a string as hex dump."""
|
||||
if isinstance(s, str):
|
||||
res = "'%s': " % s
|
||||
for b in s:
|
||||
res += "%02x " % ord(b)
|
||||
return res
|
||||
return "%s" % s
|
||||
|
||||
|
||||
|
||||
def byteNumberString(number, thousandsSep=True, partition=False, base1024=True, appendBytes=True):
|
||||
"""Convert bytes into human-readable representation."""
|
||||
magsuffix = ""
|
||||
bytesuffix = ""
|
||||
|
||||
if partition:
|
||||
magnitude = 0
|
||||
if base1024:
|
||||
while number >= 1024:
|
||||
magnitude += 1
|
||||
number = number >> 10
|
||||
else:
|
||||
while number >= 1000:
|
||||
magnitude += 1
|
||||
number /= 1000.0
|
||||
# TODO: use "9 KB" instead of "9K Bytes"?
|
||||
# TODO use 'kibi' for base 1024?
|
||||
# http://en.wikipedia.org/wiki/Kibi-#IEC_standard_prefixes
|
||||
magsuffix = ["", "K", "M", "G", "T", "P"][magnitude]
|
||||
|
||||
if appendBytes:
|
||||
if number == 1:
|
||||
bytesuffix = " Byte"
|
||||
else:
|
||||
bytesuffix = " Bytes"
|
||||
|
||||
if thousandsSep and (number >= 1000 or magsuffix):
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
# TODO: make precision configurable
|
||||
snum = locale.format("%d", number, thousandsSep)
|
||||
else:
|
||||
snum = str(number)
|
||||
|
||||
return "%s%s%s" % (snum, magsuffix, bytesuffix)
|
||||
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# WSGI
|
||||
#===============================================================================
|
||||
def getContentLength(environ):
|
||||
"""Return a positive CONTENT_LENGTH in a safe way (return 0 otherwise)."""
|
||||
# TODO: http://www.wsgi.org/wsgi/WSGI_2.0
|
||||
try:
|
||||
return max(0, long(environ.get("CONTENT_LENGTH", 0)))
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def getRealm(environ):
|
||||
return getUriRealm(environ["SCRIPT_NAME"] + environ["PATH_INFO"])
|
||||
#===============================================================================
|
||||
# URLs
|
||||
#===============================================================================
|
||||
|
||||
|
||||
def getUri(environ):
|
||||
return environ["PATH_INFO"]
|
||||
|
||||
|
||||
def getUriRealm(uri):
|
||||
"""Return realm, i.e. first part of URI with a leading '/'."""
|
||||
if uri.strip() in ("", "/"):
|
||||
return "/"
|
||||
return uri.strip("/").split("/")[0]
|
||||
#def getUriRealm(uri):
|
||||
# """Return realm, i.e. first part of URI with a leading '/'."""
|
||||
# if uri.strip() in ("", "/"):
|
||||
# return "/"
|
||||
# return uri.strip("/").split("/")[0]
|
||||
|
||||
|
||||
def getUriName(uri):
|
||||
"""Return local name, i.e. last part of URI."""
|
||||
"""Return local name, i.e. last segment of URI."""
|
||||
return uri.strip("/").split("/")[-1]
|
||||
|
||||
|
||||
def getUriParent(uri):
|
||||
"""Return URI of parent collection."""
|
||||
"""Return URI of parent collection with trailing '/', or None, if URI is top-level.
|
||||
|
||||
This function simply strips the last segment. It does not test, if the
|
||||
target is a 'collection', or even exists.
|
||||
"""
|
||||
if not uri or uri.strip() == "/":
|
||||
return None
|
||||
return uri.rstrip("/").rsplit("/", 1)[0] + "/"
|
||||
|
||||
|
||||
def isChildUri(parentUri, childUri):
|
||||
"""Return True, if childUri is a child of parentUri.
|
||||
|
||||
This function accounts for the fact that '/a/b/c' and 'a/b/c/' are
|
||||
children of '/a/b' (and also of '/a/b/').
|
||||
Note that '/a/b/cd' is NOT a child of 'a/b/c'.
|
||||
"""
|
||||
return parentUri and childUri and childUri.rstrip("/").startswith(parentUri.rstrip("/")+"/")
|
||||
|
||||
|
||||
def isEqualOrChildUri(parentUri, childUri):
|
||||
"""Return True, if childUri is a child of parentUri or maps to the same resource.
|
||||
|
||||
Similar to <util.isChildUri>_ , but this method also returns True, if parent
|
||||
equals child. ('/a/b' is considered identical with '/a/b/').
|
||||
"""
|
||||
return parentUri and childUri and (childUri.rstrip("/")+"/").startswith(parentUri.rstrip("/")+"/")
|
||||
|
||||
|
||||
def makeCompleteUrl(environ, localUri=None):
|
||||
"""URL reconstruction according to PEP 333.
|
||||
@see http://www.python.org/dev/peps/pep-0333/#id33
|
||||
"""
|
||||
url = environ['wsgi.url_scheme']+'://'
|
||||
url = environ["wsgi.url_scheme"]+"://"
|
||||
|
||||
if environ.get('HTTP_HOST'):
|
||||
url += environ['HTTP_HOST']
|
||||
if environ.get("HTTP_HOST"):
|
||||
url += environ["HTTP_HOST"]
|
||||
else:
|
||||
url += environ['SERVER_NAME']
|
||||
url += environ["SERVER_NAME"]
|
||||
|
||||
if environ['wsgi.url_scheme'] == 'https':
|
||||
if environ['SERVER_PORT'] != '443':
|
||||
url += ':' + environ['SERVER_PORT']
|
||||
if environ["wsgi.url_scheme"] == "https":
|
||||
if environ["SERVER_PORT"] != "443":
|
||||
url += ":" + environ["SERVER_PORT"]
|
||||
else:
|
||||
if environ['SERVER_PORT'] != '80':
|
||||
url += ':' + environ['SERVER_PORT']
|
||||
if environ["SERVER_PORT"] != "80":
|
||||
url += ":" + environ["SERVER_PORT"]
|
||||
|
||||
url += quote(environ.get('SCRIPT_NAME',''))
|
||||
url += quote(environ.get("SCRIPT_NAME",""))
|
||||
|
||||
if localUri is None:
|
||||
url += quote(environ.get('PATH_INFO',''))
|
||||
if environ.get('QUERY_STRING'):
|
||||
url += '?' + environ['QUERY_STRING']
|
||||
url += quote(environ.get("PATH_INFO",""))
|
||||
if environ.get("QUERY_STRING"):
|
||||
url += "?" + environ["QUERY_STRING"]
|
||||
else:
|
||||
url += localUri # TODO: quote?
|
||||
return url
|
||||
|
@ -216,8 +472,37 @@ def makeCompleteUrl(environ, localUri=None):
|
|||
# XML
|
||||
#===============================================================================
|
||||
|
||||
def xmlToString(element, encoding="UTF-8", pretty_print=False):
|
||||
"""Wrapper for etree.tostring, that takes care of unsupported pretty_print option."""
|
||||
assert encoding == "UTF-8" # TODO: remove this
|
||||
if useLxml:
|
||||
return etree.tostring(element, encoding=encoding, pretty_print=pretty_print)
|
||||
return etree.tostring(element, encoding)
|
||||
|
||||
|
||||
def makeMultistatusEL():
|
||||
"""Wrapper for etree.Element, that takes care of unsupported nsmap option."""
|
||||
if useLxml:
|
||||
return etree.Element("{DAV:}multistatus", nsmap={"D": "DAV:"})
|
||||
return etree.Element("{DAV:}multistatus")
|
||||
|
||||
|
||||
def makePropEL():
|
||||
"""Wrapper for etree.Element, that takes care of unsupported nsmap option."""
|
||||
if useLxml:
|
||||
return etree.Element("{DAV:}prop", nsmap={"D": "DAV:"})
|
||||
return etree.Element("{DAV:}prop")
|
||||
|
||||
|
||||
def makeSubElement(parent, tag, nsmap=None):
|
||||
"""Wrapper for etree.SubElement, that takes care of unsupported nsmap option."""
|
||||
if useLxml:
|
||||
return etree.SubElement(parent, tag, nsmap=nsmap)
|
||||
return etree.SubElement(parent, tag)
|
||||
|
||||
|
||||
def parseXmlBody(environ, allowEmpty=False):
|
||||
"""Read request body XML into an lxml.etree.Element.
|
||||
"""Read request body XML into an etree.Element.
|
||||
|
||||
Return None, if no request body was sent.
|
||||
Raise HTTP_BAD_REQUEST, if something else went wrong.
|
||||
|
@ -239,7 +524,8 @@ def parseXmlBody(environ, allowEmpty=False):
|
|||
At least it locked, when I tried it with a request that had a missing
|
||||
content-type and no body.
|
||||
|
||||
Current approach: if CONTENT_LENGTH is
|
||||
Current approach: if CONTENT_LENGTH is
|
||||
|
||||
- valid and >0:
|
||||
read body (exactly <CONTENT_LENGTH> bytes) and parse the result.
|
||||
- 0:
|
||||
|
@ -285,7 +571,7 @@ def parseXmlBody(environ, allowEmpty=False):
|
|||
except Exception, e:
|
||||
raise DAVError(HTTP_BAD_REQUEST, "Invalid XML format.", srcexception=e)
|
||||
|
||||
if environ['wsgidav.verbose'] >= 1 and environ["REQUEST_METHOD"] in environ.get('wsgidav.debug_methods', []):
|
||||
if environ["wsgidav.verbose"] >= 1 and environ["REQUEST_METHOD"] in environ.get("wsgidav.debug_methods", []):
|
||||
print "XML request for %s:\n%s" % (environ["REQUEST_METHOD"], etree.tostring(rootEL, pretty_print=True))
|
||||
return rootEL
|
||||
|
||||
|
@ -301,7 +587,7 @@ def elementContentAsString(element):
|
|||
return element.text or "" # Make sure, None is returned as ''
|
||||
stream = StringIO()
|
||||
for childnode in element:
|
||||
print >>stream, etree.tostring(childnode, pretty_print=False)
|
||||
print >>stream, xmlToString(childnode, pretty_print=False)
|
||||
s = stream.getvalue()
|
||||
stream.close()
|
||||
return s
|
||||
|
@ -309,15 +595,16 @@ def elementContentAsString(element):
|
|||
|
||||
def sendMultiStatusResponse(environ, start_response, multistatusEL):
|
||||
# Send response
|
||||
start_response('207 Multistatus', [("Content-Type", "application/xml"),
|
||||
start_response("207 Multistatus", [("Content-Type", "application/xml"),
|
||||
("Date", getRfc1123Time()),
|
||||
])
|
||||
# Hotfix for Windows XP: PPROPFIND XML response is not recognized, when
|
||||
# pretty_print = True!
|
||||
# pretty_print = True
|
||||
# Hotfix for Windows XP
|
||||
# PROPFIND XML response is not recognized, when pretty_print = True!
|
||||
# (Vista and others would accept this).
|
||||
# log(xmlToString(multistatusEL, pretty_print=True))
|
||||
pretty_print = False
|
||||
return ["<?xml version='1.0' ?>",
|
||||
etree.tostring(multistatusEL, pretty_print=pretty_print) ]
|
||||
return ["<?xml version='1.0' encoding='UTF-8' ?>",
|
||||
xmlToString(multistatusEL, pretty_print=pretty_print) ]
|
||||
|
||||
|
||||
def sendSimpleResponse(environ, start_response, status):
|
||||
|
@ -334,10 +621,10 @@ def addPropertyResponse(multistatusEL, href, propList):
|
|||
<prop> node depends on the value type:
|
||||
- str or unicode: add element with this content
|
||||
- None: add an empty element
|
||||
- lxml.etree.Element: add XML element as child
|
||||
- etree.Element: add XML element as child
|
||||
- DAVError: add an empty element to an own <propstatus> for this status code
|
||||
|
||||
@param multistatusEL: lxml.etree.Element
|
||||
@param multistatusEL: etree.Element
|
||||
@param href: global URL of the resource, e.g. 'http://server:port/path'.
|
||||
@param propList: list of 2-tuples (name, value)
|
||||
"""
|
||||
|
@ -365,26 +652,34 @@ def addPropertyResponse(multistatusEL, href, propList):
|
|||
propDict.setdefault(status, []).append( (name, value) )
|
||||
|
||||
# <response>
|
||||
responseEL = SubElement(multistatusEL, "{DAV:}response",
|
||||
nsmap=nsMap)
|
||||
SubElement(responseEL, "{DAV:}href").text = href
|
||||
# responseEL = etree.SubElement(multistatusEL, "{DAV:}response",
|
||||
# nsmap=nsMap)
|
||||
responseEL = makeSubElement(multistatusEL, "{DAV:}response",
|
||||
nsmap=nsMap)
|
||||
|
||||
# log("href value:%s" % (stringRepr(href)))
|
||||
# etree.SubElement(responseEL, "{DAV:}href").text = toUnicode(href)
|
||||
etree.SubElement(responseEL, "{DAV:}href").text = href
|
||||
# etree.SubElement(responseEL, "{DAV:}href").text = urllib.quote(href, safe="/" + "!*'()," + "$-_|.")
|
||||
|
||||
|
||||
# One <propstat> per status code
|
||||
for status in propDict:
|
||||
propstatEL = SubElement(responseEL, "{DAV:}propstat")
|
||||
propstatEL = etree.SubElement(responseEL, "{DAV:}propstat")
|
||||
# List of <prop>
|
||||
propEL = SubElement(propstatEL, "{DAV:}prop")
|
||||
propEL = etree.SubElement(propstatEL, "{DAV:}prop")
|
||||
for name, value in propDict[status]:
|
||||
if value is None:
|
||||
SubElement(propEL, name)
|
||||
etree.SubElement(propEL, name)
|
||||
elif isinstance(value, etree._Element):
|
||||
propEL.append(value)
|
||||
else:
|
||||
# value must be string or unicode
|
||||
# log("%s value:%s" % (name, value))
|
||||
SubElement(propEL, name).text = value
|
||||
# log("%s value:%s" % (name, stringRepr(value)))
|
||||
# etree.SubElement(propEL, name).text = value
|
||||
etree.SubElement(propEL, name).text = toUnicode(value)
|
||||
# <status>
|
||||
SubElement(propstatEL, "{DAV:}status").text = "HTTP/1.1 %s" % status
|
||||
etree.SubElement(propstatEL, "{DAV:}status").text = "HTTP/1.1 %s" % status
|
||||
|
||||
|
||||
#===============================================================================
|
||||
|
@ -396,18 +691,28 @@ def getETag(filePath):
|
|||
http://www.webdav.org/specs/rfc4918.html#etag
|
||||
|
||||
Returns the following as entity tags::
|
||||
|
||||
Non-file - md5(pathname)
|
||||
Win32 - md5(pathname)-lastmodifiedtime-filesize
|
||||
Others - inode-lastmodifiedtime-filesize
|
||||
"""
|
||||
if not os.path.isfile(filePath):
|
||||
return md5.new(filePath).hexdigest()
|
||||
if sys.platform == 'win32':
|
||||
statresults = os.stat(filePath)
|
||||
return md5.new(filePath).hexdigest() + '-' + str(statresults[stat.ST_MTIME]) + '-' + str(statresults[stat.ST_SIZE])
|
||||
# (At least on Vista) os.path.exists returns False, if a file name contains
|
||||
# special characters, even if it is correctly UTF-8 encoded.
|
||||
# So we convert to unicode. On the other hand, md5() needs a byte string.
|
||||
if isinstance(filePath, unicode):
|
||||
unicodeFilePath = filePath
|
||||
filePath = filePath.encode("utf8")
|
||||
else:
|
||||
statresults = os.stat(filePath)
|
||||
return str(statresults[stat.ST_INO]) + '-' + str(statresults[stat.ST_MTIME]) + '-' + str(statresults[stat.ST_SIZE])
|
||||
unicodeFilePath = toUnicode(filePath)
|
||||
|
||||
if not os.path.isfile(unicodeFilePath):
|
||||
return md5(filePath).hexdigest()
|
||||
if sys.platform == "win32":
|
||||
statresults = os.stat(unicodeFilePath)
|
||||
return md5(filePath).hexdigest() + "-" + str(statresults[stat.ST_MTIME]) + "-" + str(statresults[stat.ST_SIZE])
|
||||
else:
|
||||
statresults = os.stat(unicodeFilePath)
|
||||
return str(statresults[stat.ST_INO]) + "-" + str(statresults[stat.ST_MTIME]) + "-" + str(statresults[stat.ST_SIZE])
|
||||
|
||||
|
||||
#===============================================================================
|
||||
|
@ -420,12 +725,13 @@ reSuffixByteRangeSpecifier = re.compile("(\-([0-9]+))")
|
|||
|
||||
def obtainContentRanges(rangetext, filesize):
|
||||
"""
|
||||
returns tuple
|
||||
list: content ranges as values to their parsed components in the tuple
|
||||
(seek_position/abs position of first byte,
|
||||
abs position of last byte,
|
||||
num_of_bytes_to_read)
|
||||
value: total length for Content-Length
|
||||
returns tuple (list, value)
|
||||
|
||||
list
|
||||
content ranges as values to their parsed components in the tuple
|
||||
(seek_position/abs position of first byte, abs position of last byte, num_of_bytes_to_read)
|
||||
value
|
||||
total length for Content-Length
|
||||
"""
|
||||
listReturn = []
|
||||
seqRanges = rangetext.split(",")
|
||||
|
@ -436,7 +742,7 @@ def obtainContentRanges(rangetext, filesize):
|
|||
if mObj:
|
||||
# print mObj.group(0), mObj.group(1), mObj.group(2), mObj.group(3)
|
||||
firstpos = long(mObj.group(2))
|
||||
if mObj.group(3) == '':
|
||||
if mObj.group(3) == "":
|
||||
lastpos = filesize - 1
|
||||
else:
|
||||
lastpos = long(mObj.group(3))
|
||||
|
@ -482,7 +788,7 @@ def obtainContentRanges(rangetext, filesize):
|
|||
# If Headers
|
||||
#===============================================================================
|
||||
|
||||
def evaluateHTTPConditionals(dav, path, lastmodified, entitytag, environ, isnewfile=False):
|
||||
def evaluateHTTPConditionals(res, lastmodified, entitytag, environ):
|
||||
"""Handle 'If-...:' headers (but not 'If:' header).
|
||||
|
||||
If-Match
|
||||
|
@ -514,47 +820,53 @@ def evaluateHTTPConditionals(dav, path, lastmodified, entitytag, environ, isnewf
|
|||
# status of 304 (Not Modified) unless doing so is consistent with all of the conditional header fields in
|
||||
# the request.
|
||||
|
||||
if 'HTTP_IF_MATCH' in environ and dav.isInfoTypeSupported(path, "etag"):
|
||||
if isnewfile:
|
||||
raise DAVError(HTTP_PRECONDITION_FAILED)
|
||||
else:
|
||||
ifmatchlist = environ['HTTP_IF_MATCH'].split(",")
|
||||
for ifmatchtag in ifmatchlist:
|
||||
ifmatchtag = ifmatchtag.strip(" \"\t")
|
||||
if ifmatchtag == entitytag or ifmatchtag == '*':
|
||||
break
|
||||
raise DAVError(HTTP_PRECONDITION_FAILED,
|
||||
"If-Match header condition failed")
|
||||
if "HTTP_IF_MATCH" in environ and res.supportEtag(): #dav.isInfoTypeSupported(path, "etag"):
|
||||
ifmatchlist = environ["HTTP_IF_MATCH"].split(",")
|
||||
for ifmatchtag in ifmatchlist:
|
||||
ifmatchtag = ifmatchtag.strip(" \"\t")
|
||||
if ifmatchtag == entitytag or ifmatchtag == "*":
|
||||
break
|
||||
raise DAVError(HTTP_PRECONDITION_FAILED,
|
||||
"If-Match header condition failed")
|
||||
|
||||
# TODO: after the refactoring
|
||||
ifModifiedSinceFailed = False
|
||||
if "HTTP_IF_MODIFIED_SINCE" in environ and res.supportModified(): #dav.isInfoTypeSupported(path, "modified"):
|
||||
ifmodtime = parseTimeString(environ["HTTP_IF_MODIFIED_SINCE"])
|
||||
if ifmodtime and ifmodtime > lastmodified:
|
||||
ifModifiedSinceFailed = True
|
||||
|
||||
# If-None-Match
|
||||
# If none of the entity tags match, then the server MAY perform the requested method as if the
|
||||
# If-None-Match header field did not exist, but MUST also ignore any If-Modified-Since header field
|
||||
# (s) in the request. That is, if no entity tags match, then the server MUST NOT return a 304 (Not Modified)
|
||||
# response.
|
||||
ignoreifmodifiedsince = False
|
||||
if 'HTTP_IF_NONE_MATCH' in environ and dav.isInfoTypeSupported(path, "etag"):
|
||||
if isnewfile:
|
||||
ignoreifmodifiedsince = True
|
||||
else:
|
||||
ifmatchlist = environ['HTTP_IF_NONE_MATCH'].split(",")
|
||||
for ifmatchtag in ifmatchlist:
|
||||
ifmatchtag = ifmatchtag.strip(" \"\t")
|
||||
if ifmatchtag == entitytag or ifmatchtag == '*':
|
||||
raise DAVError(HTTP_PRECONDITION_FAILED,
|
||||
"If-None-Match header condition failed")
|
||||
ignoreifmodifiedsince = True
|
||||
ignoreIfModifiedSince = False
|
||||
if "HTTP_IF_NONE_MATCH" in environ and res.supportEtag(): #dav.isInfoTypeSupported(path, "etag"):
|
||||
ifmatchlist = environ["HTTP_IF_NONE_MATCH"].split(",")
|
||||
for ifmatchtag in ifmatchlist:
|
||||
ifmatchtag = ifmatchtag.strip(" \"\t")
|
||||
if ifmatchtag == entitytag or ifmatchtag == "*":
|
||||
# ETag matched. If it's a GET request and we don't have an
|
||||
# conflicting If-Modified header, we return NOT_MODIFIED
|
||||
if environ["REQUEST_METHOD"] in ("GET", "HEAD") and not ifModifiedSinceFailed:
|
||||
raise DAVError(HTTP_NOT_MODIFIED,
|
||||
"If-None-Match header failed")
|
||||
raise DAVError(HTTP_PRECONDITION_FAILED,
|
||||
"If-None-Match header condition failed")
|
||||
ignoreIfModifiedSince = True
|
||||
|
||||
if not isnewfile and 'HTTP_IF_UNMODIFIED_SINCE' in environ and dav.isInfoTypeSupported(path, "modified"):
|
||||
ifunmodtime = parseTimeString(environ['HTTP_IF_UNMODIFIED_SINCE'])
|
||||
if "HTTP_IF_UNMODIFIED_SINCE" in environ and res.supportModified(): #dav.isInfoTypeSupported(path, "modified"):
|
||||
ifunmodtime = parseTimeString(environ["HTTP_IF_UNMODIFIED_SINCE"])
|
||||
if ifunmodtime and ifunmodtime <= lastmodified:
|
||||
raise DAVError(HTTP_PRECONDITION_FAILED,
|
||||
"If-Unmodified-Since header condition failed")
|
||||
|
||||
if not isnewfile and 'HTTP_IF_MODIFIED_SINCE' in environ and not ignoreifmodifiedsince and dav.isInfoTypeSupported(path, "modified"):
|
||||
ifmodtime = parseTimeString(environ['HTTP_IF_MODIFIED_SINCE'])
|
||||
if ifmodtime and ifmodtime > lastmodified:
|
||||
raise DAVError(HTTP_NOT_MODIFIED,
|
||||
"If-Modified-Since header condition failed")
|
||||
if ifModifiedSinceFailed and not ignoreIfModifiedSince:
|
||||
raise DAVError(HTTP_NOT_MODIFIED,
|
||||
"If-Modified-Since header condition failed")
|
||||
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
@ -566,7 +878,7 @@ reIfTagListContents = re.compile(r'(\S+)')
|
|||
|
||||
|
||||
def parseIfHeaderDict(environ):
|
||||
"""Parse HTTP_IF header into a dictionary and lists, and cache result.
|
||||
"""Parse HTTP_IF header into a dictionary and lists, and cache the result.
|
||||
|
||||
@see http://www.webdav.org/specs/rfc2518.html#HEADER_If
|
||||
"""
|
||||
|
@ -574,32 +886,32 @@ def parseIfHeaderDict(environ):
|
|||
return
|
||||
|
||||
if not "HTTP_IF" in environ:
|
||||
environ['wsgidav.conditions.if'] = None
|
||||
environ['wsgidav.ifLockTokenList'] = []
|
||||
environ["wsgidav.conditions.if"] = None
|
||||
environ["wsgidav.ifLockTokenList"] = []
|
||||
return
|
||||
|
||||
iftext = environ["HTTP_IF"].strip()
|
||||
if not iftext.startswith('<'):
|
||||
iftext = '<*>' + iftext
|
||||
if not iftext.startswith("<"):
|
||||
iftext = "<*>" + iftext
|
||||
|
||||
ifDict = dict([])
|
||||
ifLockList = []
|
||||
|
||||
resource1 = '*'
|
||||
resource1 = "*"
|
||||
for (tmpURLVar, URLVar, _tmpContentVar, contentVar) in reIfSeparator.findall(iftext):
|
||||
if tmpURLVar != '':
|
||||
if tmpURLVar != "":
|
||||
resource1 = URLVar
|
||||
else:
|
||||
listTagContents = []
|
||||
testflag = True
|
||||
for listitem in reIfTagListContents.findall(contentVar):
|
||||
if listitem.upper() != 'NOT':
|
||||
if listitem.startswith('['):
|
||||
listTagContents.append((testflag,'entity',listitem.strip('\"[]')))
|
||||
if listitem.upper() != "NOT":
|
||||
if listitem.startswith("["):
|
||||
listTagContents.append((testflag,"entity",listitem.strip('\"[]')))
|
||||
else:
|
||||
listTagContents.append((testflag,'locktoken',listitem.strip('<>')))
|
||||
listTagContents.append((testflag,"locktoken",listitem.strip("<>")))
|
||||
ifLockList.append(listitem.strip("<>"))
|
||||
testflag = listitem.upper() != 'NOT'
|
||||
testflag = listitem.upper() != "NOT"
|
||||
|
||||
if resource1 in ifDict:
|
||||
listTag = ifDict[resource1]
|
||||
|
@ -608,60 +920,34 @@ def parseIfHeaderDict(environ):
|
|||
ifDict[resource1] = listTag
|
||||
listTag.append(listTagContents)
|
||||
|
||||
environ['wsgidav.conditions.if'] = ifDict
|
||||
environ['wsgidav.ifLockTokenList'] = ifLockList
|
||||
environ["wsgidav.conditions.if"] = ifDict
|
||||
environ["wsgidav.ifLockTokenList"] = ifLockList
|
||||
debug("if", "parseIfHeaderDict", ifDict)
|
||||
return
|
||||
|
||||
|
||||
#def _lookForLockTokenInSubDict(locktoken, listTest):
|
||||
# for listTestConds in listTest:
|
||||
# for (testflag, checkstyle, checkvalue) in listTestConds:
|
||||
# if checkstyle == 'locktoken' and testflag:
|
||||
# if locktoken == checkvalue:
|
||||
# return True
|
||||
# return False
|
||||
|
||||
|
||||
|
||||
|
||||
#def testForLockTokenInIfHeaderDict(dictIf, locktoken, fullurl, headurl):
|
||||
# if '*' in dictIf:
|
||||
# if _lookForLockTokenInSubDict(locktoken, dictIf['*']):
|
||||
# return True
|
||||
#
|
||||
# if fullurl in dictIf:
|
||||
# if _lookForLockTokenInSubDict(locktoken, dictIf[fullurl]):
|
||||
# return True
|
||||
#
|
||||
# if headurl in dictIf:
|
||||
# if _lookForLockTokenInSubDict(locktoken, dictIf[headurl]):
|
||||
# return True
|
||||
|
||||
|
||||
|
||||
|
||||
def testIfHeaderDict(dav, path, dictIf, fullurl, locktokenlist, entitytag):
|
||||
|
||||
log("testIfHeaderDict(%s, %s, %s, %s)" % (path, fullurl, locktokenlist, entitytag),
|
||||
dictIf)
|
||||
def testIfHeaderDict(res, dictIf, fullurl, locktokenlist, entitytag):
|
||||
debug("if", "testIfHeaderDict(%s, %s, %s)" % (fullurl, locktokenlist, entitytag),
|
||||
dictIf)
|
||||
|
||||
if fullurl in dictIf:
|
||||
listTest = dictIf[fullurl]
|
||||
elif '*' in dictIf:
|
||||
listTest = dictIf['*']
|
||||
elif "*" in dictIf:
|
||||
listTest = dictIf["*"]
|
||||
else:
|
||||
return True
|
||||
|
||||
supportEntityTag = dav.isInfoTypeSupported(path, "etag")
|
||||
# supportEntityTag = dav.isInfoTypeSupported(path, "etag")
|
||||
supportEntityTag = res.supportEtag()
|
||||
for listTestConds in listTest:
|
||||
matchfailed = False
|
||||
|
||||
for (testflag, checkstyle, checkvalue) in listTestConds:
|
||||
if checkstyle == 'entity' and supportEntityTag:
|
||||
if checkstyle == "entity" and supportEntityTag:
|
||||
testresult = entitytag == checkvalue
|
||||
elif checkstyle == 'entity':
|
||||
elif checkstyle == "entity":
|
||||
testresult = testflag
|
||||
elif checkstyle == 'locktoken':
|
||||
elif checkstyle == "locktoken":
|
||||
testresult = checkvalue in locktokenlist
|
||||
else: # unknown
|
||||
testresult = True
|
||||
|
@ -671,14 +957,41 @@ def testIfHeaderDict(dav, path, dictIf, fullurl, locktokenlist, entitytag):
|
|||
break
|
||||
if not matchfailed:
|
||||
return True
|
||||
debug("if", " -> FAILED")
|
||||
return False
|
||||
|
||||
testIfHeaderDict.__test__ = False # Tell nose to ignore this function
|
||||
|
||||
#===============================================================================
|
||||
# TEST
|
||||
#===============================================================================
|
||||
if __name__ == "__main__":
|
||||
#n = etree.XML("<a><b/></a><c/>")
|
||||
n = etree.XML("abc")
|
||||
print etree.tostring(n)
|
||||
def testLogging():
|
||||
enable_loggers = ["test",
|
||||
]
|
||||
initLogging(0, enable_loggers)
|
||||
|
||||
pass
|
||||
_baseLogger = logging.getLogger(BASE_LOGGER_NAME)
|
||||
_enabledLogger = getModuleLogger("test")
|
||||
_disabledLogger = getModuleLogger("test2")
|
||||
|
||||
_baseLogger.debug("_baseLogger.debug")
|
||||
_baseLogger.info("_baseLogger.info")
|
||||
_baseLogger.warning("_baseLogger.warning")
|
||||
_baseLogger.error("_baseLogger.error")
|
||||
print >>sys.stderr, ""
|
||||
|
||||
_enabledLogger.debug("_enabledLogger.debug")
|
||||
_enabledLogger.info("_enabledLogger.info")
|
||||
_enabledLogger.warning("_enabledLogger.warning")
|
||||
_enabledLogger.error("_enabledLogger.error")
|
||||
print >>sys.stderr, ""
|
||||
|
||||
_disabledLogger.debug("_disabledLogger.debug")
|
||||
_disabledLogger.info("_disabledLogger.info")
|
||||
_disabledLogger.warning("_disabledLogger.warning")
|
||||
_disabledLogger.error("_disabledLogger.error")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
testLogging()
|
||||
pass
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
"""
|
||||
Current WsgiDAV version number.
|
||||
|
||||
http://peak.telecommunity.com/DevCenter/setuptools#specifying-your-project-s-version
|
||||
http://peak.telecommunity.com/DevCenter/setuptools#tagging-and-daily-build-or-snapshot-releases
|
||||
"""
|
||||
__version__ = "0.4.0.pre"
|
||||
__version__ = "0.4.0b1"
|
||||
|
|
|
@ -12,6 +12,46 @@ wsgidav_app
|
|||
WSGI container, that handles the HTTP requests. This object is passed to the
|
||||
WSGI server and represents our WsgiDAV application to the outside.
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
provider_mapping
|
||||
Type: dictionary, default: {}
|
||||
{shareName: DAVProvider,
|
||||
}
|
||||
user_mapping
|
||||
Type: dictionary, default: {}
|
||||
host
|
||||
Type: str, default: 'localhost'
|
||||
port
|
||||
Type: int, default: 8080
|
||||
ext_servers
|
||||
Type: string list
|
||||
enable_loggers
|
||||
List
|
||||
propsmanager
|
||||
Default: None (use property_manager.PropertyManager)
|
||||
locksmanager
|
||||
Default: None (use lock_manager.LockManager)
|
||||
domaincontroller
|
||||
Default: None (use domain_controller.WsgiDAVDomainController(user_mapping))
|
||||
verbose
|
||||
Type: int, default: 2
|
||||
0 no output (excepting application exceptions)
|
||||
1 - show single line request summaries (for HTTP logging)
|
||||
2 - show additional events
|
||||
3 - show full request/response header info (HTTP Logging) request body and GET response bodies not shown
|
||||
|
||||
# HTTP Authentication Options
|
||||
"acceptbasic": True, # Allow basic authentication, True or False
|
||||
"acceptdigest": True, # Allow digest authentication, True or False
|
||||
"defaultdigest": True, # True (default digest) or False (default basic)
|
||||
|
||||
# Organizational Information - printed as a footer on html output
|
||||
"response_trailer": None,
|
||||
|
||||
|
||||
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
||||
|
||||
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
|
||||
|
@ -19,10 +59,11 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
|
|||
from fs_dav_provider import FilesystemProvider
|
||||
from wsgidav.dir_browser import WsgiDavDirBrowser
|
||||
from wsgidav.dav_provider import DAVProvider
|
||||
import time
|
||||
import sys
|
||||
import threading
|
||||
import urllib
|
||||
import util
|
||||
import os
|
||||
from error_printer import ErrorPrinter
|
||||
from debug_filter import WsgiDavDebugFilter
|
||||
from http_authenticator import HTTPAuthenticator
|
||||
|
@ -34,7 +75,46 @@ from lock_manager import LockManager
|
|||
__docformat__ = "reStructuredText"
|
||||
|
||||
|
||||
_defaultOpts = {}
|
||||
# Use these settings, if config file does not define them (or is totally missing)
|
||||
DEFAULT_CONFIG = {
|
||||
"mount_path": None, # Application root, e.g. <mount_path>/<share_name>/<res_path>
|
||||
"provider_mapping": {},
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"ext_servers": [
|
||||
# "paste",
|
||||
# "cherrypy",
|
||||
# "wsgiref",
|
||||
"wsgidav",
|
||||
],
|
||||
|
||||
"enable_loggers": [
|
||||
],
|
||||
|
||||
"propsmanager": None, # None: use property_manager.PropertyManager
|
||||
"locksmanager": None, # None: use lock_manager.LockManager
|
||||
|
||||
# HTTP Authentication Options
|
||||
"user_mapping": {}, # dictionary of dictionaries
|
||||
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
|
||||
"acceptbasic": True, # Allow basic authentication, True or False
|
||||
"acceptdigest": True, # Allow digest authentication, True or False
|
||||
"defaultdigest": True, # True (default digest) or False (default basic)
|
||||
|
||||
# Verbose Output
|
||||
"verbose": 2, # 0 - no output (excepting application exceptions)
|
||||
# 1 - show single line request summaries (for HTTP logging)
|
||||
# 2 - show additional events
|
||||
# 3 - show full request/response header info (HTTP Logging)
|
||||
# request body and GET response bodies not shown
|
||||
|
||||
|
||||
# Organizational Information - printed as a footer on html output
|
||||
"response_trailer": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def _checkConfig(config):
|
||||
mandatoryFields = ["provider_mapping",
|
||||
|
@ -44,37 +124,49 @@ def _checkConfig(config):
|
|||
raise ValueError("Invalid configuration: missing required field '%s'" % field)
|
||||
|
||||
|
||||
|
||||
|
||||
class WsgiDAVApp(object):
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
util.initLogging(config["verbose"], config.get("enable_loggers", []))
|
||||
|
||||
util.log("Default encoding: %s (file system: %s)" % (sys.getdefaultencoding(), sys.getfilesystemencoding()))
|
||||
|
||||
# Evaluate configuration and set defaults
|
||||
_checkConfig(config)
|
||||
provider_mapping = self.config["provider_mapping"]
|
||||
response_trailer = config.get("response_trailer", "")
|
||||
self._verbose = config.get("verbose", 2)
|
||||
|
||||
locksmanager = config.get("locksmanager")
|
||||
if not locksmanager:
|
||||
locksfile = config.get("locksfile") or "wsgidav-locks.shelve"
|
||||
locksfile = os.path.abspath(locksfile)
|
||||
locksmanager = LockManager(locksfile)
|
||||
locksManager = config.get("locksmanager")
|
||||
if not locksManager:
|
||||
locksManager = LockManager()
|
||||
|
||||
propsmanager = config.get("propsmanager")
|
||||
if not propsmanager:
|
||||
propsfile = config.get("propsfile") or "wsgidav-props.shelve"
|
||||
propsfile = os.path.abspath(propsfile)
|
||||
propsmanager = PropertyManager(propsfile)
|
||||
propsManager = config.get("propsmanager")
|
||||
if not propsManager:
|
||||
propsManager = PropertyManager()
|
||||
|
||||
mount_path = config.get("mount_path")
|
||||
|
||||
user_mapping = self.config.get("user_mapping", {})
|
||||
domaincontrollerobj = config.get("domaincontroller") or WsgiDAVDomainController(user_mapping)
|
||||
domainController = config.get("domaincontroller") or WsgiDAVDomainController(user_mapping)
|
||||
isDefaultDC = isinstance(domainController, WsgiDAVDomainController)
|
||||
|
||||
# authentication fields
|
||||
authacceptbasic = config.get("acceptbasic", False)
|
||||
authacceptbasic = config.get("acceptbasic", True)
|
||||
authacceptdigest = config.get("acceptdigest", True)
|
||||
authdefaultdigest = config.get("defaultdigest", True)
|
||||
|
||||
|
||||
# Check configuration for NTDomainController
|
||||
# We don't use 'isinstance', because include would fail on non-windows boxes.
|
||||
wdcName = "NTDomainController"
|
||||
if domainController.__class__.__name__ == wdcName:
|
||||
if authacceptdigest or authdefaultdigest or not authacceptbasic:
|
||||
print >>sys.stderr, "WARNING: %s requires basic authentication.\n\tSet acceptbasic=True, acceptdigest=False, defaultdigest=False" % wdcName
|
||||
|
||||
# Instantiate DAV resource provider objects for every share
|
||||
self.providerMap = {}
|
||||
for (share, provider) in provider_mapping.items():
|
||||
|
@ -89,30 +181,39 @@ class WsgiDAVApp(object):
|
|||
assert isinstance(provider, DAVProvider)
|
||||
|
||||
provider.setSharePath(share)
|
||||
if mount_path:
|
||||
provider.setMountPath(mount_path)
|
||||
|
||||
# TODO: someday we may want to configure different lock/prop managers per provider
|
||||
provider.setLockManager(locksmanager)
|
||||
provider.setPropManager(propsmanager)
|
||||
provider.setLockManager(locksManager)
|
||||
provider.setPropManager(propsManager)
|
||||
|
||||
self.providerMap[share] = provider
|
||||
|
||||
|
||||
if self._verbose >= 2:
|
||||
print "Using lock manager: %s" % locksManager
|
||||
print "Using property manager: %s" % propsManager
|
||||
print "Using domain controller: %s" % domainController
|
||||
print "Registered DAV providers:"
|
||||
for k, v in self.providerMap.items():
|
||||
for share, provider in self.providerMap.items():
|
||||
hint = ""
|
||||
if not user_mapping.get(share):
|
||||
hint = "Anonymous!"
|
||||
print " Share '%s': %s%s" % (k, v, hint)
|
||||
if self._verbose >= 1:
|
||||
if isDefaultDC and not user_mapping.get(share):
|
||||
hint = " Anonymous!"
|
||||
print " Share '%s': %s%s" % (share, provider, hint)
|
||||
|
||||
# If the default DC is used, emit a warning for anonymous realms
|
||||
if isDefaultDC and self._verbose >= 1:
|
||||
for share in self.providerMap:
|
||||
if not user_mapping.get(share):
|
||||
# TODO: we should only warn here, if --no-auth is not given
|
||||
print "WARNING: share '%s' will allow anonymous access." % share
|
||||
|
||||
# Define WSGI application stack
|
||||
application = RequestResolver()
|
||||
application = WsgiDavDirBrowser(application)
|
||||
application = HTTPAuthenticator(application,
|
||||
domaincontrollerobj,
|
||||
domainController,
|
||||
authacceptbasic,
|
||||
authacceptdigest,
|
||||
authdefaultdigest)
|
||||
|
@ -127,13 +228,8 @@ class WsgiDAVApp(object):
|
|||
|
||||
def __call__(self, environ, start_response):
|
||||
|
||||
print >> environ["wsgi.errors"], "%s SCRIPT_NAME:'%s', PATH_INFO:'%s', %s\n %s" % (
|
||||
environ.get("REQUEST_METHOD"),
|
||||
environ.get("SCRIPT_NAME"),
|
||||
environ.get("PATH_INFO"),
|
||||
environ.get("REMOTE_USER"),
|
||||
environ.get("HTTP_AUTHORIZATION"),
|
||||
)
|
||||
util.log("SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
|
||||
|
||||
# We unquote PATH_INFO here, although this should already be done by
|
||||
# the server.
|
||||
path = urllib.unquote(environ["PATH_INFO"])
|
||||
|
@ -157,29 +253,27 @@ class WsgiDAVApp(object):
|
|||
share = r
|
||||
break
|
||||
elif path.upper() == r.upper() or path.upper().startswith(r.upper()+"/"):
|
||||
# print path, ":->" , r
|
||||
share = r
|
||||
break
|
||||
|
||||
provider = self.providerMap.get(share)
|
||||
|
||||
# Note: we call the next app, even if provider is None, because OPTIONS
|
||||
# must still be handled
|
||||
# must still be handled.
|
||||
# All other requests will result in '404 Not Found'
|
||||
environ["wsgidav.provider"] = provider
|
||||
|
||||
# TODO: test with multi-level realms: 'aa/bb'
|
||||
# TODO: test security: url contains '..'
|
||||
# TODO: SCRIPT_NAME could already contain <approot>
|
||||
# See @@
|
||||
|
||||
# Transform SCRIPT_NAME and PATH_INFO
|
||||
# (Since path and share are unquoted, this also fixes quoted values.)
|
||||
# (Since path and share are unquoted, this also fixes quoted values.)
|
||||
if share == "/" or not share:
|
||||
environ["SCRIPT_NAME"] = ""
|
||||
environ["PATH_INFO"] = path
|
||||
else:
|
||||
environ["SCRIPT_NAME"] = share
|
||||
environ["SCRIPT_NAME"] += share
|
||||
environ["PATH_INFO"] = path[len(share):]
|
||||
# util.log("--> SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
|
||||
|
||||
# See http://mail.python.org/pipermail/web-sig/2007-January/002475.html
|
||||
# for some clarification about SCRIPT_NAME/PATH_INFO format
|
||||
|
@ -189,20 +283,49 @@ class WsgiDAVApp(object):
|
|||
assert environ["SCRIPT_NAME"] in ("", "/") or not environ["SCRIPT_NAME"].endswith("/")
|
||||
# PATH_INFO starts with '/'
|
||||
assert environ["PATH_INFO"] == "" or environ["PATH_INFO"].startswith("/")
|
||||
|
||||
# Log HTTP request
|
||||
if self._verbose >= 1:
|
||||
threadInfo = ""
|
||||
userInfo = ""
|
||||
if self._verbose >= 2:
|
||||
threadInfo = "<%s>" % threading._get_ident() #.currentThread())
|
||||
if not environ.get("HTTP_AUTHORIZATION"):
|
||||
|
||||
start_time = time.time()
|
||||
def _start_response_wrapper(status, response_headers, exc_info=None):
|
||||
# Log request
|
||||
if self._verbose >= 1:
|
||||
threadInfo = ""
|
||||
userInfo = environ.get("http_authenticator.username")
|
||||
if not userInfo:
|
||||
userInfo = "(anonymous)"
|
||||
if self._verbose >= 1:
|
||||
threadInfo = "<%s> " % threading._get_ident()
|
||||
extra = []
|
||||
if environ.get("CONTENT_LENGTH", "") != "":
|
||||
extra.append("length=%s" % environ.get("CONTENT_LENGTH"))
|
||||
if "HTTP_DEPTH" in environ:
|
||||
extra.append("depth=%s" % environ.get("HTTP_DEPTH"))
|
||||
if "HTTP_OVERWRITE" in environ:
|
||||
extra.append("overwrite=%s" % environ.get("HTTP_OVERWRITE"))
|
||||
if "HTTP_DESTINATION" in environ:
|
||||
extra.append('dest="%s"' % environ.get("HTTP_DESTINATION"))
|
||||
if self._verbose >= 2 and "HTTP_USER_AGENT" in environ:
|
||||
extra.append('agent="%s"' % environ.get("HTTP_USER_AGENT"))
|
||||
if self._verbose >= 1:
|
||||
extra.append('elap=%.3fsec' % (time.time() - start_time))
|
||||
extra = ", ".join(extra)
|
||||
|
||||
print >> environ["wsgi.errors"], "[", util.getRfc1123Time(),"] from ", environ.get("REMOTE_ADDR","unknown"), " ", threadInfo, environ.get("REQUEST_METHOD","unknown"), " ", environ.get("PATH_INFO","unknown"), " ", environ.get("HTTP_DESTINATION", ""), userInfo
|
||||
|
||||
# This is the CherryPy format:
|
||||
# 127.0.0.1 - - [08/Jul/2009:17:25:23] "GET /loginPrompt?redirect=/renderActionList%3Frelation%3Dpersonal%26key%3D%26filter%3DprivateSchedule&reason=0 HTTP/1.1" 200 1944 "http://127.0.0.1:8002/command?id=CMD_Schedule" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1) Gecko/20090624 Firefox/3.5"
|
||||
print >>sys.stderr, '%s - %s - [%s] "%s" %s -> %s' % (
|
||||
threadInfo + environ.get("REMOTE_ADDR",""),
|
||||
userInfo,
|
||||
util.getLogTime(),
|
||||
environ.get("REQUEST_METHOD") + " " + environ.get("PATH_INFO", ""),
|
||||
extra,
|
||||
status,
|
||||
# response Content-Length
|
||||
# referer
|
||||
)
|
||||
return start_response(status, response_headers, exc_info)
|
||||
|
||||
# Call next middleware
|
||||
for v in self._application(environ, start_response):
|
||||
# for v in self._application(environ, start_response):
|
||||
for v in self._application(environ, _start_response_wrapper):
|
||||
util.debug("sc", "WsgiDAVApp: yield start")
|
||||
yield v
|
||||
util.debug("sc", "WsgiDAVApp: yield end")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue