WEB开发网
开发学院软件开发Java 开放源码 CMS 入门,第 6 部分: 为 Jakarta Slide... 阅读

开放源码 CMS 入门,第 6 部分: 为 Jakarta Slide 构建 Python WebDAV 客户机

 2010-04-16 00:00:00 来源:WEB开发网   
核心提示:读前须知了解从本教程中可以获得哪些信息,以及如何利用本教程,开放源码 CMS 入门,第 6 部分: 为 Jakarta Slide 构建 Python WebDAV 客户机,关于本系列本教程是一个系列中的第 6 篇,在这个系列中,还您将使用 PHP 创建一个用于管理 Slide 的内容的 Web 应用程序,结果将会是一

读前须知

了解从本教程中可以获得哪些信息,以及如何利用本教程。

关于本系列

本教程是一个系列中的第 6 篇,在这个系列中,您将使用 Eclipse、Java™技术、Apache Derby 和其他开放源代码工具创建一个定制的开放源代码内容管理系统 (CMS)。在前一个教程中,您创建了一个 PHP WebDAV 客户机。因此在本教程中,您可以在此基础上实现您的基于 Derby 的 Python 内容管理需求。

结合 Eclipse,您将使用 WebDAV(Jakarta Slide 的一部分)和其他插件。结合 Java 技术,您将使用 Slide 和 Apache Tomcat,当然还有 Python 和 Python Eclipse 插件 (PyDev)。

关于本教程

本教程是为那些要使用 Python 并通过创建 Python WebDAV 客户机来访问 Jakarta Slide WebDAV 服务器(或任何其他 WebDAV 服务器)的开发人员编写的。通过构建这个客户机,您将获得构建其他 Python 应用程序(比如以 Slide 为后端的 Python 内容管理应用程序)所需的基本知识。

完成本教程后,您将能够从您的 Python 应用程序访问 Slide 或任何其他 WebDAV 服务器(例如 Microsoft® Exchange Server Web 文件夹或 Microsoft Windows® SharePoint™ Services Web 文件夹)。通过这种方法,您将得到一个能处理数百个用户和数千个文档、具有文档(内容)和元数据(访问控制列表、资源层次结构等)的中央存储的系统。

在本教程中,您将:

下载和设置 Python 环境。

下载和安装 PyDev。

在 Eclipse 中创建 Python 客户机项目。

开发并用 Slide 测试 Python 客户机。

回顾和总结。

必要知识

您需要懂得基本的 Python 编程,并且知道如何使用 Eclipse,以便能完成本教程。

系统要求

若要运行本教程中的代码,需要:

Python V2.5 或更高版本。

Eclipse V3.1 或更高版本。

Slide/Tomcat bundle (在本系列的第 1 部分中已安装)。

注意:也可以使用本系列的第 4 部分中采用的启用了 Derby 的那个版本。

Python Eclipse 插件 PyDev。

如果当前没有使用 Slide/Tomcat bundle,则可以使用 Apache Tomcat V5.0.30。

注意:Slide 源代码中不支持带 SDK V1.5 的 Tomcat V5.5。该代码所支持的最新版本是 Tomcat V5.0.30 和 SDK V1.4。

任何版本的 Linux® 或 Windows 操作系统都可以运行本教程中的例子。这里的要求很低,所以如果您想在 Slide 所在的同一台计算上运行该客户机,那么应该没有问题,Slide 是轻量级的,可以在 Tomcat 能运行的任何地方运行。

开始一个 Eclipse 项目

本节中将设置 Eclipse,以创建 Python WebDAV 客户机。

安装 Eclipse

注意:如果系统上已经安装了 Eclipse V3.0.1 或更高版本,那么可以省略这一步。

下载、安装和运行 Eclipse 很简单,只需完成以下步骤:

下载要使用的 Eclipse 版本。本教程使用 Eclipse V3.1 for Windows。

将 .zip 或 .tar 文件夹解压到您选择的目录中。

运行 Eclipse.exe (或 UNIX® 中相应的文件)。

如果您还没有完成本系列的 第 4 部分,那么请现在完成该部分。为了开始本教程,需要在 Eclipse 中安装和构建 Slide V2.1 的完整源代码。

安装 Python

下载和安装 Python 也很容易,只需完成以下步骤:

下载和安装 Python。

通读 Python 文档,包括 Python FAQ。

下载 Slide bundle

在前面的教程中,您从 Tomcat bundle 安装了 Slide。在本教程中,将使用这个 bundle 版的 Slide。

开始 Python WebDAV 项目

在 Eclipse 中创建一个 Python 项目。如果您还没有完成 第 1 部分,那么现在就完成该教程,直到可以运行 Slide V2.1。由于本教程不包括任何 Java 编程,因此可以使用那个 bundle。 如果要使用 Derby,那么请完成本系列的 第 4 部分,使 Slide 和 Derby 能够运行。

如果您已经设置好 Python Eclipse 环境,那么可以直接跳到 创建 Python 项目小节。否则,为了让本教程中的项目能够运行,必须适当地设置 PyDev。为此,可以遵循 PyDev Setup 中的说明。

创建 Python 项目

现在需要在 Eclipse 中创建一个新的 Python 项目。

创建新的 Python 项目

首先,在 Eclipse 中创建一个新的 Python 项目。为此,单击 File > New > Project > Python > Python Project。这样将显示如图 1 所示的 Python New Project 窗口,在此窗口中可以为 Python WebDAV 客户机创建新项目。

图 1. 创建新的 Python 项目
开放源码 <a target=CMS 入门,第 6 部分: 为 Jakarta Slide 构建 Python WebDAV 客户机" border="0" onload="return imgzoom(this,550);" style="cursor:pointer;" onclick="javascript:window.open(this.src);"/>

创建两个新的 Python 文件:其中一个文件名为 davclienttest.py,该文件将作为测试页面,另一个文件名为 davclientlib.py,该文件将作为实际的类源文件,用于与 WebDAV 服务器(比如您的 Slide V2.1 服务器)进行交互。

当在 Eclipse 中单击 File > New > File 创建文件时,得到的是一个空的初始文件,如清单 1 所示。根据设置 Python 模板的方式,您可以使用 Eclipse 的 Code Assist 特性来插入头部。

清单 1. 新 Python 项目的初始文件

# /usr/bin/python 
# 
# Created on Dec 17, 2005 
# 

可以在 Eclipse 的 Preferences 视图中修改这个默认的头部,使新文件可以包含更多信息,例如版权信息和项目信息。但是在这里我们不必关心这一点。

根据 Python FAQ Getting Started 小节的介绍,为 davclientlib.py 和 davclienttest.py 文件插入一行 Hello World 代码。于是就可以通过在 Python Perspective 视图中的 Console 视图中查看 Hello World。通过添加这行代码,可以检查设置是否正确,是否可以开始编写代码。首先像清单 2 中那样修改 davclientlib.py。这里为 HTTPConnectionAuth 类编写了初始的类代码,这个类包装了 httplib.HTTPConnection 类,并简化了使用。

清单 2. davclientlib.py 的初始代码

# 
# DAV client library 
# 
 
import httplib 
import urllib 
import string 
import types 
import mimetypes 
import base64 
try: 
 import pyexpat 
except ImportError: 
 from xml.parsers import pyexpat 
 
error = __name__ + '.error' 
 
 
INFINITY = 'infinity' 
XML_DOC_HEADER = '<?xml version="1.0" encoding="utf-8"?>' 
XML_CONTENT_TYPE = 'text/xml; charset=utf-8' 
 
# block size for copying files up to the server 
BLOCKSIZE = 16384 
#default user 
USERNAME = 'root' 
PASSWORD = 'root' 
 
class HTTPConnectionAuth(httplib.HTTPConnection): 
 def __init__(self, *args, **kw): 
  httplib.HTTPConnection.__init__(self, *args, **kw) 
 
  self.__username = USERNAME 
  self.__password = PASSWORD 
  self.__nonce = None 
  self.__opaque = None 
 
 def setauth(self, username, password): 
   USERNAME = username 
   PASSWORD = password 
   self.__username = USERNAME 
   self.__password = PASSWORD 

注意:下载小节中有经过这一步修改的完整的 davclientlib.py 文件。

可以使用标准 Python 库 httplib.py 来管理到 Slide 的连接。传入 HOST、PORT、USERNAME、PASSWORD 和 URI 声明,连接将打开,并且保持在实例中。

所有类代码都出现在第一层缩进 class 声明之间,并且一直延续到下一个第一层缩进 class 声明( davclientlib 声明的括号。)

在本教程中,您将实现以下 WebDAV HTTP 命令:

mkcol

propfind

get

put

copy

move

delete

options

其他命令也大同小异,所以必要时应该不难编写那些命令。

第一组方法: 和

在本教程中发现您所需的方法。

将数据放入 Slide 和请求 Slide 中的数据

清单 3 展示了 DAV 类的开始部分,这个类包含所有 WebDAV 方法。第一个方法是一个包装器,通过它可以重用连接,然后传入方法、主体以及任何附加的头部,以遵从 WebDAV 协议标准。

添加 Basic 身份验证的额外头部。由于未知的原因,base64.encodestring() 方法添加了一个新行 -- \n。 这样添加新行有问题,因为额外的头部出现在原有的头部之后,我必须使用一个协议分析器才能发现被抛出的错误不是 XML 的解析器错误,而是一个协议错误,这个协议错误是由于额外的新行导致 Slide 将主体解释为提前两个字符开始而造成的。因此在 encodestring(auth_value) 调用的后面添加 .strip()。

清单 3. DAV 类实现

class DAV(HTTPConnectionAuth): 
 
 response_class = DAVResponse 
  
 def _request(self, method, url, body=None, extra_hdrs={}): 
  auth_value = '%s:%s' % (USERNAME, PASSWORD) 
  auth_value = 'Basic ' + base64.encodestring(auth_value).strip() 
  extra_hdrs['Authorization'] = auth_value 
  self.request(method, url, body, extra_hdrs) 
  return self.getresponse()   

_request() 方法是一系列用于格式化请求的方法中的最后一个方法,它实现了 WebDAV 协议请求。

定义 DAV 类的方法

清单 4 展示了为要实现的 DAV 类定义的方法。这里为某些参数添加了 print 语句,以便能执行测试。当您将实现代码添加到一些关键的方法中时,这些方法看上去就更加清晰起来。这样一来,您就可以开始编写自己的 davclienttest.py file 文件来练习使用这些方法,并使用一个 print 语句显示结果。

清单 4. WebDAV method stubs

class DAV(HTTPConnectionAuth): 
 
 response_class = DAVResponse 
 
 def get(self, url, extra_hdrs={ }): 
  print 'get() ' + url 
 
 def head(self, url, extra_hdrs={ }): 
  print 'head() ' + url 
 
 def post(self, url, data={ }, body=None, extra_hdrs={ }): 
  print 'post() ' + url 
 
 def options(self, url='*', extra_hdrs={ }): 
  print 'options() ' + url 
 
 def trace(self, url, extra_hdrs={ }): 
  print 'trace() ' + url 
 
 def put(self, url, contents, 
   print 'put() ' + url 
 
 def delete(self, url, extra_hdrs={ }): 
  print 'delete() ' + url 
 
 def propfind(self, url, body=None, depth=None, extra_hdrs={ }): 
  print 'propfind() ' + url 
 
 def proppatch(self, url, body, extra_hdrs={ }): 
  print 'proppatch() ' + url 
 
 def mkcol(self, url, extra_hdrs={ }): 
  print 'mkcol() ' + url 
 
 def move(self, src, dst, extra_hdrs={ }): 
  print 'move() ' + src + ' ' + dst 
 
 def copy(self, src, dst, depth=None, extra_hdrs={ }): 
  print 'copy() ' + src + ' ' + dst 
 
 def lock(self, url, owner='', timeout=None, depth=None, 
      scope='exclusive', type='write', extra_hdrs={ }): 
  print 'lock() ' + url 
 
 def unlock(self, url, locktoken, extra_hdrs={ }): 
  print 'unlock() ' + url 
 
 def _request(self, method, url, body=None, extra_hdrs={}): 
  print '_request() ' + url 
   
 def allprops(self, url, depth=None): 
  print 'allprops() ' + url 
 
 def propnames(self, url, depth=None): 
  print 'propnames() ' + url 
 
 def getprops(self, url, *names, **kw): 
  print 'getprops() ' + url 
 
 def delprops(self, url, *names, **kw): 
  print 'delprops() ' + url 
 
 def setprops(self, url, *xmlprops, **props): 
  print 'setprops() ' + url 
 
 def get_lock(self, url, owner='', timeout=None, depth=None): 
  print 'get_lock() ' + url 

创建内部类声明

清单 5 展示了为使用的内部类创建的 class 声明。

清单 5. 内部类声明

class _blank: 
 def __init__(self, **kw): 
  self.__dict__.update(kw) 
class _propstat(_blank): pass 
class _response(_blank): pass 

_multistatus 类

Slide 为 WebDAV 方法返回的大多数内容是多状态 (multistatus) XML。您需要一个如清单 6 中所示的类,这个类将查看被返回的多状态 XML 的类型,然后将其解析成能使用的某种东西。

清单 6. _multistatus 类

class _multistatus(_blank): pass 
 
def _extract_propstat(elem): 
 ps = _propstat(prop={}, status=None, responsedescription=None) 
 for child in elem.children: 
  if child.ns != 'DAV:': 
   continue 
  if child.name == 'prop': 
   for prop in child.children: 
    ps.prop[(prop.ns, prop.name)] = prop 
  elif child.name == 'status': 
   ps.status = _parse_status(child) 
  elif child.name == 'responsedescription': 
   ps.responsedescription = child.textof() 
  ### unknown element name 
 
 return ps 
 
def _extract_response(elem): 
 resp = _response(href=[], status=None, responsedescription=None, propstat=[]) 
 for child in elem.children: 
  if child.ns != 'DAV:': 
   continue 
  if child.name == 'href': 
   resp.href.append(child.textof()) 
  elif child.name == 'status': 
   resp.status = _parse_status(child) 
  elif child.name == 'responsedescription': 
   resp.responsedescription = child.textof() 
  elif child.name == 'propstat': 
   resp.propstat.append(_extract_propstat(child)) 
  ### unknown child element 
 
 return resp 
 
def _get_multistatus_response(root): 
 if root.ns != 'DAV:' or root.name != 'multistatus': 
  raise 'invalid response: <DAV:multistatus> expected' 
 
 msr = _multistatus(responses=[ ], responsedescription=None) 
 
 for child in root.children: 
  if child.ns != 'DAV:': 
   continue 
  if child.name == 'responsedescription': 
   msr.responsedescription = child.textof() 
  elif child.name == 'response': 
   msr.responses.append(_extract_response(child)) 
  ### unknown child element 
 
 return msr 
 
def _extract_locktoken(root): 
 if root.ns != 'DAV:' or root.name != 'prop': 
  raise 'invalid response: <DAV:prop> expected' 
 elem = root.find('lockdiscovery', 'DAV:') 
 if not elem: 
  raise 'invalid response: <DAV:lockdiscovery> expected' 
 elem = elem.find('activelock', 'DAV:') 
 if not elem: 
  raise 'invalid response: <DAV:activelock> expected' 
 elem = elem.find('locktoken', 'DAV:') 
 if not elem: 
  raise 'invalid response: <DAV:locktoken> expected' 
 elem = elem.find('href', 'DAV:') 
 if not elem: 
  raise 'invalid response: <DAV:href> expected' 
 return elem.textof() 

显然,WebDAV 服务器需要用一个特定于 WebDAV 的请求(例如 OPTIONS)来响应。因此,您在 OPTIONS 中实现了第一个 WebDAV 方法,并用它来验证服务器是否是 WebDAV 服务器。

对 OPTIONS 的响应如清单 7 所示:

清单 7. 对 OPTIONS 请求的响应

REQUEST: OPTIONS http://192.168.1.100:8080/slide/files/FirstTestText.txt 
STATUS: 200 
REASON: OK 
Pragma: No-cache 
Cache-Control: no-cache 
Expires: Wed, 31 Dec 1969 16:00:00 PST 
Set-Cookie: JSESSIONID=40EBDAF41C4DEAAF054626DBB3024640; Path=/slide 
DAV: 1, 2, slide, access-control, binding 
DAV: version-control, version-history, checkout-in-place 
DAV: workspace, working-resource, update, label 
Allow: PROPPATCH, COPY, DELETE, POST, GET, REPORT, PROPFIND, PUT, VERSION-CONTROL, MOVE, 
UNLOCK, TRACE, OPTIONS, HEAD, ACL, LOCK, CONNECT, SEARCH 
DASL: <DAV:basicsearch> 
MS-Author-Via: DAV 
Content-Length: 0 
Date: Sun, 19 Mar 2006 15:10:44 GMT 
Server: Apache-Coyote/1.1 

如果不确定是否联系上了一个 WebDAV 服务器,那么可以对那台服务器运行这个命令,看是否能收到上述响应。如果收到那样的响应,则说明该服务器是 WebDAV 服务器。

第一次测试

您有足够多的信息来尝试您的系统,但是首先还是需要开始编写测试页面。

连接到 WebDAV 服务器

清单 8 展示了使用 davclientlib 类、建立连接并报告结果的 Python 代码。将该代码插入 davclienttest.py。(要进行更完整的测试,请参阅下载小节。)

清单 8. WebDAV Client 测试脚本

# 
# WebDAV Client test 
# author Mike Oliver 
# 
 
import sys 
import davclientlib 
import string 
import StringIO 
 
 
HOST = 'localhost' 
PORT = 8080 
USERNAME = 'root' 
PASSWORD = 'root' 
 
NAMESPACE = 'slide' 
BASE = 'http://%s:%s/%s' % (HOST, PORT, NAMESPACE) 
 
class webdavtest(davclientlib.DAV): 
 def _request(self, method, url, *rest, **kw): 
  print 'REQUEST:', method, url 
  response = apply(davclientlib.DAV._request, (self, method, url) + rest, kw) 
 
  print "STATUS:", response.status 
  print "REASON:", response.reason 
  for hdr in response.msg.headers: 
   print string.strip(hdr) 
  print '-'*70 
  if response.status == 207: 
   #print response.doc.dump() 
   #print response.doc.toxml() 
   response.parse_multistatus() 
   davclientlib.dump(sys.stdout, response.root) 
  elif method == 'LOCK' and response.status == 200: 
   response.parse_lock_response() 
   davclientlib.dump(sys.stdout, response.root) 
  else: 
   print response.read() 
  print '-'*70 
  return response 
 def get_lock(self, url, owner='', timeout=None, depth=None): 
  return self.lock(url, owner, timeout, depth).locktoken 
 
 
def _davclient(): 
  davclient = webdavtest(HOST, PORT) 
  davclient.setauth(USERNAME, PASSWORD) 
  return davclient 
 
def getvalue(url, ns, prop): 
 response = _davclienttest().getproperties(url, prop, ns=ns) 
 resp = response.msr.responses[0] 
 if resp.status and resp.status[0] != 200: 
  raise 'error retrieving property', response.status 
 propertiestat = resp.propertiestat[0] 
 if propertiestat.status and propertiestat.status[0] != 200: 
  raise 'error retrieving property', propertiestat.status 
 return propertiestat.prop[(ns, prop)] 
 
def load_test(): 
 # some property values 
 prop1 = '<prop1>prop1</prop1>' 
 prop2 = '<prop2/>' 
 
 
 # TestCollection 
 _davclient().mkcol(BASE + '/files/TestCollection') 
 _davclient().mkcol(BASE + '/files/TestCollection/subCollection') 
 _davclient().put(BASE + \ 
  '/files/TestCollection/subCollection/TestFile.txt', \ 
  'TestFile.txt contents\n') 
 
 # attach a bunch of properties 
 _davclient().setproperties(BASE + '/files/TestCollection', 
         prop1, prop2) 
 _davclient().setproperties(BASE + '/files/TestCollection/subCollection/TestFile.txt', 
         prop1, prop2) 
  
 # do some moves and copies 
 _davclient().move(BASE + '/files/TestCollection/TestFile.txt', BASE + \ 
   '/files/TestCollection/newTestFile.txt') 
 _davclient().copy(BASE + \ 
   '/files/TestCollection/newCollection/ThirdTestFile.txt', BASE + 
   '/files/TestCollection/newCollection/ThirdTestFile.txtcopy') 
  
 # dump all the data 
 _davclient().allproperties(BASE + '/files/TestCollection') 
 
 
def delete_test(): 
 _davclient().delete(BASE + '/files/TestCollection') 
 
def gettest(): 
 _davclient()._request('GET', BASE + '/files/test.html') 
 
def if_test(): 
 _davclient().put(BASE + '/files/FirstTestText.txt', 'FirstTestText.txt contents\n') 
 etag = qp_xml.textof(getvalue(BASE + '/files/FirstTestText.txt', 'DAV:', 'getetag')) 
 print 'ETAG:', etag 
 _davclient()._request('DELETE', BASE + '/files/FirstTestText.txt', extra_hdrs={ 
  'If' : '(["abc"])', 
  }) 
 _davclient()._request('DELETE', BASE + '/files/FirstTestText.txt', extra_hdrs={ 
  'If' : '([' + etag + '])', 
  }) 
 
def lock_test(): 
 _davclient().delete(BASE + '/files/locktest') 
 _davclient().mkcol(BASE + '/files/locktest') 
 _davclient().mkcol(BASE + '/files/locktest/subCollection') 
 
 # test a locknull resource 
 r = _davclient().lock(BASE + '/files/locktest/locknull') 
 
def test(): 
 _davclient().put(BASE + '/files/FirstTestText.txt', 'FirstTestText.txt contents\n') 
 _davclient().options(BASE + '/files/FirstTestText.txt') 
 _davclient().delete(BASE + '/files/soonToBeDeleted') 
 _davclient().mkcol(BASE + '/files/soonToBeDeleted') 
 _davclient().getproperties(BASE + '/files', 'author', 'slideber', 'title') 
 body = '<?xml version="1.0" encoding="utf-8"?>' 
 body += '<D:propfind xmlns:D="DAV:"><D:prop>' 
 body += '<D:displayname/><D:getcontentlength/><D:getcontenttype/>' 
 body += '<D:resourcetype/><D:getlastmodified/>' 
 body += '<D:lockdiscovery/></D:prop></D:propfind>' 
         
 _davclient().propfind(NAMESPACE, body, 0) 
 _davclient().propfind(BASE + '/files', body, 0) 
 
 delete_test() 
 load_test() 
 
 
if __name__ == '__main__': 
 if HOST == 'FILL THIS IN': 
  import sys 
  sys.stdout = sys.stderr 
  print 'ERROR: set the HOST/PORT values to your running slide' 
  sys.exit(1) 
 
 test() 

现在,确信您的 Slide bundle 正在运行。(它应该指向类似于 http://localhost:8080/slide 的 URL。)测试是否能直接从一个浏览器使用将在 davclienttest.py 中使用的用户名和密码建立连接。知道 Slide 正在运行之后,就可以连接到 Slide,验证 davclienttest.py 中设置了相同的信息,并运行它。Eclipse 的 Python Perspective 视图中的 Python Browser 视图应该显示 Success: server does support webdav and user/password is accepted!。

我常常对我的程序员说 "picollo passo" 或 "small steps"。这里,您通过最少的步骤连接到了服务器并运行测试。现在,您可以以此为基础。如果这部分不能运行,那么重新检查一下,确定出错的地方。您可以使用浏览器来查看 Slide 内容,看看 davclienttest.py 脚本运行后的结果。您还可以看看 ShellOut.txt 文件,这是在我的带有 AJCS 名称空间的 Slide 上测试整个 davclienttest.py 脚本所得到的输出。

运行测试

现在运行测试则只会打印出传给每个方法的参数。清单 9 展示了 propfind() 方法。

清单 9. propfind() 方法

#davclientlib.py DAV class 
 
def propfind(self, url, body=None, depth=None, extra_hdrs={ }): 
  headers = extra_hdrs.copy() 
  headers['Content-Type'] = XML_CONTENT_TYPE 
  if depth is not None: 
   headers['Depth'] = str(depth) 
  return self._request('PROPFIND', url, body, headers) 
 
#davclienttest.py the tests that call this method. 
 
 body = '<?xml version="1.0" encoding="utf-8"?>' 
 body += '<D:propfind xmlns:D="DAV:"><D:prop>' 
 body += '<D:displayname/><D:getcontentlength/><D:getcontenttype/>' 
 body += '<D:resourcetype/><D:getlastmodified/><D:lockdiscovery/>' 
 body += '</D:prop></D:propfind>' 
         
 _davclient().propfind(NAMESPACE, body, 0) 

现在,在 davclienttest.py 页面中,调用类 list 方法来显示结果。主体将是一个 XML 文档,其中有一个 propfind 元素和一个 prop 元素,并且有一些定义哪些属性在哪个名称空间的元素。

在清单 10 所示的例子中,所有属性都是受 Slide 支持的标准 DAV 名称空间属性。 这个例子是可扩展的,您可以在自己的名称空间中拥有自己的属性。只需记住,您必须消除那些可能在多个具有名称空间标识符的名称空间中有属性名称的属性的歧义。

清单 10. allproperties() 方法

#davclienttest.py 
#davclienttest.py 
 _davclient().allproperties(BASE + '/files', 1) 
 
#davclientlib.py allproperties() 
 def allproperties(self, url, depth=None): 
  body = XML_DOC_HEADER + \ 
  '<D:propfind xmlns:D="DAV:"><D:prop>' + \ 
  '<D:displayname/><D:getcontentlength/><D:getcontenttype/>' + \ 
  '<D:resourcetype/><D:getlastmodified/><D:lockdiscovery/>' + \ 
  </D:prop<>/D:propfind>' 
  return self.propfind(url, body, depth=depth) 

您可以看到,propfind 的 allproperties 包装器用所有已知的属性格式化请求的主体,这样您就不必每次构建那个 XML。

注意:如果您添加自己的属性,也要考虑在这里添加它们。

运行 davclienttest.py。Python Console 现在应该显示如 图 2 所示的界面。

图 2. 新的测试页面
开放源码 <a target=CMS 入门,第 6 部分: 为 Jakarta Slide 构建 Python WebDAV 客户机" border="0" onload="return imgzoom(this,550);" style="cursor:pointer;" onclick="javascript:window.open(this.src);"/>

Slide 为 GET 命令使用一个旧的 HTTP 接口,Slide servlet 看到该命令,并用链接构建一个 HTML 页面。由于您要从服务器链接到服务器,并且这个客户机不是浏览器,因此这些链接被取消引用,并且它们是相对于服务器 context.refore 的,在查看 davclienttest.py 的浏览器中无法运行它们。

清单 11 展示了您共享的一些实用方法。

清单 11. 其他实用方法

class DAVResponse(httplib.HTTPResponse): 
 def parse_multistatus(self): 
  self.root = Parser().parse(self) 
  self.msr = _get_multistatus_response(self.root) 
 
 def parse_lock_response(self): 
  self.root = Parser().parse(self) 
  self.locktoken = _extract_locktoken(self.root) 
   
# 
# handy function for dumping a tree that is returned by Parser 
# 
def dump(f, root): 
 f.write('<?xml version="1.0"?>\n') 
 namespaces = _collect_ns(root) 
 _dump_recurse(f, root, namespaces, dump_ns=1) 
 f.write('\n') 
 
 
# 
# This function returns the element's CDATA. Note:this is not recursive -- 
# it only returns the CDATA immediately within the element, excluding the 
# CDATA in child elements. 
# 
def textof(elem): 
 return elem.textof() 

mkcol()、get() 和 put() 方法

在 WebDAV 客户机中使用 mkcol()、get() 和 put() 方法。

mkcol() 方法

接下来,添加 mkcol() 方法,该方法在 Slide 储存库中创建一个集合 (collection ) —— 一个文件夹或目录。别忘了,WebDAV 规范是 HTTP 的一个扩展,因此 HTTP 响应代码将是类似的。mkcol() HTTP 请求返回一个响应代码,并附有一些文本作为解释。WebDAV 规范 (RFC 2518) 是这样描述这些代码的:

201 (Created):集合或结构化资源是完整地创建的。

403 (Forbidden):这个错误表明至少出现以下两种情况中的一种:1) 服务器不允许在其名称空间中的给定位置上创建集合,或者 2) Uniform Resource Indicator (URI) 请求的父集合存在,但是不接受成员。

405 (Method Not Allowed): mkcol() 方法只能在被删除或不存在的资源上执行。

409 (Conflict):只有在创建了一个或多个中间集合之后才能在被请求的 URI 上建立集合。

415 (Unsupported Media Type):服务器不支持主体的请求类型。

507 (Insufficient Storage):在执行该方法后资源没有足够的空间来记录资源的状态。

清单 12 展示了 mkcol() 方法,这个方法非常简单。

清单 12. mkcol() 方法

def mkcol(self, url, extra_hdrs={ }): 
  return self._request('MKCOL', url, extra_hdrs=extra_hdrs) 

清单 13 展示了在类中用于创建 WebDAV 请求和响应的各个部分的一些私有方法。

清单 13. 创建请求和响应的私有方法

def _request(self, method, url, body=None, extra_hdrs={}): 
  "Internal method for sending a request." 
 
  self.request(method, url, body, extra_hdrs) 
  return self.getresponse() 

get() 方法

如清单 14 所示,get() 方法用于返回一个缓冲区,其中包含内容服务器上的资源,即文件。Slide 检测到 get() 是用于一个资源的,而不是用于一个集合的。

清单 14. get() 方法

def get(self, url, extra_hdrs={ }): 
  return self._request('GET', url, extra_hdrs=extra_hdrs) 

post() 和 put() 方法

清单 15 展示了这两个重要的方法。post() 方法用于执行一个 HTTP post 操作,将数据 post 到一个 URL 上,所有更改 WebDAV 服务器上元数据值的方法都可以调用该方法;但是,这个方法不用于上传内容。put() 方法用于上传内容(文件)。这两个方法都返回一个多状态响应。

清单 15. post() 和 put() 方法

def post(self, url, data={ }, body=None, extra_hdrs={ }): 
  headers = extra_hdrs.copy() 
 
  assert body or data, "body or data must be supplied" 
  assert not (body and data), "cannot supply both body and data" 
  if data: 
   body = '' 
   for key, value in data.items(): 
    if isinstance(value, types.ListType): 
     for item in value: 
      body = body + '&' + key + '=' + urllib.quote(str(item)) 
    else: 
     body = body + '&' + key + '=' + urllib.quote(str(value)) 
   body = body[1:] 
   headers['Content-Type'] = 'application/x-www-form-urlencoded' 
 
  return self._request('POST', url, body, headers) 
 
def put(self, url, contents, 
     content_type=None, content_enc=None, extra_hdrs={ }): 
 
  if not content_type: 
   content_type, content_enc = mimetypes.guess_type(url) 
 
  headers = extra_hdrs.copy() 
  if content_type: 
   headers['Content-Type'] = content_type 
  if content_enc: 
   headers['Content-Encoding'] = content_enc 
  return self._request('PUT', url, contents, headers) 

在 davclienttest.py 中使用所有方法

在 davclienttest.py 页面中应该使用 mkcol() 和 put() 以及 copy()、move() 和 delete() 等方法。清单 16 展示了使用那些方法的剩下来的代码。这些方法都是带一个或两个参数,并返回一个状态代码。这里的目的是练习使用 client 类方法。

清单 16. 用于移动文件的代码

 _davclient().delete(BASE + '/files/soonToBeDeleted') 
 _davclient().mkcol(BASE + '/files/soonToBeDeleted') 
 _davclient().put(BASE + '/files/TestFile.txt', 'body of TestFile.txt\n') 

上传到 Slide 服务器

可以使用 davclienttest.py 中最后一个主要的方法将文件上传或 put 到 Slide 服务器。如果下载附带的代码并查看代码,您会发现有两个方法用不同的方式来处理文件和集合,这些文件和集合是通过使用文件名创建用于读或写的流来创建的。

copy() 和 move() 方法

清单 17 展示了在 davclienttest.py 测试中使用的最后两个主要的方法:copy() 和 move()。现在,您可能已经注意到,所有方法的模式都是类似的:每个方法带有两个 URI 字符串作为参数,在完成一些处理后返回一个状态代码。















清单 17. move() 和 copy() 方法

 _davclient().move(BASE + '/files/SomeText.txt', BASE + '/files/SomeOtherText.txt') 
 _davclient().copy(BASE + '/files/subCollectiondir', BASE + '/files/subCollectiondir3') 

状态代码有以下几种:

201 (Created):源资源被成功移动,在目标上创建了一个新资源。

204 (No Content):源资源被成功地移动到一个预先存在的目标资源上。

403 (Forbidden):这个错误表明至少出现以下两种情况之一:1) 服务器不允许在其名称空间中的给定位置上创建集合,或者 2) Uniform Resource Indicator (URI) 请求的父集合存在,但是不接受成员。

405 (Method Not Allowed): mkcol() 方法只能在被删除或不存在的资源上执行。

409 (Conflict):只有在创建了一个或多个中间集合之后才能在目标上建立集合。

412 (Precondition Failed):服务器不能维持 propertybehavior XML 属性中列出的属性的存活,或者 Overwrite 的头是 F,目标资源的状态不为 null。

415 (Unsupported Media Type):服务器不支持主体的请求类型。

423 (Locked):源资源或目标资源被锁。

502 (Bad Gateway):当目标在另一台服务器上,且目标服务器拒绝接受资源时,将出现这种错误。

507 (Insufficient Storage):在执行该方法后资源没有足够的空间来记录资源的状态。

结果

运行 davclienttest.py 模块会在 Python IDLE Shell 或 Eclipse 控制台中产生大量的输出。在下载小节可以找到在源文件中运行的测试的完整文本。这一节将帮助您理解 Slide 发送给您的东西,并展示如何解析它。

201 状态响应

最简单的响应是 201 状态响应。清单 18 展示了一个对 put 请求的响应。

清单 18. 对 put 请求的 201 响应

REQUEST: PUT http://192.168.1.100:8080/slide/files/FirstTestText.txt 
STATUS: 201 
REASON: Created 
Pragma: No-cache 
Cache-Control: no-cache 
Expires: Wed, 31 Dec 1969 16:00:00 PST 
Set-Cookie: JSESSIONID=C2AC8B91A42B7717BBFD6C52592D883F; Path=/slide 
ETag: 9113e733263f333eea12a9835881432c 
Content-Length: 0 
Date: Sun, 19 Mar 2006 15:10:44 GMT 
Server: Apache-Coyote/1.1 

Davclienttest.py 定义 _request() 方法,格式化它从 DAV 客户机得到的响应,并且将输出打印到 shell。清单 19 展示了格式化来自响应的所有 print 语句的代码。

清单 19. _request() 方法

def _request(self, method, url, *rest, **kw): 
  print 'REQUEST:', method, url 
  response = apply(davclientlib.DAV._request, (self, method, url) \ 
 + rest, kw) 
 
  print "STATUS:", response.status 
  print "REASON:", response.reason 
  for hdr in response.msg.headers: 
   print string.strip(hdr) 
  print '-'*70 
  if response.status == 207: 
   #print response.doc.dump() 
   #print response.doc.toxml() 
   response.parse_multistatus() 
   davclientlib.dump(sys.stdout, response.root) 
  elif method == 'LOCK' and response.status == 200: 
   response.parse_lock_response() 
   davclientlib.dump(sys.stdout, response.root) 
  else: 
   print response.read() 
  print '-'*70 
  return response         

您可以看到,除了状态代码 207(多状态),对于所有响应状态代码,响应都比较简单,只由状态代码和头部组成。

207 状态响应

状态响应 207 是通用响应格式,以 XML 格式返回更复杂的响应。207 状态响应是 WebDAV 方法,例如测试中的 propfind() 和 propatch() 以及 WebDAV 方法,例如 delete() 的常见响应。如果资源不是在被请求的 URI 中标识的资源,那么出现这种错误时响应必须 是一个 207 (多状态)响应。

一个多状态响应还可以包含一个或多个其他状态代码。例如:

200 (OK):命令成功。主体可以同时包含 set() 和 remove() 方法,所以 201 (Created) 消息似乎不合适。

403 (Forbidden):由于服务器没有指定,客户机不能修改某个属性。

409 (Conflict):客户机提供了一个值,但是该值的语义不适合于属性。这包括试图设置只读属性。

423 (Locked):指定的资源被锁住。或者是因为客户机不是一个锁所有者,或者是因为锁的类型要求提交一个锁标志,但是客户机没有提交它。

507 (Insufficient Storage):服务器没有足够的空间来记录属性。

有些 WebDAV 服务器可以扩展多状态响应,添加它们自己的有效载荷,甚至添加状态代码。而其他一些 WebDAV 服务器在出现不止一个起因或者要提供附加信息时,会返回多状态响应。例如,RFC 将该选项添加到对 copy() 和 move() 方法的响应中:If an error in executing the copy method occurs with a resource other than the resource identified in the requested URI, the response MUST be a 207 (Multistatus)。清单 20 展示了来自一个 propfind() 方法的响应,该方法提供详细信息。

清单 20. 对 propfind() 方法的多状态响应

---------------------------------------------------------------------- 
REQUEST: PROPFIND http://192.168.1.100:8080/slide/files/SomeOtherText.txt 
STATUS: 207 
REASON: Multi-Status 
Pragma: No-cache 
Cache-Control: no-cache 
Expires: Wed, 31 Dec 1969 16:00:00 PST 
Set-Cookie: JSESSIONID=DBD247DC8C386AFC8D65036DF901CB7B; Path=/slide 
Content-Type: text/xml;charset=UTF-8 
Transfer-Encoding: chunked 
Date: Sun, 19 Mar 2006 15:10:57 GMT 
Server: Apache-Coyote/1.1 
---------------------------------------------------------------------- 
<?xml version="1.0"?> 
<ns0:multistatus xmlns:ns0="DAV:"> 
  <ns0:response> 
    <ns0:href>/slide/files/SomeOtherText.txt</ns0:href> 
    <ns0:propstat> 
      <ns0:prop> 
        <ns0:displayname>SomeOtherText.txt</ns0:displayname> 
        <ns0:getcontentlength>21</ns0:getcontentlength> 
        <ns0:getcontenttype>text/plain</ns0:getcontenttype> 
        <ns0:resourcetype/> 
        <ns0:getlastmodified>Sun, 19 Mar 2006 15:10:54 GMT 
   </ns0:getlastmodified> 
        <ns0:lockdiscovery/> 
      </ns0:prop> 
      <ns0:status>HTTP/1.1 200 OK</ns0:status> 
    </ns0:propstat> 
  </ns0:response> 
</ns0:multistatus> 
---------------------------------------------------------------------- 

如清单 21 所示,该响应是对 davclienttest.py:: _davclient().propfind(BASE + '/files/SomeOtherText.txt', body, 0) 的响应,其主体是 XML 片段。

清单 21. 生成显示内容

 body = '<?xml version="1.0" encoding="utf-8"?>' 
 body += '<D:propfind xmlns:D="DAV:"><D:prop>' 
 body += '<D:displayname/><D:getcontentlength/><D:getcontenttype/>' 
 body += '<D:resourcetype/><D:getlastmodified/><D:lockdiscovery/>' 
 body += '</D:prop></D:propfind>' 

注意,这里深度为 0。propfind() 方法引用一个文档资源,所以 propfind() 方法只返回那六个属性。

但是,当您在一个集合(例如 davclienttest.py:: _davclient().allproperties(BASE + '/files/TestCollection'))上运行 allproperties 时,可以得到比清单 22 中看到的多得多的内容。您还将看到,在这个来自 ShellOut.txt 文件的有所删减的摘录中,这个命令显示集合的内容。在对一个 XML 多状态的一个取操作的输出中,包括集合的所有子集合。

清单 22. allproperties 多状态响应

---------------------------------------------------------------------- 
REQUEST: PROPFIND http://192.168.1.100:8080/slide/files/TestCollection 
STATUS: 207 
REASON: Multi-Status 
Pragma: No-cache 
Cache-Control: no-cache 
Expires: Wed, 31 Dec 1969 16:00:00 PST 
Set-Cookie: JSESSIONID=14D3DB962207193196D9B5462491FA56; Path=/slide 
Content-Type: text/xml;charset=UTF-8 
Transfer-Encoding: chunked 
Date: Sun, 19 Mar 2006 15:11:19 GMT 
Server: Apache-Coyote/1.1 
---------------------------------------------------------------------- 
<?xml version="1.0"?> 
<ns0:multistatus xmlns:ns0="DAV:"> 
<ns0:response> 
  <ns0:href>/slide/files/TestCollection</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>TestCollection</ns0:displayname> 
      <ns0:getcontentlength>0</ns0:getcontentlength> 
      <ns0:resourcetype> 
        <ns0:collection/> 
      </ns0:resourcetype> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:03 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:getcontenttype/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 404 Not Found</ns0:status> 
  </ns0:propstat> 
</ns0:response><ns0:response> 
  <ns0:href>/slide/files/TestCollection/subCollectioncopy</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>subCollectioncopy</ns0:displayname> 
      <ns0:getcontentlength>0</ns0:getcontentlength> 
      <ns0:resourcetype> 
        <ns0:collection/> 
      </ns0:resourcetype> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:19 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:getcontenttype/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 404 Not Found</ns0:status> 
  </ns0:propstat> 
</ns0:response><ns0:response> 
  <ns0:href>/slide/files/TestCollection/ 
 subCollectionTestFile.txtcopy</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>subCollectionTestFile.txtcopy 
  </ns0:displayname> 
      <ns0:getcontentlength>22</ns0:getcontentlength> 
      <ns0:getcontenttype>text/plain</ns0:getcontenttype> 
      <ns0:resourcetype/> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:19 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
</ns0:response><ns0:response> 
  <ns0:href>/slide/files/TestCollection/ 
 newCollectionSecondTestFile.txt</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>newCollectionSecondTestFile.txt 
  </ns0:displayname> 
      <ns0:getcontentlength>22</ns0:getcontentlength> 
      <ns0:getcontenttype>text/plain</ns0:getcontenttype> 
      <ns0:resourcetype/> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:12 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
</ns0:response><ns0:response> 
  <ns0:href>/slide/files/TestCollection/newCollection</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>newCollection</ns0:displayname> 
      <ns0:getcontentlength>0</ns0:getcontentlength> 
      <ns0:resourcetype> 
        <ns0:collection/> 
      </ns0:resourcetype> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:10 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:getcontenttype/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 404 Not Found</ns0:status> 
  </ns0:propstat> 
</ns0:response><ns0:response> 
  <ns0:href>/slide/files/TestCollection/newTestFile.txt</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>newTestFile.txt</ns0:displayname> 
      <ns0:getcontentlength>22</ns0:getcontentlength> 
      <ns0:getcontenttype>text/plain</ns0:getcontenttype> 
      <ns0:resourcetype/> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:05 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
</ns0:response> 
<!-- Some children have been omitted but you should note that the collections 
that are children have children listed as well --> 
<ns0:response> 
  <ns0:href>/slide/files/TestCollection/newCollection/ 
 ThirdTestFile.txtcopy</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>ThirdTestFile.txtcopy</ns0:displayname> 
      <ns0:getcontentlength>22</ns0:getcontentlength> 
      <ns0:getcontenttype>text/plain</ns0:getcontenttype> 
      <ns0:resourcetype/> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:18 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
</ns0:response><ns0:response> 
  <ns0:href>/slide/files/TestCollection/subCollectioncopy/ 
 TestFile.txt</ns0:href> 
  <ns0:propstat> 
    <ns0:prop> 
      <ns0:displayname>TestFile.txt</ns0:displayname> 
      <ns0:getcontentlength>22</ns0:getcontentlength> 
      <ns0:getcontenttype>text/plain</ns0:getcontenttype> 
      <ns0:resourcetype/> 
      <ns0:getlastmodified>Sun, 19 Mar 2006 15:11:19 GMT 
  </ns0:getlastmodified> 
      <ns0:lockdiscovery/> 
    </ns0:prop> 
    <ns0:status>HTTP/1.1 200 OK</ns0:status> 
  </ns0:propstat> 
</ns0:response> 
</ns0:multistatus> 
---------------------------------------------------------------------- 

因此,您可以控制 propfind() 方法的深度和属性,将 propfind 命令的任一参数包括在主体和深度中。或者您可以使用某一个包装器方法,例如 allproperties()。这些都由您来决定。

未来展望

现在来了解一下用您刚创建好的 Python WebDAV 客户机可以做哪些事情。

WebDAV 客户机的未来

那么接下来做什么呢?这个 Python davclientlib 可以用来做什么?您有用于存储文件的 Python 应用程序吗?存储文件的多个版本或者存储带元数据属性的文件如何?您想扫描收到的发货单吗,用光学扫描识别 (OCR) 扫描它,根据扫描创建发货单,然后将所有这些用 XML 格式存储?

我们只是浅显地描绘了 Slide 的用途,随着时间的推移,这个客户机应该不断成长,以满足您的需求。Slide 在存储和检索 XML 文档方面表现良好,而且您可以将元数据属性存储为 XML,并将它们绑定到资源或集合。

这个 davclientlib 类的最明显的用法是使用您所知道的语言将 Python 内容管理构建到 Web 站点中。您可以运行 Slide/Tomcat bundle,而不必编写任何 Java 程序。您的 Python 技能可以帮助您构建一个功能完善的 CMS,并且 它仍然具有与 Microsoft Office 应用程序或任何其他 WebDAV 感知的应用程序集成的 WebDAV 功能。

注意:有些处理 ACL 的 Slide 命令超出了基本的 WebDAV 命令,本客户机库中没有包括的其他命令有:

GRANT

DENY

REVOKE

BIND

UNBIND

结束语

Python 可以通过一个 socket 连接和 HTTP 与任何 WebDAV 服务器会话。记住 WebDAV 规范,您在本教程中编写的 davclientlib 类可以简化为了将该客户机用于构建 Python 应用程序而需要编写的其他代码。这些 Python 应用程序可以是存储和检索资源所需的任何东西。那些资源不一定需要是充满页面和图的网站中的内容,但是也可以是这样的内容。我相信您需要从一个客户机类开始,扩展它的一些方法,并添加您自己的一些方法。

在本系列的下一个教程中,您将使用另一种开放源码编程语言 —— PHP —— 来使用 Slide。还您将使用 PHP 创建一个用于管理 Slide 的内容的 Web 应用程序。结果将会是一个简单的应用程序,使您的 PHP 程序员得以着手构建您自己的相对于 Slide 的内容管理前端。下次再见!

下载

描述名字大小下载方法
Results of running davclienttest.py scriptShellOut.txt.zip5KBHTTP
Class source file for WebDAV serversdavclientlib.py.zip4KBHTTP
Your test pagedavclienttest.py.zip2KBHTTP

Tags:开放 源码 CMS

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接