import repository from arizona
[raven.git] / lib / arizona-lib / transfer / arizonatransfer_bittorrent.py
1 #! /usr/bin/env python
2 """
3 Stork Project (http://www.cs.arizona.edu/stork/)
4 Module: arizonatransfer_bittorrent
5 Description:   Provides a general file transferring via BitTorrent
6
7 """
8 \r
9 """arizonaconfig\r
10 options=[['',"--bittorrenttrackerhost","arizonabittorrenttrackerhost","store","string","quadrus.cs.arizona.edu","TrackerHost","The host name of the tracker server."],\r
11          ['',"--bittorrenttrackerport","arizonabittorrenttrackerport","store","int",6880,"TrackerPort","The number to specify as the tracker port number for torrents."],\r
12          ['',"--bittorrentuploadrate","arizonabittorrentuploadrate","store","int",0,"UploadRate","The max upload rate for the bittorrent processes (0 means no limit)."],\r
13          ['',"--bittorrentseedlookuptimeout","arizonabittorrentseedlookuptimeout","store","int",30,"SeedLookupTimeout","The number of seconds bittorrent should timeout after not finding a seed."]]\r
14 includes=[]\r
15 """\r
16
17 import arizonareport
18 import time
19 import os
20 import urllib,urllib2, math, random, sys, os, socket, string\r
21 import arizonaconfig\r
22 \r
23 from storkbtdownloadheadless import *
24 \r
25 \rdef log_transfer(function, pid, timestamp, timestampend):
26 \r   try:
27       import storklog
28       storklog.log_transfer(function, pid, timestamp, timestampend)
29    except:
30       pass
31 \r
32 #default settings list\r
33 defaultset=[60,"/tmp/aztorrents/files","/tmp/aztorrents/updates",0,True,"quadrus.cs.arizona.edu",6880,30]\r
34
35 # Makes the profiler stop complaining...
36 glo_filename = ''\r
37 glo_confname = "/tmp/aztorrents/btshare.conf"\r
38 glo_daemonname = "/etc/init.d/storkbtsharedaemon"\r
39 glo_updatedir = defaultset[2]\r
40 \r
41 def get_option(optname,default=None):\r
42    """
43    <Purpose>
44       A small wrapper for the arizona_config.get_option method.
45
46    <Arguments>
47       None.
48    
49    <Exceptions>
50       None.
51
52    <Side Effects>
53       None.
54
55    <Returns>
56       True.
57    """\r
58    ret=arizonaconfig.get_option(optname)\r
59    if ret is None:ret=default\r
60    return ret\r
61
62 def close_transfer_program():
63    """
64    <Purpose>
65       This closes a connection (dummy function for BitTorrent).
66
67    <Arguments>
68       None.
69    
70    <Exceptions>
71       None.
72
73    <Side Effects>
74       None.
75
76    <Returns>
77       True.
78    """
79  
80    return True
81
82
83
84 def init_transfer_program(ignore=None, ignore2=None, ignore3=None, ignore4=None):
85    """
86    <Purpose>
87       This initializes a connection (dummy function for BitTorrent).
88
89    <Arguments>
90       None.
91    
92    <Exceptions>
93       None.
94
95    <Side Effects>
96       None.
97
98    <Returns>
99       True.
100    """
101    return True
102
103 def ensure_settings(checkinterval=60,filedir="/usr/local/stork/torrents/files",updatedir="/usr/local/stork/torrents/updates",uploadrate=0,sharing=True):\r
104    """<Purpose>\r
105    ensures that the current instance of the torrent sharing program has the correct settings\r
106 and also stops or starts the instance of the sharing program depending on sharing status\r
107 \r
108 <Arguments>\r
109    checkinterval:\r
110       how frequently the sharing program should check the update directory\r
111       (default=60)\r
112    filedir:\r
113       the directory where the sharing program should look for downloaded files\r
114       (default="/usr/local/stork/torrents/torrents")\r
115    updatedir:\r
116       the directory where the sharing program should check for torrent files\r
117       (default="/usr/local/stork/torrents/updates")\r
118    uploadrate:\r
119       the maximum rate set for uploading (and therefore downloading) files (0 means no max)\r
120       (default=0)\r
121    sharing:\r
122       whether or not the sharing program should be running\r
123       (default=True)\r
124 \r
125 <Exceptions>\r
126    None.\r
127 \r
128 <Side Effects>\r
129    writes to the file in glo_confname and may start/stop/restart the storkbtshare daemon\r
130 \r
131 <Returns>\r
132    None.\r
133 \r
134 <Comments>\r
135    None.\r
136 """\r
137    global glo_confname\r
138    global glo_daemonname\r
139    global defaultset\r
140    altered=False\r
141 \r
142    #exists can throw an exception if the directory doesn't exist\r
143    try:os.path.exists(glo_confname)\r
144    except:\r
145       os.makedirs(glo_confname[:glo_confname.rfind('/')])\r
146    \r
147    if os.path.exists(glo_confname):\r
148       fo=open(glo_confname)\r
149       rd=fo.readlines()\r
150       fo.close()\r
151       ccheckint=None\r
152       cfiledir=None\r
153       #cdestdir=None\r
154       cuploadrate=None\r
155       cupdatedir=None\r
156       #cforcedshareduration=None\r
157       for i in rd:\r
158          s=i.split("=")\r
159          s[0]=s[0].strip()\r
160          s[1]=s[1].strip()\r
161          if s[0]=="checkinterval":\r
162             ccheckint=s[1]\r
163             if s[1]!=str(checkinterval):altered=True\r
164          elif s[0]=="filedir":\r
165             cfiledir=s[1]\r
166             if s[1]!=filedir:altered=True\r
167          #elif s[0]=="downloaddir":\r
168          #   cdestdir=s[1]\r
169          #   if s[1]!=destdir:altered=True\r
170          elif s[0]=="updatedir":\r
171             cupdatedir=s[1]\r
172             if s[1]!=updatedir:altered=True\r
173          elif s[0]=="uploadrate":\r
174             cuploadrate=s[1]\r
175             if s[1]!=str(uploadrate):altered=True\r
176          #elif s[0]=="forcedshareduration":\r
177          #   cforcedshareduration=s[1]\r
178          #   if s[1]!=forcedshareduration:altererd=True\r
179       if not altered:\r
180          #check if the settings are missing and the settings passed to this function are different than the defaults\r
181          if ccheckint is None and checkinterval!=defaultset[0]:altered=True\r
182          if cfiledir is None and filedir!=defaultset[1]:altered=True\r
183          #if cdestdir is None and destdir!="/tmp":altered=True\r
184          if cupdatedir is None and updatedir!=defaultset[2]:altered=True\r
185          if cuploadrate is None and uploadrate!=defaultset[3]:altered=True\r
186          #if cforcedshareduration is None and forcedshareduration!=-2.0:altered=True\r
187    elif checkinterval!=defaultset[0] or filedir!=defaultset[1] or updatedir!=defaultset[2] or uploadrate!=defaultset[3]: #or destdir!="/tmp" \r
188       altered=True\r
189    if altered:#save the settings\r
190       fo=open(glo_confname,"w")\r
191       fo.write("checkinterval="+str(checkinterval)+"\n")\r
192       fo.write("torrentdir="+filedir+"\n")\r
193       #fo.write("downloaddir="+destdir+"\n")\r
194       fo.write("updatedir="+updatedir+"\n")\r
195       fo.write("uploadrate="+str(uploadrate)+"\n")\r
196       #fo.write("forcedshareduration="+forcedshareduration+"\n")\r
197       fo.close()\r
198    #restart the daemon\r
199    #is it running?\r
200    po=os.popen(glo_daemonname+" status")\r
201    rd=po.read()\r
202    po.close()\r
203    running="running" in rd.lower()\r
204    if altered and sharing and not running:os.popen(glo_daemonname+" start").close()\r
205    elif altered and sharing and running:os.popen(glo_daemonname+" restart").close()\r
206    elif sharing and not running:os.popen(glo_daemonname+" start").close()\r
207    elif not sharing and running:os.popen(glo_daemonname+" stop") .close()
208 \r
209 def make_torrent(filename,destfile,host,port):\r
210    """\r
211    <purpose>\r
212       Creates torrent files from the given arguments.  The torrent file should be saved as filename.\r
213    <arguments>\r
214       filename\r
215          the file to save the torrent as\r
216       destfile\r
217          the file to make the torrent from\r
218       host\r
219          the host name of the tracker (should not include the port or protocol)\r
220       port\r
221          the port the tracker is running on\r
222    """   \r
223    os.popen("/usr/bin/btmaketorrent.py --target "+filename+" http://"+host+":"+str(port)+"/announce "+destfile).close()
224
225 def retrieve_files(host, filelist, destdir='.', indicator=None):
226    """
227    <Purpose>
228       This retrieves files from a host to a destdir.
229
230    <Arguments>
231       host:
232          'host' holds two things, a server name and target directory.
233          For example, if you want to retrieve files from '/tmp/' directory
234          in 'quadrus.cs.arizona.edu' server, the 'host' will be
235          'quadrus.cs.arizona.edu/tmp'.         
236
237       filelist:
238          'filelist' is a list of files which need to be retrieved.
239
240       junk_hashlist:
241          'junk_hashlist' is a list of the hashes for this list of files.
242          It should be a list of strings.
243
244       destdir:
245          'destdir' is a destination directory where retrieved files will 
246          be placed. A user should have 'destdir' exist before retrieving 
247          files. 'destdir' should be a string. Default is a current dir.
248
249       indicator:
250          'indicator' is a module which has set_filename and
251          download_indicator functions. 'indicator' will be passed in 
252          'urlretrieve' function so that progress bar will be shown 
253          while downloading files. Default is 'None'.
254
255    <Exceptions>
256       All exceptions should be caught.
257
258    <Side Effects>
259       If the storkbtshare daemon is set to be running, this method will create\r
260       hard links, torrent files, and ensure that the daemon is running.
261
262    <Returns>
263       (True, grabbed_list)
264       'grabbed_list' is a list of files which are retrieved
265    """\r
266    global defaultset
267    # set grabbed_list as a empty list. Later it will be appended with retrieved files
268    grabbed_list = []
269
270    #arizonareport.send_out(3, "[Bittorrent Debug]: retrieve_files: "+str(filelist)+" "+str(junk_hashlist) )
271    # hack to make bt work
272    try:
273       def errorfunc():
274           pass
275       from BitTorrent.ConvertedMetainfo import set_filesystem_encoding
276       set_filesystem_encoding("ascii", errorfunc)
277    except:
278       pass
279
280    # check if host is a string   
281    if not isinstance(host, str):
282       arizonareport.send_syslog(arizonareport.ERR, "retrieve_files(): host should be a string")
283       # return false and empty list
284       return (False, grabbed_list)
285    
286    # check if filelist contains only strings
287    # Later should just use something like justin.valid_sl   TODO!!!
288    # TODO - check for valid tuple list
289    #if not valid_sl(filelist):
290    #   arizonareport.send_syslog(arizonareport.ERR, "retrieve_files(): filelist should be a list of strings")
291    #   # return false and empty list
292    #   return (False, grabbed_list)
293    
294    # check if destdir is a string
295    if not isinstance(destdir,str):
296       arizonareport.send_syslog(arizonareport.ERR, "retrieve_files(): destdir should be a string")
297       # return false and empty list
298       return (False, grabbed_list)
299
300    # check that the destination directory exists  
301    if not os.path.isdir(destdir):
302       arizonareport.send_syslog(arizonareport.ERR, "\nretrieve_files(): The destination directory '" + destdir + "' for a requested does not exist")
303       # return false and empty list
304       return (False, grabbed_list)
305
306    # if destdir is a empty string, then make it as a current directory
307    if destdir == '':
308       destdir = '.'
309
310    #verify the connection and that the directory exists\r
311    # this has issues with this transfer stub due to the way bittorrent may\r
312    #  interoperate with certain standard modules (and possibly their naming\r
313    #  conventions).\r
314    #if not __verify_connection(host):
315    #   # return false and empty list
316    #   return (False, grabbed_list)
317 \r
318 \r
319 \r
320 \r
321 \r
322    #make sure the sharing program is working:\r
323    set=defaultset[:]#[60,"/usr/local/stork/torrents/files","/usr/local/stork/torrents/updates",0,True,"quadrus.cs.arizona.edu",6880,30]\r
324 \r
325    set[0]=get_option("arizonabittorrentcheckinterval",set[0])\r
326    set[1]=get_option("arizonabittorrentfiledir",set[1])\r
327    set[2]=get_option("arizonabittorrentupdatedir",set[2])\r
328    glo_updatedir=set[2]\r
329    set[3]=get_option("arizonabittorrentuploadrate",set[3])\r
330    set[4]=get_option("arizonabittorrentsharing",str(set[4])).lower().strip()=="true"\r
331    set[5]=get_option("arizonabittorrenttrackerhost",set[5])\r
332    set[6]=get_option("arizonabittorrenttrackerport",set[6])\r
333    set[7]=get_option("arizonabittorrentseedlookuptimeout",set[7])\r
334 \r
335    #the share daemon should take care of itself now.\r
336    #ensure_settings(set[0],set[1],set[2],set[3],set[4])\r
337 \r
338    #make sure the torrent and update directories exist
339    if not os.path.isdir(set[1]):
340       os.makedirs(set[1])\r
341    if not os.path.isdir(set[2]):
342       os.makedirs(set[2])\r
343
344    # go through every file in the file list
345    for file in filelist:
346       filename = file['filename']
347       hash = file.get('hash', None)
348
349       if hash:
350          filename = "/" + hash + "/" + filename
351
352       starttime = time()
353 \r
354       # build url which specifies host and filename to be retrieved
355       thisurl = __build_url(host,filename)\r
356 \r
357       # find the name of the torrent file and its directory\r
358       ufilename=thisurl[thisurl.rfind('/')+1:]\r
359       #make sure it's the right length:\r
360       if len(ufilename)>255:ufilename=ufilename[-255:]\r
361       udir=thisurl[:thisurl.rfind('/')]                   \r
362       #find the real file name to download to\r
363       rfilename=filename[filename.rfind('/')+1:]\r
364 \r
365       #to allow for multiple users to have differing files of the same name and still
366       # find and use the corrent .torrent file, the torrent file names are long enough to
367       # hopefully include enough of the file's subdirectory(ies) to be distinguishable.
368       tfilename=ufilename+".torrent"\r
369       #since python cannot handle files with names longer than 255 characters, cut off the front characters\r
370       # (this will keep the hash)
371       if len(tfilename)>255:tfilename=tfilename[-255:]
372
373       #arizonareport.send_out(3, "[Bittorrent Debug]: about to try to download: "+udir+"/"+tfilename )
374
375       # download the file\r
376       btargs=["storkbtdownloadheadless.py","--display_interval","4.0", "--max_upload_rate", str(set[3]),"--save_as",destdir+"/"+rfilename,"--url",udir+'/'+tfilename]\r
377 \r
378       # if the file already exists, it must be unlinked to avoid possible errors with overwriting hard-linked files\r
379       #if os.path.exists(destdir+"/"+rfilename):\r
380       #   os.unlink(destdir+"/"+rfilename)\r
381 \r
382       prog_indic=None\r
383 \r
384       #try downloading
385       status=-1
386 \r
387       # if an idicator is passed in
388       if (indicator):
389          # make indicator_file store a file name which will be used in download_indicator function
390          indicator_file = rfilename
391          try:
392             # set the filename so that indicator module can use the name to show for progress bar
393             indicator.set_filename(indicator_file)
394          # indicator doesn't have method set_filename or download_indicator
395          except AttributeError:
396             arizonareport.send_syslog(arizonareport.ERR, 'retrieve_files(): indicator module passed in is incorrect')
397             return (False, grabbed_list)
398          # if indicator_file which used for set_filename is not a string
399          except TypeError:
400             arizonareport.send_syslog(arizonareport.ERR, 'retrieve_files(): indicator_file is incorrect')
401             return (False, grabbed_list)\r
402          prog_indic=indicator.download_indicator\r
403          arizonareport.send_out(3, "")\r
404       try:
405          #operating in quiet mode - only output should be via the progess indicator.
406          #try this when assured the that host argument is correct:
407          #status=btdownloadheadless(btargs,host,6969,1,prog_indic)
408          status=btdownloadheadless(btargs,set[5],set[6],1,prog_indic,0,None,set[7])
409       except IOError,e:
410          arizonareport.send_syslog(arizonareport.ERR, "retrieve_files(): azbt - error reading torrent file: " + str(e))\r
411          return (False, grabbed_list)
412       except BTFailure,e:
413          arizonareport.send_syslog(arizonareport.ERR, "retrieve_files(): azbt - "+str(e))
414          return (False, grabbed_list)
415
416       if status==1:#sucess should mean that it exists
417          grabbed_list.append(file)\r
418          endtime = time()\r
419          storklog.log_transfer("bittorrent",str( os.getpid() ), str(starttime), str(endtime) )
420          #taken care of by the share daemon:\r
421          #if it's sharing make a torrent and hard link in the appropriate directories\r
422          #if set[4]:\r
423          #    #make a hard link\r
424          #    os.popen("/bin/ln "+destdir+"/"+rfilename+" "+set[1]+"/"+ufilename).close()\r
425          #    #make a torrent for it\r
426          #    make_torrent(set[2]+"/"+tfilename,set[1]+"/"+ufilename,set[5],set[6])\r
427       else:#an error must have occurred?
428          arizonareport.send_syslog(arizonareport.ERR, "retrieve_files(): azbt - unknown transfer error: success= "+str(status))
429          return (False, grabbed_list)
430
431       # TODO FIXME? commented this out because blank lines were appearing in the stork output - JLJ
432       #arizonareport.send_out(0, "")
433
434    if (grabbed_list) :
435       return (True, grabbed_list)
436    # if nothing in grabbed_list
437    else:
438       return (False, grabbed_list)
439
440 def transfer_name():
441    """
442    <Purpose>
443       This gives the name of this transfer method.
444
445    <Arguments>
446       None.
447
448    <Exceptions>
449       None.
450
451    <Side Effects>
452       None.
453
454    <Returns>
455       'arizona_bittorrent' as an string
456    """
457
458    return 'arizona_bittorrent'
459
460
461
462 def __build_url(host,fname):
463    """
464    <Purpose>
465       This builds a url string with Http address.
466
467    <Arguments>
468        host:
469          'host' holds two things, a server name and target directory.
470          For example, if you want to retrieve files from '/tmp/' directory
471          in 'quadrus.cs.arizona.edu' server, the 'host' will be
472          'quadrus.cs.arizona.edu/tmp'.
473       fname:
474          A file name to be retrieved
475    
476    <Exceptions>
477       None.
478
479    <Side Effects>
480       None.
481
482    <Returns>
483       A whole url string created\r
484 \r
485    <Comments>\r
486       This function changes the URL to be virtually that of the torrent file.
487    """
488    
489    # remove the 'http://' or 'ftp://'
490    host = host.replace("ftp://", "")      
491    host = host.replace("http://","")\r
492 \r
493    #remove the directory information on the host\r
494    # and replace it with the torrent directory\r
495    dir = "/torrents/"\r
496    dpos=host.find("/")\r
497    if dpos!=-1:\r
498        host=host[:dpos]
499 \r
500    # make the fname into a single filename using the final hash/directory of the filename\r
501    #arizonareport.send_out(3, "[Bittorrent Debug]: __build_url fname="+fname)
502    fpos=fname.rfind('/')\r
503    fpos2=fname.rfind('/',0,fpos)\r
504    #arizonareport.send_out(3, "[Bittorrent Debug]: __build_url fpos="+str(fpos)+", fpos2="+str(fpos2) )
505    if not fpos==-1:\r
506        fname=fname[fpos+1:]+'-'+fname[fpos2+1:fpos]\r
507    
508    # return url which contains host and filename\r
509    # should now look like: http://quadrus.cs.arizona.edu/torrents/file.rpm-hash
510    return "http://" + host + dir + fname
511
512           
513       
514 def __verify_connection(host):
515    """
516    <Purpose>
517       This verifies a connection, testing a host and target directory.
518
519    <Arguments>
520       host:
521          'host' holds two things, a server name and target directory.
522          For example, if you want to retrieve files from '/tmp/' directory
523          in 'quadrus.cs.arizona.edu' server, the 'host' will be
524          'quadrus.cs.arizona.edu/tmp'.
525             
526    <Exceptions>
527       urllib2.URLError:
528          If host name is incorrect or host is dead, then return False
529
530       urllib2.HTTPError:
531          If given target directory is incorrect, then return False
532
533    <Side Effects>
534       None.
535
536    <Returns>
537       True or False (see above)
538    """
539    
540    # split host into server name and directory\r
541    host.replace("http://","")\r
542    host.replace("ftp://","")
543    index = host.find('/')
544    # set hostname to hold only a server name
545    if index != -1:
546       hostname = host[:index]
547    else :
548       hostname = host   
549
550    # checking only host\r
551    #the port number is for the odd bittorrent imports which interfere with\r
552    # the regular usage of urllib2.urlopen
553    checkurl = __build_url("http://"+hostname+"80", "")
554
555    # urllib2 is used since urllib doestn't offer a nice way to check the connection is valid
556    try :
557       urllib2.urlopen(checkurl)
558    # incorrect host name or host is dead
559    except urllib2.URLError, (msg):      
560       arizonareport.send_syslog(arizonareport.ERR, '__verify_connection(): "' + hostname + '" '+ str(msg).split("'")[1])
561       return False
562
563
564    # checking if the directory exists in the server
565    checkurl = __build_url(host, "")
566    try:
567       urllib2.urlopen(checkurl)
568    # if the directory doesn't exist in the server
569    except urllib2.HTTPError, (strerror):
570       arizonareport.send_syslog(arizonareport.ERR, '__verify_connection(): "' + str(strerror) + '" on the url "' + checkurl + '"')
571       return False
572
573    # everything is fine       
574    return True
575    
576
577
578
579 # TODO: should go away!!!
580 def valid_sl(stringlist):
581    """
582    <Purpose>
583       This returns True if stringlist is a list of strings or False if it is 
584       not.
585
586    <Arguments>
587       stringlist:
588           The variable to be checked.
589
590    <Exceptions>
591       None
592
593    <Side Effects>
594       None
595
596    <Returns>
597       True or False (see above)
598    """
599
600    # If it's a list
601    if isinstance(stringlist,list):
602
603       for item in stringlist:
604          # If an item in the list isn't a string then False
605          if not isinstance(item,str):
606             return False
607       else:
608          # It's a list of strings so True
609          return True
610    else:
611       # Not a list so false
612       return False
613
614