0
|
1 #!/usr/bin/env python
|
|
2 #Processes uploads from the user.
|
|
3
|
|
4 # WARNING: Changes in this tool (particularly as related to parsing) may need
|
|
5 # to be reflected in galaxy.web.controllers.tool_runner and galaxy.tools
|
|
6
|
|
7 import urllib, sys, os, gzip, tempfile, shutil, re, gzip, zipfile, codecs, binascii
|
|
8 from galaxy import eggs
|
|
9 # need to import model before sniff to resolve a circular import dependency
|
|
10 import galaxy.model
|
|
11 from galaxy.datatypes.checkers import *
|
|
12 from galaxy.datatypes import sniff
|
|
13 from galaxy.datatypes.binary import *
|
|
14 from galaxy.datatypes.images import Pdf
|
|
15 from galaxy.datatypes.registry import Registry
|
|
16 from galaxy import util
|
|
17 from galaxy.datatypes.util.image_util import *
|
|
18 from galaxy.util.json import *
|
|
19
|
|
20 try:
|
|
21 import Image as PIL
|
|
22 except ImportError:
|
|
23 try:
|
|
24 from PIL import Image as PIL
|
|
25 except:
|
|
26 PIL = None
|
|
27
|
|
28 try:
|
|
29 import bz2
|
|
30 except:
|
|
31 bz2 = None
|
|
32
|
|
33 assert sys.version_info[:2] >= ( 2, 4 )
|
|
34
|
|
35 def stop_err( msg, ret=1 ):
|
|
36 sys.stderr.write( msg )
|
|
37 sys.exit( ret )
|
|
38 def file_err( msg, dataset, json_file ):
|
5
|
39 json_file.write( to_json_string( dict( type = 'dataset',
|
0
|
40 ext = 'data',
|
|
41 dataset_id = dataset.dataset_id,
|
|
42 stderr = msg ) ) + "\n" )
|
|
43 # never remove a server-side upload
|
|
44 if dataset.type in ( 'server_dir', 'path_paste' ):
|
|
45 return
|
|
46 try:
|
|
47 os.remove( dataset.path )
|
|
48 except:
|
|
49 pass
|
|
50 def safe_dict(d):
|
|
51 """
|
|
52 Recursively clone json structure with UTF-8 dictionary keys
|
|
53 http://mellowmachines.com/blog/2009/06/exploding-dictionary-with-unicode-keys-as-python-arguments/
|
|
54 """
|
|
55 if isinstance(d, dict):
|
|
56 return dict([(k.encode('utf-8'), safe_dict(v)) for k,v in d.iteritems()])
|
|
57 elif isinstance(d, list):
|
|
58 return [safe_dict(x) for x in d]
|
|
59 else:
|
|
60 return d
|
|
61 def parse_outputs( args ):
|
|
62 rval = {}
|
|
63 for arg in args:
|
|
64 id, files_path, path = arg.split( ':', 2 )
|
|
65 rval[int( id )] = ( path, files_path )
|
|
66 return rval
|
|
67 def add_file( dataset, registry, json_file, output_path ):
|
|
68 data_type = None
|
|
69 line_count = None
|
|
70 converted_path = None
|
|
71 stdout = None
|
|
72 link_data_only = dataset.get( 'link_data_only', 'copy_files' )
|
|
73 in_place = dataset.get( 'in_place', True )
|
|
74
|
|
75 try:
|
|
76 ext = dataset.file_type
|
|
77 except AttributeError:
|
|
78 file_err( 'Unable to process uploaded file, missing file_type parameter.', dataset, json_file )
|
|
79 return
|
|
80
|
|
81 if dataset.type == 'url':
|
|
82 try:
|
|
83 page = urllib.urlopen( dataset.path ) #page will be .close()ed by sniff methods
|
|
84 temp_name, dataset.is_multi_byte = sniff.stream_to_file( page, prefix='url_paste', source_encoding=util.get_charset_from_http_headers( page.headers ) )
|
|
85 except Exception, e:
|
|
86 file_err( 'Unable to fetch %s\n%s' % ( dataset.path, str( e ) ), dataset, json_file )
|
|
87 return
|
|
88 dataset.path = temp_name
|
|
89 # See if we have an empty file
|
|
90 if not os.path.exists( dataset.path ):
|
|
91 file_err( 'Uploaded temporary file (%s) does not exist.' % dataset.path, dataset, json_file )
|
|
92 return
|
|
93 if not os.path.getsize( dataset.path ) > 0:
|
|
94 file_err( 'The uploaded file is empty', dataset, json_file )
|
|
95 return
|
|
96 if not dataset.type == 'url':
|
|
97 # Already set is_multi_byte above if type == 'url'
|
|
98 try:
|
|
99 dataset.is_multi_byte = util.is_multi_byte( codecs.open( dataset.path, 'r', 'utf-8' ).read( 100 ) )
|
|
100 except UnicodeDecodeError, e:
|
|
101 dataset.is_multi_byte = False
|
|
102 # Is dataset an image?
|
|
103 image = check_image( dataset.path )
|
|
104 if image:
|
|
105 if not PIL:
|
|
106 image = None
|
|
107 # get_image_ext() returns None if nor a supported Image type
|
|
108 ext = get_image_ext( dataset.path, image )
|
|
109 data_type = ext
|
|
110 # Is dataset content multi-byte?
|
|
111 elif dataset.is_multi_byte:
|
|
112 data_type = 'multi-byte char'
|
|
113 ext = sniff.guess_ext( dataset.path, is_multi_byte=True )
|
|
114 # Is dataset content supported sniffable binary?
|
|
115 else:
|
|
116 type_info = Binary.is_sniffable_binary( dataset.path )
|
|
117 if type_info:
|
|
118 data_type = type_info[0]
|
|
119 ext = type_info[1]
|
5
|
120 data_type = "compressed archive"
|
0
|
121 if not data_type:
|
5
|
122 # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress
|
|
123 is_gzipped, is_valid = check_gzip( dataset.path )
|
|
124 if is_gzipped and not is_valid:
|
|
125 file_err( 'The gzipped uploaded file contains inappropriate content', dataset, json_file )
|
|
126 return
|
|
127 elif is_gzipped and is_valid:
|
|
128 if link_data_only == 'copy_files':
|
|
129 # We need to uncompress the temp_name file, but BAM files must remain compressed in the BGZF format
|
|
130 CHUNK_SIZE = 2**20 # 1Mb
|
|
131 fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_gunzip_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False )
|
|
132 gzipped_file = gzip.GzipFile( dataset.path, 'rb' )
|
|
133 while 1:
|
|
134 try:
|
|
135 chunk = gzipped_file.read( CHUNK_SIZE )
|
|
136 except IOError:
|
|
137 os.close( fd )
|
|
138 os.remove( uncompressed )
|
|
139 file_err( 'Problem decompressing gzipped data', dataset, json_file )
|
|
140 return
|
|
141 if not chunk:
|
|
142 break
|
|
143 os.write( fd, chunk )
|
|
144 os.close( fd )
|
|
145 gzipped_file.close()
|
|
146 # Replace the gzipped file with the decompressed file if it's safe to do so
|
|
147 if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place:
|
|
148 dataset.path = uncompressed
|
|
149 else:
|
|
150 shutil.move( uncompressed, dataset.path )
|
|
151 os.chmod(dataset.path, 0644)
|
|
152 dataset.name = dataset.name.rstrip( '.gz' )
|
|
153 data_type = 'gzip'
|
|
154 if not data_type and bz2 is not None:
|
|
155 # See if we have a bz2 file, much like gzip
|
|
156 is_bzipped, is_valid = check_bz2( dataset.path )
|
|
157 if is_bzipped and not is_valid:
|
2
|
158 file_err( 'The gzipped uploaded file contains inappropriate content', dataset, json_file )
|
|
159 return
|
5
|
160 elif is_bzipped and is_valid:
|
2
|
161 if link_data_only == 'copy_files':
|
5
|
162 # We need to uncompress the temp_name file
|
|
163 CHUNK_SIZE = 2**20 # 1Mb
|
|
164 fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_bunzip2_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False )
|
|
165 bzipped_file = bz2.BZ2File( dataset.path, 'rb' )
|
2
|
166 while 1:
|
|
167 try:
|
5
|
168 chunk = bzipped_file.read( CHUNK_SIZE )
|
2
|
169 except IOError:
|
|
170 os.close( fd )
|
|
171 os.remove( uncompressed )
|
5
|
172 file_err( 'Problem decompressing bz2 compressed data', dataset, json_file )
|
2
|
173 return
|
|
174 if not chunk:
|
|
175 break
|
|
176 os.write( fd, chunk )
|
|
177 os.close( fd )
|
5
|
178 bzipped_file.close()
|
|
179 # Replace the bzipped file with the decompressed file if it's safe to do so
|
2
|
180 if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place:
|
|
181 dataset.path = uncompressed
|
|
182 else:
|
|
183 shutil.move( uncompressed, dataset.path )
|
|
184 os.chmod(dataset.path, 0644)
|
5
|
185 dataset.name = dataset.name.rstrip( '.bz2' )
|
|
186 data_type = 'bz2'
|
|
187 if not data_type:
|
|
188 # See if we have a zip archive
|
|
189 is_zipped = check_zip( dataset.path )
|
|
190 if is_zipped:
|
|
191 if link_data_only == 'copy_files':
|
|
192 CHUNK_SIZE = 2**20 # 1Mb
|
|
193 uncompressed = None
|
|
194 uncompressed_name = None
|
|
195 unzipped = False
|
|
196 z = zipfile.ZipFile( dataset.path )
|
|
197 for name in z.namelist():
|
|
198 if name.endswith('/'):
|
|
199 continue
|
|
200 if unzipped:
|
|
201 stdout = 'ZIP file contained more than one file, only the first file was added to Galaxy.'
|
|
202 break
|
|
203 fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_zip_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False )
|
|
204 if sys.version_info[:2] >= ( 2, 6 ):
|
|
205 zipped_file = z.open( name )
|
|
206 while 1:
|
|
207 try:
|
|
208 chunk = zipped_file.read( CHUNK_SIZE )
|
|
209 except IOError:
|
|
210 os.close( fd )
|
|
211 os.remove( uncompressed )
|
|
212 file_err( 'Problem decompressing zipped data', dataset, json_file )
|
|
213 return
|
|
214 if not chunk:
|
|
215 break
|
|
216 os.write( fd, chunk )
|
|
217 os.close( fd )
|
|
218 zipped_file.close()
|
|
219 uncompressed_name = name
|
|
220 unzipped = True
|
|
221 else:
|
|
222 # python < 2.5 doesn't have a way to read members in chunks(!)
|
3
|
223 try:
|
5
|
224 outfile = open( uncompressed, 'wb' )
|
|
225 outfile.write( z.read( name ) )
|
|
226 outfile.close()
|
|
227 uncompressed_name = name
|
|
228 unzipped = True
|
3
|
229 except IOError:
|
|
230 os.close( fd )
|
|
231 os.remove( uncompressed )
|
5
|
232 file_err( 'Problem decompressing zipped data', dataset, json_file )
|
3
|
233 return
|
5
|
234 z.close()
|
|
235 # Replace the zipped file with the decompressed file if it's safe to do so
|
|
236 if uncompressed is not None:
|
3
|
237 if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place:
|
|
238 dataset.path = uncompressed
|
|
239 else:
|
|
240 shutil.move( uncompressed, dataset.path )
|
|
241 os.chmod(dataset.path, 0644)
|
5
|
242 dataset.name = uncompressed_name
|
|
243 data_type = 'zip'
|
|
244 if not data_type:
|
|
245 if check_binary( dataset.path ):
|
|
246 # We have a binary dataset, but it is not Bam, Sff or Pdf
|
|
247 data_type = 'binary'
|
|
248 #binary_ok = False
|
|
249 parts = dataset.name.split( "." )
|
|
250 if len( parts ) > 1:
|
|
251 ext = parts[-1].strip().lower()
|
|
252 if not Binary.is_ext_unsniffable(ext):
|
|
253 file_err( 'The uploaded binary file contains inappropriate content', dataset, json_file )
|
|
254 return
|
|
255 elif Binary.is_ext_unsniffable(ext) and dataset.file_type != ext:
|
|
256 err_msg = "You must manually set the 'File Format' to '%s' when uploading %s files." % ( ext.capitalize(), ext )
|
|
257 file_err( err_msg, dataset, json_file )
|
|
258 return
|
|
259 if not data_type:
|
|
260 # We must have a text file
|
|
261 if check_html( dataset.path ):
|
|
262 file_err( 'The uploaded file contains inappropriate HTML content', dataset, json_file )
|
|
263 return
|
|
264 if data_type != 'binary':
|
|
265 if link_data_only == 'copy_files':
|
|
266 if dataset.type in ( 'server_dir', 'path_paste' ) and data_type not in [ 'gzip', 'bz2', 'zip' ]:
|
|
267 in_place = False
|
|
268 # Convert universal line endings to Posix line endings, but allow the user to turn it off,
|
|
269 # so that is becomes possible to upload gzip, bz2 or zip files with binary data without
|
|
270 # corrupting the content of those files.
|
|
271 if dataset.to_posix_lines:
|
|
272 if dataset.space_to_tab:
|
|
273 line_count, converted_path = sniff.convert_newlines_sep2tabs( dataset.path, in_place=in_place )
|
|
274 else:
|
|
275 line_count, converted_path = sniff.convert_newlines( dataset.path, in_place=in_place )
|
|
276 if dataset.file_type == 'auto':
|
|
277 ext = sniff.guess_ext( dataset.path, registry.sniff_order )
|
|
278 else:
|
|
279 ext = dataset.file_type
|
|
280 data_type = ext
|
0
|
281 # Save job info for the framework
|
|
282 if ext == 'auto' and dataset.ext:
|
|
283 ext = dataset.ext
|
|
284 if ext == 'auto':
|
|
285 ext = 'data'
|
|
286 datatype = registry.get_datatype_by_extension( ext )
|
|
287 if dataset.type in ( 'server_dir', 'path_paste' ) and link_data_only == 'link_to_files':
|
|
288 # Never alter a file that will not be copied to Galaxy's local file store.
|
|
289 if datatype.dataset_content_needs_grooming( dataset.path ):
|
|
290 err_msg = 'The uploaded files need grooming, so change your <b>Copy data into Galaxy?</b> selection to be ' + \
|
|
291 '<b>Copy files into Galaxy</b> instead of <b>Link to files without copying into Galaxy</b> so grooming can be performed.'
|
|
292 file_err( err_msg, dataset, json_file )
|
|
293 return
|
|
294 if link_data_only == 'copy_files' and dataset.type in ( 'server_dir', 'path_paste' ) and data_type not in [ 'gzip', 'bz2', 'zip' ]:
|
|
295 # Move the dataset to its "real" path
|
|
296 if converted_path is not None:
|
|
297 shutil.copy( converted_path, output_path )
|
|
298 try:
|
|
299 os.remove( converted_path )
|
|
300 except:
|
|
301 pass
|
|
302 else:
|
|
303 # This should not happen, but it's here just in case
|
|
304 shutil.copy( dataset.path, output_path )
|
|
305 elif link_data_only == 'copy_files':
|
2
|
306 shutil.move( dataset.path, output_path )
|
0
|
307 # Write the job info
|
|
308 stdout = stdout or 'uploaded %s file' % data_type
|
|
309 info = dict( type = 'dataset',
|
|
310 dataset_id = dataset.dataset_id,
|
|
311 ext = ext,
|
|
312 stdout = stdout,
|
|
313 name = dataset.name,
|
|
314 line_count = line_count )
|
|
315 if dataset.get('uuid', None) is not None:
|
|
316 info['uuid'] = dataset.get('uuid')
|
5
|
317 json_file.write( to_json_string( info ) + "\n" )
|
0
|
318
|
|
319 if link_data_only == 'copy_files' and datatype.dataset_content_needs_grooming( output_path ):
|
|
320 # Groom the dataset content if necessary
|
|
321 datatype.groom_dataset_content( output_path )
|
|
322
|
|
323 def add_composite_file( dataset, registry, json_file, output_path, files_path ):
|
|
324 if dataset.composite_files:
|
|
325 os.mkdir( files_path )
|
|
326 for name, value in dataset.composite_files.iteritems():
|
|
327 value = util.bunch.Bunch( **value )
|
|
328 if dataset.composite_file_paths[ value.name ] is None and not value.optional:
|
|
329 file_err( 'A required composite data file was not provided (%s)' % name, dataset, json_file )
|
|
330 break
|
|
331 elif dataset.composite_file_paths[value.name] is not None:
|
|
332 dp = dataset.composite_file_paths[value.name][ 'path' ]
|
|
333 isurl = dp.find('://') <> -1 # todo fixme
|
|
334 if isurl:
|
|
335 try:
|
|
336 temp_name, dataset.is_multi_byte = sniff.stream_to_file( urllib.urlopen( dp ), prefix='url_paste' )
|
|
337 except Exception, e:
|
|
338 file_err( 'Unable to fetch %s\n%s' % ( dp, str( e ) ), dataset, json_file )
|
|
339 return
|
|
340 dataset.path = temp_name
|
|
341 dp = temp_name
|
|
342 if not value.is_binary:
|
|
343 if dataset.composite_file_paths[ value.name ].get( 'space_to_tab', value.space_to_tab ):
|
5
|
344 sniff.convert_newlines_sep2tabs( dp )
|
0
|
345 else:
|
5
|
346 sniff.convert_newlines( dp )
|
0
|
347 shutil.move( dp, os.path.join( files_path, name ) )
|
|
348 # Move the dataset to its "real" path
|
|
349 shutil.move( dataset.primary_file, output_path )
|
|
350 # Write the job info
|
|
351 info = dict( type = 'dataset',
|
|
352 dataset_id = dataset.dataset_id,
|
|
353 stdout = 'uploaded %s file' % dataset.file_type )
|
5
|
354 json_file.write( to_json_string( info ) + "\n" )
|
0
|
355
|
|
356 def __main__():
|
|
357
|
|
358 if len( sys.argv ) < 4:
|
|
359 print >>sys.stderr, 'usage: upload.py <root> <datatypes_conf> <json paramfile> <output spec> ...'
|
|
360 sys.exit( 1 )
|
|
361
|
|
362 output_paths = parse_outputs( sys.argv[4:] )
|
|
363 json_file = open( 'galaxy.json', 'w' )
|
|
364
|
|
365 registry = Registry()
|
|
366 registry.load_datatypes( root_dir=sys.argv[1], config=sys.argv[2] )
|
|
367
|
|
368 for line in open( sys.argv[3], 'r' ):
|
5
|
369 dataset = from_json_string( line )
|
0
|
370 dataset = util.bunch.Bunch( **safe_dict( dataset ) )
|
|
371 try:
|
|
372 output_path = output_paths[int( dataset.dataset_id )][0]
|
|
373 except:
|
|
374 print >>sys.stderr, 'Output path for dataset %s not found on command line' % dataset.dataset_id
|
|
375 sys.exit( 1 )
|
|
376 if dataset.type == 'composite':
|
|
377 files_path = output_paths[int( dataset.dataset_id )][1]
|
|
378 add_composite_file( dataset, registry, json_file, output_path, files_path )
|
|
379 else:
|
|
380 add_file( dataset, registry, json_file, output_path )
|
|
381
|
|
382 # clean up paramfile
|
|
383 # TODO: this will not work when running as the actual user unless the
|
|
384 # parent directory is writable by the user.
|
|
385 try:
|
|
386 os.remove( sys.argv[3] )
|
|
387 except:
|
|
388 pass
|
|
389
|
|
390 if __name__ == '__main__':
|
|
391 __main__()
|