slicerNetwork.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. import qt
  2. import urllib2
  3. import socket
  4. import ssl
  5. import cookielib
  6. import xml.etree.ElementTree as ET
  7. import re
  8. import StringIO
  9. import slicer
  10. import shutil
  11. import distutils
  12. import os
  13. import base64
  14. import json
  15. #see https://gist.github.com/logic/2715756, allow requests to do Put
  16. class MethodRequest(urllib2.Request):
  17. def __init__(self, *args, **kwargs):
  18. if 'method' in kwargs:
  19. self._method = kwargs['method']
  20. del kwargs['method']
  21. else:
  22. self._method = None
  23. return urllib2.Request.__init__(self, *args, **kwargs)
  24. def get_method(self, *args, **kwargs):
  25. if self._method is not None:
  26. return self._method
  27. return urllib2.Request.get_method(self, *args, **kwargs)
  28. class ConnectionError(Exception):
  29. def __init__(self,message):
  30. self.message=message
  31. class slicerNetwork(slicer.ScriptedLoadableModule.ScriptedLoadableModule):
  32. def __init__(self, parent):
  33. slicer.ScriptedLoadableModule.ScriptedLoadableModule.__init__(self,parent)
  34. self.parent.title="slicerNetwork"
  35. pass
  36. class labkeyURIHandler(slicer.vtkURIHandler):
  37. def __init__(self):
  38. slicer.vtkURIHandler.__init__(self)
  39. self.className="labkeyURIHandler"
  40. slicer.mrmlScene.AddURIHandler(self)
  41. try:
  42. fhome=os.environ["HOME"]
  43. except:
  44. #in windows, the variable is called HOMEPATH
  45. fhome=os.environ['HOMEDRIVE']+os.environ['HOMEPATH']
  46. self.localCacheDirectory=os.path.join(fhome,"labkeyCache")
  47. self.configDir=os.path.join(fhome,".labkey")
  48. self.mode="http"
  49. #try initializing network from default config file, if found
  50. self.initFromConfig()
  51. def CanHandleURI(self,uri):
  52. print("labkeyURIHandler::CanHandleURI({0})").format(uri)
  53. if uri.find('labkey://')==0:
  54. return 1
  55. return 0
  56. def GetClassName(self):
  57. return self.className
  58. def GetHostName(self):
  59. return self.hostname
  60. def SetHostName(self,host):
  61. self.hostname=host
  62. def SetLocalCahceDirectory(self,dir):
  63. self.localCacheDirectory=dir
  64. def GetLocalCacheDirectory(self):
  65. return self.localCacheDirectory
  66. def GetLabkeyUrl(self):
  67. return self.hostname+"/labkey"
  68. def GetLabkeyWebdavUrl(self):
  69. return self.GetLabkeyUrl()+"/_webdav"
  70. #configuration part
  71. def configureSSL(self,cert,key,pwd,cacert):
  72. #do this first
  73. try:
  74. self.ctx=ssl.SSLContext(ssl.PROTOCOL_SSLv23)
  75. self.ctx.load_cert_chain(cert,keyfile=key,password=pwd)
  76. self.ctx.verify_mode=ssl.CERT_REQUIRED
  77. self.ctx.load_verify_locations(cacert)
  78. except ssl.SSLError as err:
  79. print(" Failed to configure SSL: {0}").format(str(err))
  80. self.mode="https"
  81. def initRemote(self):
  82. if self.mode=="https":
  83. http_handler=urllib2.HTTPSHandler(context=self.ctx)
  84. if self.mode=="http":
  85. http_handler=urllib2.HTTPHandler()
  86. #cookie part
  87. cj=cookielib.CookieJar()
  88. cookie_handler=urllib2.HTTPCookieProcessor(cj)
  89. self.opener=urllib2.build_opener(http_handler,cookie_handler)
  90. def initFromConfig(self):
  91. path=os.path.join(self.configDir,"Remote.json")
  92. try:
  93. self.parseConfig(path)
  94. except OSError:
  95. return
  96. self.initRemote()
  97. def parseConfig(self,fname):
  98. try:
  99. f=open(fname)
  100. except OSError as e:
  101. print("Confgiuration error: OS error({0}): {1}").format(e.errno, e.strerror)
  102. raise
  103. dt=json.load(f)
  104. self.mode="http"
  105. if dt.has_key('SSL'):
  106. self.configureSSL(
  107. dt['SSL']['user'],
  108. dt['SSL']['key'],
  109. dt['SSL']['keyPwd'],
  110. dt['SSL']['ca']
  111. )
  112. self.hostname=dt['host']
  113. self.auth_name=dt['labkey']['user']
  114. self.auth_pass=dt['labkey']['password']
  115. #path convention
  116. #localPath is a path on local filesystem. It means it has to adhere to OS
  117. #policy, especially in separators
  118. #relativePath is just the portion of the path that is equal both locally
  119. #and remotely. In relativePath, separator is according to http convention,
  120. #which equals separator in osx and *nix
  121. #labkeyPath or remotePath is the full URL to the resource,
  122. #including http-like header and all intermediate portions
  123. #the following functions convert between different notations
  124. #was GetLocalPath
  125. def GetLocalPathFromRelativePath(self,relativePath):
  126. debug=False
  127. relativePath=re.sub('labkey://','',relativePath)
  128. sp=os.sep.encode('string-escape')
  129. if debug:
  130. print("Substituting / with {0} in {1}").format(sp,relativePath)
  131. relativePath=re.sub('/',sp,relativePath)
  132. return os.path.join(self.GetLocalCacheDirectory(),relativePath)
  133. #was GetRemotePath
  134. def GetLabkeyPathFromRelativePath(self,source):
  135. return self.GetLabkeyWebdavUrl()+"/"+source
  136. #was GetLabkeyPathFromLocalPath
  137. def GetRelativePathFromLocalPath(self,f):
  138. debug=False
  139. #report it with URL separator, forward-slash
  140. if f.find(self.localCacheDirectory)<-1:
  141. print("Localpath misformation. Exiting")
  142. return "NULL"
  143. f0=re.sub('\\\\','\\\\\\\\',self.localCacheDirectory)
  144. relativePath=re.sub(re.compile(f0),'',f)
  145. if debug:
  146. print("[SEP] Relative path {}").format(relativePath)
  147. #leaves /ContextPath/%40files/subdirectory_list/files
  148. #remove first separator
  149. sp=os.path.sep
  150. f1=re.sub('\\\\','\\\\\\\\',sp)
  151. if relativePath[0]==sp:
  152. relativePath=relativePath[1:len(relativePath)]
  153. if debug:
  154. print("[SLASH] Relative path {}").format(relativePath)
  155. return re.sub(f1,'/',relativePath)
  156. #was GetLabkeyPathFromRemotePath
  157. def GetRelativePathFromLabkeyPath(self,f):
  158. #used to query labkey stuff, so URL separator is used
  159. #in old conventions, labkey://was used to tag a labkeyPath
  160. f=re.sub('labkey://','',f)
  161. f=re.sub(self.GetLabkeyWebdavUrl(),'',f)
  162. if f[0]=='/':
  163. f=f[1:len(f)]
  164. return f;
  165. #standard HTTP
  166. def get(self,url):
  167. debug=False
  168. if debug:
  169. print("GET: {0}").format(url)
  170. print("as {0}").format(self.auth_name)
  171. r=urllib2.Request(url)
  172. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  173. r.add_header("Authorization", "Basic %s" % base64string)
  174. try:
  175. return self.opener.open(r)
  176. #f contains json as a return value
  177. except urllib2.HTTPError as e:
  178. print e.code
  179. print e.read()
  180. return e
  181. def post(self,url,data):
  182. debug=False
  183. r=urllib2.Request(url)
  184. #makes it a post
  185. r.add_data(data)
  186. r.add_header("Content-Type","application/json")
  187. #add csrf
  188. csrf=self.getCSRF()
  189. r.add_header("X-LABKEY-CSRF",csrf)
  190. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  191. r.add_header("Authorization", "Basic %s" % base64string)
  192. if debug:
  193. print("{}: {}").format(r.get_method(),r.get_full_url())
  194. print("data: {}").format(r.get_data())
  195. print("Content-Type: {}").format(r.get_header('Content-Type'))
  196. try:
  197. return self.opener.open(r)
  198. except urllib2.HTTPError as e:
  199. print e.code
  200. print e.read()
  201. return e
  202. #f contains json as a return value
  203. def put(self,url,data):
  204. debug=False
  205. if debug:
  206. print("PUT: {}").format(url)
  207. r=MethodRequest(url.encode('utf-8'),method="PUT")
  208. #makes it a post
  209. r.add_data(data)
  210. print("PUT: data size: {}").format(len(data))
  211. r.add_header("content-type","application/octet-stream")
  212. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  213. r.add_header("Authorization", "Basic %s" % base64string)
  214. return self.opener.open(r)
  215. #f contains json as a return value
  216. def getCSRF(self):
  217. url=self.GetLabkeyUrl()+'/login/whoAmI.view'
  218. jsonData=json.load(self.get(url))
  219. return jsonData["CSRF"]
  220. #file manipulation routiens
  221. #was GetFile
  222. def DownloadFileToCache(self,relativePath):
  223. debug=False
  224. # check for file in cache. If available, use, if not, download
  225. localPath=self.GetLocalPathFromRelativePath(relativePath)
  226. if os.path.isfile(localPath):
  227. if debug:
  228. print("Returning localFile {}".format(localPath))
  229. return localPath
  230. if debug:
  231. print("labkeyURIHandler::DownloadFileToCache({0}->{1})").format(relativePath,localPath)
  232. #make all necessary directories LOCALLY
  233. path=os.path.dirname(localPath)
  234. if debug:
  235. print("Target directory: '{}''".format(path))
  236. if not os.path.isdir(path):
  237. os.makedirs(path)
  238. localBuffer=open(localPath,'wb')
  239. #make sure we are at the begining of the file
  240. #labkeyPath=self.GetLabkeyPathFromRelativePath(relativePath)
  241. remoteBuffer=self.readFileToBuffer(relativePath)
  242. #check file size
  243. if debug:
  244. remoteBuffer.seek(0,2)
  245. sz=remoteBuffer.tell()
  246. print("Remote size: {0}").format(sz)
  247. remoteBuffer.seek(0)
  248. shutil.copyfileobj(remoteBuffer,localBuffer)
  249. if debug:
  250. print("Local size: {0}").format(localBuffer.tell())
  251. localBuffer.close()
  252. return localPath
  253. def fileTypesAvailable(self):
  254. return ('VolumeFile','SegmentationFile','TransformFile')
  255. #mimic slicer.util.loadNodeFromFile
  256. def loadNode(self, relativeName, filetype, properties={},returnNode=False):
  257. #this is the only relevant part - file must be downloaded to cache
  258. #labkeyName is just the relative part (from labkey onwards)
  259. localPath=self.DownloadFileToCache(relativeName)
  260. print localPath
  261. if not returnNode:
  262. slicer.util.loadNodeFromFile(localPath,filetype,properties,returnNode=False)
  263. return
  264. ok,node=slicer.util.loadNodeFromFile(localPath,filetype,properties,returnNode=True)
  265. if ok:
  266. return node
  267. return None
  268. # #remove retrieved file
  269. def storeNode(self,node,project,dir=None):
  270. removeFile=True
  271. relativePath=project+'/%40files'
  272. if not dir==None:
  273. relativePath+='/'+dir
  274. labkeyPath=self.GetLabkeyPathFromRelativePath(relativePath)
  275. print ("Remote: {}").format(labkeyPath)
  276. #checks if exists
  277. self.mkdir(labkeyPath)
  278. localPath=self.GetLocalPathFromRelativePath(relativePath)
  279. localPath.replace('/',os.path.sep)
  280. nodeName=node.GetName()
  281. suffix=".nrrd"
  282. if node.__class__.__name__=="vtkMRMLDoubleArrayNode":
  283. suffix=".mcsv"
  284. if node.__class__.__name__=="vtkMRMLTransformNode":
  285. suffix=".h5"
  286. fileName=nodeName+suffix
  287. file=os.path.join(localPath,fileName)
  288. slicer.util.saveNode(node,file)
  289. print("Stored to: {}").format(file)
  290. f=open(file,"rb")
  291. f.seek(0,2)
  292. sz=f.tell()
  293. print("File size in memory: {0}").format(sz)
  294. f.seek(0)
  295. localBuffer=f.read()
  296. print("Local buffer size: {}").format(len(localBuffer))
  297. remoteFile=labkeyPath+'/'+fileName
  298. resp=self.put(remoteFile,localBuffer)
  299. print(resp.read())
  300. f.close()
  301. if removeFile:
  302. os.remove(file)
  303. def remoteDirExists(self,url):
  304. status,dirs=self.listRemoteDir(url);
  305. return status
  306. def mkdir(self,remoteDir):
  307. if self.remoteDirExists(remoteDir):
  308. return False
  309. r=MethodRequest(remoteDir,method="MKCOL")
  310. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  311. r.add_header("Authorization", "Basic %s" % base64string)
  312. try:
  313. f=self.opener.open(r)
  314. except:
  315. print("Error: Failed MKCOL {}").format(remoteDir)
  316. return False
  317. return True
  318. def mkdirs(self,remoteDir):
  319. relativePath=self.GetRelativePathFromLabkeyPath(remoteDir)
  320. s=0
  321. while True:
  322. s1=relativePath.find('/',s)
  323. if s1<0:
  324. break
  325. path=relativePath[0:s1]
  326. remotePath=self.GetLabkeyPathFromRelativePath(relativePath)
  327. dirExists=self.remoteDirExists(remotePath)
  328. if not dirExists:
  329. if not self.mkdir(remotePath):
  330. return False
  331. s=s1+1
  332. return self.mkdir(remoteDir)
  333. def rmdir(self,remoteDir):
  334. if not self.remoteDirExists(remoteDir):
  335. return True
  336. r=MethodRequest(remoteDir,method="DELETE")
  337. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  338. r.add_header("Authorization", "Basic %s" % base64string)
  339. try:
  340. f=self.opener.open(r)
  341. except:
  342. print("Error: Failed DELETE {}").format(remoteDir)
  343. return False
  344. return True
  345. #was listDir
  346. def listRelativeDir(self,relativeDir):
  347. print("Listing for {0}").format(relativeDir)
  348. dirUrl=self.GetLabkeyPathFromRelativePath(relativeDir)
  349. status,dirs=self.listRemoteDir(dirUrl)
  350. dirs=[self.GetRelativePathFromLabkeyPath(d) for d in dirs];
  351. return dirs
  352. #was isDir
  353. def isRemoteDir(self, remotePath):
  354. #print "isDir: {}".format(remotePath)
  355. r=MethodRequest(remotePath,method="PROPFIND")
  356. PROPFIND=u"""<?xml version="1.0" encoding="utf-8"?>\n
  357. <a:propfind xmlns:a="DAV:">\n
  358. <a:prop>\n
  359. <a:resourcetype/>\n
  360. </a:prop>\n
  361. </a:propfind>"""
  362. r.add_header('content-type','text/xml; charset="utf-8"')
  363. r.add_header('content-length',str(len(PROPFIND)))
  364. r.add_header('Depth','0')
  365. r.add_data(PROPFIND)
  366. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  367. r.add_header("Authorization", "Basic %s" % base64string)
  368. print("PROPFIND: {0}").format(remotePath)
  369. try:
  370. f=self.opener.open(r)
  371. except:
  372. return False
  373. tree=ET.XML(f.read())
  374. try:
  375. rps=tree.find('{DAV:}response').find('{DAV:}propstat')
  376. rps=rps.find('{DAV:}prop')
  377. rps=rps.find('{DAV:}resourcetype').find('{DAV:}collection')
  378. if rps != None:
  379. return True
  380. else:
  381. return False
  382. except:
  383. return False
  384. def listRemoteDir(self,dirUrl):
  385. #input is remoteDir, result are remoteDirs
  386. r=MethodRequest(dirUrl,method="PROPFIND")
  387. PROPFIND=u"""<?xml version="1.0" encoding="utf-8"?>\n
  388. <propfind xmlns="DAV:">\n
  389. <prop>\n
  390. <getetag/>\n
  391. </prop>\n
  392. </propfind>"""
  393. r.add_header('content-type','text/xml; charset="utf-8"')
  394. r.add_header('content-length',str(len(PROPFIND)))
  395. r.add_header('Depth','1')
  396. r.add_data(PROPFIND)
  397. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  398. r.add_header("Authorization", "Basic %s" % base64string)
  399. print("PROPFIND: {0}").format(dirUrl)
  400. dirs=[]
  401. try:
  402. f=self.opener.open(r)
  403. except:
  404. return False,dirs
  405. tree=ET.XML(f.read())
  406. rps=tree.findall('{DAV:}response')
  407. for r in rps:
  408. hr=r.find('{DAV:}href')
  409. dirent=hr.text
  410. #dirent=re.sub('/labkey/_webdav/','',dirent)
  411. dirent=self.GetHostName()+dirent
  412. dirs.append(dirent)
  413. del dirs[0]
  414. return True,dirs
  415. def toRelativePath(self,dirs):
  416. flist1=[]
  417. for d in dirs:
  418. if d[-1]=='/':
  419. d=d[:-1]
  420. if d.rfind('/')>-1:
  421. d=d[d.rfind('/')+1:]
  422. flist1.append(d)
  423. return flist1
  424. def readFileToBuffer(self, relativePath):
  425. dirUrl=self.GetLabkeyPathFromRelativePath(relativePath)
  426. f=self.get(dirUrl)
  427. return StringIO.StringIO(f.read())
  428. def uploadFile(self,localPath):
  429. #get upstream directories sorted out
  430. relativePath=self.GetRelativePathFromLocalPath(localPath)
  431. if relativePath=="NULL":
  432. errorCode="Failed to upload {}. Potential incorrect location"
  433. errorCode+=". Should be in labkeyCache!"
  434. print(errorCode.format(relativePath))
  435. return False
  436. relativeDir=relativePath[0:labkeyPath.rfind('/')]
  437. remoteDir=self.GetLabkeyPathFromRemotePath(relativeDir)
  438. if not self.remoteDirExists(remoteDir):
  439. if not self.mkdirs(remoteDir):
  440. errorCode="UploadFile: Could not create directory {}"
  441. print(errorCode.format(remoteDir))
  442. return False
  443. #make an URL request
  444. with open(localPath, 'rb') as f:
  445. data=f.read()
  446. remotePath=self.GetLabkeyPathFromRelativePath(relativePath)
  447. self.put(remotePath,data)
  448. #was copyFileToRemote
  449. def copyLocalFileToRemote(self,localPath, remotePath):
  450. #get upstream directories sorted out
  451. remoteDir=os.path.dirname(remotePath)
  452. if not self.remoteDirExists(remoteDir):
  453. if not self.mkdirs(remoteDir):
  454. errorCode="UploadFile: Could not create directory {}"
  455. print(errorCode.format(remoteDir))
  456. return False
  457. #make an URL request
  458. if (self.isRemoteDir(remotePath)):
  459. remotePath=remotePath+'/'+os.path.basename(localPath)
  460. with open(localPath, 'rb') as f:
  461. data=f.read()
  462. itry=0
  463. while itry<10:
  464. try:
  465. self.put(remotePath,data)
  466. break
  467. except (socket.error,urllib2.URLError):
  468. print("[{}] Failed to execute put(), retry".format(itry))
  469. itry=itry+1
  470. if itry==10:
  471. raise ConnectionError('Could not copy file, number of tries exceeded')
  472. #was loadDir
  473. def DownloadDirToCache(self, relativePath):
  474. files=self.listRelativeDir(relativePath)
  475. fdir="NONE"
  476. for f in files:
  477. #f is local path
  478. try:
  479. localDir=os.path.dirname(self.DownloadFileToCache(f))
  480. except:
  481. #fails if there is a subdirectory; go recursively
  482. print("self.readDir(f) not implemented")
  483. return localDir
  484. #database routines
  485. def loadDataset(self,project,dataset):
  486. url=self.GetLabkeyUrl()+'/'+project
  487. url+='/query-selectRows.api?schemaName=study&query.queryName='+dataset
  488. return json.load(self.get(url))
  489. def filterDataset(self,project,dataset,filter):
  490. debug=False
  491. url=self.GetLabkeyUrl()+'/'+project
  492. url+='/query-selectRows.api?schemaName=study&query.queryName='+dataset
  493. for f in filter:
  494. url+="&query."+f['variable']+"~"+f['oper']+"="+f['value']
  495. if debug:
  496. print("Sending {}").format(url)
  497. return json.load(self.get(url))
  498. def modifyDataset(self,method,project,dataset,rows):
  499. #method can be insert or update
  500. data={}
  501. data['schemaName']='study'
  502. data['queryName']=dataset
  503. data['rows']=rows
  504. url=self.GetLabkeyUrl()+'/'+project
  505. url+='/query-'+method+'Rows.api?'
  506. return self.post(url,json.dumps(data))
  507. def filterList(self,project,dataset,filter):
  508. schemaName='lists'
  509. debug=False
  510. url=self.GetLabkeyUrl()+'/'+project
  511. url+='/query-selectRows.api?schemaName='+schemaName+'&query.queryName='+dataset
  512. for f in filter:
  513. url+="&query."+f['variable']+"~"+f['oper']+"="+f['value']
  514. if debug:
  515. print("Sending {}").format(url)
  516. return json.load(self.get(url))
  517. def modifyList(self,method,project,dataset,rows):
  518. #method can be insert or update
  519. data={}
  520. schemaName='lists'
  521. data['schemaName']=schemaName
  522. data['queryName']=dataset
  523. data['rows']=rows
  524. url=self.GetLabkeyUrl()+'/'+project
  525. url+='/query-'+method+'Rows.api?'
  526. return self.post(url,json.dumps(data))