Commit 2d1d87c9 authored by Amos Latteier's avatar Amos Latteier

First alpha of FTP support

parent 016fba5d
ZServer Changes
This file gives information on changes made to ZServer over time,
things that need to be done before the next release and future
plans.
Releases
Zserver 1.0a1
New Features
Inital release. Includes new threaded architecture--currently
limited to a thread pool of 1. Includes preliminary FTP
support. Includes HTTP support.
Bugs Fixed
ZServer Release 1.0a1
---------------------
Welcome to the first Zope ZServer alpha release. This release provides
a first look at Zope/Medusa integration, and introduces FTP support in
Zope.
What is ZServer?
ZServer is an integration of the Zope application server and the
Medusa information server. See the ZServer architecture document for
more information.::
http://www.zope.org/Documentation/Reference/ZServer
ZServer gives you HTTP and FTP access. In later releases it will
probably offer more protocols such as PCGI, WebDAV, etc.
What is Medusa?
Medusa is a Python server framework with uses a single threaded
asynchronous sockets approach. For more information see::
http://www.nightmare.com/medusa
There's also an interesting Medusa tutorial at::
http://www.nightmare.com:8080/nm/apps/medusa/docs/programming.html
ZServer FTP support
FTP access to Zope allows you to FTP to the Zope object hierarchy in
order to perform managerial tasks. You can:
* Navigate the object hierarchy with 'cd'
* Replace the content of Documents, Images, and Files
* Create Documents, Images, Files, Folders
* Delete any sort of object
So basically you can do more than is possible with PUT. Also, unlike
PUT, FTP gives you access to Document content. So when you download
a Document you are getting its content, not what it looks like when
it rendered.
FTP permissions
FTP support is provided for Folders, Documents, Images, and Files.
You can control access to FTP via the new 'FTP access' permission.
This permission controls the ability to 'cd' to a Folder and to
download objects. Uploading and deleting and creating objects are
controlled by existing permissions.
Properties and FTP: The next step
The next phase of FTP support will allow you to edit properties of
all Zope objects. Probably properties will be exposed via special
files which will contain an XML representation of the object's
properties. You could then download the file, edit the XML and
upload it to change the object's properties.
We do not currently have a target date for FTP property support.
It will probably need to wait until Zope has property sheets.
Differences between ZopeHTTPServer and ZServer
Both ZopeHTTPServer and ZServer are Python HTTP servers.
ZopeHTTPServer is built on the standard Python SimpleHTTPServer
framework. ZServer is built on Medusa.
ZopeHTTPServer is very limited. It can only publish one module at a
time. It can only publish via HTTP. It has no support for thread
pools. And more importantly, it is no longer being actively
developed, since its author has moved on the ZServer.
ZServer on the other hand is more complex and supports publishing
multiple modules, thread pools, and it uses a new threaded
architecture for accessing ZPublisher. Right now the thread pool is
limited to one thread, since the object database cannot yet support
concurrent access. This should change within the next few months.
How does FTP work?
The ZServer's FTP channel object translates FTP requests into
ZPublisher requests. The FTP channel then analyses the response and
formulates an appropriate FTP response. The FTP channel stores some
state such as the current working directory and the username and
password.
On the Zope side of things, the 'FTPSupport.py' module provides a
few mix-in classes for Document, Folder, and File (which Image
inherits from). These mix in classes provide directory listing and
stat-like methods. All the other FTP functions are handled by
existing methods like 'manage_delObjects', and 'PUT', etc.
Who should use ZServer?
This release is *alpha* quality. It should be used by Zope hackers.
If you are not inquisitive and self-reliant, this release will
frustrate you.
Installation
To install ZServer you need to do two things: edit the start script
and update some Zope files to introduce FTP support.
To edit the start up script, open 'start.py' in your favorite editor
and change the configuration variables. If you understand Medusa,
you can also change the rest of the script.
To enable FTP support in Zope you need to update some files in
'lib/python/OFS'. You should probably first back up your 'OFS'
directory, or at least make copies of the files that will be
replaced ('Document.py', 'Image.py', 'Folder.py'). Then copy the
contents of ZServer's 'OFS' directory to your Zope 'lib/python/OFS'
directory.
Finally make sure the shebang line is right on the start script. You
can use your own copy of Python, or the copy that came with Zope (if
you are using a binary distribution).
Now your ready to go.
Usage
To start ZServer run the start script::
./start.py
To stop the server type 'control-c'.
You should see some logging information come up on the screen.
Once you start ZServer is will publish Zope (or any Python module)
on HTTP and/or FTP. To access Zope via HTTP point your browser at
the server like so::
http://www.example.com:9673/Main
This assumes that you have chosen to put HTTP on port 9673 and that
you are publishing a module named 'Main'. Note: to publish Zope
normally you publish the 'lib/python/Main.py' module.
To access Zope via FTP you need to FTP to it at the port you set FTP
to run on. For example::
ftp www.example.com 8021
This starts and FTP session to your machine on port 8021, ZServer's
default FTP port. When you are prompted to log in you should supply
a Zope username and password. (Probably you should use an account
with the 'Manager' role, unless you have configured Zope to allow
FTP access to the 'Anonymous' role.) You can also enter 'anonymous'
and any password for anonymous FTP access. Once you have logged in
you can start issuing normal FTP commands. Right now ZServer only
supports basic FTP commands. Note: When you log in your working
directory is set to '/'. If you do not have FTP permissions in this
directory, you will need to 'cd' to a directory where you have
permissions before you can do anything.
Support
Questions and comments should go to 'support@digicool.com'.
You can report bugs and check on the status of bugs using the Zope
bug collector.::
http://www.zope.org/Collector/
License
ZServer is covered by the ZPL despite the fact that it comes with
much of the Medusa source code. The portions of Medusa that come
with ZServer are licensed under the ZPL.
Outstanding issues
PCGI support is not done yet. The FTP interface for Zope objects may
be changed, i.e. 'manage_FTPlist' and 'manage_FTPstat' maybe changed
and/or renamed. When FTP support for properties is added, this may
change a lot. WebDAV support will also probably cause a little
reorganization.
Currently ZServer's Medusa files are a bit modified from the
originals. It would be good idea to try and keep deviations to a
minimum. For this reason we have been feeding bug reports and change
requests back to Sam Rushing for inclusion in the official Medusa
souces. It is possible, however, that Medusa and ZServer will
diverge in some small respects despite our best efforts to keep them
unified. One area in particular where ZServer currently departs from
Medusa is in the use of future producers. Change is likely in
ZServer's use of future producers.
"""ZServer FTP Channel for use the medusa's ftp server.
"""
from PubCore import handle
from medusa.ftp_server import ftp_channel, ftp_server
from medusa import asynchat, filesys
from medusa.producers import NotReady
from cStringIO import StringIO
import string
import os
from regsub import gsub
from base64 import encodestring
from mimetypes import guess_type
import marshal
import stat
import time
class zope_ftp_channel(ftp_channel):
"Passes its commands to Zope, not a filesystem"
read_only=0
def __init__ (self, server, conn, addr, module):
ftp_channel.__init__(self,server,conn,addr)
self.module=module
self.userid=''
self.password=''
self.path='/'
def _get_env(self):
"Returns a CGI style environment"
env={}
env['SCRIPT_NAME']='/%s' % self.module
env['PATH_INFO']=self.path
env['REQUEST_METHOD']='GET' # XXX what should this be?
env['SERVER_SOFTWARE']=self.server.SERVER_IDENT
if self.userid != 'anonymous':
env['HTTP_AUTHORIZATION']='Basic %s' % gsub('\012','',
encodestring('%s:%s' % (self.userid,self.password)))
env['BOBO_DEBUG_MODE']='1'
env['SERVER_NAME']=self.server.hostname
env['SERVER_PORT']=str(self.server.port)
env['REMOTE_ADDR']=self.client_addr[0]
env['GATEWAY_INTERFACE']='CGI/1.1' # that's stretching it ;-)
# XXX etcetera -- probably set many of these at the start
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=string.replace(path,os.sep,'/')
return path
def make_response(self,resp):
self.log ('==> %s' % resp)
return resp + '\r\n'
# Overriden async_chat methods
writable=asynchat.async_chat.writable_future
# Overriden ftp_channel methods
def cmd_nlst (self, line):
'give name list of files in directory'
self.push_with_producer(self.get_dir_list (line, 0))
def cmd_list (self, line):
'give list files in a directory'
# XXX should handle listing a file, not just a directory
# also should maybe glob, but this is hard to do...
self.push_with_producer(self.get_dir_list (line, 1))
def get_dir_list (self, line, long=0):
# we need to scan the command line for arguments to '/bin/ls'...
# XXX clean this up
if len(line) > 1:
args = string.split(line[1])
else:
args =[]
path_args = []
for arg in args:
if arg[0] != '-':
path_args.append (arg)
else:
if 'l' in arg:
long=1
if len(path_args) < 1:
dir = '.'
else:
dir = path_args[0]
return self.listdir (dir, long)
def listdir (self, path, long=0):
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_FTPlist')
outpipe=handle(self.module,env,StringIO())
return ResponseProducer(outpipe,self._do_listdir,(long,))
def _do_listdir(self,long,response):
#print "do list response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
dir_list=''
file_infos=marshal.loads(response.content)
if type(file_infos[0])==type(''):
file_infos=(file_infos,)
if long:
for id, stat_info in file_infos:
dir_list=dir_list+filesys.unix_longify(id,stat_info)+'\r\n'
else:
for id, stat_info in file_infos:
dir_list=dir_list+id+'\r\n'
self.make_xmit_channel()
self.client_dc.push(dir_list)
self.client_dc.close_when_done()
return self.make_response(
'150 Opening %s mode data connection for file list' % (
self.type_map[self.current_mode]
)
)
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Could not list directory.')
def cmd_cwd (self, line):
'change working directory'
# try to call manage_FTPlist on the path
env=self._get_env()
path=line[1]
path=self._join_paths(self.path,path,'manage_FTPlist')
env['PATH_INFO']=path
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer
(outpipe,self._cmd_cwd,(path[:-15],)))
def _cmd_cwd(self,path,response):
code=response.headers['Status'][:3]
if code=='200':
listing=marshal.loads(response.content)
# check to see if we are cding to a non-foldoid object
if type(listing[0])==type(''):
return self.make_response('550 No such directory.')
self.path=path or '/'
return self.make_response('250 CWD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 No such directory.')
def cmd_cdup (self, line):
'change to parent of current working directory'
self.cmd_cwd((None,'..'))
def cmd_pwd (self, line):
'print the current working directory'
self.respond (
'257 "%s" is the current directory.' % (
self.path
)
)
cmd_xpwd=cmd_pwd
def cmd_mdtm (self, line):
'show last modification time of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mdtm))
def _cmd_mdtm(self,response):
#print "mdtm response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
mtime=marshal.loads(response.content)[stat.ST_MTIME]
mtime=time.gmtime(mtime)
return self.make_response('213 %4d%02d%02d%02d%02d%02d' % (
mtime[0],
mtime[1],
mtime[2],
mtime[3],
mtime[4],
mtime[5]
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file modification time.')
def cmd_size (self, line):
'return size of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_size))
def _cmd_size(self,response):
#print "size response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('213 %d'%
marshal.loads(response.content)[stat.ST_SIZE])
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file size.')
self.client_dc.close_when_done()
def cmd_retr(self,line):
if len(line) < 2:
self.command_not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPget')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe,
self._cmd_retr, (line[1],)))
def _cmd_retr(self,file,response):
#print "retr response\n", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
#fd=StringIO(response.content)
self.make_xmit_channel()
#if self.restart_position:
# # try to position the file as requested, but
# # give up silently on failure (the 'file object'
# # may not support seek())
# try:
# fd.seek (self.restart_position)
# except:
# pass
# self.restart_position = 0
self.client_dc.push(response.content)
self.client_dc.close_when_done()
return self.make_response(
"150 Opening %s mode data connection for file '%s'" % (
self.type_map[self.current_mode],
file
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error opening file.')
def cmd_stor (self, line, mode='wb'):
'store a file'
if len (line) < 2:
self.command_not_understood (string.join (line))
return
elif self.restart_position:
restart_position = 0
self.respond ('553 restart on STOR not yet supported')
return
# XXX Check for possible problems first? Like authorization...
# But how? Once we agree to receive the file, can we still
# bail later?
fd=ContentReceiver(self._do_cmd_stor,
(self._join_paths(self.path,line[1]),))
self.respond (
'150 Opening %s connection for %s' % (
self.type_map[self.current_mode],
line[1]
)
)
self.make_recv_channel (fd)
def _do_cmd_stor(self,path,data):
'callback to do the STOR, after we have the input'
#print "stor callback", path, data
env=self._get_env()
env['PATH_INFO']=path
env['REQUEST_METHOD']='PUT'
ctype=guess_type(path)[0]
if ctype is not None:
env['CONTENT_TYPE']=ctype
env['CONTENT_LENGTH']=len(data.getvalue())
outpipe=handle(self.module,env,data)
self.push_with_producer(ResponseProducer(outpipe, self._cmd_stor))
def _cmd_stor(self,response):
code=response.headers['Status'][:3]
if code in ('200','204','302'):
return self.make_response('257 STOR command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating file.')
def cmd_dele(self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_dele))
def _cmd_dele(self,response):
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 DELE command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error deleting file.')
def cmd_mkd (self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,path,'manage_addFolder')
env['QUERY_STRING']='id=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mkd))
cmd_xmkd=cmd_mkd
def _cmd_mkd(self,response):
#print "mkd response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('257 MKD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating directory.')
def cmd_rmd (self, line):
# XXX should object be checked to see if it's folderish
# before we allow it to be RMD'd?
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_rmd))
cmd_xrmd=cmd_rmd
def _cmd_rmd(self,response):
#print "rmd response", response.__dict__
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 RMD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error removing directory.')
def cmd_user (self, line):
'specify user name'
if len(line) > 1:
self.userid = line[1]
self.respond ('331 Password required.')
else:
self.command_not_understood (string.join (line))
def cmd_pass (self, line):
'specify password'
if len(line) < 2:
pw = ''
else:
pw = line[1]
self.password=pw
self.respond ('230 Login successful.')
self.authorized = 1
self.log ('Successful login.')
class ZResponseReceiver:
"""Given an output pipe reads response and parses it.
After a call to ready returns true, you can read
the headers as a dictiony and the content as a string."""
def __init__(self,pipe):
self.pipe=pipe
self.data=''
self.headers={}
self.content=''
def ready(self):
if self.pipe is None:
return 1
if self.pipe.ready():
data=self.pipe.read()
if data:
self.data=self.data+data
else:
self.parse()
return 1
def parse(self):
headers,html=string.split(self.data,'\n\n',1)
self.data=''
for header in string.split(headers,'\n'):
k,v=string.split(header,': ',1)
self.headers[k]=v
self.content=html
self.pipe=None
class ResponseProducer:
"Allows responses which need to make Zope requests first."
def __init__(self,pipe,callback,args=None):
self.response=ZResponseReceiver(pipe)
self.callback=callback
self.args=args or ()
self.done=None
def ready(self):
if self.response is not None:
return self.response.ready()
else:
return 1
def more(self):
if not self.done:
if not self.response.ready():
raise NotReady()
self.done=1
r=self.response
c=self.callback
args=self.args+(r,)
self.response=None
self.callback=None
self.args=None
return apply(c,args)
else:
return ''
class ContentReceiver:
"Write-only file object used to receive data from FTP"
def __init__(self,callback,args=None):
self.data=StringIO()
self.callback=callback
self.args=args or ()
def write(self,data):
self.data.write(data)
def close(self):
self.data.seek(0)
args=self.args+(self.data,)
c=self.callback
self.callback=None
self.args=None
apply(c,args)
class zope_ftp_server(ftp_server):
ftp_channel_class = zope_ftp_channel
def __init__(self,module,hostname,port,resolver,logger_object):
ftp_server.__init__(self,None,hostname,port,resolver,logger_object)
self.module=module
def handle_accept (self):
conn, addr = self.accept()
self.total_sessions.increment()
print 'Incoming connection from %s:%d' % (addr[0], addr[1])
self.ftp_channel_class (self, conn, addr, self.module)
\ No newline at end of file
ZServer Changes
This file gives information on changes made to ZServer over time,
things that need to be done before the next release and future
plans.
Releases
Zserver 1.0a1
New Features
Inital release. Includes new threaded architecture--currently
limited to a thread pool of 1. Includes preliminary FTP
support. Includes HTTP support.
Bugs Fixed
ZServer Release 1.0a1
---------------------
Welcome to the first Zope ZServer alpha release. This release provides
a first look at Zope/Medusa integration, and introduces FTP support in
Zope.
What is ZServer?
ZServer is an integration of the Zope application server and the
Medusa information server. See the ZServer architecture document for
more information.::
http://www.zope.org/Documentation/Reference/ZServer
ZServer gives you HTTP and FTP access. In later releases it will
probably offer more protocols such as PCGI, WebDAV, etc.
What is Medusa?
Medusa is a Python server framework with uses a single threaded
asynchronous sockets approach. For more information see::
http://www.nightmare.com/medusa
There's also an interesting Medusa tutorial at::
http://www.nightmare.com:8080/nm/apps/medusa/docs/programming.html
ZServer FTP support
FTP access to Zope allows you to FTP to the Zope object hierarchy in
order to perform managerial tasks. You can:
* Navigate the object hierarchy with 'cd'
* Replace the content of Documents, Images, and Files
* Create Documents, Images, Files, Folders
* Delete any sort of object
So basically you can do more than is possible with PUT. Also, unlike
PUT, FTP gives you access to Document content. So when you download
a Document you are getting its content, not what it looks like when
it rendered.
FTP permissions
FTP support is provided for Folders, Documents, Images, and Files.
You can control access to FTP via the new 'FTP access' permission.
This permission controls the ability to 'cd' to a Folder and to
download objects. Uploading and deleting and creating objects are
controlled by existing permissions.
Properties and FTP: The next step
The next phase of FTP support will allow you to edit properties of
all Zope objects. Probably properties will be exposed via special
files which will contain an XML representation of the object's
properties. You could then download the file, edit the XML and
upload it to change the object's properties.
We do not currently have a target date for FTP property support.
It will probably need to wait until Zope has property sheets.
Differences between ZopeHTTPServer and ZServer
Both ZopeHTTPServer and ZServer are Python HTTP servers.
ZopeHTTPServer is built on the standard Python SimpleHTTPServer
framework. ZServer is built on Medusa.
ZopeHTTPServer is very limited. It can only publish one module at a
time. It can only publish via HTTP. It has no support for thread
pools. And more importantly, it is no longer being actively
developed, since its author has moved on the ZServer.
ZServer on the other hand is more complex and supports publishing
multiple modules, thread pools, and it uses a new threaded
architecture for accessing ZPublisher. Right now the thread pool is
limited to one thread, since the object database cannot yet support
concurrent access. This should change within the next few months.
How does FTP work?
The ZServer's FTP channel object translates FTP requests into
ZPublisher requests. The FTP channel then analyses the response and
formulates an appropriate FTP response. The FTP channel stores some
state such as the current working directory and the username and
password.
On the Zope side of things, the 'FTPSupport.py' module provides a
few mix-in classes for Document, Folder, and File (which Image
inherits from). These mix in classes provide directory listing and
stat-like methods. All the other FTP functions are handled by
existing methods like 'manage_delObjects', and 'PUT', etc.
Who should use ZServer?
This release is *alpha* quality. It should be used by Zope hackers.
If you are not inquisitive and self-reliant, this release will
frustrate you.
Installation
To install ZServer you need to do two things: edit the start script
and update some Zope files to introduce FTP support.
To edit the start up script, open 'start.py' in your favorite editor
and change the configuration variables. If you understand Medusa,
you can also change the rest of the script.
To enable FTP support in Zope you need to update some files in
'lib/python/OFS'. You should probably first back up your 'OFS'
directory, or at least make copies of the files that will be
replaced ('Document.py', 'Image.py', 'Folder.py'). Then copy the
contents of ZServer's 'OFS' directory to your Zope 'lib/python/OFS'
directory.
Finally make sure the shebang line is right on the start script. You
can use your own copy of Python, or the copy that came with Zope (if
you are using a binary distribution).
Now your ready to go.
Usage
To start ZServer run the start script::
./start.py
To stop the server type 'control-c'.
You should see some logging information come up on the screen.
Once you start ZServer is will publish Zope (or any Python module)
on HTTP and/or FTP. To access Zope via HTTP point your browser at
the server like so::
http://www.example.com:9673/Main
This assumes that you have chosen to put HTTP on port 9673 and that
you are publishing a module named 'Main'. Note: to publish Zope
normally you publish the 'lib/python/Main.py' module.
To access Zope via FTP you need to FTP to it at the port you set FTP
to run on. For example::
ftp www.example.com 8021
This starts and FTP session to your machine on port 8021, ZServer's
default FTP port. When you are prompted to log in you should supply
a Zope username and password. (Probably you should use an account
with the 'Manager' role, unless you have configured Zope to allow
FTP access to the 'Anonymous' role.) You can also enter 'anonymous'
and any password for anonymous FTP access. Once you have logged in
you can start issuing normal FTP commands. Right now ZServer only
supports basic FTP commands. Note: When you log in your working
directory is set to '/'. If you do not have FTP permissions in this
directory, you will need to 'cd' to a directory where you have
permissions before you can do anything.
Support
Questions and comments should go to 'support@digicool.com'.
You can report bugs and check on the status of bugs using the Zope
bug collector.::
http://www.zope.org/Collector/
License
ZServer is covered by the ZPL despite the fact that it comes with
much of the Medusa source code. The portions of Medusa that come
with ZServer are licensed under the ZPL.
Outstanding issues
PCGI support is not done yet. The FTP interface for Zope objects may
be changed, i.e. 'manage_FTPlist' and 'manage_FTPstat' maybe changed
and/or renamed. When FTP support for properties is added, this may
change a lot. WebDAV support will also probably cause a little
reorganization.
Currently ZServer's Medusa files are a bit modified from the
originals. It would be good idea to try and keep deviations to a
minimum. For this reason we have been feeding bug reports and change
requests back to Sam Rushing for inclusion in the official Medusa
souces. It is possible, however, that Medusa and ZServer will
diverge in some small respects despite our best efforts to keep them
unified. One area in particular where ZServer currently departs from
Medusa is in the use of future producers. Change is likely in
ZServer's use of future producers.
"""ZServer FTP Channel for use the medusa's ftp server.
"""
from PubCore import handle
from medusa.ftp_server import ftp_channel, ftp_server
from medusa import asynchat, filesys
from medusa.producers import NotReady
from cStringIO import StringIO
import string
import os
from regsub import gsub
from base64 import encodestring
from mimetypes import guess_type
import marshal
import stat
import time
class zope_ftp_channel(ftp_channel):
"Passes its commands to Zope, not a filesystem"
read_only=0
def __init__ (self, server, conn, addr, module):
ftp_channel.__init__(self,server,conn,addr)
self.module=module
self.userid=''
self.password=''
self.path='/'
def _get_env(self):
"Returns a CGI style environment"
env={}
env['SCRIPT_NAME']='/%s' % self.module
env['PATH_INFO']=self.path
env['REQUEST_METHOD']='GET' # XXX what should this be?
env['SERVER_SOFTWARE']=self.server.SERVER_IDENT
if self.userid != 'anonymous':
env['HTTP_AUTHORIZATION']='Basic %s' % gsub('\012','',
encodestring('%s:%s' % (self.userid,self.password)))
env['BOBO_DEBUG_MODE']='1'
env['SERVER_NAME']=self.server.hostname
env['SERVER_PORT']=str(self.server.port)
env['REMOTE_ADDR']=self.client_addr[0]
env['GATEWAY_INTERFACE']='CGI/1.1' # that's stretching it ;-)
# XXX etcetera -- probably set many of these at the start
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=string.replace(path,os.sep,'/')
return path
def make_response(self,resp):
self.log ('==> %s' % resp)
return resp + '\r\n'
# Overriden async_chat methods
writable=asynchat.async_chat.writable_future
# Overriden ftp_channel methods
def cmd_nlst (self, line):
'give name list of files in directory'
self.push_with_producer(self.get_dir_list (line, 0))
def cmd_list (self, line):
'give list files in a directory'
# XXX should handle listing a file, not just a directory
# also should maybe glob, but this is hard to do...
self.push_with_producer(self.get_dir_list (line, 1))
def get_dir_list (self, line, long=0):
# we need to scan the command line for arguments to '/bin/ls'...
# XXX clean this up
if len(line) > 1:
args = string.split(line[1])
else:
args =[]
path_args = []
for arg in args:
if arg[0] != '-':
path_args.append (arg)
else:
if 'l' in arg:
long=1
if len(path_args) < 1:
dir = '.'
else:
dir = path_args[0]
return self.listdir (dir, long)
def listdir (self, path, long=0):
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_FTPlist')
outpipe=handle(self.module,env,StringIO())
return ResponseProducer(outpipe,self._do_listdir,(long,))
def _do_listdir(self,long,response):
#print "do list response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
dir_list=''
file_infos=marshal.loads(response.content)
if type(file_infos[0])==type(''):
file_infos=(file_infos,)
if long:
for id, stat_info in file_infos:
dir_list=dir_list+filesys.unix_longify(id,stat_info)+'\r\n'
else:
for id, stat_info in file_infos:
dir_list=dir_list+id+'\r\n'
self.make_xmit_channel()
self.client_dc.push(dir_list)
self.client_dc.close_when_done()
return self.make_response(
'150 Opening %s mode data connection for file list' % (
self.type_map[self.current_mode]
)
)
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Could not list directory.')
def cmd_cwd (self, line):
'change working directory'
# try to call manage_FTPlist on the path
env=self._get_env()
path=line[1]
path=self._join_paths(self.path,path,'manage_FTPlist')
env['PATH_INFO']=path
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer
(outpipe,self._cmd_cwd,(path[:-15],)))
def _cmd_cwd(self,path,response):
code=response.headers['Status'][:3]
if code=='200':
listing=marshal.loads(response.content)
# check to see if we are cding to a non-foldoid object
if type(listing[0])==type(''):
return self.make_response('550 No such directory.')
self.path=path or '/'
return self.make_response('250 CWD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 No such directory.')
def cmd_cdup (self, line):
'change to parent of current working directory'
self.cmd_cwd((None,'..'))
def cmd_pwd (self, line):
'print the current working directory'
self.respond (
'257 "%s" is the current directory.' % (
self.path
)
)
cmd_xpwd=cmd_pwd
def cmd_mdtm (self, line):
'show last modification time of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mdtm))
def _cmd_mdtm(self,response):
#print "mdtm response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
mtime=marshal.loads(response.content)[stat.ST_MTIME]
mtime=time.gmtime(mtime)
return self.make_response('213 %4d%02d%02d%02d%02d%02d' % (
mtime[0],
mtime[1],
mtime[2],
mtime[3],
mtime[4],
mtime[5]
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file modification time.')
def cmd_size (self, line):
'return size of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_size))
def _cmd_size(self,response):
#print "size response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('213 %d'%
marshal.loads(response.content)[stat.ST_SIZE])
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file size.')
self.client_dc.close_when_done()
def cmd_retr(self,line):
if len(line) < 2:
self.command_not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPget')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe,
self._cmd_retr, (line[1],)))
def _cmd_retr(self,file,response):
#print "retr response\n", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
#fd=StringIO(response.content)
self.make_xmit_channel()
#if self.restart_position:
# # try to position the file as requested, but
# # give up silently on failure (the 'file object'
# # may not support seek())
# try:
# fd.seek (self.restart_position)
# except:
# pass
# self.restart_position = 0
self.client_dc.push(response.content)
self.client_dc.close_when_done()
return self.make_response(
"150 Opening %s mode data connection for file '%s'" % (
self.type_map[self.current_mode],
file
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error opening file.')
def cmd_stor (self, line, mode='wb'):
'store a file'
if len (line) < 2:
self.command_not_understood (string.join (line))
return
elif self.restart_position:
restart_position = 0
self.respond ('553 restart on STOR not yet supported')
return
# XXX Check for possible problems first? Like authorization...
# But how? Once we agree to receive the file, can we still
# bail later?
fd=ContentReceiver(self._do_cmd_stor,
(self._join_paths(self.path,line[1]),))
self.respond (
'150 Opening %s connection for %s' % (
self.type_map[self.current_mode],
line[1]
)
)
self.make_recv_channel (fd)
def _do_cmd_stor(self,path,data):
'callback to do the STOR, after we have the input'
#print "stor callback", path, data
env=self._get_env()
env['PATH_INFO']=path
env['REQUEST_METHOD']='PUT'
ctype=guess_type(path)[0]
if ctype is not None:
env['CONTENT_TYPE']=ctype
env['CONTENT_LENGTH']=len(data.getvalue())
outpipe=handle(self.module,env,data)
self.push_with_producer(ResponseProducer(outpipe, self._cmd_stor))
def _cmd_stor(self,response):
code=response.headers['Status'][:3]
if code in ('200','204','302'):
return self.make_response('257 STOR command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating file.')
def cmd_dele(self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_dele))
def _cmd_dele(self,response):
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 DELE command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error deleting file.')
def cmd_mkd (self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,path,'manage_addFolder')
env['QUERY_STRING']='id=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mkd))
cmd_xmkd=cmd_mkd
def _cmd_mkd(self,response):
#print "mkd response", response.__dict__
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('257 MKD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating directory.')
def cmd_rmd (self, line):
# XXX should object be checked to see if it's folderish
# before we allow it to be RMD'd?
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_rmd))
cmd_xrmd=cmd_rmd
def _cmd_rmd(self,response):
#print "rmd response", response.__dict__
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 RMD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error removing directory.')
def cmd_user (self, line):
'specify user name'
if len(line) > 1:
self.userid = line[1]
self.respond ('331 Password required.')
else:
self.command_not_understood (string.join (line))
def cmd_pass (self, line):
'specify password'
if len(line) < 2:
pw = ''
else:
pw = line[1]
self.password=pw
self.respond ('230 Login successful.')
self.authorized = 1
self.log ('Successful login.')
class ZResponseReceiver:
"""Given an output pipe reads response and parses it.
After a call to ready returns true, you can read
the headers as a dictiony and the content as a string."""
def __init__(self,pipe):
self.pipe=pipe
self.data=''
self.headers={}
self.content=''
def ready(self):
if self.pipe is None:
return 1
if self.pipe.ready():
data=self.pipe.read()
if data:
self.data=self.data+data
else:
self.parse()
return 1
def parse(self):
headers,html=string.split(self.data,'\n\n',1)
self.data=''
for header in string.split(headers,'\n'):
k,v=string.split(header,': ',1)
self.headers[k]=v
self.content=html
self.pipe=None
class ResponseProducer:
"Allows responses which need to make Zope requests first."
def __init__(self,pipe,callback,args=None):
self.response=ZResponseReceiver(pipe)
self.callback=callback
self.args=args or ()
self.done=None
def ready(self):
if self.response is not None:
return self.response.ready()
else:
return 1
def more(self):
if not self.done:
if not self.response.ready():
raise NotReady()
self.done=1
r=self.response
c=self.callback
args=self.args+(r,)
self.response=None
self.callback=None
self.args=None
return apply(c,args)
else:
return ''
class ContentReceiver:
"Write-only file object used to receive data from FTP"
def __init__(self,callback,args=None):
self.data=StringIO()
self.callback=callback
self.args=args or ()
def write(self,data):
self.data.write(data)
def close(self):
self.data.seek(0)
args=self.args+(self.data,)
c=self.callback
self.callback=None
self.args=None
apply(c,args)
class zope_ftp_server(ftp_server):
ftp_channel_class = zope_ftp_channel
def __init__(self,module,hostname,port,resolver,logger_object):
ftp_server.__init__(self,None,hostname,port,resolver,logger_object)
self.module=module
def handle_accept (self):
conn, addr = self.accept()
self.total_sessions.increment()
print 'Incoming connection from %s:%d' % (addr[0], addr[1])
self.ftp_channel_class (self, conn, addr, self.module)
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment