FTP aware file object

Andy Jewell andy at wild-flower.co.uk
Fri May 30 17:31:22 EDT 2003


Just after some *constructive* criticism on a little class I've written, based 
on the builtin 'file' class.

Firstly: what it does.

It's a wrapper that fakes the ability to open of files over ftp, and treat 
them as if they were just bog ordinary files.  It has a 'file' compatable 
interface,  but it notices when you pass it an ftp url, and will download the 
remote file before opening a local copy (rather like urllib).  The 
difference, though, is that If the file is opened for any kind of writing 
operation, it is uploaded back to the server when you close it.

Secondly: why.

I'm trying to fully grok new style classes, and I wanted to have a way to make 
other programs I've written ftp-aware without having to rewrite them.  Oh, 
and maybe others (idle for instance).

I've read Guido's essay on new-style classes, and I think I've applied what 
I've learned with this little monstrosity, but, in particular with super() 
calls, I wanted some sort of confirmation that i was doing it 'right'.

Another question is about my 'repatching' the close() method to refer to 
super(ftpfile,self).close when operating on solely local files... is this the 
best way, or should I just supercall it within my own close meth?  I had some 
vague notion about better performance doing it this way ;-) - am I right?

Lastly, is it feasable to rebind 'file' and 'open' globally, or do I have to 
do this in each and every module that I want to use this functionality in?  
What I mean, is, within the toplevel module of a program, can I just do:

	import ftpfile
	file=ftpfile.ftpfile
	open=ftpfile.ftpfile

?

Note that this is still seriously 'development' code - I haven't tested every 
single aspect, yet, so it's *bound* to have errors - oh, and it still prints 
debugging info in various places, and there are some glaringly stupidly 
hard-coded bits; plain read and write operations *do* seem to work correctly, 
though.

Go on, have a laugh... you know you want to :-p

-andyj
------8<------------------8<------------------8<------------------8<------------------


import ftplib
import os

localfilecache="c:/temp/ftpcache/"

class ftpfile(file):
	""" url,mode="r",buffering=-1
	
	    FTP aware file object which delegates "normal" file operations
	    to the superclass.

	    If the file name begins "ftp://", FTP mode is enabled.

	    In FTP mode, where reading of the file is required, the file is
	    first downloaded from the remote host,  and where writing is
	    required, the file is uploaded when it is closed.

	    NOTE THAT FTP READS AND WRITES ARE PERFORMED ON A LOCAL COPY
	    
	"""

	def __init__(self,name,mode="r",buffering=-1):
		if not name.startswith("ftp://"):
			return super(ftpfile).__init__(name,mode,buffering)
		self.filemode=mode		
		self.decodeurl(name)
		self.localfilename=os.path.join(localfilecache,
						self.host,self.user,
						self.path,self.filename)
		localpath=os.path.join(localfilecache,
				       self.host,self.user,
				       self.path)
		if not os.path.exists(localpath):
			os.makedirs(localpath)
		# Decide if we need to download the file first
		if self.filemode[0] not in ["w","wb"]:
			self.downloadfile(name,self.filemode)
		# Short-circuit our close() method if file doesn't need
		# to be uploaded
		if self.filemode in ["r","rb"]:
			self.close=super(ftpfile,self).close
		#self.localfilehandle=file(self.localfilename,self.filemode)
		return super(ftpfile,self).__init__(self.localfilename,mode,buffering)

	def close(self):
		# ... then upload it
		ftpconn=ftplib.FTP(self.host,self.user,self.passwd,None)
		ftpconn.cwd(self.path)
		try:
			ftpconn.delete(self.filename)
		except:
			pass
		super(ftpfile,self).close()
		self.localfilehandle=file(self.localfilename,"r")
		if "b" in self.mode:
			ftpconn.storbinary("STOR "+self.filename,self.localfilehandle)
		else:
			ftpconn.storlines("STOR "+self.filename,self.localfilehandle)			
		ftpconn.close()
		# Close the file...
		self.localfilehandle.close()
		
	def downloadfile(self,name,mode):
		# Use correct file mode
		if "b" in mode:
			retr=ftplib.FTP.retrbinary
			self.writer=self.binwriter
			localmode="wb"
		else:
			retr=ftplib.FTP.retrlines
			self.writer=self.txtwriter
			localmode="w"
		# Download the file
		if os.path.exists(self.localfilename):
			os.remove(self.localfilename)
		self.localfilehandle=file(self.localfilename,localmode)
		ftpconn=ftplib.FTP(self.host,self.user,self.passwd)
		ftpconn.cwd(self.path)
		if "b" in mode:
			ftpconn.retrbinary("RETR "+self.filename,self.binwriter)
		else:
			ftpconn.retrlines("RETR "+self.filename,self.txtwriter)
		ftpconn.close()
		self.localfilehandle.close()
			
	def decodeurl(self,url):
		""" decodes an ftp url into its component fields:

			ftp://[user[:passwd]@]host[:port]/path/filename
			     /     /       /      /      /    /
			     |     |       |      |      |    lastslash
			     |     |       |      |       firstslash
			     |     |       at     portcolon
			     |     colon
			     6
		"""

		# Sanity check:
		if not url.startswith("ftp://"):
			raise "Malformed url:",url
		
		# Do we have a user:passwd pair?
		at=url.find("@",6)
		colon=url.find(":",6)
		portcolon=url.find(":",at)
		if at == -1:
			at=6
			self.user=None
			self.passwd=None
		else:
			if colon == -1:
				colon=at
			self.user=url[6:colon]
			self.passwd=url[colon+1:at]
			if self.passwd == "":
				self.passwd=None
		firstslash=url.find("/",at)
		if firstslash == -1:
			raise "Malformed url:",url
		if portcolon !=-1:			
			self.host=url[at+1:portcolon]
			self.port=url[portcolon:firstslash]
		else:
			self.host=url[at+1:firstslash]
			self.port=":21"
		lastslash=url.rfind("/")
		if lastslash == -1:
			raise "Malformed url:",url
		self.filename=url[lastslash+1:]
		self.path=url[firstslash:lastslash]
		print self.user+":"+self.passwd+"."
		print self.host,self.path,self.filename

	def binwriter(self,block):
		self.localfilehandle.write(block)

	def txtwriter(self,line):
		self.localfilehandle.write(line+"\n")

	def write(self,string):
		print "Writing",repr(string)
		super(ftpfile,self).write(string)









More information about the Python-list mailing list