0
|
1 #!/usr/bin/python
|
|
2
|
1
|
3 # Orthanc - A Lightweight, RESTful DICOM Store
|
|
4 # Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
|
|
5 # Department, University Hospital of Liege, Belgium
|
|
6 #
|
|
7 # This program is free software: you can redistribute it and/or
|
|
8 # modify it under the terms of the GNU General Public License as
|
|
9 # published by the Free Software Foundation, either version 3 of the
|
|
10 # License, or (at your option) any later version.
|
|
11 #
|
|
12 # This program is distributed in the hope that it will be useful, but
|
|
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
15 # General Public License for more details.
|
|
16 #
|
|
17 # You should have received a copy of the GNU General Public License
|
|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
|
0
|
19
|
|
20
|
|
21 import hashlib
|
|
22 import httplib2
|
|
23 import json
|
4
|
24 import os
|
1
|
25 import re
|
4
|
26 import signal
|
1
|
27 import subprocess
|
4
|
28 import threading
|
0
|
29 import time
|
1
|
30 import zipfile
|
0
|
31
|
3
|
32 from PIL import Image
|
|
33 from urllib import urlencode
|
|
34
|
0
|
35
|
1
|
36 HERE = os.path.dirname(__file__)
|
|
37
|
0
|
38
|
|
39 # http://stackoverflow.com/a/1313868/881731
|
|
40 try:
|
|
41 from cStringIO import StringIO
|
|
42 except:
|
|
43 from StringIO import StringIO
|
|
44
|
|
45
|
1
|
46 def DefineOrthanc(url = 'http://localhost:8042',
|
|
47 username = None,
|
|
48 password = None,
|
|
49 aet = 'ORTHANC',
|
|
50 dicomPort = 4242):
|
|
51 m = re.match(r'(http|https)://([^:]+):([^@]+)@([^@]+)', url)
|
|
52 if m != None:
|
|
53 url = m.groups()[0] + '://' + m.groups()[3]
|
|
54 username = m.groups()[1]
|
|
55 password = m.groups()[2]
|
0
|
56
|
|
57 if not url.endswith('/'):
|
|
58 url += '/'
|
|
59
|
1
|
60 return {
|
|
61 'Url' : url,
|
|
62 'Username' : username,
|
|
63 'Password' : password,
|
|
64 'DicomAet' : aet,
|
|
65 'DicomPort' : dicomPort
|
|
66 }
|
0
|
67
|
|
68
|
|
69 def _SetupCredentials(orthanc, http):
|
1
|
70 if (orthanc['Username'] != None and
|
|
71 orthanc['Password'] != None):
|
|
72 http.add_credentials(orthanc['Username'], orthanc['Password'])
|
0
|
73
|
|
74
|
|
75 def DoGet(orthanc, uri, data = {}, body = None, headers = {}):
|
|
76 d = ''
|
|
77 if len(data.keys()) > 0:
|
|
78 d = '?' + urlencode(data)
|
|
79
|
|
80 http = httplib2.Http()
|
|
81 _SetupCredentials(orthanc, http)
|
|
82
|
1
|
83 resp, content = http.request(orthanc['Url'] + uri + d, 'GET', body = body,
|
0
|
84 headers = headers)
|
|
85 if not (resp.status in [ 200 ]):
|
|
86 raise Exception(resp.status)
|
|
87 else:
|
|
88 try:
|
|
89 return json.loads(content)
|
|
90 except:
|
|
91 return content
|
|
92
|
|
93 def _DoPutOrPost(orthanc, uri, method, data, contentType, headers):
|
|
94 http = httplib2.Http()
|
|
95 _SetupCredentials(orthanc, http)
|
|
96
|
|
97 if isinstance(data, str):
|
|
98 body = data
|
|
99 if len(contentType) != 0:
|
|
100 headers['content-type'] = contentType
|
|
101 else:
|
|
102 body = json.dumps(data)
|
|
103 headers['content-type'] = 'application/json'
|
|
104
|
|
105 headers['expect'] = ''
|
|
106
|
1
|
107 resp, content = http.request(orthanc['Url'] + uri, method,
|
0
|
108 body = body,
|
|
109 headers = headers)
|
|
110 if not (resp.status in [ 200, 302 ]):
|
|
111 raise Exception(resp.status)
|
|
112 else:
|
|
113 try:
|
|
114 return json.loads(content)
|
|
115 except:
|
|
116 return content
|
|
117
|
|
118 def DoDelete(orthanc, uri):
|
|
119 http = httplib2.Http()
|
|
120 _SetupCredentials(orthanc, http)
|
|
121
|
1
|
122 resp, content = http.request(orthanc['Url'] + uri, 'DELETE')
|
0
|
123 if not (resp.status in [ 200 ]):
|
|
124 raise Exception(resp.status)
|
|
125 else:
|
|
126 try:
|
|
127 return json.loads(content)
|
|
128 except:
|
|
129 return content
|
|
130
|
|
131 def DoPut(orthanc, uri, data = {}, contentType = ''):
|
|
132 return DoPutOrPost(orthanc, uri, 'PUT', data, contentType)
|
|
133
|
|
134 def DoPost(orthanc, uri, data = {}, contentType = '', headers = {}):
|
|
135 return _DoPutOrPost(orthanc, uri, 'POST', data, contentType, headers)
|
|
136
|
|
137 def UploadInstance(orthanc, filename):
|
1
|
138 global HERE
|
3
|
139 p = os.path.join(HERE, '..', 'Database', filename)
|
0
|
140 f = open(p, 'rb')
|
|
141 d = f.read()
|
|
142 f.close()
|
|
143 return DoPost(orthanc, '/instances', d, 'application/dicom')
|
|
144
|
|
145 def UploadFolder(orthanc, path):
|
1
|
146 global HERE
|
6
|
147 p = os.path.join(HERE, '..', 'Database', path)
|
1
|
148 for i in os.listdir(p):
|
|
149 try:
|
|
150 UploadInstance(orthanc, os.path.join(path, i))
|
|
151 except:
|
|
152 pass
|
0
|
153
|
|
154 def DropOrthanc(orthanc):
|
|
155 # Reset the Lua callbacks
|
|
156 DoPost(orthanc, '/tools/execute-script', 'function OnStoredInstance(instanceId, tags, metadata) end', 'application/lua')
|
|
157
|
|
158 DoDelete(orthanc, '/exports')
|
|
159
|
|
160 for s in DoGet(orthanc, '/patients'):
|
|
161 DoDelete(orthanc, '/patients/%s' % s)
|
|
162
|
|
163 def ComputeMD5(data):
|
|
164 m = hashlib.md5()
|
|
165 m.update(data)
|
|
166 return m.hexdigest()
|
|
167
|
|
168 def GetImage(orthanc, uri):
|
|
169 # http://www.pythonware.com/library/pil/handbook/introduction.htm
|
|
170 data = DoGet(orthanc, uri)
|
|
171 return Image.open(StringIO(data))
|
|
172
|
|
173 def GetArchive(orthanc, uri):
|
|
174 # http://stackoverflow.com/a/1313868/881731
|
|
175 s = DoGet(orthanc, uri)
|
|
176 return zipfile.ZipFile(StringIO(s), "r")
|
|
177
|
1
|
178 def IsDefinedInLua(orthanc, name):
|
0
|
179 s = DoPost(orthanc, '/tools/execute-script', 'print(type(%s))' % name, 'application/lua')
|
|
180 return (s.strip() != 'nil')
|
|
181
|
1
|
182 def WaitEmpty(orthanc):
|
0
|
183 while True:
|
1
|
184 if len(DoGet(orthanc, '/instances')) == 0:
|
0
|
185 return
|
|
186 time.sleep(0.1)
|
|
187
|
1
|
188 def GetDockerHostAddress():
|
|
189 route = subprocess.check_output([ '/sbin/ip', 'route' ])
|
|
190 m = re.search(r'default via ([0-9.]+)', route)
|
|
191 if m == None:
|
|
192 return 'localhost'
|
|
193 else:
|
|
194 return m.groups()[0]
|
4
|
195
|
|
196
|
|
197
|
|
198
|
|
199 class ExternalCommandThread:
|
|
200 @staticmethod
|
|
201 def ExternalCommandFunction(arg, stop_event, command, env):
|
|
202 external = subprocess.Popen(command, env = env)
|
|
203
|
|
204 while (not stop_event.is_set()):
|
|
205 error = external.poll()
|
|
206 if error != None:
|
|
207 # http://stackoverflow.com/a/1489838/881731
|
|
208 os._exit(-1)
|
|
209 stop_event.wait(0.1)
|
|
210
|
|
211 print 'Stopping the external command'
|
|
212 external.terminate()
|
9
|
213 external.communicate() # Wait for the command to stop
|
4
|
214
|
|
215 def __init__(self, command, env = None):
|
|
216 self.thread_stop = threading.Event()
|
|
217 self.thread = threading.Thread(target = self.ExternalCommandFunction,
|
|
218 args = (10, self.thread_stop, command, env))
|
9
|
219 #self.daemon = True
|
4
|
220 self.thread.start()
|
|
221
|
|
222 def stop(self):
|
|
223 self.thread_stop.set()
|
|
224 self.thread.join()
|