update to trunk
This commit is contained in:
		
							
								
								
									
										236
									
								
								bin/st
									
									
									
									
									
								
							
							
						
						
									
										236
									
								
								bin/st
									
									
									
									
									
								
							| @@ -581,7 +581,8 @@ def put_object(url, token, container, name, contents, content_length=None, | |||||||
|     :param container: container name that the object is in |     :param container: container name that the object is in | ||||||
|     :param name: object name to put |     :param name: object name to put | ||||||
|     :param contents: a string or a file like object to read object data from |     :param contents: a string or a file like object to read object data from | ||||||
|     :param content_length: value to send as content-length header |     :param content_length: value to send as content-length header; also limits | ||||||
|  |                            the amount read from contents | ||||||
|     :param etag: etag of contents |     :param etag: etag of contents | ||||||
|     :param chunk_size: chunk size of data to write |     :param chunk_size: chunk size of data to write | ||||||
|     :param content_type: value to send as content-type header |     :param content_type: value to send as content-type header | ||||||
| @@ -611,18 +612,24 @@ def put_object(url, token, container, name, contents, content_length=None, | |||||||
|         conn.putrequest('PUT', path) |         conn.putrequest('PUT', path) | ||||||
|         for header, value in headers.iteritems(): |         for header, value in headers.iteritems(): | ||||||
|             conn.putheader(header, value) |             conn.putheader(header, value) | ||||||
|         if not content_length: |         if content_length is None: | ||||||
|             conn.putheader('Transfer-Encoding', 'chunked') |             conn.putheader('Transfer-Encoding', 'chunked') | ||||||
|             conn.endheaders() |             conn.endheaders() | ||||||
|             chunk = contents.read(chunk_size) |             chunk = contents.read(chunk_size) | ||||||
|             while chunk: |             while chunk: | ||||||
|             if not content_length: |  | ||||||
|                 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |                 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) | ||||||
|             else: |  | ||||||
|                 conn.send(chunk) |  | ||||||
|                 chunk = contents.read(chunk_size) |                 chunk = contents.read(chunk_size) | ||||||
|         if not content_length: |  | ||||||
|             conn.send('0\r\n\r\n') |             conn.send('0\r\n\r\n') | ||||||
|  |         else: | ||||||
|  |             conn.endheaders() | ||||||
|  |             left = content_length | ||||||
|  |             while left > 0: | ||||||
|  |                 size = chunk_size | ||||||
|  |                 if size > left: | ||||||
|  |                     size = left | ||||||
|  |                 chunk = contents.read(size) | ||||||
|  |                 conn.send(chunk) | ||||||
|  |                 left -= len(chunk) | ||||||
|     else: |     else: | ||||||
|         conn.request('PUT', path, contents, headers) |         conn.request('PUT', path, contents, headers) | ||||||
|     resp = conn.getresponse() |     resp = conn.getresponse() | ||||||
| @@ -860,15 +867,20 @@ class QueueFunctionThread(Thread): | |||||||
|  |  | ||||||
|  |  | ||||||
| st_delete_help = ''' | st_delete_help = ''' | ||||||
| delete --all OR delete container [object] [object] ... | delete --all OR delete container [--leave-segments] [object] [object] ... | ||||||
|     Deletes everything in the account (with --all), or everything in a |     Deletes everything in the account (with --all), or everything in a | ||||||
|     container, or a list of objects depending on the args given.'''.strip('\n') |     container, or a list of objects depending on the args given. Segments of | ||||||
|  |     manifest objects will be deleted as well, unless you specify the | ||||||
|  |     --leave-segments option.'''.strip('\n') | ||||||
|  |  | ||||||
|  |  | ||||||
| def st_delete(parser, args, print_queue, error_queue): | def st_delete(parser, args, print_queue, error_queue): | ||||||
|     parser.add_option('-a', '--all', action='store_true', dest='yes_all', |     parser.add_option('-a', '--all', action='store_true', dest='yes_all', | ||||||
|         default=False, help='Indicates that you really want to delete ' |         default=False, help='Indicates that you really want to delete ' | ||||||
|         'everything in the account') |         'everything in the account') | ||||||
|  |     parser.add_option('', '--leave-segments', action='store_true', | ||||||
|  |         dest='leave_segments', default=False, help='Indicates that you want ' | ||||||
|  |         'the segments of manifest objects left alone') | ||||||
|     (options, args) = parse_args(parser, args) |     (options, args) = parse_args(parser, args) | ||||||
|     args = args[1:] |     args = args[1:] | ||||||
|     if (not args and not options.yes_all) or (args and options.yes_all): |     if (not args and not options.yes_all) or (args and options.yes_all): | ||||||
| @@ -876,11 +888,42 @@ def st_delete(parser, args, print_queue, error_queue): | |||||||
|                         (basename(argv[0]), st_delete_help)) |                         (basename(argv[0]), st_delete_help)) | ||||||
|         return |         return | ||||||
|  |  | ||||||
|  |     def _delete_segment((container, obj), conn): | ||||||
|  |         conn.delete_object(container, obj) | ||||||
|  |         if options.verbose: | ||||||
|  |             print_queue.put('%s/%s' % (container, obj)) | ||||||
|  |  | ||||||
|     object_queue = Queue(10000) |     object_queue = Queue(10000) | ||||||
|  |  | ||||||
|     def _delete_object((container, obj), conn): |     def _delete_object((container, obj), conn): | ||||||
|         try: |         try: | ||||||
|  |             old_manifest = None | ||||||
|  |             if not options.leave_segments: | ||||||
|  |                 try: | ||||||
|  |                     old_manifest = conn.head_object(container, obj).get( | ||||||
|  |                         'x-object-manifest') | ||||||
|  |                 except ClientException, err: | ||||||
|  |                     if err.http_status != 404: | ||||||
|  |                         raise | ||||||
|             conn.delete_object(container, obj) |             conn.delete_object(container, obj) | ||||||
|  |             if old_manifest: | ||||||
|  |                 segment_queue = Queue(10000) | ||||||
|  |                 scontainer, sprefix = old_manifest.split('/', 1) | ||||||
|  |                 for delobj in conn.get_container(scontainer, | ||||||
|  |                                                  prefix=sprefix)[1]: | ||||||
|  |                     segment_queue.put((scontainer, delobj['name'])) | ||||||
|  |                 if not segment_queue.empty(): | ||||||
|  |                     segment_threads = [QueueFunctionThread(segment_queue, | ||||||
|  |                         _delete_segment, create_connection()) for _ in | ||||||
|  |                         xrange(10)] | ||||||
|  |                     for thread in segment_threads: | ||||||
|  |                         thread.start() | ||||||
|  |                     while not segment_queue.empty(): | ||||||
|  |                         sleep(0.01) | ||||||
|  |                     for thread in segment_threads: | ||||||
|  |                         thread.abort = True | ||||||
|  |                         while thread.isAlive(): | ||||||
|  |                             thread.join(0.01) | ||||||
|             if options.verbose: |             if options.verbose: | ||||||
|                 path = options.yes_all and join(container, obj) or obj |                 path = options.yes_all and join(container, obj) or obj | ||||||
|                 if path[:1] in ('/', '\\'): |                 if path[:1] in ('/', '\\'): | ||||||
| @@ -891,6 +934,7 @@ def st_delete(parser, args, print_queue, error_queue): | |||||||
|                 raise |                 raise | ||||||
|             error_queue.put('Object %s not found' % |             error_queue.put('Object %s not found' % | ||||||
|                             repr('%s/%s' % (container, obj))) |                             repr('%s/%s' % (container, obj))) | ||||||
|  |  | ||||||
|     container_queue = Queue(10000) |     container_queue = Queue(10000) | ||||||
|  |  | ||||||
|     def _delete_container(container, conn): |     def _delete_container(container, conn): | ||||||
| @@ -956,6 +1000,10 @@ def st_delete(parser, args, print_queue, error_queue): | |||||||
|                 raise |                 raise | ||||||
|             error_queue.put('Account not found') |             error_queue.put('Account not found') | ||||||
|     elif len(args) == 1: |     elif len(args) == 1: | ||||||
|  |         if '/' in args[0]: | ||||||
|  |             print >> stderr, 'WARNING: / in container name; you might have ' \ | ||||||
|  |                              'meant %r instead of %r.' % \ | ||||||
|  |                              (args[0].replace('/', ' ', 1), args[0]) | ||||||
|         conn = create_connection() |         conn = create_connection() | ||||||
|         _delete_container(args[0], conn) |         _delete_container(args[0], conn) | ||||||
|     else: |     else: | ||||||
| @@ -976,7 +1024,7 @@ def st_delete(parser, args, print_queue, error_queue): | |||||||
|  |  | ||||||
|  |  | ||||||
| st_download_help = ''' | st_download_help = ''' | ||||||
| download --all OR download container [object] [object] ... | download --all OR download container [options] [object] [object] ... | ||||||
|     Downloads everything in the account (with --all), or everything in a |     Downloads everything in the account (with --all), or everything in a | ||||||
|     container, or a list of objects depending on the args given. For a single |     container, or a list of objects depending on the args given. For a single | ||||||
|     object download, you may use the -o [--output] <filename> option to |     object download, you may use the -o [--output] <filename> option to | ||||||
| @@ -1015,19 +1063,25 @@ def st_download(options, args, print_queue, error_queue): | |||||||
|             headers, body = \ |             headers, body = \ | ||||||
|                 conn.get_object(container, obj, resp_chunk_size=65536) |                 conn.get_object(container, obj, resp_chunk_size=65536) | ||||||
|             content_type = headers.get('content-type') |             content_type = headers.get('content-type') | ||||||
|  |             if 'content-length' in headers: | ||||||
|                 content_length = int(headers.get('content-length')) |                 content_length = int(headers.get('content-length')) | ||||||
|  |             else: | ||||||
|  |                 content_length = None | ||||||
|             etag = headers.get('etag') |             etag = headers.get('etag') | ||||||
|             path = options.yes_all and join(container, obj) or obj |             path = options.yes_all and join(container, obj) or obj | ||||||
|             if path[:1] in ('/', '\\'): |             if path[:1] in ('/', '\\'): | ||||||
|                 path = path[1:] |                 path = path[1:] | ||||||
|  |             md5sum = None | ||||||
|             make_dir = out_file != "-" |             make_dir = out_file != "-" | ||||||
|             if content_type.split(';', 1)[0] == 'text/directory': |             if content_type.split(';', 1)[0] == 'text/directory': | ||||||
|                 if make_dir and not isdir(path): |                 if make_dir and not isdir(path): | ||||||
|                     mkdirs(path) |                     mkdirs(path) | ||||||
|                 read_length = 0 |                 read_length = 0 | ||||||
|  |                 if 'x-object-manifest' not in headers: | ||||||
|                     md5sum = md5() |                     md5sum = md5() | ||||||
|                 for chunk in body: |                 for chunk in body: | ||||||
|                     read_length += len(chunk) |                     read_length += len(chunk) | ||||||
|  |                     if md5sum: | ||||||
|                         md5sum.update(chunk) |                         md5sum.update(chunk) | ||||||
|             else: |             else: | ||||||
|                 dirpath = dirname(path) |                 dirpath = dirname(path) | ||||||
| @@ -1040,16 +1094,18 @@ def st_download(options, args, print_queue, error_queue): | |||||||
|                 else: |                 else: | ||||||
|                     fp = open(path, 'wb') |                     fp = open(path, 'wb') | ||||||
|                 read_length = 0 |                 read_length = 0 | ||||||
|  |                 if 'x-object-manifest' not in headers: | ||||||
|                     md5sum = md5() |                     md5sum = md5() | ||||||
|                 for chunk in body: |                 for chunk in body: | ||||||
|                     fp.write(chunk) |                     fp.write(chunk) | ||||||
|                     read_length += len(chunk) |                     read_length += len(chunk) | ||||||
|  |                     if md5sum: | ||||||
|                         md5sum.update(chunk) |                         md5sum.update(chunk) | ||||||
|                 fp.close() |                 fp.close() | ||||||
|             if md5sum.hexdigest() != etag: |             if md5sum and md5sum.hexdigest() != etag: | ||||||
|                 error_queue.put('%s: md5sum != etag, %s != %s' % |                 error_queue.put('%s: md5sum != etag, %s != %s' % | ||||||
|                                 (path, md5sum.hexdigest(), etag)) |                                 (path, md5sum.hexdigest(), etag)) | ||||||
|             if read_length != content_length: |             if content_length is not None and read_length != content_length: | ||||||
|                 error_queue.put('%s: read_length != content_length, %d != %d' % |                 error_queue.put('%s: read_length != content_length, %d != %d' % | ||||||
|                                 (path, read_length, content_length)) |                                 (path, read_length, content_length)) | ||||||
|             if 'x-object-meta-mtime' in headers and not options.out_file: |             if 'x-object-meta-mtime' in headers and not options.out_file: | ||||||
| @@ -1110,6 +1166,10 @@ def st_download(options, args, print_queue, error_queue): | |||||||
|                 raise |                 raise | ||||||
|             error_queue.put('Account not found') |             error_queue.put('Account not found') | ||||||
|     elif len(args) == 1: |     elif len(args) == 1: | ||||||
|  |         if '/' in args[0]: | ||||||
|  |             print >> stderr, 'WARNING: / in container name; you might have ' \ | ||||||
|  |                              'meant %r instead of %r.' % \ | ||||||
|  |                              (args[0].replace('/', ' ', 1), args[0]) | ||||||
|         _download_container(args[0], create_connection()) |         _download_container(args[0], create_connection()) | ||||||
|     else: |     else: | ||||||
|         if len(args) == 2: |         if len(args) == 2: | ||||||
| @@ -1223,6 +1283,10 @@ Containers: %d | |||||||
|                 raise |                 raise | ||||||
|             error_queue.put('Account not found') |             error_queue.put('Account not found') | ||||||
|     elif len(args) == 1: |     elif len(args) == 1: | ||||||
|  |         if '/' in args[0]: | ||||||
|  |             print >> stderr, 'WARNING: / in container name; you might have ' \ | ||||||
|  |                              'meant %r instead of %r.' % \ | ||||||
|  |                              (args[0].replace('/', ' ', 1), args[0]) | ||||||
|         try: |         try: | ||||||
|             headers = conn.head_container(args[0]) |             headers = conn.head_container(args[0]) | ||||||
|             object_count = int(headers.get('x-container-object-count', 0)) |             object_count = int(headers.get('x-container-object-count', 0)) | ||||||
| @@ -1259,14 +1323,19 @@ Write ACL: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], | |||||||
|        Account: %s |        Account: %s | ||||||
|      Container: %s |      Container: %s | ||||||
|         Object: %s |         Object: %s | ||||||
|   Content Type: %s |   Content Type: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], | ||||||
| Content Length: %s |                                      args[1], headers.get('content-type'))) | ||||||
|  Last Modified: %s |             if 'content-length' in headers: | ||||||
|           ETag: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], |                 print_queue.put('Content Length: %s' % | ||||||
|                                      args[1], headers.get('content-type'), |                                 headers['content-length']) | ||||||
|                                      headers.get('content-length'), |             if 'last-modified' in headers: | ||||||
|                                      headers.get('last-modified'), |                 print_queue.put(' Last Modified: %s' % | ||||||
|                                      headers.get('etag'))) |                                 headers['last-modified']) | ||||||
|  |             if 'etag' in headers: | ||||||
|  |                 print_queue.put('          ETag: %s' % headers['etag']) | ||||||
|  |             if 'x-object-manifest' in headers: | ||||||
|  |                 print_queue.put('      Manifest: %s' % | ||||||
|  |                                 headers['x-object-manifest']) | ||||||
|             for key, value in headers.items(): |             for key, value in headers.items(): | ||||||
|                 if key.startswith('x-object-meta-'): |                 if key.startswith('x-object-meta-'): | ||||||
|                     print_queue.put('%14s: %s' % ('Meta %s' % |                     print_queue.put('%14s: %s' % ('Meta %s' % | ||||||
| @@ -1274,7 +1343,7 @@ Content Length: %s | |||||||
|             for key, value in headers.items(): |             for key, value in headers.items(): | ||||||
|                 if not key.startswith('x-object-meta-') and key not in ( |                 if not key.startswith('x-object-meta-') and key not in ( | ||||||
|                         'content-type', 'content-length', 'last-modified', |                         'content-type', 'content-length', 'last-modified', | ||||||
|                         'etag', 'date'): |                         'etag', 'date', 'x-object-manifest'): | ||||||
|                     print_queue.put( |                     print_queue.put( | ||||||
|                         '%14s: %s' % (key.title(), value)) |                         '%14s: %s' % (key.title(), value)) | ||||||
|         except ClientException, err: |         except ClientException, err: | ||||||
| @@ -1326,6 +1395,10 @@ def st_post(options, args, print_queue, error_queue): | |||||||
|                 raise |                 raise | ||||||
|             error_queue.put('Account not found') |             error_queue.put('Account not found') | ||||||
|     elif len(args) == 1: |     elif len(args) == 1: | ||||||
|  |         if '/' in args[0]: | ||||||
|  |             print >> stderr, 'WARNING: / in container name; you might have ' \ | ||||||
|  |                              'meant %r instead of %r.' % \ | ||||||
|  |                              (args[0].replace('/', ' ', 1), args[0]) | ||||||
|         headers = {} |         headers = {} | ||||||
|         for item in options.meta: |         for item in options.meta: | ||||||
|             split_item = item.split(':') |             split_item = item.split(':') | ||||||
| @@ -1363,23 +1436,48 @@ st_upload_help = ''' | |||||||
| upload [options] container file_or_directory [file_or_directory] [...] | upload [options] container file_or_directory [file_or_directory] [...] | ||||||
|     Uploads to the given container the files and directories specified by the |     Uploads to the given container the files and directories specified by the | ||||||
|     remaining args. -c or --changed is an option that will only upload files |     remaining args. -c or --changed is an option that will only upload files | ||||||
|     that have changed since the last upload.'''.strip('\n') |     that have changed since the last upload. -S <size> or --segment-size <size> | ||||||
|  |     and --leave-segments are options as well (see --help for more). | ||||||
|  | '''.strip('\n') | ||||||
|  |  | ||||||
|  |  | ||||||
| def st_upload(options, args, print_queue, error_queue): | def st_upload(options, args, print_queue, error_queue): | ||||||
|     parser.add_option('-c', '--changed', action='store_true', dest='changed', |     parser.add_option('-c', '--changed', action='store_true', dest='changed', | ||||||
|         default=False, help='Will only upload files that have changed since ' |         default=False, help='Will only upload files that have changed since ' | ||||||
|         'the last upload') |         'the last upload') | ||||||
|  |     parser.add_option('-S', '--segment-size', dest='segment_size', help='Will ' | ||||||
|  |         'upload files in segments no larger than <size> and then create a ' | ||||||
|  |         '"manifest" file that will download all the segments as if it were ' | ||||||
|  |         'the original file. The segments will be uploaded to a ' | ||||||
|  |         '<container>_segments container so as to not pollute the main ' | ||||||
|  |         '<container> listings.') | ||||||
|  |     parser.add_option('', '--leave-segments', action='store_true', | ||||||
|  |         dest='leave_segments', default=False, help='Indicates that you want ' | ||||||
|  |         'the older segments of manifest objects left alone (in the case of ' | ||||||
|  |         'overwrites)') | ||||||
|     (options, args) = parse_args(parser, args) |     (options, args) = parse_args(parser, args) | ||||||
|     args = args[1:] |     args = args[1:] | ||||||
|     if len(args) < 2: |     if len(args) < 2: | ||||||
|         error_queue.put('Usage: %s [options] %s' % |         error_queue.put('Usage: %s [options] %s' % | ||||||
|                         (basename(argv[0]), st_upload_help)) |                         (basename(argv[0]), st_upload_help)) | ||||||
|         return |         return | ||||||
|  |     object_queue = Queue(10000) | ||||||
|  |  | ||||||
|     file_queue = Queue(10000) |     def _segment_job(job, conn): | ||||||
|  |         if job.get('delete', False): | ||||||
|  |             conn.delete_object(job['container'], job['obj']) | ||||||
|  |         else: | ||||||
|  |             fp = open(job['path'], 'rb') | ||||||
|  |             fp.seek(job['segment_start']) | ||||||
|  |             conn.put_object(job.get('container', args[0] + '_segments'), | ||||||
|  |                 job['obj'], fp, content_length=job['segment_size']) | ||||||
|  |         if options.verbose and 'log_line' in job: | ||||||
|  |             print_queue.put(job['log_line']) | ||||||
|  |  | ||||||
|     def _upload_file((path, dir_marker), conn): |     def _object_job(job, conn): | ||||||
|  |         path = job['path'] | ||||||
|  |         container = job.get('container', args[0]) | ||||||
|  |         dir_marker = job.get('dir_marker', False) | ||||||
|         try: |         try: | ||||||
|             obj = path |             obj = path | ||||||
|             if obj.startswith('./') or obj.startswith('.\\'): |             if obj.startswith('./') or obj.startswith('.\\'): | ||||||
| @@ -1388,7 +1486,7 @@ def st_upload(options, args, print_queue, error_queue): | |||||||
|             if dir_marker: |             if dir_marker: | ||||||
|                 if options.changed: |                 if options.changed: | ||||||
|                     try: |                     try: | ||||||
|                         headers = conn.head_object(args[0], obj) |                         headers = conn.head_object(container, obj) | ||||||
|                         ct = headers.get('content-type') |                         ct = headers.get('content-type') | ||||||
|                         cl = int(headers.get('content-length')) |                         cl = int(headers.get('content-length')) | ||||||
|                         et = headers.get('etag') |                         et = headers.get('etag') | ||||||
| @@ -1401,24 +1499,86 @@ def st_upload(options, args, print_queue, error_queue): | |||||||
|                     except ClientException, err: |                     except ClientException, err: | ||||||
|                         if err.http_status != 404: |                         if err.http_status != 404: | ||||||
|                             raise |                             raise | ||||||
|                 conn.put_object(args[0], obj, '', content_length=0, |                 conn.put_object(container, obj, '', content_length=0, | ||||||
|                                 content_type='text/directory', |                                 content_type='text/directory', | ||||||
|                                 headers=put_headers) |                                 headers=put_headers) | ||||||
|             else: |             else: | ||||||
|                 if options.changed: |                 # We need to HEAD all objects now in case we're overwriting a | ||||||
|  |                 # manifest object and need to delete the old segments | ||||||
|  |                 # ourselves. | ||||||
|  |                 old_manifest = None | ||||||
|  |                 if options.changed or not options.leave_segments: | ||||||
|                     try: |                     try: | ||||||
|                         headers = conn.head_object(args[0], obj) |                         headers = conn.head_object(container, obj) | ||||||
|                         cl = int(headers.get('content-length')) |                         cl = int(headers.get('content-length')) | ||||||
|                         mt = headers.get('x-object-meta-mtime') |                         mt = headers.get('x-object-meta-mtime') | ||||||
|                         if cl == getsize(path) and \ |                         if options.changed and cl == getsize(path) and \ | ||||||
|                                 mt == put_headers['x-object-meta-mtime']: |                                 mt == put_headers['x-object-meta-mtime']: | ||||||
|                             return |                             return | ||||||
|  |                         if not options.leave_segments: | ||||||
|  |                             old_manifest = headers.get('x-object-manifest') | ||||||
|                     except ClientException, err: |                     except ClientException, err: | ||||||
|                         if err.http_status != 404: |                         if err.http_status != 404: | ||||||
|                             raise |                             raise | ||||||
|                 conn.put_object(args[0], obj, open(path, 'rb'), |                 if options.segment_size and \ | ||||||
|                                 content_length=getsize(path), |                         getsize(path) < options.segment_size: | ||||||
|  |                     full_size = getsize(path) | ||||||
|  |                     segment_queue = Queue(10000) | ||||||
|  |                     segment_threads = [QueueFunctionThread(segment_queue, | ||||||
|  |                         _segment_job, create_connection()) for _ in xrange(10)] | ||||||
|  |                     for thread in segment_threads: | ||||||
|  |                         thread.start() | ||||||
|  |                     segment = 0 | ||||||
|  |                     segment_start = 0 | ||||||
|  |                     while segment_start < full_size: | ||||||
|  |                         segment_size = int(options.segment_size) | ||||||
|  |                         if segment_start + segment_size > full_size: | ||||||
|  |                             segment_size = full_size - segment_start | ||||||
|  |                         segment_queue.put({'path': path, | ||||||
|  |                             'obj': '%s/%s/%s/%08d' % (obj, | ||||||
|  |                                 put_headers['x-object-meta-mtime'], full_size, | ||||||
|  |                                 segment), | ||||||
|  |                             'segment_start': segment_start, | ||||||
|  |                             'segment_size': segment_size, | ||||||
|  |                             'log_line': '%s segment %s' % (obj, segment)}) | ||||||
|  |                         segment += 1 | ||||||
|  |                         segment_start += segment_size | ||||||
|  |                     while not segment_queue.empty(): | ||||||
|  |                         sleep(0.01) | ||||||
|  |                     for thread in segment_threads: | ||||||
|  |                         thread.abort = True | ||||||
|  |                         while thread.isAlive(): | ||||||
|  |                             thread.join(0.01) | ||||||
|  |                     new_object_manifest = '%s_segments/%s/%s/%s/' % ( | ||||||
|  |                         container, obj, put_headers['x-object-meta-mtime'], | ||||||
|  |                         full_size) | ||||||
|  |                     if old_manifest == new_object_manifest: | ||||||
|  |                         old_manifest = None | ||||||
|  |                     put_headers['x-object-manifest'] = new_object_manifest | ||||||
|  |                     conn.put_object(container, obj, '', content_length=0, | ||||||
|                                     headers=put_headers) |                                     headers=put_headers) | ||||||
|  |                 else: | ||||||
|  |                     conn.put_object(container, obj, open(path, 'rb'), | ||||||
|  |                         content_length=getsize(path), headers=put_headers) | ||||||
|  |                 if old_manifest: | ||||||
|  |                     segment_queue = Queue(10000) | ||||||
|  |                     scontainer, sprefix = old_manifest.split('/', 1) | ||||||
|  |                     for delobj in conn.get_container(scontainer, | ||||||
|  |                                                      prefix=sprefix)[1]: | ||||||
|  |                         segment_queue.put({'delete': True, | ||||||
|  |                             'container': scontainer, 'obj': delobj['name']}) | ||||||
|  |                     if not segment_queue.empty(): | ||||||
|  |                         segment_threads = [QueueFunctionThread(segment_queue, | ||||||
|  |                             _segment_job, create_connection()) for _ in | ||||||
|  |                             xrange(10)] | ||||||
|  |                         for thread in segment_threads: | ||||||
|  |                             thread.start() | ||||||
|  |                         while not segment_queue.empty(): | ||||||
|  |                             sleep(0.01) | ||||||
|  |                         for thread in segment_threads: | ||||||
|  |                             thread.abort = True | ||||||
|  |                             while thread.isAlive(): | ||||||
|  |                                 thread.join(0.01) | ||||||
|             if options.verbose: |             if options.verbose: | ||||||
|                 print_queue.put(obj) |                 print_queue.put(obj) | ||||||
|         except OSError, err: |         except OSError, err: | ||||||
| @@ -1429,22 +1589,22 @@ def st_upload(options, args, print_queue, error_queue): | |||||||
|     def _upload_dir(path): |     def _upload_dir(path): | ||||||
|         names = listdir(path) |         names = listdir(path) | ||||||
|         if not names: |         if not names: | ||||||
|             file_queue.put((path, True)) # dir_marker = True |             object_queue.put({'path': path, 'dir_marker': True}) | ||||||
|         else: |         else: | ||||||
|             for name in listdir(path): |             for name in listdir(path): | ||||||
|                 subpath = join(path, name) |                 subpath = join(path, name) | ||||||
|                 if isdir(subpath): |                 if isdir(subpath): | ||||||
|                     _upload_dir(subpath) |                     _upload_dir(subpath) | ||||||
|                 else: |                 else: | ||||||
|                     file_queue.put((subpath, False)) # dir_marker = False |                     object_queue.put({'path': subpath}) | ||||||
|  |  | ||||||
|     url, token = get_auth(options.auth, options.user, options.key, |     url, token = get_auth(options.auth, options.user, options.key, | ||||||
|         snet=options.snet) |         snet=options.snet) | ||||||
|     create_connection = lambda: Connection(options.auth, options.user, |     create_connection = lambda: Connection(options.auth, options.user, | ||||||
|         options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |         options.key, preauthurl=url, preauthtoken=token, snet=options.snet) | ||||||
|     file_threads = [QueueFunctionThread(file_queue, _upload_file, |     object_threads = [QueueFunctionThread(object_queue, _object_job, | ||||||
|         create_connection()) for _ in xrange(10)] |         create_connection()) for _ in xrange(10)] | ||||||
|     for thread in file_threads: |     for thread in object_threads: | ||||||
|         thread.start() |         thread.start() | ||||||
|     conn = create_connection() |     conn = create_connection() | ||||||
|     # Try to create the container, just in case it doesn't exist. If this |     # Try to create the container, just in case it doesn't exist. If this | ||||||
| @@ -1453,6 +1613,8 @@ def st_upload(options, args, print_queue, error_queue): | |||||||
|     # it'll surface on the first object PUT. |     # it'll surface on the first object PUT. | ||||||
|     try: |     try: | ||||||
|         conn.put_container(args[0]) |         conn.put_container(args[0]) | ||||||
|  |         if options.segment_size is not None: | ||||||
|  |             conn.put_container(args[0] + '_segments') | ||||||
|     except: |     except: | ||||||
|         pass |         pass | ||||||
|     try: |     try: | ||||||
| @@ -1460,10 +1622,10 @@ def st_upload(options, args, print_queue, error_queue): | |||||||
|             if isdir(arg): |             if isdir(arg): | ||||||
|                 _upload_dir(arg) |                 _upload_dir(arg) | ||||||
|             else: |             else: | ||||||
|                 file_queue.put((arg, False)) # dir_marker = False |                 object_queue.put({'path': arg}) | ||||||
|         while not file_queue.empty(): |         while not object_queue.empty(): | ||||||
|             sleep(0.01) |             sleep(0.01) | ||||||
|         for thread in file_threads: |         for thread in object_threads: | ||||||
|             thread.abort = True |             thread.abort = True | ||||||
|             while thread.isAlive(): |             while thread.isAlive(): | ||||||
|                 thread.join(0.01) |                 thread.join(0.01) | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ Overview and Concepts | |||||||
|     overview_replication |     overview_replication | ||||||
|     overview_stats |     overview_stats | ||||||
|     ratelimit |     ratelimit | ||||||
|  |     overview_large_objects | ||||||
|  |  | ||||||
| Developer Documentation | Developer Documentation | ||||||
| ======================= | ======================= | ||||||
|   | |||||||
							
								
								
									
										177
									
								
								doc/source/overview_large_objects.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								doc/source/overview_large_objects.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | ==================== | ||||||
|  | Large Object Support | ||||||
|  | ==================== | ||||||
|  |  | ||||||
|  | -------- | ||||||
|  | Overview | ||||||
|  | -------- | ||||||
|  |  | ||||||
|  | Swift has a limit on the size of a single uploaded object; by default this is | ||||||
|  | 5GB. However, the download size of a single object is virtually unlimited with | ||||||
|  | the concept of segmentation. Segments of the larger object are uploaded and a | ||||||
|  | special manifest file is created that, when downloaded, sends all the segments | ||||||
|  | concatenated as a single object. This also offers much greater upload speed | ||||||
|  | with the possibility of parallel uploads of the segments. | ||||||
|  |  | ||||||
|  | ---------------------------------- | ||||||
|  | Using ``st`` for Segmented Objects | ||||||
|  | ---------------------------------- | ||||||
|  |  | ||||||
|  | The quickest way to try out this feature is use the included ``st`` Swift Tool. | ||||||
|  | You can use the ``-S`` option to specify the segment size to use when splitting | ||||||
|  | a large file. For example:: | ||||||
|  |  | ||||||
|  |     st upload test_container -S 1073741824 large_file | ||||||
|  |  | ||||||
|  | This would split the large_file into 1G segments and begin uploading those | ||||||
|  | segments in parallel. Once all the segments have been uploaded, ``st`` will | ||||||
|  | then create the manifest file so the segments can be downloaded as one. | ||||||
|  |  | ||||||
|  | So now, the following ``st`` command would download the entire large object:: | ||||||
|  |  | ||||||
|  |     st download test_container large_file | ||||||
|  |  | ||||||
|  | ``st`` uses a strict convention for its segmented object support. In the above | ||||||
|  | example it will upload all the segments into a second container named | ||||||
|  | test_container_segments. These segments will have names like | ||||||
|  | large_file/1290206778.25/21474836480/00000000, | ||||||
|  | large_file/1290206778.25/21474836480/00000001, etc. | ||||||
|  |  | ||||||
|  | The main benefit for using a separate container is that the main container | ||||||
|  | listings will not be polluted with all the segment names. The reason for using | ||||||
|  | the segment name format of <name>/<timestamp>/<size>/<segment> is so that an | ||||||
|  | upload of a new file with the same name won't overwrite the contents of the | ||||||
|  | first until the last moment when the manifest file is updated. | ||||||
|  |  | ||||||
|  | ``st`` will manage these segment files for you, deleting old segments on | ||||||
|  | deletes and overwrites, etc. You can override this behavior with the | ||||||
|  | ``--leave-segments`` option if desired; this is useful if you want to have | ||||||
|  | multiple versions of the same large object available. | ||||||
|  |  | ||||||
|  | ---------- | ||||||
|  | Direct API | ||||||
|  | ---------- | ||||||
|  |  | ||||||
|  | You can also work with the segments and manifests directly with HTTP requests | ||||||
|  | instead of having ``st`` do that for you. You can just upload the segments like | ||||||
|  | you would any other object and the manifest is just a zero-byte file with an | ||||||
|  | extra ``X-Object-Manifest`` header. | ||||||
|  |  | ||||||
|  | All the object segments need to be in the same container, have a common object | ||||||
|  | name prefix, and their names sort in the order they should be concatenated. | ||||||
|  | They don't have to be in the same container as the manifest file will be, which | ||||||
|  | is useful to keep container listings clean as explained above with ``st``. | ||||||
|  |  | ||||||
|  | The manifest file is simply a zero-byte file with the extra | ||||||
|  | ``X-Object-Manifest: <container>/<prefix>`` header, where ``<container>`` is | ||||||
|  | the container the object segments are in and ``<prefix>`` is the common prefix | ||||||
|  | for all the segments. | ||||||
|  |  | ||||||
|  | It is best to upload all the segments first and then create or update the | ||||||
|  | manifest. In this way, the full object won't be available for downloading until | ||||||
|  | the upload is complete. Also, you can upload a new set of segments to a second | ||||||
|  | location and then update the manifest to point to this new location. During the | ||||||
|  | upload of the new segments, the original manifest will still be available to | ||||||
|  | download the first set of segments. | ||||||
|  |  | ||||||
|  | Here's an example using ``curl`` with tiny 1-byte segments:: | ||||||
|  |  | ||||||
|  |     # First, upload the segments | ||||||
|  |     curl -X PUT -H 'X-Auth-Token: <token>' \ | ||||||
|  |         http://<storage_url>/container/myobject/1 --data-binary '1' | ||||||
|  |     curl -X PUT -H 'X-Auth-Token: <token>' \ | ||||||
|  |         http://<storage_url>/container/myobject/2 --data-binary '2' | ||||||
|  |     curl -X PUT -H 'X-Auth-Token: <token>' \ | ||||||
|  |         http://<storage_url>/container/myobject/3 --data-binary '3' | ||||||
|  |  | ||||||
|  |     # Next, create the manifest file | ||||||
|  |     curl -X PUT -H 'X-Auth-Token: <token>' \ | ||||||
|  |         -H 'X-Object-Manifest: container/myobject/' \ | ||||||
|  |         http://<storage_url>/container/myobject --data-binary '' | ||||||
|  |  | ||||||
|  |     # And now we can download the segments as a single object | ||||||
|  |     curl -H 'X-Auth-Token: <token>' \ | ||||||
|  |         http://<storage_url>/container/myobject | ||||||
|  |  | ||||||
|  | ---------------- | ||||||
|  | Additional Notes | ||||||
|  | ---------------- | ||||||
|  |  | ||||||
|  | * With a ``GET`` or ``HEAD`` of a manifest file, the ``X-Object-Manifest: | ||||||
|  |   <container>/<prefix>`` header will be returned with the concatenated object | ||||||
|  |   so you can tell where it's getting its segments from. | ||||||
|  |  | ||||||
|  | * The response's ``Content-Length`` for a ``GET`` or ``HEAD`` on the manifest | ||||||
|  |   file will be the sum of all the segments in the ``<container>/<prefix>`` | ||||||
|  |   listing, dynamically. So, uploading additional segments after the manifest is | ||||||
|  |   created will cause the concatenated object to be that much larger; there's no | ||||||
|  |   need to recreate the manifest file. | ||||||
|  |  | ||||||
|  | * The response's ``Content-Type`` for a ``GET`` or ``HEAD`` on the manifest | ||||||
|  |   will be the same as the ``Content-Type`` set during the ``PUT`` request that | ||||||
|  |   created the manifest. You can easily change the ``Content-Type`` by reissuing | ||||||
|  |   the ``PUT``. | ||||||
|  |  | ||||||
|  | * The response's ``ETag`` for a ``GET`` or ``HEAD`` on the manifest file will | ||||||
|  |   be the MD5 sum of the concatenated string of ETags for each of the segments | ||||||
|  |   in the ``<container>/<prefix>`` listing, dynamically. Usually in Swift the | ||||||
|  |   ETag is the MD5 sum of the contents of the object, and that holds true for | ||||||
|  |   each segment independently. But, it's not feasible to generate such an ETag | ||||||
|  |   for the manifest itself, so this method was chosen to at least offer change | ||||||
|  |   detection. | ||||||
|  |  | ||||||
|  | ------- | ||||||
|  | History | ||||||
|  | ------- | ||||||
|  |  | ||||||
|  | Large object support has gone through various iterations before settling on | ||||||
|  | this implementation. | ||||||
|  |  | ||||||
|  | The primary factor driving the limitation of object size in swift is | ||||||
|  | maintaining balance among the partitions of the ring.  To maintain an even | ||||||
|  | dispersion of disk usage throughout the cluster the obvious storage pattern | ||||||
|  | was to simply split larger objects into smaller segments, which could then be | ||||||
|  | glued together during a read. | ||||||
|  |  | ||||||
|  | Before the introduction of large object support some applications were already | ||||||
|  | splitting their uploads into segments and re-assembling them on the client | ||||||
|  | side after retrieving the individual pieces.  This design allowed the client | ||||||
|  | to support backup and archiving of large data sets, but was also frequently | ||||||
|  | employed to improve performance or reduce errors due to network interruption. | ||||||
|  | The major disadvantage of this method is that knowledge of the original | ||||||
|  | partitioning scheme is required to properly reassemble the object, which is | ||||||
|  | not practical for some use cases, such as CDN origination. | ||||||
|  |  | ||||||
|  | In order to eliminate any barrier to entry for clients wanting to store | ||||||
|  | objects larger than 5GB, initially we also prototyped fully transparent | ||||||
|  | support for large object uploads.  A fully transparent implementation would | ||||||
|  | support a larger max size by automatically splitting objects into segments | ||||||
|  | during upload within the proxy without any changes to the client API.  All | ||||||
|  | segments were completely hidden from the client API. | ||||||
|  |  | ||||||
|  | This solution introduced a number of challenging failure conditions into the | ||||||
|  | cluster, wouldn't provide the client with any option to do parallel uploads, | ||||||
|  | and had no basis for a resume feature.  The transparent implementation was | ||||||
|  | deemed just too complex for the benefit. | ||||||
|  |  | ||||||
|  | The current "user manifest" design was chosen in order to provide a | ||||||
|  | transparent download of large objects to the client and still provide the | ||||||
|  | uploading client a clean API to support segmented uploads. | ||||||
|  |  | ||||||
|  | Alternative "explicit" user manifest options were discussed which would have | ||||||
|  | required a pre-defined format for listing the segments to "finalize" the | ||||||
|  | segmented upload.  While this may offer some potential advantages, it was | ||||||
|  | decided that pushing an added burden onto the client which could potentially | ||||||
|  | limit adoption should be avoided in favor of a simpler "API" (essentially just | ||||||
|  | the format of the 'X-Object-Manifest' header). | ||||||
|  |  | ||||||
|  | During development it was noted that this "implicit" user manifest approach | ||||||
|  | which is based on the path prefix can be potentially affected by the eventual | ||||||
|  | consistency window of the container listings, which could theoretically cause | ||||||
|  | a GET on the manifest object to return an invalid whole object for that short | ||||||
|  | term.  In reality you're unlikely to encounter this scenario unless you're | ||||||
|  | running very high concurrency uploads against a small testing environment | ||||||
|  | which isn't running the object-updaters or container-replicators. | ||||||
|  |  | ||||||
|  | Like all of swift, Large Object Support is living feature which will continue | ||||||
|  | to improve and may change over time. | ||||||
| @@ -23,7 +23,7 @@ class_path = swift.stats.access_processor.AccessLogProcessor | |||||||
| # load balancer private ips is for load balancer ip addresses that should be | # load balancer private ips is for load balancer ip addresses that should be | ||||||
| # counted as servicenet | # counted as servicenet | ||||||
| # lb_private_ips = | # lb_private_ips = | ||||||
| # server_name = proxy | # server_name = proxy-server | ||||||
| # user = swift | # user = swift | ||||||
| # warn_percent = 0.8 | # warn_percent = 0.8 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -569,7 +569,8 @@ def put_object(url, token, container, name, contents, content_length=None, | |||||||
|     :param container: container name that the object is in |     :param container: container name that the object is in | ||||||
|     :param name: object name to put |     :param name: object name to put | ||||||
|     :param contents: a string or a file like object to read object data from |     :param contents: a string or a file like object to read object data from | ||||||
|     :param content_length: value to send as content-length header |     :param content_length: value to send as content-length header; also limits | ||||||
|  |                            the amount read from contents | ||||||
|     :param etag: etag of contents |     :param etag: etag of contents | ||||||
|     :param chunk_size: chunk size of data to write |     :param chunk_size: chunk size of data to write | ||||||
|     :param content_type: value to send as content-type header |     :param content_type: value to send as content-type header | ||||||
| @@ -599,18 +600,24 @@ def put_object(url, token, container, name, contents, content_length=None, | |||||||
|         conn.putrequest('PUT', path) |         conn.putrequest('PUT', path) | ||||||
|         for header, value in headers.iteritems(): |         for header, value in headers.iteritems(): | ||||||
|             conn.putheader(header, value) |             conn.putheader(header, value) | ||||||
|         if not content_length: |         if content_length is None: | ||||||
|             conn.putheader('Transfer-Encoding', 'chunked') |             conn.putheader('Transfer-Encoding', 'chunked') | ||||||
|             conn.endheaders() |             conn.endheaders() | ||||||
|             chunk = contents.read(chunk_size) |             chunk = contents.read(chunk_size) | ||||||
|             while chunk: |             while chunk: | ||||||
|             if not content_length: |  | ||||||
|                 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |                 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) | ||||||
|             else: |  | ||||||
|                 conn.send(chunk) |  | ||||||
|                 chunk = contents.read(chunk_size) |                 chunk = contents.read(chunk_size) | ||||||
|         if not content_length: |  | ||||||
|             conn.send('0\r\n\r\n') |             conn.send('0\r\n\r\n') | ||||||
|  |         else: | ||||||
|  |             conn.endheaders() | ||||||
|  |             left = content_length | ||||||
|  |             while left > 0: | ||||||
|  |                 size = chunk_size | ||||||
|  |                 if size > left: | ||||||
|  |                     size = left | ||||||
|  |                 chunk = contents.read(size) | ||||||
|  |                 conn.send(chunk) | ||||||
|  |                 left -= len(chunk) | ||||||
|     else: |     else: | ||||||
|         conn.request('PUT', path, contents, headers) |         conn.request('PUT', path, contents, headers) | ||||||
|     resp = conn.getresponse() |     resp = conn.getresponse() | ||||||
|   | |||||||
| @@ -113,6 +113,17 @@ def check_object_creation(req, object_name): | |||||||
|     if not check_utf8(req.headers['Content-Type']): |     if not check_utf8(req.headers['Content-Type']): | ||||||
|         return HTTPBadRequest(request=req, body='Invalid Content-Type', |         return HTTPBadRequest(request=req, body='Invalid Content-Type', | ||||||
|                     content_type='text/plain') |                     content_type='text/plain') | ||||||
|  |     if 'x-object-manifest' in req.headers: | ||||||
|  |         value = req.headers['x-object-manifest'] | ||||||
|  |         container = prefix = None | ||||||
|  |         try: | ||||||
|  |             container, prefix = value.split('/', 1) | ||||||
|  |         except ValueError: | ||||||
|  |             pass | ||||||
|  |         if not container or not prefix or '?' in value or '&' in value or \ | ||||||
|  |                 prefix[0] == '/': | ||||||
|  |             return HTTPBadRequest(request=req, | ||||||
|  |                 body='X-Object-Manifest must in the format container/prefix') | ||||||
|     return check_metadata(req, 'object') |     return check_metadata(req, 'object') | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -394,6 +394,9 @@ class ObjectController(object): | |||||||
|                 'ETag': etag, |                 'ETag': etag, | ||||||
|                 'Content-Length': str(os.fstat(fd).st_size), |                 'Content-Length': str(os.fstat(fd).st_size), | ||||||
|             } |             } | ||||||
|  |             if 'x-object-manifest' in request.headers: | ||||||
|  |                 metadata['X-Object-Manifest'] = \ | ||||||
|  |                     request.headers['x-object-manifest'] | ||||||
|             metadata.update(val for val in request.headers.iteritems() |             metadata.update(val for val in request.headers.iteritems() | ||||||
|                     if val[0].lower().startswith('x-object-meta-') and |                     if val[0].lower().startswith('x-object-meta-') and | ||||||
|                     len(val[0]) > 14) |                     len(val[0]) > 14) | ||||||
| @@ -463,7 +466,8 @@ class ObjectController(object): | |||||||
|                         'application/octet-stream'), app_iter=file, |                         'application/octet-stream'), app_iter=file, | ||||||
|                         request=request, conditional_response=True) |                         request=request, conditional_response=True) | ||||||
|         for key, value in file.metadata.iteritems(): |         for key, value in file.metadata.iteritems(): | ||||||
|             if key.lower().startswith('x-object-meta-'): |             if key == 'X-Object-Manifest' or \ | ||||||
|  |                     key.lower().startswith('x-object-meta-'): | ||||||
|                 response.headers[key] = value |                 response.headers[key] = value | ||||||
|         response.etag = file.metadata['ETag'] |         response.etag = file.metadata['ETag'] | ||||||
|         response.last_modified = float(file.metadata['X-Timestamp']) |         response.last_modified = float(file.metadata['X-Timestamp']) | ||||||
| @@ -491,7 +495,8 @@ class ObjectController(object): | |||||||
|         response = Response(content_type=file.metadata['Content-Type'], |         response = Response(content_type=file.metadata['Content-Type'], | ||||||
|                             request=request, conditional_response=True) |                             request=request, conditional_response=True) | ||||||
|         for key, value in file.metadata.iteritems(): |         for key, value in file.metadata.iteritems(): | ||||||
|             if key.lower().startswith('x-object-meta-'): |             if key == 'X-Object-Manifest' or \ | ||||||
|  |                     key.lower().startswith('x-object-meta-'): | ||||||
|                 response.headers[key] = value |                 response.headers[key] = value | ||||||
|         response.etag = file.metadata['ETag'] |         response.etag = file.metadata['ETag'] | ||||||
|         response.last_modified = float(file.metadata['X-Timestamp']) |         response.last_modified = float(file.metadata['X-Timestamp']) | ||||||
|   | |||||||
| @@ -14,16 +14,24 @@ | |||||||
| # limitations under the License. | # limitations under the License. | ||||||
|  |  | ||||||
| from __future__ import with_statement | from __future__ import with_statement | ||||||
|  | try: | ||||||
|  |     import simplejson as json | ||||||
|  | except ImportError: | ||||||
|  |     import json | ||||||
| import mimetypes | import mimetypes | ||||||
| import os | import os | ||||||
|  | import re | ||||||
| import time | import time | ||||||
| import traceback | import traceback | ||||||
| from ConfigParser import ConfigParser | from ConfigParser import ConfigParser | ||||||
|  | from datetime import datetime | ||||||
| from urllib import unquote, quote | from urllib import unquote, quote | ||||||
| import uuid | import uuid | ||||||
| import functools | import functools | ||||||
| from gettext import gettext as _ | from gettext import gettext as _ | ||||||
|  | from hashlib import md5 | ||||||
|  |  | ||||||
|  | from eventlet import sleep | ||||||
| from eventlet.timeout import Timeout | from eventlet.timeout import Timeout | ||||||
| from webob.exc import HTTPBadRequest, HTTPMethodNotAllowed, \ | from webob.exc import HTTPBadRequest, HTTPMethodNotAllowed, \ | ||||||
|     HTTPNotFound, HTTPPreconditionFailed, \ |     HTTPNotFound, HTTPPreconditionFailed, \ | ||||||
| @@ -37,8 +45,8 @@ from swift.common.utils import get_logger, normalize_timestamp, split_path, \ | |||||||
|     cache_from_env |     cache_from_env | ||||||
| from swift.common.bufferedhttp import http_connect | from swift.common.bufferedhttp import http_connect | ||||||
| from swift.common.constraints import check_metadata, check_object_creation, \ | from swift.common.constraints import check_metadata, check_object_creation, \ | ||||||
|     check_utf8, MAX_ACCOUNT_NAME_LENGTH, MAX_CONTAINER_NAME_LENGTH, \ |     check_utf8, CONTAINER_LISTING_LIMIT, MAX_ACCOUNT_NAME_LENGTH, \ | ||||||
|     MAX_FILE_SIZE |     MAX_CONTAINER_NAME_LENGTH, MAX_FILE_SIZE | ||||||
| from swift.common.exceptions import ChunkReadTimeout, \ | from swift.common.exceptions import ChunkReadTimeout, \ | ||||||
|     ChunkWriteTimeout, ConnectionTimeout |     ChunkWriteTimeout, ConnectionTimeout | ||||||
|  |  | ||||||
| @@ -96,6 +104,161 @@ def get_container_memcache_key(account, container): | |||||||
|     return 'container/%s/%s' % (account, container) |     return 'container/%s/%s' % (account, container) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SegmentedIterable(object): | ||||||
|  |     """ | ||||||
|  |     Iterable that returns the object contents for a segmented object in Swift. | ||||||
|  |  | ||||||
|  |     If set, the response's `bytes_transferred` value will be updated (used to | ||||||
|  |     log the size of the request). Also, if there's a failure that cuts the | ||||||
|  |     transfer short, the response's `status_int` will be updated (again, just | ||||||
|  |     for logging since the original status would have already been sent to the | ||||||
|  |     client). | ||||||
|  |  | ||||||
|  |     :param controller: The ObjectController instance to work with. | ||||||
|  |     :param container: The container the object segments are within. | ||||||
|  |     :param listing: The listing of object segments to iterate over; this may | ||||||
|  |                     be an iterator or list that returns dicts with 'name' and | ||||||
|  |                     'bytes' keys. | ||||||
|  |     :param response: The webob.Response this iterable is associated with, if | ||||||
|  |                      any (default: None) | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, controller, container, listing, response=None): | ||||||
|  |         self.controller = controller | ||||||
|  |         self.container = container | ||||||
|  |         self.listing = iter(listing) | ||||||
|  |         self.segment = -1 | ||||||
|  |         self.segment_dict = None | ||||||
|  |         self.segment_peek = None | ||||||
|  |         self.seek = 0 | ||||||
|  |         self.segment_iter = None | ||||||
|  |         self.position = 0 | ||||||
|  |         self.response = response | ||||||
|  |         if not self.response: | ||||||
|  |             self.response = Response() | ||||||
|  |         self.next_get_time = 0 | ||||||
|  |  | ||||||
|  |     def _load_next_segment(self): | ||||||
|  |         """ | ||||||
|  |         Loads the self.segment_iter with the next object segment's contents. | ||||||
|  |  | ||||||
|  |         :raises: StopIteration when there are no more object segments. | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             self.segment += 1 | ||||||
|  |             self.segment_dict = self.segment_peek or self.listing.next() | ||||||
|  |             self.segment_peek = None | ||||||
|  |             partition, nodes = self.controller.app.object_ring.get_nodes( | ||||||
|  |                 self.controller.account_name, self.container, | ||||||
|  |                 self.segment_dict['name']) | ||||||
|  |             path = '/%s/%s/%s' % (self.controller.account_name, self.container, | ||||||
|  |                 self.segment_dict['name']) | ||||||
|  |             req = Request.blank(path) | ||||||
|  |             if self.seek: | ||||||
|  |                 req.range = 'bytes=%s-' % self.seek | ||||||
|  |                 self.seek = 0 | ||||||
|  |             if self.segment > 10: | ||||||
|  |                 sleep(max(self.next_get_time - time.time(), 0)) | ||||||
|  |                 self.next_get_time = time.time() + 1 | ||||||
|  |             resp = self.controller.GETorHEAD_base(req, 'Object', partition, | ||||||
|  |                 self.controller.iter_nodes(partition, nodes, | ||||||
|  |                 self.controller.app.object_ring), path, | ||||||
|  |                 self.controller.app.object_ring.replica_count) | ||||||
|  |             if resp.status_int // 100 != 2: | ||||||
|  |                 raise Exception('Could not load object segment %s: %s' % (path, | ||||||
|  |                     resp.status_int)) | ||||||
|  |             self.segment_iter = resp.app_iter | ||||||
|  |         except StopIteration: | ||||||
|  |             raise | ||||||
|  |         except Exception, err: | ||||||
|  |             if not getattr(err, 'swift_logged', False): | ||||||
|  |                 self.controller.app.logger.exception('ERROR: While processing ' | ||||||
|  |                     'manifest /%s/%s/%s %s' % (self.controller.account_name, | ||||||
|  |                     self.controller.container_name, | ||||||
|  |                     self.controller.object_name, self.controller.trans_id)) | ||||||
|  |                 err.swift_logged = True | ||||||
|  |                 self.response.status_int = 503 | ||||||
|  |             raise | ||||||
|  |  | ||||||
|  |     def next(self): | ||||||
|  |         return iter(self).next() | ||||||
|  |  | ||||||
|  |     def __iter__(self): | ||||||
|  |         """ Standard iterator function that returns the object's contents. """ | ||||||
|  |         try: | ||||||
|  |             while True: | ||||||
|  |                 if not self.segment_iter: | ||||||
|  |                     self._load_next_segment() | ||||||
|  |                 while True: | ||||||
|  |                     with ChunkReadTimeout(self.controller.app.node_timeout): | ||||||
|  |                         try: | ||||||
|  |                             chunk = self.segment_iter.next() | ||||||
|  |                             break | ||||||
|  |                         except StopIteration: | ||||||
|  |                             self._load_next_segment() | ||||||
|  |                 self.position += len(chunk) | ||||||
|  |                 self.response.bytes_transferred = getattr(self.response, | ||||||
|  |                     'bytes_transferred', 0) + len(chunk) | ||||||
|  |                 yield chunk | ||||||
|  |         except StopIteration: | ||||||
|  |             raise | ||||||
|  |         except Exception, err: | ||||||
|  |             if not getattr(err, 'swift_logged', False): | ||||||
|  |                 self.controller.app.logger.exception('ERROR: While processing ' | ||||||
|  |                     'manifest /%s/%s/%s %s' % (self.controller.account_name, | ||||||
|  |                     self.controller.container_name, | ||||||
|  |                     self.controller.object_name, self.controller.trans_id)) | ||||||
|  |                 err.swift_logged = True | ||||||
|  |                 self.response.status_int = 503 | ||||||
|  |             raise | ||||||
|  |  | ||||||
|  |     def app_iter_range(self, start, stop): | ||||||
|  |         """ | ||||||
|  |         Non-standard iterator function for use with Webob in serving Range | ||||||
|  |         requests more quickly. This will skip over segments and do a range | ||||||
|  |         request on the first segment to return data from, if needed. | ||||||
|  |  | ||||||
|  |         :param start: The first byte (zero-based) to return. None for 0. | ||||||
|  |         :param stop: The last byte (zero-based) to return. None for end. | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if start: | ||||||
|  |                 self.segment_peek = self.listing.next() | ||||||
|  |                 while start >= self.position + self.segment_peek['bytes']: | ||||||
|  |                     self.segment += 1 | ||||||
|  |                     self.position += self.segment_peek['bytes'] | ||||||
|  |                     self.segment_peek = self.listing.next() | ||||||
|  |                 self.seek = start - self.position | ||||||
|  |             else: | ||||||
|  |                 start = 0 | ||||||
|  |             if stop is not None: | ||||||
|  |                 length = stop - start | ||||||
|  |             else: | ||||||
|  |                 length = None | ||||||
|  |             for chunk in self: | ||||||
|  |                 if length is not None: | ||||||
|  |                     length -= len(chunk) | ||||||
|  |                     if length < 0: | ||||||
|  |                         # Chop off the extra: | ||||||
|  |                         self.response.bytes_transferred = \ | ||||||
|  |                            getattr(self.response, 'bytes_transferred', 0) \ | ||||||
|  |                            + length | ||||||
|  |                         yield chunk[:length] | ||||||
|  |                         break | ||||||
|  |                 yield chunk | ||||||
|  |         except StopIteration: | ||||||
|  |             raise | ||||||
|  |         except Exception, err: | ||||||
|  |             if not getattr(err, 'swift_logged', False): | ||||||
|  |                 self.controller.app.logger.exception('ERROR: While processing ' | ||||||
|  |                     'manifest /%s/%s/%s %s' % (self.controller.account_name, | ||||||
|  |                     self.controller.container_name, | ||||||
|  |                     self.controller.object_name, self.controller.trans_id)) | ||||||
|  |                 err.swift_logged = True | ||||||
|  |                 self.response.status_int = 503 | ||||||
|  |             raise | ||||||
|  |  | ||||||
|  |  | ||||||
| class Controller(object): | class Controller(object): | ||||||
|     """Base WSGI controller class for the proxy""" |     """Base WSGI controller class for the proxy""" | ||||||
|  |  | ||||||
| @@ -536,9 +699,137 @@ class ObjectController(Controller): | |||||||
|                 return aresp |                 return aresp | ||||||
|         partition, nodes = self.app.object_ring.get_nodes( |         partition, nodes = self.app.object_ring.get_nodes( | ||||||
|             self.account_name, self.container_name, self.object_name) |             self.account_name, self.container_name, self.object_name) | ||||||
|         return self.GETorHEAD_base(req, 'Object', partition, |         resp = self.GETorHEAD_base(req, 'Object', partition, | ||||||
|                 self.iter_nodes(partition, nodes, self.app.object_ring), |                 self.iter_nodes(partition, nodes, self.app.object_ring), | ||||||
|                 req.path_info, self.app.object_ring.replica_count) |                 req.path_info, self.app.object_ring.replica_count) | ||||||
|  |         # If we get a 416 Requested Range Not Satisfiable we have to check if | ||||||
|  |         # we were actually requesting a manifest object and then redo the range | ||||||
|  |         # request on the whole object. | ||||||
|  |         if resp.status_int == 416: | ||||||
|  |             req_range = req.range | ||||||
|  |             req.range = None | ||||||
|  |             resp2 = self.GETorHEAD_base(req, 'Object', partition, | ||||||
|  |                     self.iter_nodes(partition, nodes, self.app.object_ring), | ||||||
|  |                     req.path_info, self.app.object_ring.replica_count) | ||||||
|  |             if 'x-object-manifest' not in resp2.headers: | ||||||
|  |                 return resp | ||||||
|  |             resp = resp2 | ||||||
|  |             req.range = req_range | ||||||
|  |  | ||||||
|  |         if 'x-object-manifest' in resp.headers: | ||||||
|  |             lcontainer, lprefix = \ | ||||||
|  |                 resp.headers['x-object-manifest'].split('/', 1) | ||||||
|  |             lpartition, lnodes = self.app.container_ring.get_nodes( | ||||||
|  |                 self.account_name, lcontainer) | ||||||
|  |             marker = '' | ||||||
|  |             listing = [] | ||||||
|  |             while True: | ||||||
|  |                 lreq = Request.blank('/%s/%s?prefix=%s&format=json&marker=%s' % | ||||||
|  |                     (quote(self.account_name), quote(lcontainer), | ||||||
|  |                      quote(lprefix), quote(marker))) | ||||||
|  |                 lresp = self.GETorHEAD_base(lreq, 'Container', lpartition, | ||||||
|  |                     lnodes, lreq.path_info, | ||||||
|  |                     self.app.container_ring.replica_count) | ||||||
|  |                 if lresp.status_int // 100 != 2: | ||||||
|  |                     lresp = HTTPNotFound(request=req) | ||||||
|  |                     lresp.headers['X-Object-Manifest'] = \ | ||||||
|  |                         resp.headers['x-object-manifest'] | ||||||
|  |                     return lresp | ||||||
|  |                 if 'swift.authorize' in req.environ: | ||||||
|  |                     req.acl = lresp.headers.get('x-container-read') | ||||||
|  |                     aresp = req.environ['swift.authorize'](req) | ||||||
|  |                     if aresp: | ||||||
|  |                         return aresp | ||||||
|  |                 sublisting = json.loads(lresp.body) | ||||||
|  |                 if not sublisting: | ||||||
|  |                     break | ||||||
|  |                 listing.extend(sublisting) | ||||||
|  |                 if len(listing) > CONTAINER_LISTING_LIMIT: | ||||||
|  |                     break | ||||||
|  |                 marker = sublisting[-1]['name'] | ||||||
|  |  | ||||||
|  |             if len(listing) > CONTAINER_LISTING_LIMIT: | ||||||
|  |                 # We will serve large objects with a ton of segments with | ||||||
|  |                 # chunked transfer encoding. | ||||||
|  |  | ||||||
|  |                 def listing_iter(): | ||||||
|  |                     marker = '' | ||||||
|  |                     while True: | ||||||
|  |                         lreq = Request.blank( | ||||||
|  |                             '/%s/%s?prefix=%s&format=json&marker=%s' % | ||||||
|  |                             (quote(self.account_name), quote(lcontainer), | ||||||
|  |                              quote(lprefix), quote(marker))) | ||||||
|  |                         lresp = self.GETorHEAD_base(lreq, 'Container', | ||||||
|  |                             lpartition, lnodes, lreq.path_info, | ||||||
|  |                             self.app.container_ring.replica_count) | ||||||
|  |                         if lresp.status_int // 100 != 2: | ||||||
|  |                             raise Exception('Object manifest GET could not ' | ||||||
|  |                                 'continue listing: %s %s' % | ||||||
|  |                                 (req.path, lreq.path)) | ||||||
|  |                         if 'swift.authorize' in req.environ: | ||||||
|  |                             req.acl = lresp.headers.get('x-container-read') | ||||||
|  |                             aresp = req.environ['swift.authorize'](req) | ||||||
|  |                             if aresp: | ||||||
|  |                                 raise Exception('Object manifest GET could ' | ||||||
|  |                                     'not continue listing: %s %s' % | ||||||
|  |                                     (req.path, aresp)) | ||||||
|  |                         sublisting = json.loads(lresp.body) | ||||||
|  |                         if not sublisting: | ||||||
|  |                             break | ||||||
|  |                         for obj in sublisting: | ||||||
|  |                             yield obj | ||||||
|  |                         marker = sublisting[-1]['name'] | ||||||
|  |  | ||||||
|  |                 headers = { | ||||||
|  |                     'X-Object-Manifest': resp.headers['x-object-manifest'], | ||||||
|  |                     'Content-Type': resp.content_type} | ||||||
|  |                 for key, value in resp.headers.iteritems(): | ||||||
|  |                     if key.lower().startswith('x-object-meta-'): | ||||||
|  |                         headers[key] = value | ||||||
|  |                 resp = Response(headers=headers, request=req, | ||||||
|  |                                 conditional_response=True) | ||||||
|  |                 if req.method == 'HEAD': | ||||||
|  |                     # These shenanigans are because webob translates the HEAD | ||||||
|  |                     # request into a webob EmptyResponse for the body, which | ||||||
|  |                     # has a len, which eventlet translates as needing a | ||||||
|  |                     # content-length header added. So we call the original | ||||||
|  |                     # webob resp for the headers but return an empty iterator | ||||||
|  |                     # for the body. | ||||||
|  |  | ||||||
|  |                     def head_response(environ, start_response): | ||||||
|  |                         resp(environ, start_response) | ||||||
|  |                         return iter([]) | ||||||
|  |  | ||||||
|  |                     head_response.status_int = resp.status_int | ||||||
|  |                     return head_response | ||||||
|  |                 else: | ||||||
|  |                     resp.app_iter = SegmentedIterable(self, lcontainer, | ||||||
|  |                                                       listing_iter(), resp) | ||||||
|  |  | ||||||
|  |             else: | ||||||
|  |                 # For objects with a reasonable number of segments, we'll serve | ||||||
|  |                 # them with a set content-length and computed etag. | ||||||
|  |                 content_length = sum(o['bytes'] for o in listing) | ||||||
|  |                 last_modified = max(o['last_modified'] for o in listing) | ||||||
|  |                 last_modified = \ | ||||||
|  |                     datetime(*map(int, re.split('[^\d]', last_modified)[:-1])) | ||||||
|  |                 etag = md5('"'.join(o['hash'] for o in listing)).hexdigest() | ||||||
|  |                 headers = { | ||||||
|  |                     'X-Object-Manifest': resp.headers['x-object-manifest'], | ||||||
|  |                     'Content-Type': resp.content_type, | ||||||
|  |                     'Content-Length': content_length, | ||||||
|  |                     'ETag': etag} | ||||||
|  |                 for key, value in resp.headers.iteritems(): | ||||||
|  |                     if key.lower().startswith('x-object-meta-'): | ||||||
|  |                         headers[key] = value | ||||||
|  |                 resp = Response(headers=headers, request=req, | ||||||
|  |                                 conditional_response=True) | ||||||
|  |                 resp.app_iter = SegmentedIterable(self, lcontainer, listing, | ||||||
|  |                                                   resp) | ||||||
|  |                 resp.content_length = content_length | ||||||
|  |                 resp.last_modified = last_modified | ||||||
|  |  | ||||||
|  |         return resp | ||||||
|  |  | ||||||
|     @public |     @public | ||||||
|     @delay_denial |     @delay_denial | ||||||
| @@ -652,9 +943,15 @@ class ObjectController(Controller): | |||||||
|                 return source_resp |                 return source_resp | ||||||
|             self.object_name = orig_obj_name |             self.object_name = orig_obj_name | ||||||
|             self.container_name = orig_container_name |             self.container_name = orig_container_name | ||||||
|             data_source = source_resp.app_iter |  | ||||||
|             new_req = Request.blank(req.path_info, |             new_req = Request.blank(req.path_info, | ||||||
|                         environ=req.environ, headers=req.headers) |                         environ=req.environ, headers=req.headers) | ||||||
|  |             if 'x-object-manifest' in source_resp.headers: | ||||||
|  |                 data_source = iter(['']) | ||||||
|  |                 new_req.content_length = 0 | ||||||
|  |                 new_req.headers['X-Object-Manifest'] = \ | ||||||
|  |                     source_resp.headers['x-object-manifest'] | ||||||
|  |             else: | ||||||
|  |                 data_source = source_resp.app_iter | ||||||
|                 new_req.content_length = source_resp.content_length |                 new_req.content_length = source_resp.content_length | ||||||
|                 new_req.etag = source_resp.etag |                 new_req.etag = source_resp.etag | ||||||
|             # we no longer need the X-Copy-From header |             # we no longer need the X-Copy-From header | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ class AccessLogProcessor(object): | |||||||
|     """Transform proxy server access logs""" |     """Transform proxy server access logs""" | ||||||
|  |  | ||||||
|     def __init__(self, conf): |     def __init__(self, conf): | ||||||
|         self.server_name = conf.get('server_name', 'proxy') |         self.server_name = conf.get('server_name', 'proxy-server') | ||||||
|         self.lb_private_ips = [x.strip() for x in \ |         self.lb_private_ips = [x.strip() for x in \ | ||||||
|                                conf.get('lb_private_ips', '').split(',')\ |                                conf.get('lb_private_ips', '').split(',')\ | ||||||
|                                if x.strip()] |                                if x.strip()] | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ class TestObject(unittest.TestCase): | |||||||
|         if skip: |         if skip: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|         self.container = uuid4().hex |         self.container = uuid4().hex | ||||||
|  |  | ||||||
|         def put(url, token, parsed, conn): |         def put(url, token, parsed, conn): | ||||||
|             conn.request('PUT', parsed.path + '/' + self.container, '', |             conn.request('PUT', parsed.path + '/' + self.container, '', | ||||||
|                          {'X-Auth-Token': token}) |                          {'X-Auth-Token': token}) | ||||||
| @@ -24,6 +25,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 201) |         self.assertEquals(resp.status, 201) | ||||||
|         self.obj = uuid4().hex |         self.obj = uuid4().hex | ||||||
|  |  | ||||||
|         def put(url, token, parsed, conn): |         def put(url, token, parsed, conn): | ||||||
|             conn.request('PUT', '%s/%s/%s' % (parsed.path, self.container, |             conn.request('PUT', '%s/%s/%s' % (parsed.path, self.container, | ||||||
|                 self.obj), 'test', {'X-Auth-Token': token}) |                 self.obj), 'test', {'X-Auth-Token': token}) | ||||||
| @@ -35,6 +37,7 @@ class TestObject(unittest.TestCase): | |||||||
|     def tearDown(self): |     def tearDown(self): | ||||||
|         if skip: |         if skip: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|  |  | ||||||
|         def delete(url, token, parsed, conn): |         def delete(url, token, parsed, conn): | ||||||
|             conn.request('DELETE', '%s/%s/%s' % (parsed.path, self.container, |             conn.request('DELETE', '%s/%s/%s' % (parsed.path, self.container, | ||||||
|                 self.obj), '', {'X-Auth-Token': token}) |                 self.obj), '', {'X-Auth-Token': token}) | ||||||
| @@ -42,6 +45,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(delete) |         resp = retry(delete) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 204) |         self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|         def delete(url, token, parsed, conn): |         def delete(url, token, parsed, conn): | ||||||
|             conn.request('DELETE', parsed.path + '/' + self.container, '', |             conn.request('DELETE', parsed.path + '/' + self.container, '', | ||||||
|                          {'X-Auth-Token': token}) |                          {'X-Auth-Token': token}) | ||||||
| @@ -53,6 +57,7 @@ class TestObject(unittest.TestCase): | |||||||
|     def test_public_object(self): |     def test_public_object(self): | ||||||
|         if skip: |         if skip: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|  |  | ||||||
|         def get(url, token, parsed, conn): |         def get(url, token, parsed, conn): | ||||||
|             conn.request('GET', |             conn.request('GET', | ||||||
|                          '%s/%s/%s' % (parsed.path, self.container, self.obj)) |                          '%s/%s/%s' % (parsed.path, self.container, self.obj)) | ||||||
| @@ -62,6 +67,7 @@ class TestObject(unittest.TestCase): | |||||||
|             raise Exception('Should not have been able to GET') |             raise Exception('Should not have been able to GET') | ||||||
|         except Exception, err: |         except Exception, err: | ||||||
|             self.assert_(str(err).startswith('No result after ')) |             self.assert_(str(err).startswith('No result after ')) | ||||||
|  |  | ||||||
|         def post(url, token, parsed, conn): |         def post(url, token, parsed, conn): | ||||||
|             conn.request('POST', parsed.path + '/' + self.container, '', |             conn.request('POST', parsed.path + '/' + self.container, '', | ||||||
|                          {'X-Auth-Token': token, |                          {'X-Auth-Token': token, | ||||||
| @@ -73,6 +79,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(get) |         resp = retry(get) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 200) |         self.assertEquals(resp.status, 200) | ||||||
|  |  | ||||||
|         def post(url, token, parsed, conn): |         def post(url, token, parsed, conn): | ||||||
|             conn.request('POST', parsed.path + '/' + self.container, '', |             conn.request('POST', parsed.path + '/' + self.container, '', | ||||||
|                          {'X-Auth-Token': token, 'X-Container-Read': ''}) |                          {'X-Auth-Token': token, 'X-Container-Read': ''}) | ||||||
| @@ -89,6 +96,7 @@ class TestObject(unittest.TestCase): | |||||||
|     def test_private_object(self): |     def test_private_object(self): | ||||||
|         if skip or skip3: |         if skip or skip3: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|  |  | ||||||
|         # Ensure we can't access the object with the third account |         # Ensure we can't access the object with the third account | ||||||
|         def get(url, token, parsed, conn): |         def get(url, token, parsed, conn): | ||||||
|             conn.request('GET', '%s/%s/%s' % (parsed.path, self.container, |             conn.request('GET', '%s/%s/%s' % (parsed.path, self.container, | ||||||
| @@ -98,8 +106,10 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(get, use_account=3) |         resp = retry(get, use_account=3) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 403) |         self.assertEquals(resp.status, 403) | ||||||
|  |  | ||||||
|         # create a shared container writable by account3 |         # create a shared container writable by account3 | ||||||
|         shared_container = uuid4().hex |         shared_container = uuid4().hex | ||||||
|  |  | ||||||
|         def put(url, token, parsed, conn): |         def put(url, token, parsed, conn): | ||||||
|             conn.request('PUT', '%s/%s' % (parsed.path, |             conn.request('PUT', '%s/%s' % (parsed.path, | ||||||
|                                            shared_container), '', |                                            shared_container), '', | ||||||
| @@ -110,6 +120,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(put) |         resp = retry(put) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 201) |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|         # verify third account can not copy from private container |         # verify third account can not copy from private container | ||||||
|         def copy(url, token, parsed, conn): |         def copy(url, token, parsed, conn): | ||||||
|             conn.request('PUT', '%s/%s/%s' % (parsed.path, |             conn.request('PUT', '%s/%s/%s' % (parsed.path, | ||||||
| @@ -123,6 +134,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(copy, use_account=3) |         resp = retry(copy, use_account=3) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 403) |         self.assertEquals(resp.status, 403) | ||||||
|  |  | ||||||
|         # verify third account can write "obj1" to shared container |         # verify third account can write "obj1" to shared container | ||||||
|         def put(url, token, parsed, conn): |         def put(url, token, parsed, conn): | ||||||
|             conn.request('PUT', '%s/%s/%s' % (parsed.path, shared_container, |             conn.request('PUT', '%s/%s/%s' % (parsed.path, shared_container, | ||||||
| @@ -131,6 +143,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(put, use_account=3) |         resp = retry(put, use_account=3) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 201) |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|         # verify third account can copy "obj1" to shared container |         # verify third account can copy "obj1" to shared container | ||||||
|         def copy2(url, token, parsed, conn): |         def copy2(url, token, parsed, conn): | ||||||
|             conn.request('COPY', '%s/%s/%s' % (parsed.path, |             conn.request('COPY', '%s/%s/%s' % (parsed.path, | ||||||
| @@ -143,6 +156,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(copy2, use_account=3) |         resp = retry(copy2, use_account=3) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 201) |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|         # verify third account STILL can not copy from private container |         # verify third account STILL can not copy from private container | ||||||
|         def copy3(url, token, parsed, conn): |         def copy3(url, token, parsed, conn): | ||||||
|             conn.request('COPY', '%s/%s/%s' % (parsed.path, |             conn.request('COPY', '%s/%s/%s' % (parsed.path, | ||||||
| @@ -155,6 +169,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(copy3, use_account=3) |         resp = retry(copy3, use_account=3) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 403) |         self.assertEquals(resp.status, 403) | ||||||
|  |  | ||||||
|         # clean up "obj1" |         # clean up "obj1" | ||||||
|         def delete(url, token, parsed, conn): |         def delete(url, token, parsed, conn): | ||||||
|             conn.request('DELETE', '%s/%s/%s' % (parsed.path, shared_container, |             conn.request('DELETE', '%s/%s/%s' % (parsed.path, shared_container, | ||||||
| @@ -163,6 +178,7 @@ class TestObject(unittest.TestCase): | |||||||
|         resp = retry(delete) |         resp = retry(delete) | ||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 204) |         self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|         # clean up shared_container |         # clean up shared_container | ||||||
|         def delete(url, token, parsed, conn): |         def delete(url, token, parsed, conn): | ||||||
|             conn.request('DELETE', |             conn.request('DELETE', | ||||||
| @@ -173,6 +189,269 @@ class TestObject(unittest.TestCase): | |||||||
|         resp.read() |         resp.read() | ||||||
|         self.assertEquals(resp.status, 204) |         self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |     def test_manifest(self): | ||||||
|  |         if skip: | ||||||
|  |             raise SkipTest | ||||||
|  |         # Data for the object segments | ||||||
|  |         segments1 = ['one', 'two', 'three', 'four', 'five'] | ||||||
|  |         segments2 = ['six', 'seven', 'eight'] | ||||||
|  |         segments3 = ['nine', 'ten', 'eleven'] | ||||||
|  |  | ||||||
|  |         # Upload the first set of segments | ||||||
|  |         def put(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('PUT', '%s/%s/segments1/%s' % (parsed.path, | ||||||
|  |                 self.container, str(objnum)), segments1[objnum], | ||||||
|  |                 {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         for objnum in xrange(len(segments1)): | ||||||
|  |             resp = retry(put, objnum) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Upload the manifest | ||||||
|  |         def put(url, token, parsed, conn): | ||||||
|  |             conn.request('PUT', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token, | ||||||
|  |                 'X-Object-Manifest': '%s/segments1/' % self.container, | ||||||
|  |                 'Content-Type': 'text/jibberish', 'Content-Length': '0'}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(put) | ||||||
|  |         resp.read() | ||||||
|  |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Get the manifest (should get all the segments as the body) | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments1)) | ||||||
|  |         self.assertEquals(resp.status, 200) | ||||||
|  |         self.assertEquals(resp.getheader('content-type'), 'text/jibberish') | ||||||
|  |  | ||||||
|  |         # Get with a range at the start of the second segment | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token, 'Range': | ||||||
|  |                 'bytes=3-'}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments1[1:])) | ||||||
|  |         self.assertEquals(resp.status, 206) | ||||||
|  |  | ||||||
|  |         # Get with a range in the middle of the second segment | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token, 'Range': | ||||||
|  |                 'bytes=5-'}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments1)[5:]) | ||||||
|  |         self.assertEquals(resp.status, 206) | ||||||
|  |  | ||||||
|  |         # Get with a full start and stop range | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token, 'Range': | ||||||
|  |                 'bytes=5-10'}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments1)[5:11]) | ||||||
|  |         self.assertEquals(resp.status, 206) | ||||||
|  |  | ||||||
|  |         # Upload the second set of segments | ||||||
|  |         def put(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('PUT', '%s/%s/segments2/%s' % (parsed.path, | ||||||
|  |                 self.container, str(objnum)), segments2[objnum], | ||||||
|  |                 {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         for objnum in xrange(len(segments2)): | ||||||
|  |             resp = retry(put, objnum) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Get the manifest (should still be the first segments of course) | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments1)) | ||||||
|  |         self.assertEquals(resp.status, 200) | ||||||
|  |  | ||||||
|  |         # Update the manifest | ||||||
|  |         def put(url, token, parsed, conn): | ||||||
|  |             conn.request('PUT', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token, | ||||||
|  |                 'X-Object-Manifest': '%s/segments2/' % self.container, | ||||||
|  |                 'Content-Length': '0'}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(put) | ||||||
|  |         resp.read() | ||||||
|  |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Get the manifest (should be the second set of segments now) | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments2)) | ||||||
|  |         self.assertEquals(resp.status, 200) | ||||||
|  |  | ||||||
|  |         if not skip3: | ||||||
|  |  | ||||||
|  |             # Ensure we can't access the manifest with the third account | ||||||
|  |             def get(url, token, parsed, conn): | ||||||
|  |                 conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                     self.container), '', {'X-Auth-Token': token}) | ||||||
|  |                 return check_response(conn) | ||||||
|  |             resp = retry(get, use_account=3) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 403) | ||||||
|  |  | ||||||
|  |             # Grant access to the third account | ||||||
|  |             def post(url, token, parsed, conn): | ||||||
|  |                 conn.request('POST', '%s/%s' % (parsed.path, self.container), | ||||||
|  |                     '', {'X-Auth-Token': token, 'X-Container-Read': | ||||||
|  |                     swift_test_user[2]}) | ||||||
|  |                 return check_response(conn) | ||||||
|  |             resp = retry(post) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |             # The third account should be able to get the manifest now | ||||||
|  |             def get(url, token, parsed, conn): | ||||||
|  |                 conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                     self.container), '', {'X-Auth-Token': token}) | ||||||
|  |                 return check_response(conn) | ||||||
|  |             resp = retry(get, use_account=3) | ||||||
|  |             self.assertEquals(resp.read(), ''.join(segments2)) | ||||||
|  |             self.assertEquals(resp.status, 200) | ||||||
|  |  | ||||||
|  |         # Create another container for the third set of segments | ||||||
|  |         acontainer = uuid4().hex | ||||||
|  |  | ||||||
|  |         def put(url, token, parsed, conn): | ||||||
|  |             conn.request('PUT', parsed.path + '/' + acontainer, '', | ||||||
|  |                          {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(put) | ||||||
|  |         resp.read() | ||||||
|  |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Upload the third set of segments in the other container | ||||||
|  |         def put(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('PUT', '%s/%s/segments3/%s' % (parsed.path, | ||||||
|  |                 acontainer, str(objnum)), segments3[objnum], | ||||||
|  |                 {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         for objnum in xrange(len(segments3)): | ||||||
|  |             resp = retry(put, objnum) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Update the manifest | ||||||
|  |         def put(url, token, parsed, conn): | ||||||
|  |             conn.request('PUT', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token, | ||||||
|  |                 'X-Object-Manifest': '%s/segments3/' % acontainer, | ||||||
|  |                 'Content-Length': '0'}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(put) | ||||||
|  |         resp.read() | ||||||
|  |         self.assertEquals(resp.status, 201) | ||||||
|  |  | ||||||
|  |         # Get the manifest to ensure it's the third set of segments | ||||||
|  |         def get(url, token, parsed, conn): | ||||||
|  |             conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(get) | ||||||
|  |         self.assertEquals(resp.read(), ''.join(segments3)) | ||||||
|  |         self.assertEquals(resp.status, 200) | ||||||
|  |  | ||||||
|  |         if not skip3: | ||||||
|  |  | ||||||
|  |             # Ensure we can't access the manifest with the third account | ||||||
|  |             # (because the segments are in a protected container even if the | ||||||
|  |             # manifest itself is not). | ||||||
|  |  | ||||||
|  |             def get(url, token, parsed, conn): | ||||||
|  |                 conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                     self.container), '', {'X-Auth-Token': token}) | ||||||
|  |                 return check_response(conn) | ||||||
|  |             resp = retry(get, use_account=3) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 403) | ||||||
|  |  | ||||||
|  |             # Grant access to the third account | ||||||
|  |             def post(url, token, parsed, conn): | ||||||
|  |                 conn.request('POST', '%s/%s' % (parsed.path, acontainer), | ||||||
|  |                     '', {'X-Auth-Token': token, 'X-Container-Read': | ||||||
|  |                     swift_test_user[2]}) | ||||||
|  |                 return check_response(conn) | ||||||
|  |             resp = retry(post) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |             # The third account should be able to get the manifest now | ||||||
|  |             def get(url, token, parsed, conn): | ||||||
|  |                 conn.request('GET', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                     self.container), '', {'X-Auth-Token': token}) | ||||||
|  |                 return check_response(conn) | ||||||
|  |             resp = retry(get, use_account=3) | ||||||
|  |             self.assertEquals(resp.read(), ''.join(segments3)) | ||||||
|  |             self.assertEquals(resp.status, 200) | ||||||
|  |  | ||||||
|  |         # Delete the manifest | ||||||
|  |         def delete(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('DELETE', '%s/%s/manifest' % (parsed.path, | ||||||
|  |                 self.container), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(delete, objnum) | ||||||
|  |         resp.read() | ||||||
|  |         self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |         # Delete the third set of segments | ||||||
|  |         def delete(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('DELETE', '%s/%s/segments3/%s' % (parsed.path, | ||||||
|  |                 acontainer, str(objnum)), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         for objnum in xrange(len(segments3)): | ||||||
|  |             resp = retry(delete, objnum) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |         # Delete the second set of segments | ||||||
|  |         def delete(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('DELETE', '%s/%s/segments2/%s' % (parsed.path, | ||||||
|  |                 self.container, str(objnum)), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         for objnum in xrange(len(segments2)): | ||||||
|  |             resp = retry(delete, objnum) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |         # Delete the first set of segments | ||||||
|  |         def delete(url, token, parsed, conn, objnum): | ||||||
|  |             conn.request('DELETE', '%s/%s/segments1/%s' % (parsed.path, | ||||||
|  |                 self.container, str(objnum)), '', {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         for objnum in xrange(len(segments1)): | ||||||
|  |             resp = retry(delete, objnum) | ||||||
|  |             resp.read() | ||||||
|  |             self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |         # Delete the extra container | ||||||
|  |         def delete(url, token, parsed, conn): | ||||||
|  |             conn.request('DELETE', '%s/%s' % (parsed.path, acontainer), '', | ||||||
|  |                 {'X-Auth-Token': token}) | ||||||
|  |             return check_response(conn) | ||||||
|  |         resp = retry(delete) | ||||||
|  |         resp.read() | ||||||
|  |         self.assertEquals(resp.status, 204) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ from webob.exc import HTTPBadRequest, HTTPLengthRequired, \ | |||||||
|  |  | ||||||
| from swift.common import constraints | from swift.common import constraints | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestConstraints(unittest.TestCase): | class TestConstraints(unittest.TestCase): | ||||||
|  |  | ||||||
|     def test_check_metadata_empty(self): |     def test_check_metadata_empty(self): | ||||||
| @@ -137,6 +138,32 @@ class TestConstraints(unittest.TestCase): | |||||||
|         self.assert_(isinstance(resp, HTTPBadRequest)) |         self.assert_(isinstance(resp, HTTPBadRequest)) | ||||||
|         self.assert_('Content-Type' in resp.body) |         self.assert_('Content-Type' in resp.body) | ||||||
|  |  | ||||||
|  |     def test_check_object_manifest_header(self): | ||||||
|  |         resp = constraints.check_object_creation(Request.blank('/', | ||||||
|  |             headers={'X-Object-Manifest': 'container/prefix', 'Content-Length': | ||||||
|  |             '0', 'Content-Type': 'text/plain'}), 'manifest') | ||||||
|  |         self.assert_(not resp) | ||||||
|  |         resp = constraints.check_object_creation(Request.blank('/', | ||||||
|  |             headers={'X-Object-Manifest': 'container', 'Content-Length': '0', | ||||||
|  |             'Content-Type': 'text/plain'}), 'manifest') | ||||||
|  |         self.assert_(isinstance(resp, HTTPBadRequest)) | ||||||
|  |         resp = constraints.check_object_creation(Request.blank('/', | ||||||
|  |             headers={'X-Object-Manifest': '/container/prefix', | ||||||
|  |             'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest') | ||||||
|  |         self.assert_(isinstance(resp, HTTPBadRequest)) | ||||||
|  |         resp = constraints.check_object_creation(Request.blank('/', | ||||||
|  |             headers={'X-Object-Manifest': 'container/prefix?query=param', | ||||||
|  |             'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest') | ||||||
|  |         self.assert_(isinstance(resp, HTTPBadRequest)) | ||||||
|  |         resp = constraints.check_object_creation(Request.blank('/', | ||||||
|  |             headers={'X-Object-Manifest': 'container/prefix&query=param', | ||||||
|  |             'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest') | ||||||
|  |         self.assert_(isinstance(resp, HTTPBadRequest)) | ||||||
|  |         resp = constraints.check_object_creation(Request.blank('/', | ||||||
|  |             headers={'X-Object-Manifest': 'http://host/container/prefix', | ||||||
|  |             'Content-Length': '0', 'Content-Type': 'text/plain'}), 'manifest') | ||||||
|  |         self.assert_(isinstance(resp, HTTPBadRequest)) | ||||||
|  |  | ||||||
|     def test_check_mount(self): |     def test_check_mount(self): | ||||||
|         self.assertFalse(constraints.check_mount('', '')) |         self.assertFalse(constraints.check_mount('', '')) | ||||||
|         constraints.os = MockTrue() # mock os module |         constraints.os = MockTrue() # mock os module | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|         self.path_to_test_xfs = os.environ.get('PATH_TO_TEST_XFS') |         self.path_to_test_xfs = os.environ.get('PATH_TO_TEST_XFS') | ||||||
|         if not self.path_to_test_xfs or \ |         if not self.path_to_test_xfs or \ | ||||||
|                 not os.path.exists(self.path_to_test_xfs): |                 not os.path.exists(self.path_to_test_xfs): | ||||||
|             print >>sys.stderr, 'WARNING: PATH_TO_TEST_XFS not set or not ' \ |             print >> sys.stderr, 'WARNING: PATH_TO_TEST_XFS not set or not ' \ | ||||||
|                 'pointing to a valid directory.\n' \ |                 'pointing to a valid directory.\n' \ | ||||||
|                 'Please set PATH_TO_TEST_XFS to a directory on an XFS file ' \ |                 'Please set PATH_TO_TEST_XFS to a directory on an XFS file ' \ | ||||||
|                 'system for testing.' |                 'system for testing.' | ||||||
| @@ -77,7 +77,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|         self.assertEquals(resp.status_int, 201) |         self.assertEquals(resp.status_int, 201) | ||||||
|  |  | ||||||
|         timestamp = normalize_timestamp(time()) |         timestamp = normalize_timestamp(time()) | ||||||
|         req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, |         req = Request.blank('/sda1/p/a/c/o', | ||||||
|  |                             environ={'REQUEST_METHOD': 'POST'}, | ||||||
|                             headers={'X-Timestamp': timestamp, |                             headers={'X-Timestamp': timestamp, | ||||||
|                                      'X-Object-Meta-3': 'Three', |                                      'X-Object-Meta-3': 'Three', | ||||||
|                                      'X-Object-Meta-4': 'Four', |                                      'X-Object-Meta-4': 'Four', | ||||||
| @@ -95,7 +96,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|         if not self.path_to_test_xfs: |         if not self.path_to_test_xfs: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|         timestamp = normalize_timestamp(time()) |         timestamp = normalize_timestamp(time()) | ||||||
|         req = Request.blank('/sda1/p/a/c/fail', environ={'REQUEST_METHOD': 'POST'}, |         req = Request.blank('/sda1/p/a/c/fail', | ||||||
|  |                             environ={'REQUEST_METHOD': 'POST'}, | ||||||
|                             headers={'X-Timestamp': timestamp, |                             headers={'X-Timestamp': timestamp, | ||||||
|                                      'X-Object-Meta-1': 'One', |                                      'X-Object-Meta-1': 'One', | ||||||
|                                      'X-Object-Meta-2': 'Two', |                                      'X-Object-Meta-2': 'Two', | ||||||
| @@ -116,29 +118,37 @@ class TestObjectController(unittest.TestCase): | |||||||
|     def test_POST_container_connection(self): |     def test_POST_container_connection(self): | ||||||
|         if not self.path_to_test_xfs: |         if not self.path_to_test_xfs: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|  |  | ||||||
|         def mock_http_connect(response, with_exc=False): |         def mock_http_connect(response, with_exc=False): | ||||||
|  |  | ||||||
|             class FakeConn(object): |             class FakeConn(object): | ||||||
|  |  | ||||||
|                 def __init__(self, status, with_exc): |                 def __init__(self, status, with_exc): | ||||||
|                     self.status = status |                     self.status = status | ||||||
|                     self.reason = 'Fake' |                     self.reason = 'Fake' | ||||||
|                     self.host = '1.2.3.4' |                     self.host = '1.2.3.4' | ||||||
|                     self.port = '1234' |                     self.port = '1234' | ||||||
|                     self.with_exc = with_exc |                     self.with_exc = with_exc | ||||||
|  |  | ||||||
|                 def getresponse(self): |                 def getresponse(self): | ||||||
|                     if self.with_exc: |                     if self.with_exc: | ||||||
|                         raise Exception('test') |                         raise Exception('test') | ||||||
|                     return self |                     return self | ||||||
|  |  | ||||||
|                 def read(self, amt=None): |                 def read(self, amt=None): | ||||||
|                     return '' |                     return '' | ||||||
|  |  | ||||||
|             return lambda *args, **kwargs: FakeConn(response, with_exc) |             return lambda *args, **kwargs: FakeConn(response, with_exc) | ||||||
|  |  | ||||||
|         old_http_connect = object_server.http_connect |         old_http_connect = object_server.http_connect | ||||||
|         try: |         try: | ||||||
|             timestamp = normalize_timestamp(time()) |             timestamp = normalize_timestamp(time()) | ||||||
|             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, |             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': | ||||||
|                     headers={'X-Timestamp': timestamp, 'Content-Type': 'text/plain', |                 'POST'}, headers={'X-Timestamp': timestamp, 'Content-Type': | ||||||
|                              'Content-Length': '0'}) |                 'text/plain', 'Content-Length': '0'}) | ||||||
|             resp = self.object_controller.PUT(req) |             resp = self.object_controller.PUT(req) | ||||||
|             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, |             req = Request.blank('/sda1/p/a/c/o', | ||||||
|  |                     environ={'REQUEST_METHOD': 'POST'}, | ||||||
|                     headers={'X-Timestamp': timestamp, |                     headers={'X-Timestamp': timestamp, | ||||||
|                              'X-Container-Host': '1.2.3.4:0', |                              'X-Container-Host': '1.2.3.4:0', | ||||||
|                              'X-Container-Partition': '3', |                              'X-Container-Partition': '3', | ||||||
| @@ -148,7 +158,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|             object_server.http_connect = mock_http_connect(202) |             object_server.http_connect = mock_http_connect(202) | ||||||
|             resp = self.object_controller.POST(req) |             resp = self.object_controller.POST(req) | ||||||
|             self.assertEquals(resp.status_int, 202) |             self.assertEquals(resp.status_int, 202) | ||||||
|             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, |             req = Request.blank('/sda1/p/a/c/o', | ||||||
|  |                     environ={'REQUEST_METHOD': 'POST'}, | ||||||
|                     headers={'X-Timestamp': timestamp, |                     headers={'X-Timestamp': timestamp, | ||||||
|                              'X-Container-Host': '1.2.3.4:0', |                              'X-Container-Host': '1.2.3.4:0', | ||||||
|                              'X-Container-Partition': '3', |                              'X-Container-Partition': '3', | ||||||
| @@ -158,7 +169,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|             object_server.http_connect = mock_http_connect(202, with_exc=True) |             object_server.http_connect = mock_http_connect(202, with_exc=True) | ||||||
|             resp = self.object_controller.POST(req) |             resp = self.object_controller.POST(req) | ||||||
|             self.assertEquals(resp.status_int, 202) |             self.assertEquals(resp.status_int, 202) | ||||||
|             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, |             req = Request.blank('/sda1/p/a/c/o', | ||||||
|  |                     environ={'REQUEST_METHOD': 'POST'}, | ||||||
|                     headers={'X-Timestamp': timestamp, |                     headers={'X-Timestamp': timestamp, | ||||||
|                              'X-Container-Host': '1.2.3.4:0', |                              'X-Container-Host': '1.2.3.4:0', | ||||||
|                              'X-Container-Partition': '3', |                              'X-Container-Partition': '3', | ||||||
| @@ -226,7 +238,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|             timestamp + '.data') |             timestamp + '.data') | ||||||
|         self.assert_(os.path.isfile(objfile)) |         self.assert_(os.path.isfile(objfile)) | ||||||
|         self.assertEquals(open(objfile).read(), 'VERIFY') |         self.assertEquals(open(objfile).read(), 'VERIFY') | ||||||
|         self.assertEquals(pickle.loads(getxattr(objfile, object_server.METADATA_KEY)), |         self.assertEquals(pickle.loads(getxattr(objfile, | ||||||
|  |                             object_server.METADATA_KEY)), | ||||||
|                           {'X-Timestamp': timestamp, |                           {'X-Timestamp': timestamp, | ||||||
|                            'Content-Length': '6', |                            'Content-Length': '6', | ||||||
|                            'ETag': '0b4c12d7e0a73840c1c4f148fda3b037', |                            'ETag': '0b4c12d7e0a73840c1c4f148fda3b037', | ||||||
| @@ -258,7 +271,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|             timestamp + '.data') |             timestamp + '.data') | ||||||
|         self.assert_(os.path.isfile(objfile)) |         self.assert_(os.path.isfile(objfile)) | ||||||
|         self.assertEquals(open(objfile).read(), 'VERIFY TWO') |         self.assertEquals(open(objfile).read(), 'VERIFY TWO') | ||||||
|         self.assertEquals(pickle.loads(getxattr(objfile, object_server.METADATA_KEY)), |         self.assertEquals(pickle.loads(getxattr(objfile, | ||||||
|  |                             object_server.METADATA_KEY)), | ||||||
|                           {'X-Timestamp': timestamp, |                           {'X-Timestamp': timestamp, | ||||||
|                            'Content-Length': '10', |                            'Content-Length': '10', | ||||||
|                            'ETag': 'b381a4c5dab1eaa1eb9711fa647cd039', |                            'ETag': 'b381a4c5dab1eaa1eb9711fa647cd039', | ||||||
| @@ -304,7 +318,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|             timestamp + '.data') |             timestamp + '.data') | ||||||
|         self.assert_(os.path.isfile(objfile)) |         self.assert_(os.path.isfile(objfile)) | ||||||
|         self.assertEquals(open(objfile).read(), 'VERIFY THREE') |         self.assertEquals(open(objfile).read(), 'VERIFY THREE') | ||||||
|         self.assertEquals(pickle.loads(getxattr(objfile, object_server.METADATA_KEY)), |         self.assertEquals(pickle.loads(getxattr(objfile, | ||||||
|  |         object_server.METADATA_KEY)), | ||||||
|                           {'X-Timestamp': timestamp, |                           {'X-Timestamp': timestamp, | ||||||
|                            'Content-Length': '12', |                            'Content-Length': '12', | ||||||
|                            'ETag': 'b114ab7b90d9ccac4bd5d99cc7ebb568', |                            'ETag': 'b114ab7b90d9ccac4bd5d99cc7ebb568', | ||||||
| @@ -316,25 +331,33 @@ class TestObjectController(unittest.TestCase): | |||||||
|     def test_PUT_container_connection(self): |     def test_PUT_container_connection(self): | ||||||
|         if not self.path_to_test_xfs: |         if not self.path_to_test_xfs: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|  |  | ||||||
|         def mock_http_connect(response, with_exc=False): |         def mock_http_connect(response, with_exc=False): | ||||||
|  |  | ||||||
|             class FakeConn(object): |             class FakeConn(object): | ||||||
|  |  | ||||||
|                 def __init__(self, status, with_exc): |                 def __init__(self, status, with_exc): | ||||||
|                     self.status = status |                     self.status = status | ||||||
|                     self.reason = 'Fake' |                     self.reason = 'Fake' | ||||||
|                     self.host = '1.2.3.4' |                     self.host = '1.2.3.4' | ||||||
|                     self.port = '1234' |                     self.port = '1234' | ||||||
|                     self.with_exc = with_exc |                     self.with_exc = with_exc | ||||||
|  |  | ||||||
|                 def getresponse(self): |                 def getresponse(self): | ||||||
|                     if self.with_exc: |                     if self.with_exc: | ||||||
|                         raise Exception('test') |                         raise Exception('test') | ||||||
|                     return self |                     return self | ||||||
|  |  | ||||||
|                 def read(self, amt=None): |                 def read(self, amt=None): | ||||||
|                     return '' |                     return '' | ||||||
|  |  | ||||||
|             return lambda *args, **kwargs: FakeConn(response, with_exc) |             return lambda *args, **kwargs: FakeConn(response, with_exc) | ||||||
|  |  | ||||||
|         old_http_connect = object_server.http_connect |         old_http_connect = object_server.http_connect | ||||||
|         try: |         try: | ||||||
|             timestamp = normalize_timestamp(time()) |             timestamp = normalize_timestamp(time()) | ||||||
|             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, |             req = Request.blank('/sda1/p/a/c/o', | ||||||
|  |                     environ={'REQUEST_METHOD': 'POST'}, | ||||||
|                     headers={'X-Timestamp': timestamp, |                     headers={'X-Timestamp': timestamp, | ||||||
|                              'X-Container-Host': '1.2.3.4:0', |                              'X-Container-Host': '1.2.3.4:0', | ||||||
|                              'X-Container-Partition': '3', |                              'X-Container-Partition': '3', | ||||||
| @@ -555,7 +578,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|         self.assertEquals(resp.status_int, 200) |         self.assertEquals(resp.status_int, 200) | ||||||
|         self.assertEquals(resp.etag, etag) |         self.assertEquals(resp.etag, etag) | ||||||
|  |  | ||||||
|         req = Request.blank('/sda1/p/a/c/o2', environ={'REQUEST_METHOD': 'GET'}, |         req = Request.blank('/sda1/p/a/c/o2', | ||||||
|  |                             environ={'REQUEST_METHOD': 'GET'}, | ||||||
|                             headers={'If-Match': '*'}) |                             headers={'If-Match': '*'}) | ||||||
|         resp = self.object_controller.GET(req) |         resp = self.object_controller.GET(req) | ||||||
|         self.assertEquals(resp.status_int, 412) |         self.assertEquals(resp.status_int, 412) | ||||||
| @@ -715,7 +739,8 @@ class TestObjectController(unittest.TestCase): | |||||||
|         """ Test swift.object_server.ObjectController.DELETE """ |         """ Test swift.object_server.ObjectController.DELETE """ | ||||||
|         if not self.path_to_test_xfs: |         if not self.path_to_test_xfs: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|         req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'DELETE'}) |         req = Request.blank('/sda1/p/a/c', | ||||||
|  |                             environ={'REQUEST_METHOD': 'DELETE'}) | ||||||
|         resp = self.object_controller.DELETE(req) |         resp = self.object_controller.DELETE(req) | ||||||
|         self.assertEquals(resp.status_int, 400) |         self.assertEquals(resp.status_int, 400) | ||||||
|  |  | ||||||
| @@ -916,21 +941,26 @@ class TestObjectController(unittest.TestCase): | |||||||
|     def test_disk_file_mkstemp_creates_dir(self): |     def test_disk_file_mkstemp_creates_dir(self): | ||||||
|         tmpdir = os.path.join(self.testdir, 'sda1', 'tmp') |         tmpdir = os.path.join(self.testdir, 'sda1', 'tmp') | ||||||
|         os.rmdir(tmpdir) |         os.rmdir(tmpdir) | ||||||
|         with object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o').mkstemp(): |         with object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', | ||||||
|  |                 'o').mkstemp(): | ||||||
|             self.assert_(os.path.exists(tmpdir)) |             self.assert_(os.path.exists(tmpdir)) | ||||||
|  |  | ||||||
|     def test_max_upload_time(self): |     def test_max_upload_time(self): | ||||||
|         if not self.path_to_test_xfs: |         if not self.path_to_test_xfs: | ||||||
|             raise SkipTest |             raise SkipTest | ||||||
|  |  | ||||||
|         class SlowBody(): |         class SlowBody(): | ||||||
|  |  | ||||||
|             def __init__(self): |             def __init__(self): | ||||||
|                 self.sent = 0 |                 self.sent = 0 | ||||||
|  |  | ||||||
|             def read(self, size=-1): |             def read(self, size=-1): | ||||||
|                 if self.sent < 4: |                 if self.sent < 4: | ||||||
|                     sleep(0.1) |                     sleep(0.1) | ||||||
|                     self.sent += 1 |                     self.sent += 1 | ||||||
|                     return ' ' |                     return ' ' | ||||||
|                 return '' |                 return '' | ||||||
|  |  | ||||||
|         req = Request.blank('/sda1/p/a/c/o', |         req = Request.blank('/sda1/p/a/c/o', | ||||||
|             environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()}, |             environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()}, | ||||||
|             headers={'X-Timestamp': normalize_timestamp(time()), |             headers={'X-Timestamp': normalize_timestamp(time()), | ||||||
| @@ -946,14 +976,18 @@ class TestObjectController(unittest.TestCase): | |||||||
|         self.assertEquals(resp.status_int, 408) |         self.assertEquals(resp.status_int, 408) | ||||||
|  |  | ||||||
|     def test_short_body(self): |     def test_short_body(self): | ||||||
|  |  | ||||||
|         class ShortBody(): |         class ShortBody(): | ||||||
|  |  | ||||||
|             def __init__(self): |             def __init__(self): | ||||||
|                 self.sent = False |                 self.sent = False | ||||||
|  |  | ||||||
|             def read(self, size=-1): |             def read(self, size=-1): | ||||||
|                 if not self.sent: |                 if not self.sent: | ||||||
|                     self.sent = True |                     self.sent = True | ||||||
|                     return '   ' |                     return '   ' | ||||||
|                 return '' |                 return '' | ||||||
|  |  | ||||||
|         req = Request.blank('/sda1/p/a/c/o', |         req = Request.blank('/sda1/p/a/c/o', | ||||||
|             environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': ShortBody()}, |             environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': ShortBody()}, | ||||||
|             headers={'X-Timestamp': normalize_timestamp(time()), |             headers={'X-Timestamp': normalize_timestamp(time()), | ||||||
| @@ -1001,11 +1035,37 @@ class TestObjectController(unittest.TestCase): | |||||||
|         resp = self.object_controller.GET(req) |         resp = self.object_controller.GET(req) | ||||||
|         self.assertEquals(resp.status_int, 200) |         self.assertEquals(resp.status_int, 200) | ||||||
|         self.assertEquals(resp.headers['content-encoding'], 'gzip') |         self.assertEquals(resp.headers['content-encoding'], 'gzip') | ||||||
|         req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'HEAD'}) |         req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': | ||||||
|  |             'HEAD'}) | ||||||
|         resp = self.object_controller.HEAD(req) |         resp = self.object_controller.HEAD(req) | ||||||
|         self.assertEquals(resp.status_int, 200) |         self.assertEquals(resp.status_int, 200) | ||||||
|         self.assertEquals(resp.headers['content-encoding'], 'gzip') |         self.assertEquals(resp.headers['content-encoding'], 'gzip') | ||||||
|  |  | ||||||
|  |     def test_manifest_header(self): | ||||||
|  |         if not self.path_to_test_xfs: | ||||||
|  |             raise SkipTest | ||||||
|  |         timestamp = normalize_timestamp(time()) | ||||||
|  |         req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, | ||||||
|  |                 headers={'X-Timestamp': timestamp, | ||||||
|  |                          'Content-Type': 'text/plain', | ||||||
|  |                          'Content-Length': '0', | ||||||
|  |                          'X-Object-Manifest': 'c/o/'}) | ||||||
|  |         resp = self.object_controller.PUT(req) | ||||||
|  |         self.assertEquals(resp.status_int, 201) | ||||||
|  |         objfile = os.path.join(self.testdir, 'sda1', | ||||||
|  |             storage_directory(object_server.DATADIR, 'p', hash_path('a', 'c', | ||||||
|  |             'o')), timestamp + '.data') | ||||||
|  |         self.assert_(os.path.isfile(objfile)) | ||||||
|  |         self.assertEquals(pickle.loads(getxattr(objfile, | ||||||
|  |             object_server.METADATA_KEY)), {'X-Timestamp': timestamp, | ||||||
|  |             'Content-Length': '0', 'Content-Type': 'text/plain', 'name': | ||||||
|  |             '/a/c/o', 'X-Object-Manifest': 'c/o/', 'ETag': | ||||||
|  |             'd41d8cd98f00b204e9800998ecf8427e'}) | ||||||
|  |         req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'GET'}) | ||||||
|  |         resp = self.object_controller.GET(req) | ||||||
|  |         self.assertEquals(resp.status_int, 200) | ||||||
|  |         self.assertEquals(resp.headers.get('x-object-manifest'), 'c/o/') | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
| @@ -35,8 +35,8 @@ import eventlet | |||||||
| from eventlet import sleep, spawn, TimeoutError, util, wsgi, listen | from eventlet import sleep, spawn, TimeoutError, util, wsgi, listen | ||||||
| from eventlet.timeout import Timeout | from eventlet.timeout import Timeout | ||||||
| import simplejson | import simplejson | ||||||
| from webob import Request | from webob import Request, Response | ||||||
| from webob.exc import HTTPUnauthorized | from webob.exc import HTTPNotFound, HTTPUnauthorized | ||||||
|  |  | ||||||
| from test.unit import connect_tcp, readuntil2crlfs | from test.unit import connect_tcp, readuntil2crlfs | ||||||
| from swift.proxy import server as proxy_server | from swift.proxy import server as proxy_server | ||||||
| @@ -53,7 +53,9 @@ logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) | |||||||
|  |  | ||||||
|  |  | ||||||
| def fake_http_connect(*code_iter, **kwargs): | def fake_http_connect(*code_iter, **kwargs): | ||||||
|  |  | ||||||
|     class FakeConn(object): |     class FakeConn(object): | ||||||
|  |  | ||||||
|         def __init__(self, status, etag=None, body=''): |         def __init__(self, status, etag=None, body=''): | ||||||
|             self.status = status |             self.status = status | ||||||
|             self.reason = 'Fake' |             self.reason = 'Fake' | ||||||
| @@ -160,6 +162,7 @@ class FakeRing(object): | |||||||
|  |  | ||||||
|  |  | ||||||
| class FakeMemcache(object): | class FakeMemcache(object): | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.store = {} |         self.store = {} | ||||||
|  |  | ||||||
| @@ -372,9 +375,12 @@ class TestController(unittest.TestCase): | |||||||
| class TestProxyServer(unittest.TestCase): | class TestProxyServer(unittest.TestCase): | ||||||
|  |  | ||||||
|     def test_unhandled_exception(self): |     def test_unhandled_exception(self): | ||||||
|  |  | ||||||
|         class MyApp(proxy_server.Application): |         class MyApp(proxy_server.Application): | ||||||
|  |  | ||||||
|             def get_controller(self, path): |             def get_controller(self, path): | ||||||
|                 raise Exception('this shouldnt be caught') |                 raise Exception('this shouldnt be caught') | ||||||
|  |  | ||||||
|         app = MyApp(None, FakeMemcache(), account_ring=FakeRing(), |         app = MyApp(None, FakeMemcache(), account_ring=FakeRing(), | ||||||
|                 container_ring=FakeRing(), object_ring=FakeRing()) |                 container_ring=FakeRing(), object_ring=FakeRing()) | ||||||
|         req = Request.blank('/account', environ={'REQUEST_METHOD': 'HEAD'}) |         req = Request.blank('/account', environ={'REQUEST_METHOD': 'HEAD'}) | ||||||
| @@ -497,8 +503,11 @@ class TestObjectController(unittest.TestCase): | |||||||
|             test_status_map((200, 200, 204, 500, 404), 503) |             test_status_map((200, 200, 204, 500, 404), 503) | ||||||
|  |  | ||||||
|     def test_PUT_connect_exceptions(self): |     def test_PUT_connect_exceptions(self): | ||||||
|  |  | ||||||
|         def mock_http_connect(*code_iter, **kwargs): |         def mock_http_connect(*code_iter, **kwargs): | ||||||
|  |  | ||||||
|             class FakeConn(object): |             class FakeConn(object): | ||||||
|  |  | ||||||
|                 def __init__(self, status): |                 def __init__(self, status): | ||||||
|                     self.status = status |                     self.status = status | ||||||
|                     self.reason = 'Fake' |                     self.reason = 'Fake' | ||||||
| @@ -518,6 +527,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|                     if self.status == -3: |                     if self.status == -3: | ||||||
|                         return FakeConn(507) |                         return FakeConn(507) | ||||||
|                     return FakeConn(100) |                     return FakeConn(100) | ||||||
|  |  | ||||||
|             code_iter = iter(code_iter) |             code_iter = iter(code_iter) | ||||||
|  |  | ||||||
|             def connect(*args, **ckwargs): |             def connect(*args, **ckwargs): | ||||||
| @@ -525,7 +535,9 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 if status == -1: |                 if status == -1: | ||||||
|                     raise HTTPException() |                     raise HTTPException() | ||||||
|                 return FakeConn(status) |                 return FakeConn(status) | ||||||
|  |  | ||||||
|             return connect |             return connect | ||||||
|  |  | ||||||
|         with save_globals(): |         with save_globals(): | ||||||
|             controller = proxy_server.ObjectController(self.app, 'account', |             controller = proxy_server.ObjectController(self.app, 'account', | ||||||
|                 'container', 'object') |                 'container', 'object') | ||||||
| @@ -546,8 +558,11 @@ class TestObjectController(unittest.TestCase): | |||||||
|             test_status_map((200, 200, 503, 503, -1), 503) |             test_status_map((200, 200, 503, 503, -1), 503) | ||||||
|  |  | ||||||
|     def test_PUT_send_exceptions(self): |     def test_PUT_send_exceptions(self): | ||||||
|  |  | ||||||
|         def mock_http_connect(*code_iter, **kwargs): |         def mock_http_connect(*code_iter, **kwargs): | ||||||
|  |  | ||||||
|             class FakeConn(object): |             class FakeConn(object): | ||||||
|  |  | ||||||
|                 def __init__(self, status): |                 def __init__(self, status): | ||||||
|                     self.status = status |                     self.status = status | ||||||
|                     self.reason = 'Fake' |                     self.reason = 'Fake' | ||||||
| @@ -611,8 +626,11 @@ class TestObjectController(unittest.TestCase): | |||||||
|             self.assertEquals(res.status_int, 413) |             self.assertEquals(res.status_int, 413) | ||||||
|  |  | ||||||
|     def test_PUT_getresponse_exceptions(self): |     def test_PUT_getresponse_exceptions(self): | ||||||
|  |  | ||||||
|         def mock_http_connect(*code_iter, **kwargs): |         def mock_http_connect(*code_iter, **kwargs): | ||||||
|  |  | ||||||
|             class FakeConn(object): |             class FakeConn(object): | ||||||
|  |  | ||||||
|                 def __init__(self, status): |                 def __init__(self, status): | ||||||
|                     self.status = status |                     self.status = status | ||||||
|                     self.reason = 'Fake' |                     self.reason = 'Fake' | ||||||
| @@ -807,6 +825,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 dev['port'] = 1 |                 dev['port'] = 1 | ||||||
|  |  | ||||||
|             class SlowBody(): |             class SlowBody(): | ||||||
|  |  | ||||||
|                 def __init__(self): |                 def __init__(self): | ||||||
|                     self.sent = 0 |                     self.sent = 0 | ||||||
|  |  | ||||||
| @@ -816,6 +835,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|                         self.sent += 1 |                         self.sent += 1 | ||||||
|                         return ' ' |                         return ' ' | ||||||
|                     return '' |                     return '' | ||||||
|  |  | ||||||
|             req = Request.blank('/a/c/o', |             req = Request.blank('/a/c/o', | ||||||
|                 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()}, |                 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()}, | ||||||
|                 headers={'Content-Length': '4', 'Content-Type': 'text/plain'}) |                 headers={'Content-Length': '4', 'Content-Type': 'text/plain'}) | ||||||
| @@ -854,11 +874,13 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 dev['port'] = 1 |                 dev['port'] = 1 | ||||||
|  |  | ||||||
|             class SlowBody(): |             class SlowBody(): | ||||||
|  |  | ||||||
|                 def __init__(self): |                 def __init__(self): | ||||||
|                     self.sent = 0 |                     self.sent = 0 | ||||||
|  |  | ||||||
|                 def read(self, size=-1): |                 def read(self, size=-1): | ||||||
|                     raise Exception('Disconnected') |                     raise Exception('Disconnected') | ||||||
|  |  | ||||||
|             req = Request.blank('/a/c/o', |             req = Request.blank('/a/c/o', | ||||||
|                 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()}, |                 environ={'REQUEST_METHOD': 'PUT', 'wsgi.input': SlowBody()}, | ||||||
|                 headers={'Content-Length': '4', 'Content-Type': 'text/plain'}) |                 headers={'Content-Length': '4', 'Content-Type': 'text/plain'}) | ||||||
| @@ -1508,7 +1530,9 @@ class TestObjectController(unittest.TestCase): | |||||||
|  |  | ||||||
|     def test_chunked_put(self): |     def test_chunked_put(self): | ||||||
|         # quick test of chunked put w/o PATH_TO_TEST_XFS |         # quick test of chunked put w/o PATH_TO_TEST_XFS | ||||||
|  |  | ||||||
|         class ChunkedFile(): |         class ChunkedFile(): | ||||||
|  |  | ||||||
|             def __init__(self, bytes): |             def __init__(self, bytes): | ||||||
|                 self.bytes = bytes |                 self.bytes = bytes | ||||||
|                 self.read_bytes = 0 |                 self.read_bytes = 0 | ||||||
| @@ -1576,6 +1600,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|         mkdirs(os.path.join(testdir, 'sdb1')) |         mkdirs(os.path.join(testdir, 'sdb1')) | ||||||
|         mkdirs(os.path.join(testdir, 'sdb1', 'tmp')) |         mkdirs(os.path.join(testdir, 'sdb1', 'tmp')) | ||||||
|         try: |         try: | ||||||
|  |             orig_container_listing_limit = proxy_server.CONTAINER_LISTING_LIMIT | ||||||
|             conf = {'devices': testdir, 'swift_dir': testdir, |             conf = {'devices': testdir, 'swift_dir': testdir, | ||||||
|                     'mount_check': 'false'} |                     'mount_check': 'false'} | ||||||
|             prolis = listen(('localhost', 0)) |             prolis = listen(('localhost', 0)) | ||||||
| @@ -1669,8 +1694,10 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 self.assertEquals(headers[:len(exp)], exp) |                 self.assertEquals(headers[:len(exp)], exp) | ||||||
|                 # Check unhandled exception |                 # Check unhandled exception | ||||||
|                 orig_update_request = prosrv.update_request |                 orig_update_request = prosrv.update_request | ||||||
|  |  | ||||||
|                 def broken_update_request(env, req): |                 def broken_update_request(env, req): | ||||||
|                     raise Exception('fake') |                     raise Exception('fake') | ||||||
|  |  | ||||||
|                 prosrv.update_request = broken_update_request |                 prosrv.update_request = broken_update_request | ||||||
|                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) |                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
|                 fd = sock.makefile() |                 fd = sock.makefile() | ||||||
| @@ -1719,8 +1746,10 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 # in a test for logging x-forwarded-for (first entry only). |                 # in a test for logging x-forwarded-for (first entry only). | ||||||
|  |  | ||||||
|                 class Logger(object): |                 class Logger(object): | ||||||
|  |  | ||||||
|                     def info(self, msg): |                     def info(self, msg): | ||||||
|                         self.msg = msg |                         self.msg = msg | ||||||
|  |  | ||||||
|                 orig_logger = prosrv.logger |                 orig_logger = prosrv.logger | ||||||
|                 prosrv.logger = Logger() |                 prosrv.logger = Logger() | ||||||
|                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) |                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
| @@ -1742,8 +1771,10 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 # Turn on header logging. |                 # Turn on header logging. | ||||||
|  |  | ||||||
|                 class Logger(object): |                 class Logger(object): | ||||||
|  |  | ||||||
|                     def info(self, msg): |                     def info(self, msg): | ||||||
|                         self.msg = msg |                         self.msg = msg | ||||||
|  |  | ||||||
|                 orig_logger = prosrv.logger |                 orig_logger = prosrv.logger | ||||||
|                 prosrv.logger = Logger() |                 prosrv.logger = Logger() | ||||||
|                 prosrv.log_headers = True |                 prosrv.log_headers = True | ||||||
| @@ -1900,6 +1931,70 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 self.assertEquals(headers[:len(exp)], exp) |                 self.assertEquals(headers[:len(exp)], exp) | ||||||
|                 body = fd.read() |                 body = fd.read() | ||||||
|                 self.assertEquals(body, 'oh hai123456789abcdef') |                 self.assertEquals(body, 'oh hai123456789abcdef') | ||||||
|  |                 # Create a container for our segmented/manifest object testing | ||||||
|  |                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
|  |                 fd = sock.makefile() | ||||||
|  |                 fd.write('PUT /v1/a/segmented HTTP/1.1\r\nHost: localhost\r\n' | ||||||
|  |                          'Connection: close\r\nX-Storage-Token: t\r\n' | ||||||
|  |                          'Content-Length: 0\r\n\r\n') | ||||||
|  |                 fd.flush() | ||||||
|  |                 headers = readuntil2crlfs(fd) | ||||||
|  |                 exp = 'HTTP/1.1 201' | ||||||
|  |                 self.assertEquals(headers[:len(exp)], exp) | ||||||
|  |                 # Create the object segments | ||||||
|  |                 for segment in xrange(5): | ||||||
|  |                     sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
|  |                     fd = sock.makefile() | ||||||
|  |                     fd.write('PUT /v1/a/segmented/name/%s HTTP/1.1\r\nHost: ' | ||||||
|  |                         'localhost\r\nConnection: close\r\nX-Storage-Token: ' | ||||||
|  |                         't\r\nContent-Length: 5\r\n\r\n1234 ' % str(segment)) | ||||||
|  |                     fd.flush() | ||||||
|  |                     headers = readuntil2crlfs(fd) | ||||||
|  |                     exp = 'HTTP/1.1 201' | ||||||
|  |                     self.assertEquals(headers[:len(exp)], exp) | ||||||
|  |                 # Create the object manifest file | ||||||
|  |                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
|  |                 fd = sock.makefile() | ||||||
|  |                 fd.write('PUT /v1/a/segmented/name HTTP/1.1\r\nHost: ' | ||||||
|  |                     'localhost\r\nConnection: close\r\nX-Storage-Token: ' | ||||||
|  |                     't\r\nContent-Length: 0\r\nX-Object-Manifest: ' | ||||||
|  |                     'segmented/name/\r\nContent-Type: text/jibberish\r\n\r\n') | ||||||
|  |                 fd.flush() | ||||||
|  |                 headers = readuntil2crlfs(fd) | ||||||
|  |                 exp = 'HTTP/1.1 201' | ||||||
|  |                 self.assertEquals(headers[:len(exp)], exp) | ||||||
|  |                 # Ensure retrieving the manifest file gets the whole object | ||||||
|  |                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
|  |                 fd = sock.makefile() | ||||||
|  |                 fd.write('GET /v1/a/segmented/name HTTP/1.1\r\nHost: ' | ||||||
|  |                     'localhost\r\nConnection: close\r\nX-Auth-Token: ' | ||||||
|  |                     't\r\n\r\n') | ||||||
|  |                 fd.flush() | ||||||
|  |                 headers = readuntil2crlfs(fd) | ||||||
|  |                 exp = 'HTTP/1.1 200' | ||||||
|  |                 self.assertEquals(headers[:len(exp)], exp) | ||||||
|  |                 self.assert_('X-Object-Manifest: segmented/name/' in headers) | ||||||
|  |                 self.assert_('Content-Type: text/jibberish' in headers) | ||||||
|  |                 body = fd.read() | ||||||
|  |                 self.assertEquals(body, '1234 1234 1234 1234 1234 ') | ||||||
|  |                 # Do it again but exceeding the container listing limit | ||||||
|  |                 proxy_server.CONTAINER_LISTING_LIMIT = 2 | ||||||
|  |                 sock = connect_tcp(('localhost', prolis.getsockname()[1])) | ||||||
|  |                 fd = sock.makefile() | ||||||
|  |                 fd.write('GET /v1/a/segmented/name HTTP/1.1\r\nHost: ' | ||||||
|  |                     'localhost\r\nConnection: close\r\nX-Auth-Token: ' | ||||||
|  |                     't\r\n\r\n') | ||||||
|  |                 fd.flush() | ||||||
|  |                 headers = readuntil2crlfs(fd) | ||||||
|  |                 exp = 'HTTP/1.1 200' | ||||||
|  |                 self.assertEquals(headers[:len(exp)], exp) | ||||||
|  |                 self.assert_('X-Object-Manifest: segmented/name/' in headers) | ||||||
|  |                 self.assert_('Content-Type: text/jibberish' in headers) | ||||||
|  |                 body = fd.read() | ||||||
|  |                 # A bit fragile of a test; as it makes the assumption that all | ||||||
|  |                 # will be sent in a single chunk. | ||||||
|  |                 self.assertEquals(body, | ||||||
|  |                     '19\r\n1234 1234 1234 1234 1234 \r\n0\r\n\r\n') | ||||||
|             finally: |             finally: | ||||||
|                 prospa.kill() |                 prospa.kill() | ||||||
|                 acc1spa.kill() |                 acc1spa.kill() | ||||||
| @@ -1909,6 +2004,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|                 obj1spa.kill() |                 obj1spa.kill() | ||||||
|                 obj2spa.kill() |                 obj2spa.kill() | ||||||
|         finally: |         finally: | ||||||
|  |             proxy_server.CONTAINER_LISTING_LIMIT = orig_container_listing_limit | ||||||
|             rmtree(testdir) |             rmtree(testdir) | ||||||
|  |  | ||||||
|     def test_mismatched_etags(self): |     def test_mismatched_etags(self): | ||||||
| @@ -2111,6 +2207,7 @@ class TestObjectController(unittest.TestCase): | |||||||
|             res = controller.COPY(req) |             res = controller.COPY(req) | ||||||
|         self.assert_(called[0]) |         self.assert_(called[0]) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestContainerController(unittest.TestCase): | class TestContainerController(unittest.TestCase): | ||||||
|     "Test swift.proxy_server.ContainerController" |     "Test swift.proxy_server.ContainerController" | ||||||
|  |  | ||||||
| @@ -2254,7 +2351,9 @@ class TestContainerController(unittest.TestCase): | |||||||
|                 self.assertEquals(resp.status_int, 404) |                 self.assertEquals(resp.status_int, 404) | ||||||
|  |  | ||||||
|     def test_put_locking(self): |     def test_put_locking(self): | ||||||
|  |  | ||||||
|         class MockMemcache(FakeMemcache): |         class MockMemcache(FakeMemcache): | ||||||
|  |  | ||||||
|             def __init__(self, allow_lock=None): |             def __init__(self, allow_lock=None): | ||||||
|                 self.allow_lock = allow_lock |                 self.allow_lock = allow_lock | ||||||
|                 super(MockMemcache, self).__init__() |                 super(MockMemcache, self).__init__() | ||||||
| @@ -2265,6 +2364,7 @@ class TestContainerController(unittest.TestCase): | |||||||
|                     yield True |                     yield True | ||||||
|                 else: |                 else: | ||||||
|                     raise MemcacheLockError() |                     raise MemcacheLockError() | ||||||
|  |  | ||||||
|         with save_globals(): |         with save_globals(): | ||||||
|             controller = proxy_server.ContainerController(self.app, 'account', |             controller = proxy_server.ContainerController(self.app, 'account', | ||||||
|                                                           'container') |                                                           'container') | ||||||
| @@ -2870,5 +2970,261 @@ class TestAccountController(unittest.TestCase): | |||||||
|             test_status_map((204, 500, 404), 503) |             test_status_map((204, 500, 404), 503) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FakeObjectController(object): | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         self.app = self | ||||||
|  |         self.logger = self | ||||||
|  |         self.account_name = 'a' | ||||||
|  |         self.container_name = 'c' | ||||||
|  |         self.object_name = 'o' | ||||||
|  |         self.trans_id = 'tx1' | ||||||
|  |         self.object_ring = FakeRing() | ||||||
|  |         self.node_timeout = 1 | ||||||
|  |  | ||||||
|  |     def exception(self, *args): | ||||||
|  |         self.exception_args = args | ||||||
|  |         self.exception_info = sys.exc_info() | ||||||
|  |  | ||||||
|  |     def GETorHEAD_base(self, *args): | ||||||
|  |         self.GETorHEAD_base_args = args | ||||||
|  |         req = args[0] | ||||||
|  |         path = args[4] | ||||||
|  |         body = data = path[-1] * int(path[-1]) | ||||||
|  |         if req.range and req.range.ranges: | ||||||
|  |             body = '' | ||||||
|  |             for start, stop in req.range.ranges: | ||||||
|  |                 body += data[start:stop] | ||||||
|  |         resp = Response(app_iter=iter(body)) | ||||||
|  |         return resp | ||||||
|  |  | ||||||
|  |     def iter_nodes(self, partition, nodes, ring): | ||||||
|  |         for node in nodes: | ||||||
|  |             yield node | ||||||
|  |         for node in ring.get_more_nodes(partition): | ||||||
|  |             yield node | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Stub(object): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestSegmentedIterable(unittest.TestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         self.controller = FakeObjectController() | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_unexpected_error(self): | ||||||
|  |         # Iterator value isn't a dict | ||||||
|  |         self.assertRaises(Exception, | ||||||
|  |             proxy_server.SegmentedIterable(self.controller, None, | ||||||
|  |             [None])._load_next_segment) | ||||||
|  |         self.assertEquals(self.controller.exception_args[0], | ||||||
|  |             'ERROR: While processing manifest /a/c/o tx1') | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_with_no_segments(self): | ||||||
|  |         self.assertRaises(StopIteration, | ||||||
|  |             proxy_server.SegmentedIterable(self.controller, 'lc', | ||||||
|  |             [])._load_next_segment) | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_with_one_segment(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}]) | ||||||
|  |         segit._load_next_segment() | ||||||
|  |         self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o1') | ||||||
|  |         data = ''.join(segit.segment_iter) | ||||||
|  |         self.assertEquals(data, '1') | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_with_two_segments(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}, {'name': 'o2'}]) | ||||||
|  |         segit._load_next_segment() | ||||||
|  |         self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o1') | ||||||
|  |         data = ''.join(segit.segment_iter) | ||||||
|  |         self.assertEquals(data, '1') | ||||||
|  |         segit._load_next_segment() | ||||||
|  |         self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2') | ||||||
|  |         data = ''.join(segit.segment_iter) | ||||||
|  |         self.assertEquals(data, '22') | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_with_two_segments_skip_first(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}, {'name': 'o2'}]) | ||||||
|  |         segit.segment = 0 | ||||||
|  |         segit.listing.next() | ||||||
|  |         segit._load_next_segment() | ||||||
|  |         self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2') | ||||||
|  |         data = ''.join(segit.segment_iter) | ||||||
|  |         self.assertEquals(data, '22') | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_with_seek(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}, {'name': 'o2'}]) | ||||||
|  |         segit.segment = 0 | ||||||
|  |         segit.listing.next() | ||||||
|  |         segit.seek = 1 | ||||||
|  |         segit._load_next_segment() | ||||||
|  |         self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2') | ||||||
|  |         self.assertEquals(str(self.controller.GETorHEAD_base_args[0].range), | ||||||
|  |             'bytes=1-') | ||||||
|  |         data = ''.join(segit.segment_iter) | ||||||
|  |         self.assertEquals(data, '2') | ||||||
|  |  | ||||||
|  |     def test_load_next_segment_with_get_error(self): | ||||||
|  |  | ||||||
|  |         def local_GETorHEAD_base(*args): | ||||||
|  |             return HTTPNotFound() | ||||||
|  |  | ||||||
|  |         self.controller.GETorHEAD_base = local_GETorHEAD_base | ||||||
|  |         self.assertRaises(Exception, | ||||||
|  |             proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}])._load_next_segment) | ||||||
|  |         self.assertEquals(self.controller.exception_args[0], | ||||||
|  |             'ERROR: While processing manifest /a/c/o tx1') | ||||||
|  |         self.assertEquals(str(self.controller.exception_info[1]), | ||||||
|  |             'Could not load object segment /a/lc/o1: 404') | ||||||
|  |  | ||||||
|  |     def test_iter_unexpected_error(self): | ||||||
|  |         # Iterator value isn't a dict | ||||||
|  |         self.assertRaises(Exception, ''.join, | ||||||
|  |             proxy_server.SegmentedIterable(self.controller, None, [None])) | ||||||
|  |         self.assertEquals(self.controller.exception_args[0], | ||||||
|  |             'ERROR: While processing manifest /a/c/o tx1') | ||||||
|  |  | ||||||
|  |     def test_iter_with_no_segments(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', []) | ||||||
|  |         self.assertEquals(''.join(segit), '') | ||||||
|  |  | ||||||
|  |     def test_iter_with_one_segment(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}]) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit), '1') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 1) | ||||||
|  |  | ||||||
|  |     def test_iter_with_two_segments(self): | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}, {'name': 'o2'}]) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit), '122') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 3) | ||||||
|  |  | ||||||
|  |     def test_iter_with_get_error(self): | ||||||
|  |  | ||||||
|  |         def local_GETorHEAD_base(*args): | ||||||
|  |             return HTTPNotFound() | ||||||
|  |  | ||||||
|  |         self.controller.GETorHEAD_base = local_GETorHEAD_base | ||||||
|  |         self.assertRaises(Exception, ''.join, | ||||||
|  |             proxy_server.SegmentedIterable(self.controller, 'lc', [{'name': | ||||||
|  |             'o1'}])) | ||||||
|  |         self.assertEquals(self.controller.exception_args[0], | ||||||
|  |             'ERROR: While processing manifest /a/c/o tx1') | ||||||
|  |         self.assertEquals(str(self.controller.exception_info[1]), | ||||||
|  |             'Could not load object segment /a/lc/o1: 404') | ||||||
|  |  | ||||||
|  |     def test_app_iter_range_unexpected_error(self): | ||||||
|  |         # Iterator value isn't a dict | ||||||
|  |         self.assertRaises(Exception, | ||||||
|  |             proxy_server.SegmentedIterable(self.controller, None, | ||||||
|  |             [None]).app_iter_range(None, None).next) | ||||||
|  |         self.assertEquals(self.controller.exception_args[0], | ||||||
|  |             'ERROR: While processing manifest /a/c/o tx1') | ||||||
|  |  | ||||||
|  |     def test_app_iter_range_with_no_segments(self): | ||||||
|  |         self.assertEquals(''.join(proxy_server.SegmentedIterable( | ||||||
|  |             self.controller, 'lc', []).app_iter_range(None, None)), '') | ||||||
|  |         self.assertEquals(''.join(proxy_server.SegmentedIterable( | ||||||
|  |             self.controller, 'lc', []).app_iter_range(3, None)), '') | ||||||
|  |         self.assertEquals(''.join(proxy_server.SegmentedIterable( | ||||||
|  |             self.controller, 'lc', []).app_iter_range(3, 5)), '') | ||||||
|  |         self.assertEquals(''.join(proxy_server.SegmentedIterable( | ||||||
|  |             self.controller, 'lc', []).app_iter_range(None, 5)), '') | ||||||
|  |  | ||||||
|  |     def test_app_iter_range_with_one_segment(self): | ||||||
|  |         listing = [{'name': 'o1', 'bytes': 1}] | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, None)), '1') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 1) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(3, None)), '') | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(3, 5)), '') | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, 5)), '1') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 1) | ||||||
|  |  | ||||||
|  |     def test_app_iter_range_with_two_segments(self): | ||||||
|  |         listing = [{'name': 'o1', 'bytes': 1}, {'name': 'o2', 'bytes': 2}] | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, None)), '122') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 3) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(1, None)), '22') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 2) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(1, 5)), '22') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 2) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, 2)), '12') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 2) | ||||||
|  |  | ||||||
|  |     def test_app_iter_range_with_many_segments(self): | ||||||
|  |         listing = [{'name': 'o1', 'bytes': 1}, {'name': 'o2', 'bytes': 2}, | ||||||
|  |             {'name': 'o3', 'bytes': 3}, {'name': 'o4', 'bytes': 4}, {'name': | ||||||
|  |             'o5', 'bytes': 5}] | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, None)), | ||||||
|  |             '122333444455555') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 15) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(3, None)), | ||||||
|  |             '333444455555') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 12) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(5, None)), '3444455555') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 10) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, 6)), '122333') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 6) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(None, 7)), '1223334') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 7) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(3, 7)), '3334') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 4) | ||||||
|  |  | ||||||
|  |         segit = proxy_server.SegmentedIterable(self.controller, 'lc', listing) | ||||||
|  |         segit.response = Stub() | ||||||
|  |         self.assertEquals(''.join(segit.app_iter_range(5, 7)), '34') | ||||||
|  |         self.assertEquals(segit.response.bytes_transferred, 2) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
| @@ -65,7 +65,7 @@ class DumbInternalProxy(object): | |||||||
|  |  | ||||||
| class TestLogProcessor(unittest.TestCase): | class TestLogProcessor(unittest.TestCase): | ||||||
|      |      | ||||||
|     access_test_line = 'Jul  9 04:14:30 saio proxy 1.2.3.4 4.5.6.7 '\ |     access_test_line = 'Jul  9 04:14:30 saio proxy-server 1.2.3.4 4.5.6.7 '\ | ||||||
|                     '09/Jul/2010/04/14/30 GET '\ |                     '09/Jul/2010/04/14/30 GET '\ | ||||||
|                     '/v1/acct/foo/bar?format=json&foo HTTP/1.0 200 - '\ |                     '/v1/acct/foo/bar?format=json&foo HTTP/1.0 200 - '\ | ||||||
|                     'curl tk4e350daf-9338-4cc6-aabb-090e49babfbd '\ |                     'curl tk4e350daf-9338-4cc6-aabb-090e49babfbd '\ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Michael Barton
					Michael Barton