mirror of
https://github.com/robweber/xbmcbackup.git
synced 2024-11-15 04:45:49 +01:00
added dropbox resource files
This commit is contained in:
parent
ad2ddfa22f
commit
1321e244a7
2
resources/dropbox/.gitignore
vendored
Normal file
2
resources/dropbox/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
*.pyc
|
3
resources/dropbox/__init__.py
Normal file
3
resources/dropbox/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from . import client, rest, session
|
965
resources/dropbox/client.py
Normal file
965
resources/dropbox/client.py
Normal file
@ -0,0 +1,965 @@
|
|||||||
|
"""
|
||||||
|
The main client API you'll be working with most often. You'll need to
|
||||||
|
configure a dropbox.session.DropboxSession for this to work, but otherwise
|
||||||
|
it's fairly self-explanatory.
|
||||||
|
|
||||||
|
Before you can begin making requests to the dropbox API, you have to
|
||||||
|
authenticate your application with Dropbox and get the user to
|
||||||
|
authorize your application to use dropbox on his behalf. A typical
|
||||||
|
progam, from the initial imports to making a simple request (``account_info``),
|
||||||
|
looks like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Include the Dropbox SDK libraries
|
||||||
|
from dropbox import client, rest, session
|
||||||
|
|
||||||
|
# Get your app key and secret from the Dropbox developer website
|
||||||
|
APP_KEY = 'INSERT_APP_KEY_HERE'
|
||||||
|
APP_SECRET = 'INSERT_SECRET_HERE'
|
||||||
|
|
||||||
|
# ACCESS_TYPE should be 'dropbox' or 'app_folder' as configured for your app
|
||||||
|
ACCESS_TYPE = 'INSERT_ACCESS_TYPE_HERE'
|
||||||
|
|
||||||
|
sess = session.DropboxSession(APP_KEY, APP_SECRET, ACCESS_TYPE)
|
||||||
|
|
||||||
|
request_token = sess.obtain_request_token()
|
||||||
|
|
||||||
|
url = sess.build_authorize_url(request_token)
|
||||||
|
|
||||||
|
# Make the user sign in and authorize this token
|
||||||
|
print "url:", url
|
||||||
|
print "Please visit this website and press the 'Allow' button, then hit 'Enter' here."
|
||||||
|
raw_input()
|
||||||
|
|
||||||
|
# This will fail if the user didn't visit the above URL and hit 'Allow'
|
||||||
|
access_token = sess.obtain_access_token(request_token)
|
||||||
|
|
||||||
|
client = client.DropboxClient(sess)
|
||||||
|
print "linked account:", client.account_info()
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from StringIO import StringIO
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
from .rest import ErrorResponse, RESTClient
|
||||||
|
|
||||||
|
def format_path(path):
|
||||||
|
"""Normalize path for use with the Dropbox API.
|
||||||
|
|
||||||
|
This function turns multiple adjacent slashes into single
|
||||||
|
slashes, then ensures that there's a leading slash but
|
||||||
|
not a trailing slash.
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
path = re.sub(r'/+', '/', path)
|
||||||
|
|
||||||
|
if path == '/':
|
||||||
|
return (u"" if isinstance(path, unicode) else "")
|
||||||
|
else:
|
||||||
|
return '/' + path.strip('/')
|
||||||
|
|
||||||
|
class DropboxClient(object):
|
||||||
|
"""
|
||||||
|
The main access point of doing REST calls on Dropbox. You should
|
||||||
|
first create and configure a dropbox.session.DropboxSession object,
|
||||||
|
and then pass it into DropboxClient's constructor. DropboxClient
|
||||||
|
then does all the work of properly calling each API method
|
||||||
|
with the correct OAuth authentication.
|
||||||
|
|
||||||
|
You should be aware that any of these methods can raise a
|
||||||
|
rest.ErrorResponse exception if the server returns a non-200
|
||||||
|
or invalid HTTP response. Note that a 401 return status at any
|
||||||
|
point indicates that the user needs to be reauthenticated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, session, rest_client=RESTClient):
|
||||||
|
"""Initialize the DropboxClient object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
``session``: A dropbox.session.DropboxSession object to use for making requests.
|
||||||
|
``rest_client``: A dropbox.rest.RESTClient-like object to use for making requests. [optional]
|
||||||
|
"""
|
||||||
|
self.session = session
|
||||||
|
self.rest_client = rest_client
|
||||||
|
|
||||||
|
def request(self, target, params=None, method='POST', content_server=False):
|
||||||
|
"""Make an HTTP request to a target API method.
|
||||||
|
|
||||||
|
This is an internal method used to properly craft the url, headers, and
|
||||||
|
params for a Dropbox API request. It is exposed for you in case you
|
||||||
|
need craft other API calls not in this library or if you want to debug it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``target``: The target URL with leading slash (e.g. '/files')
|
||||||
|
- ``params``: A dictionary of parameters to add to the request
|
||||||
|
- ``method``: An HTTP method (e.g. 'GET' or 'POST')
|
||||||
|
- ``content_server``: A boolean indicating whether the request is to the
|
||||||
|
API content server, for example to fetch the contents of a file
|
||||||
|
rather than its metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A tuple of (url, params, headers) that should be used to make the request.
|
||||||
|
OAuth authentication information will be added as needed within these fields.
|
||||||
|
"""
|
||||||
|
assert method in ['GET','POST', 'PUT'], "Only 'GET', 'POST', and 'PUT' are allowed."
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
host = self.session.API_CONTENT_HOST if content_server else self.session.API_HOST
|
||||||
|
base = self.session.build_url(host, target)
|
||||||
|
headers, params = self.session.build_access_headers(method, base, params)
|
||||||
|
|
||||||
|
if method in ('GET', 'PUT'):
|
||||||
|
url = self.session.build_url(host, target, params)
|
||||||
|
else:
|
||||||
|
url = self.session.build_url(host, target)
|
||||||
|
|
||||||
|
return url, params, headers
|
||||||
|
|
||||||
|
|
||||||
|
def account_info(self):
|
||||||
|
"""Retrieve information about the user's account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing account information.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#account-info
|
||||||
|
"""
|
||||||
|
url, params, headers = self.request("/account/info", method='GET')
|
||||||
|
|
||||||
|
return self.rest_client.GET(url, headers)
|
||||||
|
|
||||||
|
def get_chunked_uploader(self, file_obj, length):
|
||||||
|
"""Creates a ChunkedUploader to upload the given file-like object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``file_obj``: The file-like object which is the source of the data
|
||||||
|
being uploaded.
|
||||||
|
- ``length``: The number of bytes to upload.
|
||||||
|
|
||||||
|
The expected use of this function is as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
bigFile = open("data.txt", 'rb')
|
||||||
|
|
||||||
|
uploader = myclient.get_chunked_uploader(bigFile, size)
|
||||||
|
print "uploading: ", size
|
||||||
|
while uploader.offset < size:
|
||||||
|
try:
|
||||||
|
upload = uploader.upload_chunked()
|
||||||
|
except rest.ErrorResponse, e:
|
||||||
|
# perform error handling and retry logic
|
||||||
|
|
||||||
|
The SDK leaves the error handling and retry logic to the developer
|
||||||
|
to implement, as the exact requirements will depend on the application
|
||||||
|
involved.
|
||||||
|
"""
|
||||||
|
return DropboxClient.ChunkedUploader(self, file_obj, length)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkedUploader(object):
|
||||||
|
"""Contains the logic around a chunked upload, which uploads a
|
||||||
|
large file to Dropbox via the /chunked_upload endpoint
|
||||||
|
"""
|
||||||
|
def __init__(self, client, file_obj, length):
|
||||||
|
self.client = client
|
||||||
|
self.offset = 0
|
||||||
|
self.upload_id = None
|
||||||
|
|
||||||
|
self.last_block = None
|
||||||
|
self.file_obj = file_obj
|
||||||
|
self.target_length = length
|
||||||
|
|
||||||
|
|
||||||
|
def upload_chunked(self, chunk_size = 4 * 1024 * 1024):
|
||||||
|
"""Uploads data from this ChunkedUploader's file_obj in chunks, until
|
||||||
|
an error occurs. Throws an exception when an error occurs, and can
|
||||||
|
be called again to resume the upload.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``chunk_size``: The number of bytes to put in each chunk. [default 4 MB]
|
||||||
|
"""
|
||||||
|
|
||||||
|
while self.offset < self.target_length:
|
||||||
|
next_chunk_size = min(chunk_size, self.target_length - self.offset)
|
||||||
|
if self.last_block == None:
|
||||||
|
self.last_block = self.file_obj.read(next_chunk_size)
|
||||||
|
|
||||||
|
try:
|
||||||
|
(self.offset, self.upload_id) = self.client.upload_chunk(StringIO(self.last_block), next_chunk_size, self.offset, self.upload_id)
|
||||||
|
self.last_block = None
|
||||||
|
except ErrorResponse, e:
|
||||||
|
reply = e.body
|
||||||
|
if "offset" in reply and reply['offset'] != 0:
|
||||||
|
if reply['offset'] > self.offset:
|
||||||
|
self.last_block = None
|
||||||
|
self.offset = reply['offset']
|
||||||
|
|
||||||
|
def finish(self, path, overwrite=False, parent_rev=None):
|
||||||
|
"""Commits the bytes uploaded by this ChunkedUploader to a file
|
||||||
|
in the users dropbox.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The full path of the file in the Dropbox.
|
||||||
|
- ``overwrite``: Whether to overwrite an existing file at the given path. [default False]
|
||||||
|
If overwrite is False and a file already exists there, Dropbox
|
||||||
|
will rename the upload to make sure it doesn't overwrite anything.
|
||||||
|
You need to check the metadata returned for the new name.
|
||||||
|
This field should only be True if your intent is to potentially
|
||||||
|
clobber changes to a file that you don't know about.
|
||||||
|
- ``parent_rev``: The rev field from the 'parent' of this upload. [optional]
|
||||||
|
If your intent is to update the file at the given path, you should
|
||||||
|
pass the parent_rev parameter set to the rev value from the most recent
|
||||||
|
metadata you have of the existing file at that path. If the server
|
||||||
|
has a more recent version of the file at the specified path, it will
|
||||||
|
automatically rename your uploaded file, spinning off a conflict.
|
||||||
|
Using this parameter effectively causes the overwrite parameter to be ignored.
|
||||||
|
The file will always be overwritten if you send the most-recent parent_rev,
|
||||||
|
and it will never be overwritten if you send a less-recent one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = "/commit_chunked_upload/%s%s" % (self.client.session.root, format_path(path))
|
||||||
|
|
||||||
|
params = dict(
|
||||||
|
overwrite = bool(overwrite),
|
||||||
|
upload_id = self.upload_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if parent_rev is not None:
|
||||||
|
params['parent_rev'] = parent_rev
|
||||||
|
|
||||||
|
url, params, headers = self.client.request(path, params, content_server=True)
|
||||||
|
|
||||||
|
return self.client.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
def upload_chunk(self, file_obj, length, offset=0, upload_id=None):
|
||||||
|
"""Uploads a single chunk of data from the given file like object. The majority of users
|
||||||
|
should use the ChunkedUploader object, which provides a simpler interface to the
|
||||||
|
chunked_upload API endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``file_obj``: The source of the data to upload
|
||||||
|
- ``length``: The number of bytes to upload in one chunk.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- The reply from the server, as a dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = dict()
|
||||||
|
|
||||||
|
if upload_id:
|
||||||
|
params['upload_id'] = upload_id
|
||||||
|
params['offset'] = offset
|
||||||
|
|
||||||
|
url, ignored_params, headers = self.request("/chunked_upload", params, method='PUT', content_server=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reply = self.rest_client.PUT(url, file_obj, headers)
|
||||||
|
return reply['offset'], reply['upload_id']
|
||||||
|
except ErrorResponse, e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def put_file(self, full_path, file_obj, overwrite=False, parent_rev=None):
|
||||||
|
"""Upload a file.
|
||||||
|
|
||||||
|
A typical use case would be as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
f = open('working-draft.txt')
|
||||||
|
response = client.put_file('/magnum-opus.txt', f)
|
||||||
|
print "uploaded:", response
|
||||||
|
|
||||||
|
which would return the metadata of the uploaded file, similar to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
{
|
||||||
|
'bytes': 77,
|
||||||
|
'icon': 'page_white_text',
|
||||||
|
'is_dir': False,
|
||||||
|
'mime_type': 'text/plain',
|
||||||
|
'modified': 'Wed, 20 Jul 2011 22:04:50 +0000',
|
||||||
|
'path': '/magnum-opus.txt',
|
||||||
|
'rev': '362e2029684fe',
|
||||||
|
'revision': 221922,
|
||||||
|
'root': 'dropbox',
|
||||||
|
'size': '77 bytes',
|
||||||
|
'thumb_exists': False
|
||||||
|
}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``full_path``: The full path to upload the file to, *including the file name*.
|
||||||
|
If the destination directory does not yet exist, it will be created.
|
||||||
|
- ``file_obj``: A file-like object to upload. If you would like, you can pass a string as file_obj.
|
||||||
|
- ``overwrite``: Whether to overwrite an existing file at the given path. [default False]
|
||||||
|
If overwrite is False and a file already exists there, Dropbox
|
||||||
|
will rename the upload to make sure it doesn't overwrite anything.
|
||||||
|
You need to check the metadata returned for the new name.
|
||||||
|
This field should only be True if your intent is to potentially
|
||||||
|
clobber changes to a file that you don't know about.
|
||||||
|
- ``parent_rev``: The rev field from the 'parent' of this upload. [optional]
|
||||||
|
If your intent is to update the file at the given path, you should
|
||||||
|
pass the parent_rev parameter set to the rev value from the most recent
|
||||||
|
metadata you have of the existing file at that path. If the server
|
||||||
|
has a more recent version of the file at the specified path, it will
|
||||||
|
automatically rename your uploaded file, spinning off a conflict.
|
||||||
|
Using this parameter effectively causes the overwrite parameter to be ignored.
|
||||||
|
The file will always be overwritten if you send the most-recent parent_rev,
|
||||||
|
and it will never be overwritten if you send a less-recent one.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the newly uploaded file.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#files-put
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 503: User over quota
|
||||||
|
|
||||||
|
Note: In Python versions below version 2.6, httplib doesn't handle file-like objects.
|
||||||
|
In that case, this code will read the entire file into memory (!).
|
||||||
|
"""
|
||||||
|
path = "/files_put/%s%s" % (self.session.root, format_path(full_path))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'overwrite': bool(overwrite),
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent_rev is not None:
|
||||||
|
params['parent_rev'] = parent_rev
|
||||||
|
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params, method='PUT', content_server=True)
|
||||||
|
|
||||||
|
return self.rest_client.PUT(url, file_obj, headers)
|
||||||
|
|
||||||
|
def get_file(self, from_path, rev=None):
|
||||||
|
"""Download a file.
|
||||||
|
|
||||||
|
Unlike most other calls, get_file returns a raw HTTPResponse with the connection open.
|
||||||
|
You should call .read() and perform any processing you need, then close the HTTPResponse.
|
||||||
|
|
||||||
|
A typical usage looks like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
out = open('magnum-opus.txt', 'w')
|
||||||
|
f, metadata = client.get_file_and_metadata('/magnum-opus.txt').read()
|
||||||
|
out.write(f)
|
||||||
|
|
||||||
|
which would download the file ``magnum-opus.txt`` and write the contents into
|
||||||
|
the file ``magnum-opus.txt`` on the local filesystem.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``from_path``: The path to the file to be downloaded.
|
||||||
|
- ``rev``: A previous rev value of the file to be downloaded. [optional]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An httplib.HTTPResponse that is the result of the request.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at the given path, or the file that was there was deleted.
|
||||||
|
- 200: Request was okay but response was malformed in some way.
|
||||||
|
"""
|
||||||
|
path = "/files/%s%s" % (self.session.root, format_path(from_path))
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if rev is not None:
|
||||||
|
params['rev'] = rev
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params, method='GET', content_server=True)
|
||||||
|
return self.rest_client.request("GET", url, headers=headers, raw_response=True)
|
||||||
|
|
||||||
|
def get_file_and_metadata(self, from_path, rev=None):
|
||||||
|
"""Download a file alongwith its metadata.
|
||||||
|
|
||||||
|
Acts as a thin wrapper around get_file() (see get_file() comments for
|
||||||
|
more details)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``from_path``: The path to the file to be downloaded.
|
||||||
|
- ``rev``: A previous rev value of the file to be downloaded. [optional]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An httplib.HTTPResponse that is the result of the request.
|
||||||
|
- A dictionary containing the metadata of the file (see
|
||||||
|
https://www.dropbox.com/developers/reference/api#metadata for details).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at the given path, or the file that was there was deleted.
|
||||||
|
- 200: Request was okay but response was malformed in some way.
|
||||||
|
"""
|
||||||
|
file_res = self.get_file(from_path, rev)
|
||||||
|
metadata = DropboxClient.__parse_metadata_as_dict(file_res)
|
||||||
|
|
||||||
|
return file_res, metadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __parse_metadata_as_dict(dropbox_raw_response):
|
||||||
|
"""Parses file metadata from a raw dropbox HTTP response, raising a
|
||||||
|
dropbox.rest.ErrorResponse if parsing fails.
|
||||||
|
"""
|
||||||
|
metadata = None
|
||||||
|
for header, header_val in dropbox_raw_response.getheaders():
|
||||||
|
if header.lower() == 'x-dropbox-metadata':
|
||||||
|
try:
|
||||||
|
metadata = json.loads(header_val)
|
||||||
|
except ValueError:
|
||||||
|
raise ErrorResponse(dropbox_raw_response)
|
||||||
|
if not metadata: raise ErrorResponse(dropbox_raw_response)
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def delta(self, cursor=None):
|
||||||
|
"""A way of letting you keep up with changes to files and folders in a
|
||||||
|
user's Dropbox. You can periodically call delta() to get a list of "delta
|
||||||
|
entries", which are instructions on how to update your local state to
|
||||||
|
match the server's state.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- ``cursor``: On the first call, omit this argument (or pass in ``None``). On
|
||||||
|
subsequent calls, pass in the ``cursor`` string returned by the previous
|
||||||
|
call.
|
||||||
|
|
||||||
|
Returns: A dict with three fields.
|
||||||
|
- ``entries``: A list of "delta entries" (described below)
|
||||||
|
- ``reset``: If ``True``, you should your local state to be an empty folder
|
||||||
|
before processing the list of delta entries. This is only ``True`` only
|
||||||
|
in rare situations.
|
||||||
|
- ``cursor``: A string that is used to keep track of your current state.
|
||||||
|
On the next call to delta(), pass in this value to return entries
|
||||||
|
that were recorded since the cursor was returned.
|
||||||
|
- ``has_more``: If ``True``, then there are more entries available; you can
|
||||||
|
call delta() again immediately to retrieve those entries. If ``False``,
|
||||||
|
then wait at least 5 minutes (preferably longer) before checking again.
|
||||||
|
|
||||||
|
Delta Entries: Each entry is a 2-item list of one of following forms:
|
||||||
|
- [*path*, *metadata*]: Indicates that there is a file/folder at the given
|
||||||
|
path. You should add the entry to your local path. (The *metadata*
|
||||||
|
value is the same as what would be returned by the ``metadata()`` call.)
|
||||||
|
|
||||||
|
- If the new entry includes parent folders that don't yet exist in your
|
||||||
|
local state, create those parent folders in your local state. You
|
||||||
|
will eventually get entries for those parent folders.
|
||||||
|
- If the new entry is a file, replace whatever your local state has at
|
||||||
|
*path* with the new entry.
|
||||||
|
- If the new entry is a folder, check what your local state has at
|
||||||
|
*path*. If it's a file, replace it with the new entry. If it's a
|
||||||
|
folder, apply the new *metadata* to the folder, but do not modify
|
||||||
|
the folder's children.
|
||||||
|
- [*path*, ``nil``]: Indicates that there is no file/folder at the *path* on
|
||||||
|
Dropbox. To update your local state to match, delete whatever is at *path*,
|
||||||
|
including any children (you will sometimes also get "delete" delta entries
|
||||||
|
for the children, but this is not guaranteed). If your local state doesn't
|
||||||
|
have anything at *path*, ignore this entry.
|
||||||
|
|
||||||
|
Remember: Dropbox treats file names in a case-insensitive but case-preserving
|
||||||
|
way. To facilitate this, the *path* strings above are lower-cased versions of
|
||||||
|
the actual path. The *metadata* dicts have the original, case-preserved path.
|
||||||
|
"""
|
||||||
|
path = "/delta"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if cursor is not None:
|
||||||
|
params['cursor'] = cursor
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def create_copy_ref(self, from_path):
|
||||||
|
"""Creates and returns a copy ref for a specific file. The copy ref can be
|
||||||
|
used to instantly copy that file to the Dropbox of another account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The path to the file for a copy ref to be created on.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary that looks like the following example:
|
||||||
|
|
||||||
|
``{"expires":"Fri, 31 Jan 2042 21:01:05 +0000", "copy_ref":"z1X6ATl6aWtzOGq0c3g5Ng"}``
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = "/copy_ref/%s%s" % (self.session.root, format_path(from_path))
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, {}, method='GET')
|
||||||
|
|
||||||
|
return self.rest_client.GET(url, headers)
|
||||||
|
|
||||||
|
def add_copy_ref(self, copy_ref, to_path):
|
||||||
|
"""Adds the file referenced by the copy ref to the specified path
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``copy_ref``: A copy ref string that was returned from a create_copy_ref call.
|
||||||
|
The copy_ref can be created from any other Dropbox account, or from the same account.
|
||||||
|
- ``path``: The path to where the file will be created.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the new copy of the file.
|
||||||
|
"""
|
||||||
|
path = "/fileops/copy"
|
||||||
|
|
||||||
|
params = {'from_copy_ref': copy_ref,
|
||||||
|
'to_path': format_path(to_path),
|
||||||
|
'root': self.session.root}
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
def file_copy(self, from_path, to_path):
|
||||||
|
"""Copy a file or folder to a new location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``from_path``: The path to the file or folder to be copied.
|
||||||
|
- ``to_path``: The destination path of the file or folder to be copied.
|
||||||
|
This parameter should include the destination filename (e.g.
|
||||||
|
from_path: '/test.txt', to_path: '/dir/test.txt'). If there's
|
||||||
|
already a file at the to_path, this copy will be renamed to
|
||||||
|
be unique.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the new copy of the file or folder.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#fileops-copy
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of:
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at given from_path.
|
||||||
|
- 503: User over storage quota.
|
||||||
|
"""
|
||||||
|
params = {'root': self.session.root,
|
||||||
|
'from_path': format_path(from_path),
|
||||||
|
'to_path': format_path(to_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
url, params, headers = self.request("/fileops/copy", params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def file_create_folder(self, path):
|
||||||
|
"""Create a folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The path of the new folder.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the newly created folder.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#fileops-create-folder
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 403: A folder at that path already exists.
|
||||||
|
"""
|
||||||
|
params = {'root': self.session.root, 'path': format_path(path)}
|
||||||
|
|
||||||
|
url, params, headers = self.request("/fileops/create_folder", params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def file_delete(self, path):
|
||||||
|
"""Delete a file or folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The path of the file or folder.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the just deleted file.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#fileops-delete
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at the given path.
|
||||||
|
"""
|
||||||
|
params = {'root': self.session.root, 'path': format_path(path)}
|
||||||
|
|
||||||
|
url, params, headers = self.request("/fileops/delete", params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def file_move(self, from_path, to_path):
|
||||||
|
"""Move a file or folder to a new location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``from_path``: The path to the file or folder to be moved.
|
||||||
|
- ``to_path``: The destination path of the file or folder to be moved.
|
||||||
|
This parameter should include the destination filename (e.g.
|
||||||
|
- ``from_path``: '/test.txt', to_path: '/dir/test.txt'). If there's
|
||||||
|
already a file at the to_path, this file or folder will be renamed to
|
||||||
|
be unique.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the new copy of the file or folder.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#fileops-move
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at given from_path.
|
||||||
|
- 503: User over storage quota.
|
||||||
|
"""
|
||||||
|
params = {'root': self.session.root, 'from_path': format_path(from_path), 'to_path': format_path(to_path)}
|
||||||
|
|
||||||
|
url, params, headers = self.request("/fileops/move", params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def metadata(self, path, list=True, file_limit=25000, hash=None, rev=None, include_deleted=False):
|
||||||
|
"""Retrieve metadata for a file or folder.
|
||||||
|
|
||||||
|
A typical use would be:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
folder_metadata = client.metadata('/')
|
||||||
|
print "metadata:", folder_metadata
|
||||||
|
|
||||||
|
which would return the metadata of the root directory. This
|
||||||
|
will look something like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
{
|
||||||
|
'bytes': 0,
|
||||||
|
'contents': [
|
||||||
|
{
|
||||||
|
'bytes': 0,
|
||||||
|
'icon': 'folder',
|
||||||
|
'is_dir': True,
|
||||||
|
'modified': 'Thu, 25 Aug 2011 00:03:15 +0000',
|
||||||
|
'path': '/Sample Folder',
|
||||||
|
'rev': '803beb471',
|
||||||
|
'revision': 8,
|
||||||
|
'root': 'dropbox',
|
||||||
|
'size': '0 bytes',
|
||||||
|
'thumb_exists': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 77,
|
||||||
|
'icon': 'page_white_text',
|
||||||
|
'is_dir': False,
|
||||||
|
'mime_type': 'text/plain',
|
||||||
|
'modified': 'Wed, 20 Jul 2011 22:04:50 +0000',
|
||||||
|
'path': '/magnum-opus.txt',
|
||||||
|
'rev': '362e2029684fe',
|
||||||
|
'revision': 221922,
|
||||||
|
'root': 'dropbox',
|
||||||
|
'size': '77 bytes',
|
||||||
|
'thumb_exists': False
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'hash': 'efdac89c4da886a9cece1927e6c22977',
|
||||||
|
'icon': 'folder',
|
||||||
|
'is_dir': True,
|
||||||
|
'path': '/',
|
||||||
|
'root': 'app_folder',
|
||||||
|
'size': '0 bytes',
|
||||||
|
'thumb_exists': False
|
||||||
|
}
|
||||||
|
|
||||||
|
In this example, the root directory contains two things: ``Sample Folder``,
|
||||||
|
which is a folder, and ``/magnum-opus.txt``, which is a text file 77 bytes long
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The path to the file or folder.
|
||||||
|
- ``list``: Whether to list all contained files (only applies when
|
||||||
|
path refers to a folder).
|
||||||
|
- ``file_limit``: The maximum number of file entries to return within
|
||||||
|
a folder. If the number of files in the directory exceeds this
|
||||||
|
limit, an exception is raised. The server will return at max
|
||||||
|
25,000 files within a folder.
|
||||||
|
- ``hash``: Every directory listing has a hash parameter attached that
|
||||||
|
can then be passed back into this function later to save on\
|
||||||
|
bandwidth. Rather than returning an unchanged folder's contents,\
|
||||||
|
the server will instead return a 304.\
|
||||||
|
- ``rev``: The revision of the file to retrieve the metadata for. [optional]
|
||||||
|
This parameter only applies for files. If omitted, you'll receive
|
||||||
|
the most recent revision metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the file or folder
|
||||||
|
(and contained files if appropriate).
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#metadata
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 304: Current directory hash matches hash parameters, so contents are unchanged.
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at given path.
|
||||||
|
- 406: Too many file entries to return.
|
||||||
|
"""
|
||||||
|
path = "/metadata/%s%s" % (self.session.root, format_path(path))
|
||||||
|
|
||||||
|
params = {'file_limit': file_limit,
|
||||||
|
'list': 'true',
|
||||||
|
'include_deleted': include_deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not list:
|
||||||
|
params['list'] = 'false'
|
||||||
|
if hash is not None:
|
||||||
|
params['hash'] = hash
|
||||||
|
if rev:
|
||||||
|
params['rev'] = rev
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params, method='GET')
|
||||||
|
|
||||||
|
return self.rest_client.GET(url, headers)
|
||||||
|
|
||||||
|
def thumbnail(self, from_path, size='large', format='JPEG'):
|
||||||
|
"""Download a thumbnail for an image.
|
||||||
|
|
||||||
|
Unlike most other calls, thumbnail returns a raw HTTPResponse with the connection open.
|
||||||
|
You should call .read() and perform any processing you need, then close the HTTPResponse.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``from_path``: The path to the file to be thumbnailed.
|
||||||
|
- ``size``: A string describing the desired thumbnail size.
|
||||||
|
At this time, 'small', 'medium', and 'large' are
|
||||||
|
officially supported sizes (32x32, 64x64, and 128x128
|
||||||
|
respectively), though others may be available. Check
|
||||||
|
https://www.dropbox.com/developers/reference/api#thumbnails for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An httplib.HTTPResponse that is the result of the request.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at the given from_path, or files of that type cannot be thumbnailed.
|
||||||
|
- 415: Image is invalid and cannot be thumbnailed.
|
||||||
|
"""
|
||||||
|
assert format in ['JPEG', 'PNG'], "expected a thumbnail format of 'JPEG' or 'PNG', got %s" % format
|
||||||
|
|
||||||
|
path = "/thumbnails/%s%s" % (self.session.root, format_path(from_path))
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, {'size': size, 'format': format}, method='GET', content_server=True)
|
||||||
|
return self.rest_client.request("GET", url, headers=headers, raw_response=True)
|
||||||
|
|
||||||
|
def thumbnail_and_metadata(self, from_path, size='large', format='JPEG'):
|
||||||
|
"""Download a thumbnail for an image alongwith its metadata.
|
||||||
|
|
||||||
|
Acts as a thin wrapper around thumbnail() (see thumbnail() comments for
|
||||||
|
more details)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``from_path``: The path to the file to be thumbnailed.
|
||||||
|
- ``size``: A string describing the desired thumbnail size. See thumbnail()
|
||||||
|
for details.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An httplib.HTTPResponse that is the result of the request.
|
||||||
|
- A dictionary containing the metadata of the file whose thumbnail
|
||||||
|
was downloaded (see https://www.dropbox.com/developers/reference/api#metadata
|
||||||
|
for details).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No file was found at the given from_path, or files of that type cannot be thumbnailed.
|
||||||
|
- 415: Image is invalid and cannot be thumbnailed.
|
||||||
|
- 200: Request was okay but response was malformed in some way.
|
||||||
|
"""
|
||||||
|
thumbnail_res = self.thumbnail(from_path, size, format)
|
||||||
|
metadata = DropboxClient.__parse_metadata_as_dict(thumbnail_res)
|
||||||
|
|
||||||
|
return thumbnail_res, metadata
|
||||||
|
|
||||||
|
def search(self, path, query, file_limit=1000, include_deleted=False):
|
||||||
|
"""Search directory for filenames matching query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The directory to search within.
|
||||||
|
- ``query``: The query to search on (minimum 3 characters).
|
||||||
|
- ``file_limit``: The maximum number of file entries to return within a folder.
|
||||||
|
The server will return at max 1,000 files.
|
||||||
|
- ``include_deleted``: Whether to include deleted files in search results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A list of the metadata of all matching files (up to
|
||||||
|
file_limit entries). For a detailed description of what
|
||||||
|
this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#search
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
"""
|
||||||
|
path = "/search/%s%s" % (self.session.root, format_path(path))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'query': query,
|
||||||
|
'file_limit': file_limit,
|
||||||
|
'include_deleted': include_deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
def revisions(self, path, rev_limit=1000):
|
||||||
|
"""Retrieve revisions of a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The file to fetch revisions for. Note that revisions
|
||||||
|
are not available for folders.
|
||||||
|
- ``rev_limit``: The maximum number of file entries to return within
|
||||||
|
a folder. The server will return at max 1,000 revisions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A list of the metadata of all matching files (up to rev_limit entries).
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#revisions
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: No revisions were found at the given path.
|
||||||
|
"""
|
||||||
|
path = "/revisions/%s%s" % (self.session.root, format_path(path))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'rev_limit': rev_limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params, method='GET')
|
||||||
|
|
||||||
|
return self.rest_client.GET(url, headers)
|
||||||
|
|
||||||
|
def restore(self, path, rev):
|
||||||
|
"""Restore a file to a previous revision.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The file to restore. Note that folders can't be restored.
|
||||||
|
- ``rev``: A previous rev value of the file to be restored to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary containing the metadata of the newly restored file.
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#restore
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: Unable to find the file at the given revision.
|
||||||
|
"""
|
||||||
|
path = "/restore/%s%s" % (self.session.root, format_path(path))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'rev': rev,
|
||||||
|
}
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, params)
|
||||||
|
|
||||||
|
return self.rest_client.POST(url, params, headers)
|
||||||
|
|
||||||
|
def media(self, path):
|
||||||
|
"""Get a temporary unauthenticated URL for a media file.
|
||||||
|
|
||||||
|
All of Dropbox's API methods require OAuth, which may cause problems in
|
||||||
|
situations where an application expects to be able to hit a URL multiple times
|
||||||
|
(for example, a media player seeking around a video file). This method
|
||||||
|
creates a time-limited URL that can be accessed without any authentication,
|
||||||
|
and returns that to you, along with an expiration time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The file to return a URL for. Folders are not supported.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary that looks like the following example:
|
||||||
|
|
||||||
|
``{'url': 'https://dl.dropbox.com/0/view/wvxv1fw6on24qw7/file.mov', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}``
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#media
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: Unable to find the file at the given path.
|
||||||
|
"""
|
||||||
|
path = "/media/%s%s" % (self.session.root, format_path(path))
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, method='GET')
|
||||||
|
|
||||||
|
return self.rest_client.GET(url, headers)
|
||||||
|
|
||||||
|
def share(self, path):
|
||||||
|
"""Create a shareable link to a file or folder.
|
||||||
|
|
||||||
|
Shareable links created on Dropbox are time-limited, but don't require any
|
||||||
|
authentication, so they can be given out freely. The time limit should allow
|
||||||
|
at least a day of shareability, though users have the ability to disable
|
||||||
|
a link from their account if they like.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``path``: The file or folder to share.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary that looks like the following example:
|
||||||
|
|
||||||
|
``{'url': 'http://www.dropbox.com/s/m/a2mbDa2', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}``
|
||||||
|
|
||||||
|
For a detailed description of what this call returns, visit:
|
||||||
|
https://www.dropbox.com/developers/reference/api#shares
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- A dropbox.rest.ErrorResponse with an HTTP status of
|
||||||
|
|
||||||
|
- 400: Bad request (may be due to many things; check e.error for details)
|
||||||
|
- 404: Unable to find the file at the given path.
|
||||||
|
"""
|
||||||
|
path = "/shares/%s%s" % (self.session.root, format_path(path))
|
||||||
|
|
||||||
|
url, params, headers = self.request(path, method='GET')
|
||||||
|
|
||||||
|
return self.rest_client.GET(url, headers)
|
317
resources/dropbox/rest.py
Normal file
317
resources/dropbox/rest.py
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
"""
|
||||||
|
A simple JSON REST request abstraction layer that is used by the
|
||||||
|
dropbox.client and dropbox.session modules. You shouldn't need to use this.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import httplib
|
||||||
|
import os
|
||||||
|
import pkg_resources
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
from . import util
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
SDK_VERSION = "1.5.1"
|
||||||
|
|
||||||
|
TRUSTED_CERT_FILE = pkg_resources.resource_filename(__name__, 'trusted-certs.crt')
|
||||||
|
|
||||||
|
class ProperHTTPSConnection(httplib.HTTPConnection):
|
||||||
|
"""
|
||||||
|
httplib.HTTPSConnection is broken because it doesn't do server certificate
|
||||||
|
validation. This class does certificate validation by ensuring:
|
||||||
|
1. The certificate sent down by the server has a signature chain to one of
|
||||||
|
the certs in our 'trusted-certs.crt' (this is mostly handled by the 'ssl'
|
||||||
|
module).
|
||||||
|
2. The hostname in the certificate matches the hostname we're connecting to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, host, port, trusted_cert_file=TRUSTED_CERT_FILE):
|
||||||
|
httplib.HTTPConnection.__init__(self, host, port)
|
||||||
|
self.ca_certs = trusted_cert_file
|
||||||
|
self.cert_reqs = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
sock = create_connection((self.host, self.port))
|
||||||
|
self.sock = ssl.wrap_socket(sock, cert_reqs=self.cert_reqs, ca_certs=self.ca_certs)
|
||||||
|
cert = self.sock.getpeercert()
|
||||||
|
hostname = self.host.split(':', 0)[0]
|
||||||
|
match_hostname(cert, hostname)
|
||||||
|
|
||||||
|
class CertificateError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _dnsname_to_pat(dn):
|
||||||
|
pats = []
|
||||||
|
for frag in dn.split(r'.'):
|
||||||
|
if frag == '*':
|
||||||
|
# When '*' is a fragment by itself, it matches a non-empty dotless
|
||||||
|
# fragment.
|
||||||
|
pats.append('[^.]+')
|
||||||
|
else:
|
||||||
|
# Otherwise, '*' matches any dotless fragment.
|
||||||
|
frag = re.escape(frag)
|
||||||
|
pats.append(frag.replace(r'\*', '[^.]*'))
|
||||||
|
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
|
||||||
|
|
||||||
|
# This was ripped from Python 3.2 so it's not tested
|
||||||
|
def match_hostname(cert, hostname):
|
||||||
|
"""Verify that *cert* (in decoded format as returned by
|
||||||
|
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
|
||||||
|
are mostly followed, but IP addresses are not accepted for *hostname*.
|
||||||
|
|
||||||
|
CertificateError is raised on failure. On success, the function
|
||||||
|
returns nothing.
|
||||||
|
"""
|
||||||
|
if not cert:
|
||||||
|
raise ValueError("empty or no certificate")
|
||||||
|
dnsnames = []
|
||||||
|
san = cert.get('subjectAltName', ())
|
||||||
|
for key, value in san:
|
||||||
|
if key == 'DNS':
|
||||||
|
if _dnsname_to_pat(value).match(hostname):
|
||||||
|
return
|
||||||
|
dnsnames.append(value)
|
||||||
|
if not san:
|
||||||
|
# The subject is only checked when subjectAltName is empty
|
||||||
|
for sub in cert.get('subject', ()):
|
||||||
|
for key, value in sub:
|
||||||
|
# XXX according to RFC 2818, the most specific Common Name
|
||||||
|
# must be used.
|
||||||
|
if key == 'commonName':
|
||||||
|
if _dnsname_to_pat(value).match(hostname):
|
||||||
|
return
|
||||||
|
dnsnames.append(value)
|
||||||
|
if len(dnsnames) > 1:
|
||||||
|
raise CertificateError("hostname %r doesn't match either of %s" % (hostname, ', '.join(map(repr, dnsnames))))
|
||||||
|
elif len(dnsnames) == 1:
|
||||||
|
raise CertificateError("hostname %r doesn't match %r" % (hostname, dnsnames[0]))
|
||||||
|
else:
|
||||||
|
raise CertificateError("no appropriate commonName or subjectAltName fields were found")
|
||||||
|
|
||||||
|
def create_connection(address):
|
||||||
|
host, port = address
|
||||||
|
err = None
|
||||||
|
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||||
|
af, socktype, proto, canonname, sa = res
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
sock = socket.socket(af, socktype, proto)
|
||||||
|
sock.connect(sa)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
except socket.error, _:
|
||||||
|
err = _
|
||||||
|
if sock is not None:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
if err is not None:
|
||||||
|
raise err
|
||||||
|
else:
|
||||||
|
raise socket.error("getaddrinfo returns an empty list")
|
||||||
|
|
||||||
|
def json_loadb(data):
|
||||||
|
if sys.version_info >= (3,):
|
||||||
|
data = data.decode('utf8')
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
class RESTClientObject(object):
|
||||||
|
def __init__(self, http_connect=None):
|
||||||
|
self.http_connect = http_connect
|
||||||
|
|
||||||
|
def request(self, method, url, post_params=None, body=None, headers=None, raw_response=False):
|
||||||
|
post_params = post_params or {}
|
||||||
|
headers = headers or {}
|
||||||
|
headers['User-Agent'] = 'OfficialDropboxPythonSDK/' + SDK_VERSION
|
||||||
|
|
||||||
|
if post_params:
|
||||||
|
if body:
|
||||||
|
raise ValueError("body parameter cannot be used with post_params parameter")
|
||||||
|
body = urllib.urlencode(post_params)
|
||||||
|
headers["Content-type"] = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
|
# maintain dynamic lookup of ProperHTTPConnection
|
||||||
|
http_connect = self.http_connect
|
||||||
|
if http_connect is None:
|
||||||
|
http_connect = ProperHTTPSConnection
|
||||||
|
|
||||||
|
host = urlparse.urlparse(url).hostname
|
||||||
|
conn = http_connect(host, 443)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This code is here because httplib in pre-2.6 Pythons
|
||||||
|
# doesn't handle file-like objects as HTTP bodies and
|
||||||
|
# thus requires manual buffering
|
||||||
|
if not hasattr(body, 'read'):
|
||||||
|
conn.request(method, url, body, headers)
|
||||||
|
else:
|
||||||
|
# Content-Length should be set to prevent upload truncation errors.
|
||||||
|
clen, raw_data = util.analyze_file_obj(body)
|
||||||
|
headers["Content-Length"] = str(clen)
|
||||||
|
conn.request(method, url, "", headers)
|
||||||
|
if raw_data is not None:
|
||||||
|
conn.send(raw_data)
|
||||||
|
else:
|
||||||
|
BLOCKSIZE = 4 * 1024 * 1024 # 4MB buffering just because
|
||||||
|
bytes_read = 0
|
||||||
|
while True:
|
||||||
|
data = body.read(BLOCKSIZE)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
# Catch Content-Length overflow before the HTTP server does
|
||||||
|
bytes_read += len(data)
|
||||||
|
if bytes_read > clen:
|
||||||
|
raise util.AnalyzeFileObjBug(clen, bytes_read)
|
||||||
|
conn.send(data)
|
||||||
|
if bytes_read != clen:
|
||||||
|
raise util.AnalyzeFileObjBug(clen, bytes_read)
|
||||||
|
|
||||||
|
except socket.error, e:
|
||||||
|
raise RESTSocketError(host, e)
|
||||||
|
except CertificateError, e:
|
||||||
|
raise RESTSocketError(host, "SSL certificate error: " + e)
|
||||||
|
|
||||||
|
r = conn.getresponse()
|
||||||
|
if r.status != 200:
|
||||||
|
raise ErrorResponse(r)
|
||||||
|
|
||||||
|
if raw_response:
|
||||||
|
return r
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = json_loadb(r.read())
|
||||||
|
except ValueError:
|
||||||
|
raise ErrorResponse(r)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def GET(self, url, headers=None, raw_response=False):
|
||||||
|
assert type(raw_response) == bool
|
||||||
|
return self.request("GET", url, headers=headers, raw_response=raw_response)
|
||||||
|
|
||||||
|
def POST(self, url, params=None, headers=None, raw_response=False):
|
||||||
|
assert type(raw_response) == bool
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
return self.request("POST", url,
|
||||||
|
post_params=params, headers=headers, raw_response=raw_response)
|
||||||
|
|
||||||
|
def PUT(self, url, body, headers=None, raw_response=False):
|
||||||
|
assert type(raw_response) == bool
|
||||||
|
return self.request("PUT", url, body=body, headers=headers, raw_response=raw_response)
|
||||||
|
|
||||||
|
class RESTClient(object):
|
||||||
|
IMPL = RESTClientObject()
|
||||||
|
|
||||||
|
"""
|
||||||
|
An class with all static methods to perform JSON REST requests that is used internally
|
||||||
|
by the Dropbox Client API. It provides just enough gear to make requests
|
||||||
|
and get responses as JSON data (when applicable). All requests happen over SSL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def request(cls, *n, **kw):
|
||||||
|
"""Perform a REST request and parse the response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``method``: An HTTP method (e.g. 'GET' or 'POST').
|
||||||
|
- ``url``: The URL to make a request to.
|
||||||
|
- ``post_params``: A dictionary of parameters to put in the body of the request.
|
||||||
|
This option may not be used if the body parameter is given.
|
||||||
|
- ``body``: The body of the request. Typically, this value will be a string.
|
||||||
|
It may also be a file-like object in Python 2.6 and above. The body
|
||||||
|
parameter may not be used with the post_params parameter.
|
||||||
|
- ``headers``: A dictionary of headers to send with the request.
|
||||||
|
- ``raw_response``: Whether to return the raw httplib.HTTPReponse object. [default False]
|
||||||
|
It's best enabled for requests that return large amounts of data that you
|
||||||
|
would want to .read() incrementally rather than loading into memory. Also
|
||||||
|
use this for calls where you need to read metadata like status or headers,
|
||||||
|
or if the body is not JSON.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- The JSON-decoded data from the server, unless raw_response is
|
||||||
|
specified, in which case an httplib.HTTPReponse object is returned instead.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
- dropbox.rest.ErrorResponse: The returned HTTP status is not 200, or the body was
|
||||||
|
not parsed from JSON successfully.
|
||||||
|
- dropbox.rest.RESTSocketError: A socket.error was raised while contacting Dropbox.
|
||||||
|
"""
|
||||||
|
return cls.IMPL.request(*n, **kw)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def GET(cls, *n, **kw):
|
||||||
|
"""Perform a GET request using RESTClient.request"""
|
||||||
|
return cls.IMPL.GET(*n, **kw)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def POST(cls, *n, **kw):
|
||||||
|
"""Perform a POST request using RESTClient.request"""
|
||||||
|
return cls.IMPL.POST(*n, **kw)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def PUT(cls, *n, **kw):
|
||||||
|
"""Perform a PUT request using RESTClient.request"""
|
||||||
|
return cls.IMPL.PUT(*n, **kw)
|
||||||
|
|
||||||
|
class RESTSocketError(socket.error):
|
||||||
|
"""
|
||||||
|
A light wrapper for socket.errors raised by dropbox.rest.RESTClient.request
|
||||||
|
that adds more information to the socket.error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, host, e):
|
||||||
|
msg = "Error connecting to \"%s\": %s" % (host, str(e))
|
||||||
|
socket.error.__init__(self, msg)
|
||||||
|
|
||||||
|
class ErrorResponse(Exception):
|
||||||
|
"""
|
||||||
|
Raised by dropbox.rest.RESTClient.request for requests that:
|
||||||
|
- Return a non-200 HTTP response, or
|
||||||
|
- Have a non-JSON response body, or
|
||||||
|
- Have a malformed/missing header in the response.
|
||||||
|
|
||||||
|
Most errors that Dropbox returns will have a error field that is unpacked and
|
||||||
|
placed on the ErrorResponse exception. In some situations, a user_error field
|
||||||
|
will also come back. Messages under user_error are worth showing to an end-user
|
||||||
|
of your app, while other errors are likely only useful for you as the developer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, http_resp):
|
||||||
|
self.status = http_resp.status
|
||||||
|
self.reason = http_resp.reason
|
||||||
|
self.body = http_resp.read()
|
||||||
|
self.headers = http_resp.getheaders()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.body = json_loadb(self.body)
|
||||||
|
self.error_msg = self.body.get('error')
|
||||||
|
self.user_error_msg = self.body.get('user_error')
|
||||||
|
except ValueError:
|
||||||
|
self.error_msg = None
|
||||||
|
self.user_error_msg = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.user_error_msg and self.user_error_msg != self.error_msg:
|
||||||
|
# one is translated and the other is English
|
||||||
|
msg = "%s (%s)" % (self.user_error_msg, self.error_msg)
|
||||||
|
elif self.error_msg:
|
||||||
|
msg = self.error_msg
|
||||||
|
elif not self.body:
|
||||||
|
msg = self.reason
|
||||||
|
else:
|
||||||
|
msg = "Error parsing response body or headers: " +\
|
||||||
|
"Body - %s Headers - %s" % (self.body, self.headers)
|
||||||
|
|
||||||
|
return "[%d] %s" % (self.status, repr(msg))
|
||||||
|
|
286
resources/dropbox/session.py
Normal file
286
resources/dropbox/session.py
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
"""
|
||||||
|
dropbox.session.DropboxSession is responsible for holding OAuth authentication info
|
||||||
|
(app key/secret, request key/secret, access key/secret) as well as configuration information for your app
|
||||||
|
('app_folder' or 'dropbox' access type, optional locale preference). It knows how to
|
||||||
|
use all of this information to craft properly constructed requests to Dropbox.
|
||||||
|
|
||||||
|
A DropboxSession object must be passed to a dropbox.client.DropboxClient object upon
|
||||||
|
initialization.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urlparse import parse_qs
|
||||||
|
except ImportError:
|
||||||
|
# fall back for Python 2.5
|
||||||
|
from cgi import parse_qs
|
||||||
|
|
||||||
|
from . import rest
|
||||||
|
|
||||||
|
class OAuthToken(object):
|
||||||
|
__slots__ = ('key', 'secret')
|
||||||
|
def __init__(self, key, secret):
|
||||||
|
self.key = key
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
class DropboxSession(object):
|
||||||
|
API_VERSION = 1
|
||||||
|
|
||||||
|
API_HOST = "api.dropbox.com"
|
||||||
|
WEB_HOST = "www.dropbox.com"
|
||||||
|
API_CONTENT_HOST = "api-content.dropbox.com"
|
||||||
|
|
||||||
|
def __init__(self, consumer_key, consumer_secret, access_type, locale=None, rest_client=rest.RESTClient):
|
||||||
|
"""Initialize a DropboxSession object.
|
||||||
|
|
||||||
|
Your consumer key and secret are available
|
||||||
|
at https://www.dropbox.com/developers/apps
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``access_type``: Either 'dropbox' or 'app_folder'. All path-based operations
|
||||||
|
will occur relative to either the user's Dropbox root directory
|
||||||
|
or your application's app folder.
|
||||||
|
- ``locale``: A locale string ('en', 'pt_PT', etc.) [optional]
|
||||||
|
The locale setting will be used to translate any user-facing error
|
||||||
|
messages that the server generates. At this time Dropbox supports
|
||||||
|
'en', 'es', 'fr', 'de', and 'ja', though we will be supporting more
|
||||||
|
languages in the future. If you send a language the server doesn't
|
||||||
|
support, messages will remain in English. Look for these translated
|
||||||
|
messages in rest.ErrorResponse exceptions as e.user_error_msg.
|
||||||
|
"""
|
||||||
|
assert access_type in ['dropbox', 'app_folder'], "expected access_type of 'dropbox' or 'app_folder'"
|
||||||
|
self.consumer_creds = OAuthToken(consumer_key, consumer_secret)
|
||||||
|
self.token = None
|
||||||
|
self.request_token = None
|
||||||
|
self.root = 'sandbox' if access_type == 'app_folder' else 'dropbox'
|
||||||
|
self.locale = locale
|
||||||
|
self.rest_client = rest_client
|
||||||
|
|
||||||
|
def is_linked(self):
|
||||||
|
"""Return whether the DropboxSession has an access token attached."""
|
||||||
|
return bool(self.token)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
"""Remove any attached access token from the DropboxSession."""
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def set_token(self, access_token, access_token_secret):
|
||||||
|
"""Attach an access token to the DropboxSession.
|
||||||
|
|
||||||
|
Note that the access 'token' is made up of both a token string
|
||||||
|
and a secret string.
|
||||||
|
"""
|
||||||
|
self.token = OAuthToken(access_token, access_token_secret)
|
||||||
|
|
||||||
|
def set_request_token(self, request_token, request_token_secret):
|
||||||
|
"""Attach an request token to the DropboxSession.
|
||||||
|
|
||||||
|
Note that the request 'token' is made up of both a token string
|
||||||
|
and a secret string.
|
||||||
|
"""
|
||||||
|
self.request_token = OAuthToken(request_token, request_token_secret)
|
||||||
|
|
||||||
|
def build_path(self, target, params=None):
|
||||||
|
"""Build the path component for an API URL.
|
||||||
|
|
||||||
|
This method urlencodes the parameters, adds them
|
||||||
|
to the end of the target url, and puts a marker for the API
|
||||||
|
version in front.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``target``: A target url (e.g. '/files') to build upon.
|
||||||
|
- ``params``: A dictionary of parameters (name to value). [optional]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- The path and parameters components of an API URL.
|
||||||
|
"""
|
||||||
|
if sys.version_info < (3,) and type(target) == unicode:
|
||||||
|
target = target.encode("utf8")
|
||||||
|
|
||||||
|
target_path = urllib.quote(target)
|
||||||
|
|
||||||
|
params = params or {}
|
||||||
|
params = params.copy()
|
||||||
|
|
||||||
|
if self.locale:
|
||||||
|
params['locale'] = self.locale
|
||||||
|
|
||||||
|
if params:
|
||||||
|
return "/%d%s?%s" % (self.API_VERSION, target_path, urllib.urlencode(params))
|
||||||
|
else:
|
||||||
|
return "/%d%s" % (self.API_VERSION, target_path)
|
||||||
|
|
||||||
|
def build_url(self, host, target, params=None):
|
||||||
|
"""Build an API URL.
|
||||||
|
|
||||||
|
This method adds scheme and hostname to the path
|
||||||
|
returned from build_path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``target``: A target url (e.g. '/files') to build upon.
|
||||||
|
- ``params``: A dictionary of parameters (name to value). [optional]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- The full API URL.
|
||||||
|
"""
|
||||||
|
return "https://%s%s" % (host, self.build_path(target, params))
|
||||||
|
|
||||||
|
def build_authorize_url(self, request_token, oauth_callback=None):
|
||||||
|
"""Build a request token authorization URL.
|
||||||
|
|
||||||
|
After obtaining a request token, you'll need to send the user to
|
||||||
|
the URL returned from this function so that they can confirm that
|
||||||
|
they want to connect their account to your app.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``request_token``: A request token from obtain_request_token.
|
||||||
|
- ``oauth_callback``: A url to redirect back to with the authorized
|
||||||
|
request token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An authorization for the given request token.
|
||||||
|
"""
|
||||||
|
params = {'oauth_token': request_token.key,
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth_callback:
|
||||||
|
params['oauth_callback'] = oauth_callback
|
||||||
|
|
||||||
|
return self.build_url(self.WEB_HOST, '/oauth/authorize', params)
|
||||||
|
|
||||||
|
def obtain_request_token(self):
|
||||||
|
"""Obtain a request token from the Dropbox API.
|
||||||
|
|
||||||
|
This is your first step in the OAuth process. You call this to get a
|
||||||
|
request_token from the Dropbox server that you can then use with
|
||||||
|
DropboxSession.build_authorize_url() to get the user to authorize it.
|
||||||
|
After it's authorized you use this token with
|
||||||
|
DropboxSession.obtain_access_token() to get an access token.
|
||||||
|
|
||||||
|
NOTE: You should only need to do this once for each user, and then you
|
||||||
|
can store the access token for that user for later operations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An dropbox.session.OAuthToken representing the request token Dropbox assigned
|
||||||
|
to this app. Also attaches the request token as self.request_token.
|
||||||
|
"""
|
||||||
|
self.token = None # clear any token currently on the request
|
||||||
|
url = self.build_url(self.API_HOST, '/oauth/request_token')
|
||||||
|
headers, params = self.build_access_headers('POST', url)
|
||||||
|
|
||||||
|
response = self.rest_client.POST(url, headers=headers, params=params, raw_response=True)
|
||||||
|
self.request_token = self._parse_token(response.read())
|
||||||
|
return self.request_token
|
||||||
|
|
||||||
|
def obtain_access_token(self, request_token=None):
|
||||||
|
"""Obtain an access token for a user.
|
||||||
|
|
||||||
|
After you get a request token, and then send the user to the authorize
|
||||||
|
URL, you can use the authorized request token with this method to get the
|
||||||
|
access token to use for future operations. The access token is stored on
|
||||||
|
the session object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``request_token``: A request token from obtain_request_token. [optional]
|
||||||
|
The request_token should have been authorized via the
|
||||||
|
authorization url from build_authorize_url. If you don't pass
|
||||||
|
a request_token, the fallback is self.request_token, which
|
||||||
|
will exist if you previously called obtain_request_token on this
|
||||||
|
DropboxSession instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- An tuple of (key, secret) representing the access token Dropbox assigned
|
||||||
|
to this app and user. Also attaches the access token as self.token.
|
||||||
|
"""
|
||||||
|
request_token = request_token or self.request_token
|
||||||
|
assert request_token, "No request_token available on the session. Please pass one."
|
||||||
|
url = self.build_url(self.API_HOST, '/oauth/access_token')
|
||||||
|
headers, params = self.build_access_headers('POST', url, request_token=request_token)
|
||||||
|
|
||||||
|
response = self.rest_client.POST(url, headers=headers, params=params, raw_response=True)
|
||||||
|
self.token = self._parse_token(response.read())
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def build_access_headers(self, method, resource_url, params=None, request_token=None):
|
||||||
|
"""Build OAuth access headers for a future request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- ``method``: The HTTP method being used (e.g. 'GET' or 'POST').
|
||||||
|
- ``resource_url``: The full url the request will be made to.
|
||||||
|
- ``params``: A dictionary of parameters to add to what's already on the url.
|
||||||
|
Typically, this would consist of POST parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A tuple of (header_dict, params) where header_dict is a dictionary
|
||||||
|
of header names and values appropriate for passing into dropbox.rest.RESTClient
|
||||||
|
and params is a dictionary like the one that was passed in, but augmented with
|
||||||
|
oauth-related parameters as appropriate.
|
||||||
|
"""
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
else:
|
||||||
|
params = params.copy()
|
||||||
|
|
||||||
|
oauth_params = {
|
||||||
|
'oauth_consumer_key' : self.consumer_creds.key,
|
||||||
|
'oauth_timestamp' : self._generate_oauth_timestamp(),
|
||||||
|
'oauth_nonce' : self._generate_oauth_nonce(),
|
||||||
|
'oauth_version' : self._oauth_version(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token = request_token if request_token is not None else self.token
|
||||||
|
|
||||||
|
if token:
|
||||||
|
oauth_params['oauth_token'] = token.key
|
||||||
|
|
||||||
|
self._oauth_sign_request(oauth_params, self.consumer_creds, token)
|
||||||
|
|
||||||
|
params.update(oauth_params)
|
||||||
|
|
||||||
|
return {}, params
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _oauth_sign_request(cls, params, consumer_pair, token_pair):
|
||||||
|
params.update({'oauth_signature_method' : 'PLAINTEXT',
|
||||||
|
'oauth_signature' : ('%s&%s' % (consumer_pair.secret, token_pair.secret)
|
||||||
|
if token_pair is not None else
|
||||||
|
'%s&' % (consumer_pair.secret,))})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _generate_oauth_timestamp(cls):
|
||||||
|
return int(time.time())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _generate_oauth_nonce(cls, length=8):
|
||||||
|
return ''.join([str(random.randint(0, 9)) for i in range(length)])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _oauth_version(cls):
|
||||||
|
return '1.0'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_token(cls, s):
|
||||||
|
if not s:
|
||||||
|
raise ValueError("Invalid parameter string.")
|
||||||
|
|
||||||
|
params = parse_qs(s, keep_blank_values=False)
|
||||||
|
if not params:
|
||||||
|
raise ValueError("Invalid parameter string: %r" % s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = params['oauth_token'][0]
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("'oauth_token' not found in OAuth request.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
secret = params['oauth_token_secret'][0]
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("'oauth_token_secret' not found in "
|
||||||
|
"OAuth request.")
|
||||||
|
|
||||||
|
return OAuthToken(key, secret)
|
11
resources/dropbox/six.py
Normal file
11
resources/dropbox/six.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
def b(str_):
|
||||||
|
if sys.version_info >= (3,):
|
||||||
|
str_ = str_.encode('latin1')
|
||||||
|
return str_
|
||||||
|
|
||||||
|
def u(str_):
|
||||||
|
if sys.version_info < (3,):
|
||||||
|
str_ = str_.decode('latin1')
|
||||||
|
return str_
|
341
resources/dropbox/trusted-certs.crt
Normal file
341
resources/dropbox/trusted-certs.crt
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
# Subject: C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com <server-certs@thawte.com>
|
||||||
|
# Issuer: C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress= server-certs@thawte.com
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx
|
||||||
|
FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
|
||||||
|
VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
|
||||||
|
biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm
|
||||||
|
MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx
|
||||||
|
MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
|
||||||
|
DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3
|
||||||
|
dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl
|
||||||
|
cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3
|
||||||
|
DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD
|
||||||
|
gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91
|
||||||
|
yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX
|
||||||
|
L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj
|
||||||
|
EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG
|
||||||
|
7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e
|
||||||
|
QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ
|
||||||
|
qdq5snUb9kLy78fyGPmJvKP/iiMucEc=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Premium Server CA/emailAddress=premium-server@thawte.com <premium-server@thawte.com>
|
||||||
|
# Issuer: C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Premium Server CA/emailAddress=premium-server@thawte.com <premium-server@thawte.com>
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx
|
||||||
|
FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
|
||||||
|
VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
|
||||||
|
biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy
|
||||||
|
dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t
|
||||||
|
MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB
|
||||||
|
MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG
|
||||||
|
A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp
|
||||||
|
b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl
|
||||||
|
cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv
|
||||||
|
bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE
|
||||||
|
VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ
|
||||||
|
ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR
|
||||||
|
uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
|
||||||
|
9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI
|
||||||
|
hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM
|
||||||
|
pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=VeriSign, Inc., OU=Class 1 Public Primary Certification Authority
|
||||||
|
# Issuer: C=US, O=VeriSign, Inc., OU=Class 1 Public Primary Certification Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICPDCCAaUCEDJQM89Q0VbzXIGtZVxPyCUwDQYJKoZIhvcNAQECBQAwXzELMAkG
|
||||||
|
A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
|
||||||
|
cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
|
||||||
|
MDEyOTAwMDAwMFoXDTIwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
|
||||||
|
BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt
|
||||||
|
YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
|
||||||
|
ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f
|
||||||
|
zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi
|
||||||
|
TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G
|
||||||
|
CSqGSIb3DQEBAgUAA4GBAEtEZmBoZOSYG/OwcuaViXzde7OVwB0u2NgZ0C00PcZQ
|
||||||
|
mhCGjKo/O6gE/DdSlcPZydvN8oYGxLEb8IKIMEKOF1AcZHq4PplJdJf8rAJD+5YM
|
||||||
|
VgQlDHx8h50kp9jwMim1pN9dokzFFjKoQvZFprY2ueC/ZTaTwtLXa9zeWdaiNfhF
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority
|
||||||
|
# Issuer: C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG
|
||||||
|
A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
|
||||||
|
cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
|
||||||
|
MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
|
||||||
|
BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
|
||||||
|
YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
|
||||||
|
ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
|
||||||
|
BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
|
||||||
|
I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
|
||||||
|
CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do
|
||||||
|
lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc
|
||||||
|
AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=RSA Data Security, Inc., OU=Secure Server Certification Authority
|
||||||
|
# Issuer: C=US, O=RSA Data Security, Inc., OU=Secure Server Certification Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG
|
||||||
|
A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD
|
||||||
|
VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0
|
||||||
|
MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV
|
||||||
|
BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy
|
||||||
|
dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ
|
||||||
|
ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII
|
||||||
|
0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI
|
||||||
|
uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI
|
||||||
|
hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3
|
||||||
|
YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc
|
||||||
|
1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=Equifax Secure Inc., CN=Equifax Secure Global eBusiness CA-1
|
||||||
|
# Issuer: C=US, O=Equifax Secure Inc., CN=Equifax Secure Global eBusiness CA-1
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc
|
||||||
|
MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT
|
||||||
|
ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw
|
||||||
|
MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj
|
||||||
|
dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l
|
||||||
|
c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC
|
||||||
|
UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc
|
||||||
|
58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/
|
||||||
|
o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH
|
||||||
|
MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr
|
||||||
|
aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA
|
||||||
|
A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA
|
||||||
|
Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv
|
||||||
|
8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, ST=UT, L=Salt Lake City, O=The USERTRUST Network, OU=http://www.usertrust.com, CN=UTN-USERFirst-Hardware
|
||||||
|
# Issuer: C=US, ST=UT, L=Salt Lake City, O=The USERTRUST Network, OU=http://www.usertrust.com, CN=UTN-USERFirst-Hardware
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB
|
||||||
|
lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
|
||||||
|
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
|
||||||
|
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt
|
||||||
|
SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG
|
||||||
|
A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe
|
||||||
|
MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v
|
||||||
|
d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh
|
||||||
|
cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn
|
||||||
|
0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ
|
||||||
|
M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a
|
||||||
|
MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd
|
||||||
|
oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI
|
||||||
|
DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy
|
||||||
|
oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD
|
||||||
|
VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0
|
||||||
|
dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy
|
||||||
|
bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF
|
||||||
|
BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM
|
||||||
|
//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli
|
||||||
|
CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE
|
||||||
|
CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t
|
||||||
|
3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS
|
||||||
|
KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=Network Solutions L.L.C., CN=Network Solutions Certificate Authority
|
||||||
|
# Issuer: C=US, O=Network Solutions L.L.C., CN=Network Solutions Certificate Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi
|
||||||
|
MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu
|
||||||
|
MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp
|
||||||
|
dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV
|
||||||
|
UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO
|
||||||
|
ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz
|
||||||
|
c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP
|
||||||
|
OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl
|
||||||
|
mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF
|
||||||
|
BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4
|
||||||
|
qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw
|
||||||
|
gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB
|
||||||
|
BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu
|
||||||
|
bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp
|
||||||
|
dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8
|
||||||
|
6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/
|
||||||
|
h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH
|
||||||
|
/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
|
||||||
|
wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN
|
||||||
|
pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=The Go Daddy Group, Inc., OU=Go Daddy Class 2 Certification Authority
|
||||||
|
# Issuer: C=US, O=The Go Daddy Group, Inc., OU=Go Daddy Class 2 Certification Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh
|
||||||
|
MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE
|
||||||
|
YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3
|
||||||
|
MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo
|
||||||
|
ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg
|
||||||
|
MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN
|
||||||
|
ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA
|
||||||
|
PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w
|
||||||
|
wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi
|
||||||
|
EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY
|
||||||
|
avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+
|
||||||
|
YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE
|
||||||
|
sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h
|
||||||
|
/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5
|
||||||
|
IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj
|
||||||
|
YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
|
||||||
|
ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy
|
||||||
|
OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P
|
||||||
|
TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
|
||||||
|
HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER
|
||||||
|
dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf
|
||||||
|
ReYNnyicsbkqWletNw+vHX/bvZ8=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, ST=Arizona, L=Scottsdale, O=GoDaddy.com, Inc., OU=http://certificates.godaddy.com/repository<http://certificates.godaddy.com/repository>, CN=Go Daddy Secure Certification Authority/serialNumber=07969287
|
||||||
|
# Issuer: C=US, O=The Go Daddy Group, Inc., OU=Go Daddy Class 2 Certification Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx
|
||||||
|
ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g
|
||||||
|
RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw
|
||||||
|
MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH
|
||||||
|
QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j
|
||||||
|
b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j
|
||||||
|
b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj
|
||||||
|
YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN
|
||||||
|
AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H
|
||||||
|
KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm
|
||||||
|
VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR
|
||||||
|
SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT
|
||||||
|
cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ
|
||||||
|
6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu
|
||||||
|
MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS
|
||||||
|
kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB
|
||||||
|
BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f
|
||||||
|
BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv
|
||||||
|
c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH
|
||||||
|
AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO
|
||||||
|
BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG
|
||||||
|
OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU
|
||||||
|
A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o
|
||||||
|
0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX
|
||||||
|
RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH
|
||||||
|
qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV
|
||||||
|
U+4=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, ST=Arizona, L=Scottsdale, O=GoDaddy.com, Inc., CN=Go Daddy Root Certificate Authority - G2
|
||||||
|
# Issuer: C=US, ST=Arizona, L=Scottsdale, O=GoDaddy.com, Inc., CN=Go Daddy Root Certificate Authority - G2
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
|
||||||
|
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
|
||||||
|
EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
|
||||||
|
ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
|
||||||
|
NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
|
||||||
|
EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
|
||||||
|
AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
|
||||||
|
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
|
||||||
|
E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
|
||||||
|
/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
|
||||||
|
DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
|
||||||
|
GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
|
||||||
|
tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
|
||||||
|
AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
|
||||||
|
FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
|
||||||
|
WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
|
||||||
|
9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
|
||||||
|
gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
|
||||||
|
2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
|
||||||
|
LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
|
||||||
|
4uJEvlz36hz1
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=GeoTrust Inc., CN=GeoTrust Global CA
|
||||||
|
# Issuer: C=US, O=GeoTrust Inc., CN=GeoTrust Global CA
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
|
||||||
|
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
|
||||||
|
YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
|
||||||
|
EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
|
||||||
|
R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
|
||||||
|
9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
|
||||||
|
fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
|
||||||
|
iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
|
||||||
|
1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
|
||||||
|
bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
|
||||||
|
MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
|
||||||
|
ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
|
||||||
|
uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
|
||||||
|
Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
|
||||||
|
tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
|
||||||
|
PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
|
||||||
|
hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
|
||||||
|
5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=GeoTrust Inc., CN=GeoTrust Primary Certification Authority
|
||||||
|
# Issuer: C=US, O=GeoTrust Inc., CN=GeoTrust Primary Certification Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY
|
||||||
|
MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo
|
||||||
|
R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx
|
||||||
|
MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK
|
||||||
|
Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp
|
||||||
|
ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||||
|
AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9
|
||||||
|
AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA
|
||||||
|
ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0
|
||||||
|
7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W
|
||||||
|
kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI
|
||||||
|
mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
|
||||||
|
A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ
|
||||||
|
KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1
|
||||||
|
6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl
|
||||||
|
4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K
|
||||||
|
oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj
|
||||||
|
UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU
|
||||||
|
AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: C=US, O=The Go Daddy Group, Inc., OU=Go Daddy Class 2 Certification Authority
|
||||||
|
# Issuer: L=ValiCert Validation Network, O=ValiCert, Inc., OU=ValiCert Class 2 Policy Validation Authority, CN=http://www.valicert.com//emailAddress=info@valicert.com<http://www.valicert.com//emailAddress=info@valicert.com>
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Zh
|
||||||
|
bGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIElu
|
||||||
|
Yy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24g
|
||||||
|
QXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAe
|
||||||
|
BgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDYyMFoX
|
||||||
|
DTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBE
|
||||||
|
YWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0
|
||||||
|
aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC
|
||||||
|
ggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv
|
||||||
|
2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+q
|
||||||
|
N1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiO
|
||||||
|
r18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lN
|
||||||
|
f4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEH
|
||||||
|
U1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ4EFgQU0sSw0pHU
|
||||||
|
TBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBuzEkMCIGA1UEBxMb
|
||||||
|
VmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQKEw5WYWxpQ2VydCwg
|
||||||
|
SW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2xpY3kgVmFsaWRhdGlv
|
||||||
|
biBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudmFsaWNlcnQuY29tLzEg
|
||||||
|
MB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CAQEwDwYDVR0TAQH/BAUw
|
||||||
|
AwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLmdv
|
||||||
|
ZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jZXJ0aWZpY2F0ZXMu
|
||||||
|
Z29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybDBLBgNVHSAERDBCMEAGBFUd
|
||||||
|
IAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNv
|
||||||
|
bS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQC1
|
||||||
|
QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+Sn1eocSxI0YGyeR+sBjUZsE4O
|
||||||
|
WBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgMQLARzLrUc+cb53S8wGd9D0Vmsf
|
||||||
|
SxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j09VZw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
# Subject: L=ValiCert Validation Network, O=ValiCert, Inc., OU=ValiCert Class 2 Policy Validation Authority, CN=http://www.valicert.com//emailAddress=info@valicert.com<http://www.valicert.com//emailAddress=info@valicert.com>
|
||||||
|
# Issuer: L=ValiCert Validation Network, O=ValiCert, Inc., OU=ValiCert Class 2 Policy Validation Authority, CN=http://www.valicert.com//emailAddress=info@valicert.com<http://www.valicert.com//emailAddress=info@valicert.com>
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0
|
||||||
|
IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz
|
||||||
|
BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y
|
||||||
|
aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG
|
||||||
|
9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy
|
||||||
|
NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y
|
||||||
|
azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
|
||||||
|
YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw
|
||||||
|
Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl
|
||||||
|
cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY
|
||||||
|
dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9
|
||||||
|
WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS
|
||||||
|
v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v
|
||||||
|
UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu
|
||||||
|
IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC
|
||||||
|
W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
|
53
resources/dropbox/util.py
Normal file
53
resources/dropbox/util.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
class AnalyzeFileObjBug(Exception):
|
||||||
|
msg = ("\n"
|
||||||
|
"Expected file object to have %d bytes, instead we read %d bytes.\n"
|
||||||
|
"File size detection may have failed (see dropbox.util.AnalyzeFileObj)\n")
|
||||||
|
def __init__(self, expected, actual):
|
||||||
|
self.expected = expected
|
||||||
|
self.actual = actual
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.msg % (self.expected, self.actual)
|
||||||
|
|
||||||
|
def analyze_file_obj(obj):
|
||||||
|
""" Get the size and contents of a file-like object.
|
||||||
|
Returns: (size, raw_data)
|
||||||
|
size: The amount of data waiting to be read
|
||||||
|
raw_data: If not None, the entire contents of the stream (as a string).
|
||||||
|
None if the stream should be read() in chunks.
|
||||||
|
"""
|
||||||
|
pos = 0
|
||||||
|
if hasattr(obj, 'tell'):
|
||||||
|
pos = obj.tell()
|
||||||
|
|
||||||
|
# Handle cStringIO and StringIO
|
||||||
|
if hasattr(obj, 'getvalue'):
|
||||||
|
# Why using getvalue() makes sense:
|
||||||
|
# For StringIO, this string is pre-computed anyway by read().
|
||||||
|
# For cStringIO, getvalue() is the only way
|
||||||
|
# to determine the length without read()'ing the whole thing.
|
||||||
|
raw_data = obj.getvalue()
|
||||||
|
if pos == 0:
|
||||||
|
return (len(raw_data), raw_data)
|
||||||
|
else:
|
||||||
|
# We could return raw_data[pos:], but that could drastically
|
||||||
|
# increase memory usage. Better to read it block at a time.
|
||||||
|
size = max(0, len(raw_data) - pos)
|
||||||
|
return (size, None)
|
||||||
|
|
||||||
|
# Handle real files
|
||||||
|
if hasattr(obj, 'fileno'):
|
||||||
|
size = max(0, os.fstat(obj.fileno()).st_size - pos)
|
||||||
|
return (size, None)
|
||||||
|
|
||||||
|
# User-defined object with len()
|
||||||
|
if hasattr(obj, '__len__'):
|
||||||
|
size = max(0, len(obj) - pos)
|
||||||
|
return (size, None)
|
||||||
|
|
||||||
|
# We don't know what kind of stream this is.
|
||||||
|
# To determine the size, we must read the whole thing.
|
||||||
|
raw_data = obj.read()
|
||||||
|
return (len(raw_data), raw_data)
|
Loading…
Reference in New Issue
Block a user