Cleaned up st command line parsing; always use included client.py as well
This commit is contained in:
373
bin/st
373
bin/st
@@ -14,32 +14,38 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from errno import EEXIST, ENOENT
|
||||||
|
from hashlib import md5
|
||||||
|
from optparse import OptionParser
|
||||||
|
from os import environ, listdir, makedirs, utime
|
||||||
|
from os.path import basename, dirname, getmtime, getsize, isdir, join
|
||||||
|
from Queue import Empty, Queue
|
||||||
|
from sys import argv, exit, stderr, stdout
|
||||||
|
from threading import enumerate as threading_enumerate, Thread
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
# Inclusion of swift.common.client for convenience of single file distribution
|
||||||
|
|
||||||
|
import socket
|
||||||
|
from cStringIO import StringIO
|
||||||
|
from httplib import HTTPException, HTTPSConnection
|
||||||
|
from re import compile, DOTALL
|
||||||
|
from tokenize import generate_tokens, STRING, NAME, OP
|
||||||
|
from urllib import quote as _quote, unquote
|
||||||
|
from urlparse import urlparse, urlunparse
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try to use installed swift.common.client...
|
|
||||||
from swift.common.client import get_auth, ClientException, Connection
|
|
||||||
except:
|
|
||||||
# But if not installed, use an included copy.
|
|
||||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
||||||
# Inclusion of swift.common.client
|
|
||||||
|
|
||||||
"""
|
|
||||||
Cloud Files client library used internally
|
|
||||||
"""
|
|
||||||
import socket
|
|
||||||
from cStringIO import StringIO
|
|
||||||
from httplib import HTTPConnection, HTTPException, HTTPSConnection
|
|
||||||
from re import compile, DOTALL
|
|
||||||
from tokenize import generate_tokens, STRING, NAME, OP
|
|
||||||
from urllib import quote as _quote, unquote
|
|
||||||
from urlparse import urlparse, urlunparse
|
|
||||||
|
|
||||||
try:
|
|
||||||
from eventlet import sleep
|
from eventlet import sleep
|
||||||
except:
|
except:
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
from swift.common.bufferedhttp \
|
||||||
|
import BufferedHTTPConnection as HTTPConnection
|
||||||
|
|
||||||
def quote(value, safe='/'):
|
|
||||||
|
def quote(value, safe='/'):
|
||||||
"""
|
"""
|
||||||
Patched version of urllib.quote that encodes utf8 strings before quoting
|
Patched version of urllib.quote that encodes utf8 strings before quoting
|
||||||
"""
|
"""
|
||||||
@@ -48,11 +54,11 @@ except:
|
|||||||
return _quote(value, safe)
|
return _quote(value, safe)
|
||||||
|
|
||||||
|
|
||||||
# look for a real json parser first
|
# look for a real json parser first
|
||||||
try:
|
try:
|
||||||
# simplejson is popular and pretty good
|
# simplejson is popular and pretty good
|
||||||
from simplejson import loads as json_loads
|
from simplejson import loads as json_loads
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
# 2.6 will have a json module in the stdlib
|
# 2.6 will have a json module in the stdlib
|
||||||
from json import loads as json_loads
|
from json import loads as json_loads
|
||||||
@@ -86,7 +92,7 @@ except:
|
|||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
class ClientException(Exception):
|
||||||
|
|
||||||
def __init__(self, msg, http_scheme='', http_host='', http_port='',
|
def __init__(self, msg, http_scheme='', http_host='', http_port='',
|
||||||
http_path='', http_query='', http_status=0, http_reason='',
|
http_path='', http_query='', http_status=0, http_reason='',
|
||||||
@@ -133,7 +139,7 @@ except:
|
|||||||
return b and '%s: %s' % (a, b) or a
|
return b and '%s: %s' % (a, b) or a
|
||||||
|
|
||||||
|
|
||||||
def http_connection(url):
|
def http_connection(url):
|
||||||
"""
|
"""
|
||||||
Make an HTTPConnection or HTTPSConnection
|
Make an HTTPConnection or HTTPSConnection
|
||||||
|
|
||||||
@@ -152,7 +158,7 @@ except:
|
|||||||
return parsed, conn
|
return parsed, conn
|
||||||
|
|
||||||
|
|
||||||
def get_auth(url, user, key, snet=False):
|
def get_auth(url, user, key, snet=False):
|
||||||
"""
|
"""
|
||||||
Get authentication/authorization credentials.
|
Get authentication/authorization credentials.
|
||||||
|
|
||||||
@@ -189,7 +195,7 @@ except:
|
|||||||
resp.getheader('x-auth-token'))
|
resp.getheader('x-auth-token'))
|
||||||
|
|
||||||
|
|
||||||
def get_account(url, token, marker=None, limit=None, prefix=None,
|
def get_account(url, token, marker=None, limit=None, prefix=None,
|
||||||
http_conn=None, full_listing=False):
|
http_conn=None, full_listing=False):
|
||||||
"""
|
"""
|
||||||
Get a listing of containers for the account.
|
Get a listing of containers for the account.
|
||||||
@@ -245,7 +251,7 @@ except:
|
|||||||
return resp_headers, json_loads(resp.read())
|
return resp_headers, json_loads(resp.read())
|
||||||
|
|
||||||
|
|
||||||
def head_account(url, token, http_conn=None):
|
def head_account(url, token, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Get account stats.
|
Get account stats.
|
||||||
|
|
||||||
@@ -275,7 +281,7 @@ except:
|
|||||||
return resp_headers
|
return resp_headers
|
||||||
|
|
||||||
|
|
||||||
def post_account(url, token, headers, http_conn=None):
|
def post_account(url, token, headers, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Update an account's metadata.
|
Update an account's metadata.
|
||||||
|
|
||||||
@@ -301,7 +307,7 @@ except:
|
|||||||
http_reason=resp.reason)
|
http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def get_container(url, token, container, marker=None, limit=None,
|
def get_container(url, token, container, marker=None, limit=None,
|
||||||
prefix=None, delimiter=None, http_conn=None,
|
prefix=None, delimiter=None, http_conn=None,
|
||||||
full_listing=False):
|
full_listing=False):
|
||||||
"""
|
"""
|
||||||
@@ -366,7 +372,7 @@ except:
|
|||||||
return resp_headers, json_loads(resp.read())
|
return resp_headers, json_loads(resp.read())
|
||||||
|
|
||||||
|
|
||||||
def head_container(url, token, container, http_conn=None):
|
def head_container(url, token, container, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Get container stats.
|
Get container stats.
|
||||||
|
|
||||||
@@ -398,7 +404,7 @@ except:
|
|||||||
return resp_headers
|
return resp_headers
|
||||||
|
|
||||||
|
|
||||||
def put_container(url, token, container, headers=None, http_conn=None):
|
def put_container(url, token, container, headers=None, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Create a container
|
Create a container
|
||||||
|
|
||||||
@@ -428,7 +434,7 @@ except:
|
|||||||
http_reason=resp.reason)
|
http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def post_container(url, token, container, headers, http_conn=None):
|
def post_container(url, token, container, headers, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Update a container's metadata.
|
Update a container's metadata.
|
||||||
|
|
||||||
@@ -456,7 +462,7 @@ except:
|
|||||||
http_reason=resp.reason)
|
http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def delete_container(url, token, container, http_conn=None):
|
def delete_container(url, token, container, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Delete a container
|
Delete a container
|
||||||
|
|
||||||
@@ -482,7 +488,7 @@ except:
|
|||||||
http_reason=resp.reason)
|
http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def get_object(url, token, container, name, http_conn=None,
|
def get_object(url, token, container, name, http_conn=None,
|
||||||
resp_chunk_size=None):
|
resp_chunk_size=None):
|
||||||
"""
|
"""
|
||||||
Get an object
|
Get an object
|
||||||
@@ -529,7 +535,7 @@ except:
|
|||||||
return resp_headers, object_body
|
return resp_headers, object_body
|
||||||
|
|
||||||
|
|
||||||
def head_object(url, token, container, name, http_conn=None):
|
def head_object(url, token, container, name, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Get object info
|
Get object info
|
||||||
|
|
||||||
@@ -561,7 +567,7 @@ except:
|
|||||||
return resp_headers
|
return resp_headers
|
||||||
|
|
||||||
|
|
||||||
def put_object(url, token, container, name, contents, content_length=None,
|
def put_object(url, token, container, name, contents, content_length=None,
|
||||||
etag=None, chunk_size=65536, content_type=None, headers=None,
|
etag=None, chunk_size=65536, content_type=None, headers=None,
|
||||||
http_conn=None):
|
http_conn=None):
|
||||||
"""
|
"""
|
||||||
@@ -625,7 +631,7 @@ except:
|
|||||||
return resp.getheader('etag').strip('"')
|
return resp.getheader('etag').strip('"')
|
||||||
|
|
||||||
|
|
||||||
def post_object(url, token, container, name, headers, http_conn=None):
|
def post_object(url, token, container, name, headers, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Update object metadata
|
Update object metadata
|
||||||
|
|
||||||
@@ -653,7 +659,7 @@ except:
|
|||||||
http_status=resp.status, http_reason=resp.reason)
|
http_status=resp.status, http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
def delete_object(url, token, container, name, http_conn=None):
|
def delete_object(url, token, container, name, http_conn=None):
|
||||||
"""
|
"""
|
||||||
Delete object
|
Delete object
|
||||||
|
|
||||||
@@ -680,7 +686,7 @@ except:
|
|||||||
http_reason=resp.reason)
|
http_reason=resp.reason)
|
||||||
|
|
||||||
|
|
||||||
class Connection(object):
|
class Connection(object):
|
||||||
"""Convenience class to make requests that will also retry the request"""
|
"""Convenience class to make requests that will also retry the request"""
|
||||||
|
|
||||||
def __init__(self, authurl, user, key, retries=5, preauthurl=None,
|
def __init__(self, authurl, user, key, retries=5, preauthurl=None,
|
||||||
@@ -811,19 +817,8 @@ except:
|
|||||||
"""Wrapper for :func:`delete_object`"""
|
"""Wrapper for :func:`delete_object`"""
|
||||||
return self._retry(delete_object, container, obj)
|
return self._retry(delete_object, container, obj)
|
||||||
|
|
||||||
# End inclusion of swift.common.client
|
# End inclusion of swift.common.client
|
||||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
|
||||||
from errno import EEXIST, ENOENT
|
|
||||||
from hashlib import md5
|
|
||||||
from optparse import OptionParser
|
|
||||||
from os import environ, listdir, makedirs, utime
|
|
||||||
from os.path import basename, dirname, getmtime, getsize, isdir, join
|
|
||||||
from Queue import Empty, Queue
|
|
||||||
from sys import argv, exit, stderr, stdout
|
|
||||||
from threading import enumerate as threading_enumerate, Thread
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
def mkdirs(path):
|
def mkdirs(path):
|
||||||
@@ -865,12 +860,21 @@ st_delete_help = '''
|
|||||||
delete --all OR delete container [object] [object] ...
|
delete --all OR delete container [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.'''.strip('\n')
|
||||||
def st_delete(options, args):
|
|
||||||
|
|
||||||
|
def st_delete(parser, args, print_queue, error_queue):
|
||||||
|
parser.add_option('-a', '--all', action='store_true', dest='yes_all',
|
||||||
|
default=False, help='Indicates that you really want to delete '
|
||||||
|
'everything in the account')
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
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):
|
||||||
options.error_queue.put('Usage: %s [options] %s' %
|
error_queue.put('Usage: %s [options] %s' %
|
||||||
(basename(argv[0]), st_delete_help))
|
(basename(argv[0]), st_delete_help))
|
||||||
return
|
return
|
||||||
|
|
||||||
object_queue = Queue(10000)
|
object_queue = Queue(10000)
|
||||||
|
|
||||||
def _delete_object((container, obj), conn):
|
def _delete_object((container, obj), conn):
|
||||||
try:
|
try:
|
||||||
conn.delete_object(container, obj)
|
conn.delete_object(container, obj)
|
||||||
@@ -878,13 +882,14 @@ def st_delete(options, args):
|
|||||||
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:]
|
||||||
options.print_queue.put(path)
|
print_queue.put(path)
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.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):
|
||||||
try:
|
try:
|
||||||
marker = ''
|
marker = ''
|
||||||
@@ -913,11 +918,12 @@ def st_delete(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Container %s not found' % repr(container))
|
error_queue.put('Container %s not found' % repr(container))
|
||||||
url, token = get_auth(options.auth, options.user, options.key, snet=options.snet)
|
|
||||||
|
url, token = get_auth(options.auth, options.user, options.key,
|
||||||
|
snet=options.snet)
|
||||||
create_connection = lambda: Connection(options.auth, options.user,
|
create_connection = lambda: Connection(options.auth, options.user,
|
||||||
options.key, preauthurl=url,
|
options.key, preauthurl=url, preauthtoken=token, snet=options.snet)
|
||||||
preauthtoken=token, snet=options.snet)
|
|
||||||
object_threads = [QueueFunctionThread(object_queue, _delete_object,
|
object_threads = [QueueFunctionThread(object_queue, _delete_object,
|
||||||
create_connection()) for _ in xrange(10)]
|
create_connection()) for _ in xrange(10)]
|
||||||
for thread in object_threads:
|
for thread in object_threads:
|
||||||
@@ -945,7 +951,7 @@ def st_delete(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Account not found')
|
error_queue.put('Account not found')
|
||||||
elif len(args) == 1:
|
elif len(args) == 1:
|
||||||
conn = create_connection()
|
conn = create_connection()
|
||||||
_delete_container(args[0], conn)
|
_delete_container(args[0], conn)
|
||||||
@@ -969,15 +975,31 @@ def st_delete(options, args):
|
|||||||
st_download_help = '''
|
st_download_help = '''
|
||||||
download --all OR download container [object] [object] ...
|
download --all OR download container [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. Use
|
container, or a list of objects depending on the args given. For a single
|
||||||
the -o [--output] <filename> option to redirect the output to a file
|
object download, you may use the -o [--output] <filename> option to
|
||||||
or if "-" then the just redirect to stdout. '''.strip('\n')
|
redirect the output to a specific file or if "-" then just redirect to
|
||||||
def st_download(options, args):
|
stdout.'''.strip('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def st_download(options, args, print_queue, error_queue):
|
||||||
|
parser.add_option('-a', '--all', action='store_true', dest='yes_all',
|
||||||
|
default=False, help='Indicates that you really want to download '
|
||||||
|
'everything in the account')
|
||||||
|
parser.add_option('-o', '--output', dest='out_file', help='For a single '
|
||||||
|
'file download, stream the output to an alternate location ')
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
args = args[1:]
|
||||||
|
if options.out_file == '-':
|
||||||
|
options.verbose = 0
|
||||||
|
if options.out_file and len(args) != 2:
|
||||||
|
exit('-o option only allowed for single file downloads')
|
||||||
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):
|
||||||
options.error_queue.put('Usage: %s [options] %s' %
|
error_queue.put('Usage: %s [options] %s' %
|
||||||
(basename(argv[0]), st_download_help))
|
(basename(argv[0]), st_download_help))
|
||||||
return
|
return
|
||||||
|
|
||||||
object_queue = Queue(10000)
|
object_queue = Queue(10000)
|
||||||
|
|
||||||
def _download_object(queue_arg, conn):
|
def _download_object(queue_arg, conn):
|
||||||
if len(queue_arg) == 2:
|
if len(queue_arg) == 2:
|
||||||
container, obj = queue_arg
|
container, obj = queue_arg
|
||||||
@@ -1015,29 +1037,31 @@ def st_download(options, args):
|
|||||||
fp = open(path, 'wb')
|
fp = open(path, 'wb')
|
||||||
read_length = 0
|
read_length = 0
|
||||||
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)
|
||||||
md5sum.update(chunk)
|
md5sum.update(chunk)
|
||||||
fp.close()
|
fp.close()
|
||||||
if md5sum.hexdigest() != etag:
|
if md5sum.hexdigest() != etag:
|
||||||
options.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 read_length != content_length:
|
||||||
options.error_queue.put(
|
error_queue.put(
|
||||||
'%s: read_length != content_length, %d != %d' %
|
'%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:
|
||||||
mtime = float(headers['x-object-meta-mtime'])
|
mtime = float(headers['x-object-meta-mtime'])
|
||||||
utime(path, (mtime, mtime))
|
utime(path, (mtime, mtime))
|
||||||
if options.verbose:
|
if options.verbose:
|
||||||
options.print_queue.put(path)
|
print_queue.put(path)
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.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 _download_container(container, conn):
|
def _download_container(container, conn):
|
||||||
try:
|
try:
|
||||||
marker = ''
|
marker = ''
|
||||||
@@ -1052,11 +1076,12 @@ def st_download(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Container %s not found' % repr(container))
|
error_queue.put('Container %s not found' % repr(container))
|
||||||
url, token = get_auth(options.auth, options.user, options.key, snet=options.snet)
|
|
||||||
|
url, token = get_auth(options.auth, options.user, options.key,
|
||||||
|
snet=options.snet)
|
||||||
create_connection = lambda: Connection(options.auth, options.user,
|
create_connection = lambda: Connection(options.auth, options.user,
|
||||||
options.key, preauthurl=url,
|
options.key, preauthurl=url, preauthtoken=token, snet=options.snet)
|
||||||
preauthtoken=token, snet=options.snet)
|
|
||||||
object_threads = [QueueFunctionThread(object_queue, _download_object,
|
object_threads = [QueueFunctionThread(object_queue, _download_object,
|
||||||
create_connection()) for _ in xrange(10)]
|
create_connection()) for _ in xrange(10)]
|
||||||
for thread in object_threads:
|
for thread in object_threads:
|
||||||
@@ -1080,7 +1105,7 @@ def st_download(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Account not found')
|
error_queue.put('Account not found')
|
||||||
elif len(args) == 1:
|
elif len(args) == 1:
|
||||||
_download_container(args[0], create_connection())
|
_download_container(args[0], create_connection())
|
||||||
else:
|
else:
|
||||||
@@ -1112,12 +1137,24 @@ list [options] [container]
|
|||||||
items with the given delimiter (see Cloud Files general documentation for
|
items with the given delimiter (see Cloud Files general documentation for
|
||||||
what this means).
|
what this means).
|
||||||
'''.strip('\n')
|
'''.strip('\n')
|
||||||
def st_list(options, args):
|
|
||||||
|
|
||||||
|
def st_list(options, args, print_queue, error_queue):
|
||||||
|
parser.add_option('-p', '--prefix', dest='prefix', help='Will only list '
|
||||||
|
'items beginning with the prefix')
|
||||||
|
parser.add_option('-d', '--delimiter', dest='delimiter', help='Will roll '
|
||||||
|
'up items with the given delimiter (see Cloud Files general '
|
||||||
|
'documentation for what this means)')
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
args = args[1:]
|
||||||
|
if options.delimiter and not args:
|
||||||
|
exit('-d option only allowed for container listings')
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
options.error_queue.put('Usage: %s [options] %s' %
|
error_queue.put('Usage: %s [options] %s' %
|
||||||
(basename(argv[0]), st_list_help))
|
(basename(argv[0]), st_list_help))
|
||||||
return
|
return
|
||||||
conn = Connection(options.auth, options.user, options.key, snet=options.snet)
|
conn = Connection(options.auth, options.user, options.key,
|
||||||
|
snet=options.snet)
|
||||||
try:
|
try:
|
||||||
marker = ''
|
marker = ''
|
||||||
while True:
|
while True:
|
||||||
@@ -1130,35 +1167,39 @@ def st_list(options, args):
|
|||||||
if not items:
|
if not items:
|
||||||
break
|
break
|
||||||
for item in items:
|
for item in items:
|
||||||
options.print_queue.put(item.get('name', item.get('subdir')))
|
print_queue.put(item.get('name', item.get('subdir')))
|
||||||
marker = items[-1].get('name', items[-1].get('subdir'))
|
marker = items[-1].get('name', items[-1].get('subdir'))
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
if not args:
|
if not args:
|
||||||
options.error_queue.put('Account not found')
|
error_queue.put('Account not found')
|
||||||
else:
|
else:
|
||||||
options.error_queue.put('Container %s not found' % repr(args[0]))
|
error_queue.put('Container %s not found' % repr(args[0]))
|
||||||
|
|
||||||
|
|
||||||
st_stat_help = '''
|
st_stat_help = '''
|
||||||
stat [container] [object]
|
stat [container] [object]
|
||||||
Displays information for the account, container, or object depending on the
|
Displays information for the account, container, or object depending on the
|
||||||
args given (if any).'''.strip('\n')
|
args given (if any).'''.strip('\n')
|
||||||
def st_stat(options, args):
|
|
||||||
|
|
||||||
|
def st_stat(options, args, print_queue, error_queue):
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
args = args[1:]
|
||||||
conn = Connection(options.auth, options.user, options.key)
|
conn = Connection(options.auth, options.user, options.key)
|
||||||
if not args:
|
if not args:
|
||||||
try:
|
try:
|
||||||
headers = conn.head_account()
|
headers = conn.head_account()
|
||||||
if options.verbose > 1:
|
if options.verbose > 1:
|
||||||
options.print_queue.put('''
|
print_queue.put('''
|
||||||
StorageURL: %s
|
StorageURL: %s
|
||||||
Auth Token: %s
|
Auth Token: %s
|
||||||
'''.strip('\n') % (conn.url, conn.token))
|
'''.strip('\n') % (conn.url, conn.token))
|
||||||
container_count = int(headers.get('x-account-container-count', 0))
|
container_count = int(headers.get('x-account-container-count', 0))
|
||||||
object_count = int(headers.get('x-account-object-count', 0))
|
object_count = int(headers.get('x-account-object-count', 0))
|
||||||
bytes_used = int(headers.get('x-account-bytes-used', 0))
|
bytes_used = int(headers.get('x-account-bytes-used', 0))
|
||||||
options.print_queue.put('''
|
print_queue.put('''
|
||||||
Account: %s
|
Account: %s
|
||||||
Containers: %d
|
Containers: %d
|
||||||
Objects: %d
|
Objects: %d
|
||||||
@@ -1166,24 +1207,24 @@ Containers: %d
|
|||||||
object_count, bytes_used))
|
object_count, bytes_used))
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
if key.startswith('x-account-meta-'):
|
if key.startswith('x-account-meta-'):
|
||||||
options.print_queue.put('%10s: %s' % ('Meta %s' %
|
print_queue.put('%10s: %s' % ('Meta %s' %
|
||||||
key[len('x-account-meta-'):].title(), value))
|
key[len('x-account-meta-'):].title(), value))
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
if not key.startswith('x-account-meta-') and key not in (
|
if not key.startswith('x-account-meta-') and key not in (
|
||||||
'content-length', 'date', 'x-account-container-count',
|
'content-length', 'date', 'x-account-container-count',
|
||||||
'x-account-object-count', 'x-account-bytes-used'):
|
'x-account-object-count', 'x-account-bytes-used'):
|
||||||
options.print_queue.put(
|
print_queue.put(
|
||||||
'%10s: %s' % (key.title(), value))
|
'%10s: %s' % (key.title(), value))
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Account not found')
|
error_queue.put('Account not found')
|
||||||
elif len(args) == 1:
|
elif len(args) == 1:
|
||||||
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))
|
||||||
bytes_used = int(headers.get('x-container-bytes-used', 0))
|
bytes_used = int(headers.get('x-container-bytes-used', 0))
|
||||||
options.print_queue.put('''
|
print_queue.put('''
|
||||||
Account: %s
|
Account: %s
|
||||||
Container: %s
|
Container: %s
|
||||||
Objects: %d
|
Objects: %d
|
||||||
@@ -1195,23 +1236,23 @@ Write ACL: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0],
|
|||||||
headers.get('x-container-write', '')))
|
headers.get('x-container-write', '')))
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
if key.startswith('x-container-meta-'):
|
if key.startswith('x-container-meta-'):
|
||||||
options.print_queue.put('%9s: %s' % ('Meta %s' %
|
print_queue.put('%9s: %s' % ('Meta %s' %
|
||||||
key[len('x-container-meta-'):].title(), value))
|
key[len('x-container-meta-'):].title(), value))
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
if not key.startswith('x-container-meta-') and key not in (
|
if not key.startswith('x-container-meta-') and key not in (
|
||||||
'content-length', 'date', 'x-container-object-count',
|
'content-length', 'date', 'x-container-object-count',
|
||||||
'x-container-bytes-used', 'x-container-read',
|
'x-container-bytes-used', 'x-container-read',
|
||||||
'x-container-write'):
|
'x-container-write'):
|
||||||
options.print_queue.put(
|
print_queue.put(
|
||||||
'%9s: %s' % (key.title(), value))
|
'%9s: %s' % (key.title(), value))
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Container %s not found' % repr(args[0]))
|
error_queue.put('Container %s not found' % repr(args[0]))
|
||||||
elif len(args) == 2:
|
elif len(args) == 2:
|
||||||
try:
|
try:
|
||||||
headers = conn.head_object(args[0], args[1])
|
headers = conn.head_object(args[0], args[1])
|
||||||
options.print_queue.put('''
|
print_queue.put('''
|
||||||
Account: %s
|
Account: %s
|
||||||
Container: %s
|
Container: %s
|
||||||
Object: %s
|
Object: %s
|
||||||
@@ -1225,21 +1266,21 @@ Content Length: %s
|
|||||||
headers.get('etag')))
|
headers.get('etag')))
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
if key.startswith('x-object-meta-'):
|
if key.startswith('x-object-meta-'):
|
||||||
options.print_queue.put('%14s: %s' % ('Meta %s' %
|
print_queue.put('%14s: %s' % ('Meta %s' %
|
||||||
key[len('x-object-meta-'):].title(), value))
|
key[len('x-object-meta-'):].title(), value))
|
||||||
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'):
|
||||||
options.print_queue.put(
|
print_queue.put(
|
||||||
'%14s: %s' % (key.title(), value))
|
'%14s: %s' % (key.title(), value))
|
||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Object %s not found' %
|
error_queue.put('Object %s not found' %
|
||||||
repr('%s/%s' % (args[0], args[1])))
|
repr('%s/%s' % (args[0], args[1])))
|
||||||
else:
|
else:
|
||||||
options.error_queue.put('Usage: %s [options] %s' %
|
error_queue.put('Usage: %s [options] %s' %
|
||||||
(basename(argv[0]), st_stat_help))
|
(basename(argv[0]), st_stat_help))
|
||||||
|
|
||||||
|
|
||||||
@@ -1252,7 +1293,22 @@ post [options] [container] [object]
|
|||||||
or --meta option is allowed on all and used to define the user meta data
|
or --meta option is allowed on all and used to define the user meta data
|
||||||
items to set in the form Name:Value. This option can be repeated. Example:
|
items to set in the form Name:Value. This option can be repeated. Example:
|
||||||
post -m Color:Blue -m Size:Large'''.strip('\n')
|
post -m Color:Blue -m Size:Large'''.strip('\n')
|
||||||
def st_post(options, args):
|
|
||||||
|
|
||||||
|
def st_post(options, args, print_queue, error_queue):
|
||||||
|
parser.add_option('-r', '--read-acl', dest='read_acl', help='Sets the '
|
||||||
|
'Read ACL for containers. Quick summary of ACL syntax: .r:*, '
|
||||||
|
'.r:-.example.com, .r:www.example.com, account1, account2:user2')
|
||||||
|
parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the '
|
||||||
|
'Write ACL for containers. Quick summary of ACL syntax: account1, '
|
||||||
|
'account2:user2')
|
||||||
|
parser.add_option('-m', '--meta', action='append', dest='meta', default=[],
|
||||||
|
help='Sets a meta data item with the syntax name:value. This option '
|
||||||
|
'may be repeated. Example: -m Color:Blue -m Size:Large')
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
args = args[1:]
|
||||||
|
if (options.read_acl or options.write_acl) and not args:
|
||||||
|
exit('-r and -w options only allowed for containers')
|
||||||
conn = Connection(options.auth, options.user, options.key)
|
conn = Connection(options.auth, options.user, options.key)
|
||||||
if not args:
|
if not args:
|
||||||
headers = {}
|
headers = {}
|
||||||
@@ -1265,7 +1321,7 @@ def st_post(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Account not found')
|
error_queue.put('Account not found')
|
||||||
elif len(args) == 1:
|
elif len(args) == 1:
|
||||||
headers = {}
|
headers = {}
|
||||||
for item in options.meta:
|
for item in options.meta:
|
||||||
@@ -1293,10 +1349,10 @@ def st_post(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Object %s not found' %
|
error_queue.put('Object %s not found' %
|
||||||
repr('%s/%s' % (args[0], args[1])))
|
repr('%s/%s' % (args[0], args[1])))
|
||||||
else:
|
else:
|
||||||
options.error_queue.put('Usage: %s [options] %s' %
|
error_queue.put('Usage: %s [options] %s' %
|
||||||
(basename(argv[0]), st_post_help))
|
(basename(argv[0]), st_post_help))
|
||||||
|
|
||||||
|
|
||||||
@@ -1305,12 +1361,21 @@ 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.'''.strip('\n')
|
||||||
def st_upload(options, args):
|
|
||||||
|
|
||||||
|
def st_upload(options, args, print_queue, error_queue):
|
||||||
|
parser.add_option('-c', '--changed', action='store_true', dest='changed',
|
||||||
|
default=False, help='Will only upload files that have changed since '
|
||||||
|
'the last upload')
|
||||||
|
(options, args) = parse_args(parser, args)
|
||||||
|
args = args[1:]
|
||||||
if len(args) < 2:
|
if len(args) < 2:
|
||||||
options.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
|
||||||
|
|
||||||
file_queue = Queue(10000)
|
file_queue = Queue(10000)
|
||||||
|
|
||||||
def _upload_file((path, dir_marker), conn):
|
def _upload_file((path, dir_marker), conn):
|
||||||
try:
|
try:
|
||||||
obj = path
|
obj = path
|
||||||
@@ -1352,11 +1417,12 @@ def st_upload(options, args):
|
|||||||
content_length=getsize(path),
|
content_length=getsize(path),
|
||||||
headers=put_headers)
|
headers=put_headers)
|
||||||
if options.verbose:
|
if options.verbose:
|
||||||
options.print_queue.put(obj)
|
print_queue.put(obj)
|
||||||
except OSError, err:
|
except OSError, err:
|
||||||
if err.errno != ENOENT:
|
if err.errno != ENOENT:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Local file %s not found' % repr(path))
|
error_queue.put('Local file %s not found' % repr(path))
|
||||||
|
|
||||||
def _upload_dir(path):
|
def _upload_dir(path):
|
||||||
names = listdir(path)
|
names = listdir(path)
|
||||||
if not names:
|
if not names:
|
||||||
@@ -1368,10 +1434,11 @@ def st_upload(options, args):
|
|||||||
_upload_dir(subpath)
|
_upload_dir(subpath)
|
||||||
else:
|
else:
|
||||||
file_queue.put((subpath, False)) # dir_marker = False
|
file_queue.put((subpath, False)) # dir_marker = False
|
||||||
url, token = get_auth(options.auth, options.user, options.key, snet=options.snet)
|
|
||||||
|
url, token = get_auth(options.auth, options.user, options.key,
|
||||||
|
snet=options.snet)
|
||||||
create_connection = lambda: Connection(options.auth, options.user,
|
create_connection = lambda: Connection(options.auth, options.user,
|
||||||
options.key, preauthurl=url,
|
options.key, preauthurl=url, preauthtoken=token, snet=options.snet)
|
||||||
preauthtoken=token, snet=options.snet)
|
|
||||||
file_threads = [QueueFunctionThread(file_queue, _upload_file,
|
file_threads = [QueueFunctionThread(file_queue, _upload_file,
|
||||||
create_connection()) for _ in xrange(10)]
|
create_connection()) for _ in xrange(10)]
|
||||||
for thread in file_threads:
|
for thread in file_threads:
|
||||||
@@ -1400,12 +1467,26 @@ def st_upload(options, args):
|
|||||||
except ClientException, err:
|
except ClientException, err:
|
||||||
if err.http_status != 404:
|
if err.http_status != 404:
|
||||||
raise
|
raise
|
||||||
options.error_queue.put('Account not found')
|
error_queue.put('Account not found')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(parser, args):
|
||||||
|
if not args:
|
||||||
|
args = ['-h']
|
||||||
|
(options, args) = parser.parse_args(args)
|
||||||
|
required_help = '''
|
||||||
|
Requires ST_AUTH, ST_USER, and ST_KEY environment variables be set or
|
||||||
|
overridden with -A, -U, or -K.'''.strip('\n')
|
||||||
|
for attr in ('auth', 'user', 'key'):
|
||||||
|
if not getattr(options, attr, None) and \
|
||||||
|
not environ.get('ST_%s' % attr.upper()):
|
||||||
|
exit(required_help)
|
||||||
|
return options, args
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = OptionParser(version='%prog 1.0', usage='''
|
parser = OptionParser(version='%prog 1.0', usage='''
|
||||||
Usage: %%prog [options] <command> [args]
|
Usage: %%prog [options] <command> [options] [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
%(st_stat_help)s
|
%(st_stat_help)s
|
||||||
@@ -1424,55 +1505,15 @@ Example:
|
|||||||
default=1, help='Print more info')
|
default=1, help='Print more info')
|
||||||
parser.add_option('-q', '--quiet', action='store_const', dest='verbose',
|
parser.add_option('-q', '--quiet', action='store_const', dest='verbose',
|
||||||
const=0, default=1, help='Suppress status output')
|
const=0, default=1, help='Suppress status output')
|
||||||
parser.add_option('-a', '--all', action='store_true', dest='yes_all',
|
|
||||||
default=False, help='Indicate that you really want the '
|
|
||||||
'whole account for commands that require --all in such '
|
|
||||||
'a case')
|
|
||||||
parser.add_option('-c', '--changed', action='store_true', dest='changed',
|
|
||||||
default=False, help='For the upload command: will '
|
|
||||||
'only upload files that have changed since the last '
|
|
||||||
'upload')
|
|
||||||
parser.add_option('-p', '--prefix', dest='prefix',
|
|
||||||
help='For the list command: will only list items '
|
|
||||||
'beginning with the prefix')
|
|
||||||
parser.add_option('-d', '--delimiter', dest='delimiter',
|
|
||||||
help='For the list command on containers: will roll up '
|
|
||||||
'items with the given delimiter (see Cloud Files '
|
|
||||||
'general documentation for what this means).')
|
|
||||||
parser.add_option('-r', '--read-acl', dest='read_acl',
|
|
||||||
help='Sets the Read ACL with post container commands. '
|
|
||||||
'Quick summary of ACL syntax: .r:*, .r:-.example.com, '
|
|
||||||
'.r:www.example.com, account1, account2:user2')
|
|
||||||
parser.add_option('-w', '--write-acl', dest='write_acl',
|
|
||||||
help='Sets the Write ACL with post container commands. '
|
|
||||||
'Quick summary of ACL syntax: account1, account2:user2')
|
|
||||||
parser.add_option('-m', '--meta', action='append', dest='meta', default=[],
|
|
||||||
help='Sets a meta data item of the syntax name:value '
|
|
||||||
'for use with post commands. This option may be '
|
|
||||||
'repeated. Example: -m Color:Blue -m Size:Large')
|
|
||||||
parser.add_option('-A', '--auth', dest='auth',
|
parser.add_option('-A', '--auth', dest='auth',
|
||||||
help='URL for obtaining an auth token')
|
help='URL for obtaining an auth token')
|
||||||
parser.add_option('-U', '--user', dest='user',
|
parser.add_option('-U', '--user', dest='user',
|
||||||
help='User name for obtaining an auth token')
|
help='User name for obtaining an auth token')
|
||||||
parser.add_option('-K', '--key', dest='key',
|
parser.add_option('-K', '--key', dest='key',
|
||||||
help='Key for obtaining an auth token')
|
help='Key for obtaining an auth token')
|
||||||
parser.add_option('-o', '--output', dest='out_file',
|
parser.disable_interspersed_args()
|
||||||
help='For a single file download stream the output other location ')
|
(options, args) = parse_args(parser, argv[1:])
|
||||||
args = argv[1:]
|
parser.enable_interspersed_args()
|
||||||
if not args:
|
|
||||||
args.append('-h')
|
|
||||||
(options, args) = parser.parse_args(args)
|
|
||||||
if options.out_file == '-':
|
|
||||||
options.verbose = 0
|
|
||||||
|
|
||||||
required_help = '''
|
|
||||||
Requires ST_AUTH, ST_USER, and ST_KEY environment variables be set or
|
|
||||||
overridden with -A, -U, or -K.'''.strip('\n')
|
|
||||||
for attr in ('auth', 'user', 'key'):
|
|
||||||
if not getattr(options, attr, None):
|
|
||||||
setattr(options, attr, environ.get('ST_%s' % attr.upper()))
|
|
||||||
if not getattr(options, attr, None):
|
|
||||||
exit(required_help)
|
|
||||||
|
|
||||||
commands = ('delete', 'download', 'list', 'post', 'stat', 'upload')
|
commands = ('delete', 'download', 'list', 'post', 'stat', 'upload')
|
||||||
if not args or args[0] not in commands:
|
if not args or args[0] not in commands:
|
||||||
@@ -1481,30 +1522,36 @@ overridden with -A, -U, or -K.'''.strip('\n')
|
|||||||
exit('no such command: %s' % args[0])
|
exit('no such command: %s' % args[0])
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
options.print_queue = Queue(10000)
|
print_queue = Queue(10000)
|
||||||
|
|
||||||
def _print(item):
|
def _print(item):
|
||||||
if isinstance(item, unicode):
|
if isinstance(item, unicode):
|
||||||
item = item.encode('utf8')
|
item = item.encode('utf8')
|
||||||
print item
|
print item
|
||||||
print_thread = QueueFunctionThread(options.print_queue, _print)
|
|
||||||
|
print_thread = QueueFunctionThread(print_queue, _print)
|
||||||
print_thread.start()
|
print_thread.start()
|
||||||
|
|
||||||
options.error_queue = Queue(10000)
|
error_queue = Queue(10000)
|
||||||
|
|
||||||
def _error(item):
|
def _error(item):
|
||||||
if isinstance(item, unicode):
|
if isinstance(item, unicode):
|
||||||
item = item.encode('utf8')
|
item = item.encode('utf8')
|
||||||
print >>stderr, item
|
print >> stderr, item
|
||||||
error_thread = QueueFunctionThread(options.error_queue, _error)
|
|
||||||
|
error_thread = QueueFunctionThread(error_queue, _error)
|
||||||
error_thread.start()
|
error_thread.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
globals()['st_%s' % args[0]](options, args[1:])
|
parser.usage = globals()['st_%s_help' % args[0]]
|
||||||
while not options.print_queue.empty():
|
globals()['st_%s' % args[0]](parser, argv[1:], print_queue,
|
||||||
|
error_queue)
|
||||||
|
while not print_queue.empty():
|
||||||
sleep(0.01)
|
sleep(0.01)
|
||||||
print_thread.abort = True
|
print_thread.abort = True
|
||||||
while print_thread.isAlive():
|
while print_thread.isAlive():
|
||||||
print_thread.join(0.01)
|
print_thread.join(0.01)
|
||||||
while not options.error_queue.empty():
|
while not error_queue.empty():
|
||||||
sleep(0.01)
|
sleep(0.01)
|
||||||
error_thread.abort = True
|
error_thread.abort = True
|
||||||
while error_thread.isAlive():
|
while error_thread.isAlive():
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ except:
|
|||||||
from swift.common.bufferedhttp \
|
from swift.common.bufferedhttp \
|
||||||
import BufferedHTTPConnection as HTTPConnection
|
import BufferedHTTPConnection as HTTPConnection
|
||||||
|
|
||||||
|
|
||||||
def quote(value, safe='/'):
|
def quote(value, safe='/'):
|
||||||
"""
|
"""
|
||||||
Patched version of urllib.quote that encodes utf8 strings before quoting
|
Patched version of urllib.quote that encodes utf8 strings before quoting
|
||||||
|
|||||||
Reference in New Issue
Block a user