I use Net::FTP to access IIS on WinNT quite a bit at work. When I have a
problem, I debug it by trying to do what perl was doing using the Windows
command line ftp client. In your case, login, cd '/wholesite' and if that
doesn't work, it is a problem with the way the IIS server is configured.
As for files showing up, but directories not showing up, I'm guessing you
will find the same behavior from the command line ftp client.
As a side note, by default you can only view the ftp directory (e.g
c:\Inetpub\wwwroot). To make it more unix-like, you can add Virtual
directories in the IIS Admin that allow you to traverse the whole file
system. For example, add /C: pointing to C:\ and now you can access
/C:/Winnt, etc... There's probably a good reason that this is not on by
default - I guess Windows doesn't have a very good (or easy to administer)
multi-user security architecture.
If anyone is interested, my first python project was an ftp script. It
takes an xml config file, are mirrors/copies/deletes directories and files.
It can do search and replace on the files during the ftp. I pasted it
below.
Perl is great, but it good not to HAVE to use it all the time. (I'm
learning perl's cousin, Ruby, as well).
Jon
#!/usr/bin/python
"""
Usage: xmlftp.py --user FTPUSER --pass FTPPASS --servers
SERVER1[,SERVER2],...] --xmlfile XMLFILE
takes a valid xml file that explains
what files and directories should be copied from
the current server to the servers specified in --servers
files and directories can contain spaces
works with Unix and Windows
Example XMLFILE:
<xml>
<copyfile source='C:\Temp\Foo.txt' dest='D:\File\Bar.txt'
callback='replace("A", "B").replace("B", "C")' > <!-- callback
string op on source text, optional dest -->
<copydir source='C:\Test' dest='D:\Testing'>
<!-- dest is optional -->
<mirrordir source='C:\Test' dest='D:\Testing'> <!-- delete
dest first --> <!-- dest is optional -->
<mkdir dest='D:\Testing'>
<rmdir dest='D:\Testing'>
</xml>
"""
import ftplib
from xml.sax import saxlib
from xml.sax.drivers import drv_xmlproc
import os
import sys
import string
import re
import getopt
Max_FTP_Attempts = 10 # will retry each high-level operation
verbose = 0
# get a temp dir
if os.path.isdir('C:\\Temp'):
tempdir = 'C:\\Temp'
elif os.path.isdir('/tmp'):
tempdir = '/tmp'
else:
raise "No valid tmpdir. C:\\Temp and /tmp don't exist"
def usage(*args):
sys.stdout = sys.stderr
for msg in args: print msg
print __doc__
sys.exit(1)
def main():
'''build list of source and destination files from xml configuration
file
and carry out the ftp'''
global verbose
ftptasks = {} # data structure to maintain files to work with
filesftpd = 0 # keep track of how many we have done
ftpuser, ftppass, servers, xmlfile = '', '', [], '' # options
try:
opts, args = getopt.getopt(sys.argv[1:], None, ['user=', 'pass=',
'servers=', 'xmlfile=', 'verbose'])
except getopt.error, msg:
usage(msg)
except TypeError, msg:
usage(msg)
for o, a in opts:
if o == '--user' : ftpuser = a
if o == '--pass' : ftppass = a
if o == '--servers' : servers = a.split(',')
if o == '--xmlfile' : xmlfile = a
if o == '--verbose' : verbose = 1
for option in 'ftpuser', 'ftppass', 'servers', 'xmlfile':
value = eval(option) # like symbolic ref
if not value: usage(option + " option must be specified")
# parse file and get back data structure
ftptasks = parse_xml(xmlfile)
# check to ensure we have something to do
numtasks = 0
for key in ftptasks.keys(): numtasks += len(ftptasks[key])
if numtasks == 0:
sys.exit("Nothing to do\n")
# validate input
for file in ftptasks['copyfiles']:
if not os.path.isfile(file['source']):
sys.exit('Source file ' + file['source'] + ' does not exist\n')
# validate and translate input - turn contents of copydirs and
mirrordirs into a copyfile
for dir in ftptasks['copydirs'] + ftptasks['mirrordirs']:
if not os.path.isdir(dir['source']):
sys.exit('Source directory ' + dir['source'] + ' does not
exist\n')
ftptasks['copyfiles'][len(ftptasks['copyfiles']):] =
get_files(dir['source'], dir['dest'])
# do the work on each server
for server in servers:
if verbose: print 'FTPing to', server
ftpsession = FTPSession(server, ftpuser, ftppass) # class keeps
track of Session attributes and calls ftp funcs
# makes
re-initing ftp on error much easier
for dir in ftptasks['rmdirs']:
if verbose: print '\tRemoving dir tree', dir['dest']
ftpsession.rmdir(dir['dest'])
for dir in ftptasks['mkdirs']:
if verbose: print '\tMKDiring tree', dir['dest']
ftpsession.mkdir(dir['dest'])
for dir in ftptasks['mirrordirs']:
if verbose: print '\tRemoving dir tree', dir['dest']
ftpsession.rmdir(dir['dest'])
for file in ftptasks['copyfiles']:
if verbose: print '\tCopying', file['source'], 'to',
file['dest']
ftpsession.copyfile(file['source'], file['dest'])
filesftpd += 1
print 'Ftpd', filesftpd, 'files across', len(servers), 'servers:',
servers
def parse_xml(xmlfile):
'''handle the XML parsing'''
files = {'copydirs': [], 'copyfiles': [], 'mkdirs': [], 'rmdirs': [],
'mirrordirs': []}
SAXparser = drv_xmlproc.SAX_XPParser()
SAXparser.setDocumentHandler(DocumentHandler(files)) # override
DocumentHandler for my tags
# files will be
loaded with data structure
# representing xml
file
SAXparser.parse(xmlfile)
return files
class DocumentHandler(saxlib.DocumentHandler):
"""set instance variables and append new dict for the attributes
of each tag we're interested in"""
def __init__(self, files):
self.copydirs = files['copydirs']
self.copyfiles = files['copyfiles']
self.mkdirs = files['mkdirs']
self.rmdirs = files['rmdirs']
self.mirrordirs = files['mirrordirs']
def startElement(self, name, attrs):
if name == 'copyfile':
validate_attributes(name, attrs.keys(), ['source', 'dest',
'callback']) # check to make sure we only get
# what we expect
if attrs.has_key('dest'):
properdest = win_ftp_path(attrs.getValue('dest')) #
Unix-like path that will work on Win and Unix
else:
properdest = win_ftp_path(attrs.getValue('source')) # Assume
same path on dest server if dest not given
# can modify a file in place before ftpping
# - currently can't have same name file in two different places
use callback (latest will overwrite)
# - callback must be an operation on a string
if attrs.has_key('callback'):
global tempdir
# use tempfile to store modified file
propersource = tempdir + os.sep +
os.path.basename(attrs.getValue('source'))
newsourcetext = eval("open('" + attrs.getValue('source') +
"').read()." + attrs.getValue('callback'))
open(propersource, 'w').write(newsourcetext)
else:
propersource = attrs.getValue('source')
self.copyfiles.append({'source': propersource, 'dest': properdest})
elif name == 'copydir':
validate_attributes(name, attrs.keys(), ['source', 'dest'])
if attrs.has_key('dest'):
properdest = win_ftp_path(attrs.getValue('dest'))
else:
properdest = win_ftp_path(attrs.getValue('source'))
self.copydirs.append({'source': attrs.getValue('source'), 'dest':
properdest})
elif name == 'mkdir':
validate_attributes(name, attrs.keys(), ['dest'])
properdest = win_ftp_path(attrs.getValue('dest'))
self.mkdirs.append({'dest': properdest})
elif name == 'rmdir':
validate_attributes(name, attrs.keys(), ['dest'])
properdest = win_ftp_path(attrs.getValue('dest'))
self.rmdirs.append({'dest': properdest})
elif name == 'mirrordir':
validate_attributes(name, attrs.keys(), ['source', 'dest'])
if attrs.has_key('dest'):
properdest = win_ftp_path(attrs.getValue('dest'))
else:
properdest = win_ftp_path(attrs.getValue('source'))
self.mirrordirs.append({'source': attrs.getValue('source'), 'dest':
properdest})
elif name == 'xml': # this is the root tag
pass
else:
sys.exit(name + ' is not a supported tag\n')
def validate_attributes (tagname, attributes, valid_attributes):
for attribute in attributes:
if attribute not in valid_attributes:
raise attribute + " attribute not supported for " + tagname + "
tag. Use " + str(valid_attributes)
def get_files(sourcedir, destdir):
'''walk directory tree and build list of source and destination files
based on a source and destination directory. And do it recursively'''
returnfiles = []
sourcefiles = os.listdir(sourcedir)
for sourcefile in sourcefiles:
destfile = destdir + '/' + sourcefile; # destfile grows with
the sourcefile
sourcefile = sourcedir + os.sep + sourcefile;
if os.path.isdir(sourcefile):
returnfiles[len(returnfiles):] = get_files(sourcefile, destfile)
# it is a dir - get its files
else:
returnfiles.append({'source': sourcefile, 'dest': destfile})
return returnfiles
class FTPSession:
"""store attributes for an ftp session and define functions for that
session"""
global Max_FTP_Attempts, verbose
def __init__(self, server, user, password, maxattempts=Max_FTP_Attempts,
verbose=verbose):
self.server = server
self.user = user
self.password = password
self.maxattempts = maxattempts
self.verbose = verbose
self.ftp = self._init_ftp()
def _init_ftp(self):
"""re-init the ftp connection. Try to close it first"""
try:
self.ftp.quit()
except:
pass
self.ftp = ftplib.FTP(self.server, self.user, self.password)
def copyfile(self, source, dest):
"""copy the source to dest, making any necessary directories and
retries along the way"""
attempts = 0
while 1:
attempts += 1
try:
if os.path.isdir(source): # shouldn't be dir, but
just in case
self._mkdir(dest)
else:
self._mkdir(unix_dirname(dest)) # make sure it's parent
directory exists
self._put(source, dest) # will this fail on
directories?
except:
if attempts >= self.maxattempts:
die(str(attempts) + " attempts to copy " + source + ' to
' + dest + " on " + self.server)
self._init_ftp() # let's try it again
with a new connection
else: # it worked
return
def mkdir(self, dir):
"""make a directory"""
attempts = 0
while 1:
attempts += 1
try:
self._mkdir(dir)
except:
if attempts >= self.maxattempts:
die(str(attempts) + " attempts making directory " + dir
+ " on " + self.server)
self._init_ftp()
else:
return
def rmdir(self, dir):
"""remove a directory and its contents"""
attempts = 0
while 1:
attempts += 1
try:
self._rmdir(dir)
except:
if attempts >= self.maxattempts:
die(str(attempts) + " attempts removing directory " +
dir + " on " + self.server)
self._init_ftp()
else:
return
def _put(self, source, dest):
'''ftp source to dest'''
if self.verbose: print '\t\tPutting', source, 'to', dest
# get the size first to ensure a successfull transfer
try:
previous_size = self.ftp.size(dest)
except ftplib.error_perm, error_resp: # file does not exist
error_resp_string = str(error_resp)
if error_resp_string.find('No such file or directory') != -1 \
or error_resp_string.find('system cannot find the file
specified') != -1:
previous_size = 0
else:
raise ftplib.error_perm, error_resp
source_size = os.path.getsize(source)
self.ftp.storbinary('STOR ' + dest, open(source, 'rb'), 1024) #
use binary mode - doesn't work with text
#
between different platforms
post_delivery_size = self.ftp.size(dest)
if post_delivery_size != source_size:
error_msg = '\tERROR ftping ' + source + ' to ' + dest + ' on '
+ self.server + '\n' + \
'\t\tSource size: ' + source_size + '\n' + \
'\t\tDest size before ftp: ' + previous_size + '\n'
+ \
'\t\tDest size after ftp: ', post_delivery_size +
'\n'
raise error_msg
def _mkdir(self, dir):
'''If a directory does not exist on the ftp server,
make it and its subdirectories - recursively'''
if self.verbose: print '\t\tMaking dir', dir
try:
self.ftp.cwd(dir)
return
except ftplib.error_perm:
self._mkdir(unix_dirname(dir))
self.ftp.mkd(dir) # dir didn't exist, but we know its dir does!
(after recursive call)
def _rmdir(self, dir):
'''If a direcotry exists on the ftp server,
remove its contents recursively'''
if self.verbose: print '\t\tRemoving dir', dir
try:
self.ftp.cwd(dir)
except ftplib.error_perm: # dir doesn't exist - we're done
return
try:
self.ftp.cwd(unix_dirname(dir))
self.ftp.rmd(dir)
except ftplib.error_perm, msg: # directory isn't empty, remove its
contents first
self.ftp.cwd(dir)
file_list = []
self.ftp.dir(file_list.append) # last arg is a callback for
each line retrieved
if len(file_list) == 0:
raise('Error removing ' + dir + ' Error msg: ', msg + '\n')
for file in get_files_from_ftpdir(file_list):
full_path = dir + '/' + file # construct
the full path
try:
self.ftp.cwd(full_path)
except ftplib.error_perm, msg: # it's a file
self._rm(full_path)
else: # it's a
directory - recursive call
self._rmdir(full_path)
ftp.cwd(unix_dirname(dir)) # now it's
contents have been deleted
ftp.rmd(dir)
def _rm(self, file):
'''Remove a file.'''
if self.verbose: print '\t\tRemoving file', file
self.ftp.cwd(unix_dirname(file))
self.ftp.delete(file)
def win_ftp_path(path):
"""Unix-like path that will work on Windows and Unix - with Unix
directory listing on Windows and proper Virtual dirs"""
path = path.replace('\\', '/')
if not path.startswith('/'): # forgot leading slash
path = '/' + path
return path
def unix_dirname(dirname):
"""Get directory name from a unix path"""
dirs = dirname.split('/')
dir = '/'.join(dirs[:-1])
return dir
def get_files_from_ftpdir(listings):
"""ftplisting has file in the 9th column"""
matchstr = re.compile(r'\s+')
file_list = []
for row in listings:
columns = re.split(matchstr, row, 8)
file_list.append(columns[-1])
return file_list
def die(msg):
"""write stderr message and traceback after an exception"""
# shouldn't use traceback from the except clause
import traceback
sys.stdout.flush()
sys.exit(msg + ":\n\t" + str(traceback.print_exc()) + "\n")
if __name__ == '__main__':
main()
>From: "Matthew Brooks" <[EMAIL PROTECTED]>
>Reply-To: "Matthew Brooks" <[EMAIL PROTECTED]>
>To: "Boston Perl Mongers" <[EMAIL PROTECTED]>
>Subject: [Boston.pm] trouble with IIS driven FTP server using Net::FTP
>Date: Fri, 3 Aug 2001 22:04:48 -0400
>
>Anyone know if there is an issue with using Net::FTP to access a MS IIS
>driven FTP server?
>
>I'm getting an error that says the server can't find the specified file,
>even though the directory in question exists. (See below for debugged
>output) Also of note, just to see what was going on I added the lines...
>
>print $ftp->dir;
>$ftp->quit;
>exit;
>
>...to the script right before the $ftp->cwd($path) call that is failing and
>I noticed that none of the directories are showing up in the list, only the
>files in the ftp / directory appear.
>
>Here's the output. The lines that don't start with Net::FTP are generated
>by
>print statements in the code:
>
>Connecting to 192.168.0.182...
>Net::FTP: Net::FTP(2.56)
>Net::FTP: Exporter(5.562)
>Net::FTP: Net::Cmd(2.18)
>Net::FTP: IO::Socket::INET(1.25)
>Net::FTP: IO::Socket(1.26)
>Net::FTP: IO::Handle(1.21)
>
>Net::FTP=GLOB(0x1a750f8)<<< 220 WWW002 Microsoft FTP Service (Version 5.0).
>OK
>Logging in...
>Net::FTP=GLOB(0x1a750f8)>>> user mjbrooks
>Net::FTP=GLOB(0x1a750f8)<<< 331 Password required for mjbrooks.
>Net::FTP=GLOB(0x1a750f8)>>> PASS ....
>Net::FTP=GLOB(0x1a750f8)<<< 230 User mjbrooks logged in.
>OK
>Setting session to binary mode...
>Net::FTP=GLOB(0x1a750f8)>>> TYPE I
>Net::FTP=GLOB(0x1a750f8)<<< 200 Type set to I.
>OK
>Changing to /wholesite...
>Net::FTP=GLOB(0x1a750f8)>>> CWD /wholesite
>Net::FTP=GLOB(0x1a750f8)<<< 550 /wholesite: The system cannot find the file
>specified.
>Net::FTP=GLOB(0x1a750f8)>>> QUIT
>Net::FTP=GLOB(0x1a750f8)<<< 221
>
>
_________________________________________________________________
Get your FREE download of MSN Explorer at http://explorer.msn.com/intl.asp