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
|
|
36 # http://stackoverflow.com/a/1313868/881731
|
|
37 try:
|
|
38 from cStringIO import StringIO
|
|
39 except:
|
|
40 from StringIO import StringIO
|
|
41
|
|
42
|
13
|
43 def DefineOrthanc(server = 'localhost',
|
|
44 restPort = 8042,
|
1
|
45 username = None,
|
|
46 password = None,
|
|
47 aet = 'ORTHANC',
|
|
48 dicomPort = 4242):
|
13
|
49 #m = re.match(r'(http|https)://([^:]+):([^@]+)@([^@]+)', url)
|
|
50 #if m != None:
|
|
51 # url = m.groups()[0] + '://' + m.groups()[3]
|
|
52 # username = m.groups()[1]
|
|
53 # password = m.groups()[2]
|
0
|
54
|
13
|
55 #if not url.endswith('/'):
|
|
56 # url += '/'
|
0
|
57
|
1
|
58 return {
|
13
|
59 'Server' : server,
|
|
60 'Url' : 'http://%s:%d/' % (server, restPort),
|
1
|
61 'Username' : username,
|
|
62 'Password' : password,
|
|
63 'DicomAet' : aet,
|
|
64 'DicomPort' : dicomPort
|
|
65 }
|
0
|
66
|
|
67
|
|
68 def _SetupCredentials(orthanc, http):
|
1
|
69 if (orthanc['Username'] != None and
|
|
70 orthanc['Password'] != None):
|
|
71 http.add_credentials(orthanc['Username'], orthanc['Password'])
|
0
|
72
|
|
73
|
|
74 def DoGet(orthanc, uri, data = {}, body = None, headers = {}):
|
|
75 d = ''
|
|
76 if len(data.keys()) > 0:
|
|
77 d = '?' + urlencode(data)
|
|
78
|
|
79 http = httplib2.Http()
|
|
80 _SetupCredentials(orthanc, http)
|
|
81
|
1
|
82 resp, content = http.request(orthanc['Url'] + uri + d, 'GET', body = body,
|
0
|
83 headers = headers)
|
|
84 if not (resp.status in [ 200 ]):
|
|
85 raise Exception(resp.status)
|
|
86 else:
|
|
87 try:
|
|
88 return json.loads(content)
|
|
89 except:
|
|
90 return content
|
|
91
|
|
92 def _DoPutOrPost(orthanc, uri, method, data, contentType, headers):
|
|
93 http = httplib2.Http()
|
|
94 _SetupCredentials(orthanc, http)
|
|
95
|
|
96 if isinstance(data, str):
|
|
97 body = data
|
|
98 if len(contentType) != 0:
|
|
99 headers['content-type'] = contentType
|
|
100 else:
|
|
101 body = json.dumps(data)
|
|
102 headers['content-type'] = 'application/json'
|
|
103
|
|
104 headers['expect'] = ''
|
|
105
|
1
|
106 resp, content = http.request(orthanc['Url'] + uri, method,
|
0
|
107 body = body,
|
|
108 headers = headers)
|
|
109 if not (resp.status in [ 200, 302 ]):
|
|
110 raise Exception(resp.status)
|
|
111 else:
|
|
112 try:
|
|
113 return json.loads(content)
|
|
114 except:
|
|
115 return content
|
|
116
|
|
117 def DoDelete(orthanc, uri):
|
|
118 http = httplib2.Http()
|
|
119 _SetupCredentials(orthanc, http)
|
|
120
|
1
|
121 resp, content = http.request(orthanc['Url'] + uri, 'DELETE')
|
0
|
122 if not (resp.status in [ 200 ]):
|
|
123 raise Exception(resp.status)
|
|
124 else:
|
|
125 try:
|
|
126 return json.loads(content)
|
|
127 except:
|
|
128 return content
|
|
129
|
|
130 def DoPut(orthanc, uri, data = {}, contentType = ''):
|
10
|
131 return _DoPutOrPost(orthanc, uri, 'PUT', data, contentType, {})
|
0
|
132
|
|
133 def DoPost(orthanc, uri, data = {}, contentType = '', headers = {}):
|
|
134 return _DoPutOrPost(orthanc, uri, 'POST', data, contentType, headers)
|
|
135
|
13
|
136 def GetDatabasePath(filename):
|
|
137 return os.path.join(os.path.dirname(__file__), '..', 'Database', filename)
|
|
138
|
0
|
139 def UploadInstance(orthanc, filename):
|
13
|
140 f = open(GetDatabasePath(filename), 'rb')
|
0
|
141 d = f.read()
|
|
142 f.close()
|
|
143 return DoPost(orthanc, '/instances', d, 'application/dicom')
|
|
144
|
|
145 def UploadFolder(orthanc, path):
|
13
|
146 for i in os.listdir(GetDatabasePath(path)):
|
1
|
147 try:
|
|
148 UploadInstance(orthanc, os.path.join(path, i))
|
|
149 except:
|
|
150 pass
|
0
|
151
|
|
152 def DropOrthanc(orthanc):
|
|
153 # Reset the Lua callbacks
|
|
154 DoPost(orthanc, '/tools/execute-script', 'function OnStoredInstance(instanceId, tags, metadata) end', 'application/lua')
|
|
155
|
|
156 DoDelete(orthanc, '/exports')
|
|
157
|
|
158 for s in DoGet(orthanc, '/patients'):
|
|
159 DoDelete(orthanc, '/patients/%s' % s)
|
|
160
|
|
161 def ComputeMD5(data):
|
|
162 m = hashlib.md5()
|
|
163 m.update(data)
|
|
164 return m.hexdigest()
|
|
165
|
|
166 def GetImage(orthanc, uri):
|
|
167 # http://www.pythonware.com/library/pil/handbook/introduction.htm
|
|
168 data = DoGet(orthanc, uri)
|
|
169 return Image.open(StringIO(data))
|
|
170
|
|
171 def GetArchive(orthanc, uri):
|
|
172 # http://stackoverflow.com/a/1313868/881731
|
|
173 s = DoGet(orthanc, uri)
|
|
174 return zipfile.ZipFile(StringIO(s), "r")
|
|
175
|
1
|
176 def IsDefinedInLua(orthanc, name):
|
0
|
177 s = DoPost(orthanc, '/tools/execute-script', 'print(type(%s))' % name, 'application/lua')
|
|
178 return (s.strip() != 'nil')
|
|
179
|
1
|
180 def WaitEmpty(orthanc):
|
0
|
181 while True:
|
1
|
182 if len(DoGet(orthanc, '/instances')) == 0:
|
0
|
183 return
|
|
184 time.sleep(0.1)
|
|
185
|
1
|
186 def GetDockerHostAddress():
|
|
187 route = subprocess.check_output([ '/sbin/ip', 'route' ])
|
|
188 m = re.search(r'default via ([0-9.]+)', route)
|
|
189 if m == None:
|
|
190 return 'localhost'
|
|
191 else:
|
|
192 return m.groups()[0]
|
4
|
193
|
|
194
|
|
195
|
|
196
|
|
197 class ExternalCommandThread:
|
|
198 @staticmethod
|
|
199 def ExternalCommandFunction(arg, stop_event, command, env):
|
|
200 external = subprocess.Popen(command, env = env)
|
|
201
|
|
202 while (not stop_event.is_set()):
|
|
203 error = external.poll()
|
|
204 if error != None:
|
|
205 # http://stackoverflow.com/a/1489838/881731
|
|
206 os._exit(-1)
|
|
207 stop_event.wait(0.1)
|
|
208
|
|
209 print 'Stopping the external command'
|
|
210 external.terminate()
|
9
|
211 external.communicate() # Wait for the command to stop
|
4
|
212
|
|
213 def __init__(self, command, env = None):
|
|
214 self.thread_stop = threading.Event()
|
|
215 self.thread = threading.Thread(target = self.ExternalCommandFunction,
|
|
216 args = (10, self.thread_stop, command, env))
|
9
|
217 #self.daemon = True
|
4
|
218 self.thread.start()
|
|
219
|
|
220 def stop(self):
|
|
221 self.thread_stop.set()
|
|
222 self.thread.join()
|