Skip to content

Commit a27836c

Browse files
Add HTTP range support for file downloads
1 parent 8d013bd commit a27836c

8 files changed

Lines changed: 437 additions & 10 deletions

File tree

dropbox/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22

3+
from ._http import build_range_headers as build_range_headers
34
from dropbox.dropbox_client import ( # noqa: F401 # pylint: disable=unused-import
45
__version__,
56
Dropbox,

dropbox/_http.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
def format_byte_range(byte_range):
2+
"""Format a byte range as an HTTP Range header value."""
3+
4+
if byte_range is None:
5+
return None
6+
7+
if not isinstance(byte_range, tuple) or len(byte_range) != 2:
8+
raise ValueError("byte_range must be a (start, end) tuple")
9+
10+
start, end = byte_range
11+
12+
if start is None and end is None:
13+
raise ValueError("byte_range must specify start or end")
14+
15+
for value in (start, end):
16+
if value is None:
17+
continue
18+
if isinstance(value, bool) or not isinstance(value, int):
19+
raise TypeError("byte_range values must be non-negative integers")
20+
if value < 0:
21+
raise ValueError("byte_range values must be non-negative")
22+
23+
if start is not None and end is not None and end < start:
24+
raise ValueError("byte_range end must be greater than or equal to start")
25+
26+
if start is None:
27+
return "bytes=-{}".format(end)
28+
29+
if end is None:
30+
return "bytes={}-".format(start)
31+
32+
return "bytes={}-{}".format(start, end)
33+
34+
35+
def build_range_headers(byte_range):
36+
"""Build HTTP Range headers for a download request."""
37+
38+
range_header = format_byte_range(byte_range)
39+
40+
if range_header is None:
41+
return None
42+
43+
return {"Range": range_header}

dropbox/base.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ class DropboxBase(object):
3333
__metaclass__ = ABCMeta
3434

3535
@abstractmethod
36-
def request(self, route, namespace, request_arg, request_binary, timeout=None):
36+
def request(
37+
self, route, namespace, request_arg, request_binary, timeout=None, extra_headers=None
38+
):
3739
pass
3840

3941
# ------------------------------------------
@@ -1491,13 +1493,14 @@ def files_delete_batch_check(self, async_job_id):
14911493
)
14921494
return r
14931495

1494-
def files_download(self, path, rev=None):
1496+
def files_download(self, path, rev=None, extra_headers=None):
14951497
"""
14961498
Download a file from a user's Dropbox.
14971499
14981500
Route attributes:
14991501
scope: files.content.read
15001502
1503+
:param object extra_headers: Additional HTTP headers for this request.
15011504
:param path: The path of the file to download.
15021505
:type path: str
15031506
:param rev: Field is deprecated. Please specify revision in ``path``
@@ -1522,17 +1525,19 @@ def files_download(self, path, rev=None):
15221525
"files",
15231526
arg,
15241527
None,
1528+
extra_headers=extra_headers,
15251529
)
15261530
return r
15271531

1528-
def files_download_to_file(self, download_path, path, rev=None):
1532+
def files_download_to_file(self, download_path, path, rev=None, extra_headers=None):
15291533
"""
15301534
Download a file from a user's Dropbox.
15311535
15321536
Route attributes:
15331537
scope: files.content.read
15341538
15351539
:param str download_path: Path on local machine to save file.
1540+
:param object extra_headers: Additional HTTP headers for this request.
15361541
:param path: The path of the file to download.
15371542
:type path: str
15381543
:param rev: Field is deprecated. Please specify revision in ``path``
@@ -1550,6 +1555,7 @@ def files_download_to_file(self, download_path, path, rev=None):
15501555
"files",
15511556
arg,
15521557
None,
1558+
extra_headers=extra_headers,
15531559
)
15541560
self._save_body_to_file(download_path, r[1])
15551561
return r[0]

dropbox/dropbox_client.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,15 @@ def clone(
284284
),
285285
)
286286

287-
def request(self, route, namespace, request_arg, request_binary, timeout=None):
287+
def request(
288+
self,
289+
route,
290+
namespace,
291+
request_arg,
292+
request_binary,
293+
timeout=None,
294+
extra_headers=None,
295+
):
288296
"""
289297
Makes a request to the Dropbox API and in the process validates that
290298
the route argument and result are the expected data types. The
@@ -304,6 +312,7 @@ def request(self, route, namespace, request_arg, request_binary, timeout=None):
304312
server. After the timeout the client will give up on
305313
connection. If `None`, will use default timeout set on
306314
Dropbox object. Defaults to `None`.
315+
:param dict extra_headers: Additional HTTP headers for this request.
307316
:return: The route's result.
308317
"""
309318

@@ -338,6 +347,7 @@ def request(self, route, namespace, request_arg, request_binary, timeout=None):
338347
auth_type,
339348
request_binary,
340349
timeout=timeout,
350+
extra_headers=extra_headers,
341351
)
342352
decoded_obj_result = json.loads(res.obj_result)
343353
if isinstance(res, RouteResult):
@@ -521,6 +531,7 @@ def request_json_string_with_retry(
521531
auth_type,
522532
request_binary,
523533
timeout=None,
534+
extra_headers=None,
524535
):
525536
"""
526537
See :meth:`request_json_object` for description of parameters.
@@ -542,6 +553,7 @@ def request_json_string_with_retry(
542553
auth_type,
543554
request_binary,
544555
timeout=timeout,
556+
extra_headers=extra_headers,
545557
)
546558
except AuthError as e:
547559
if e.error and e.error.is_expired_access_token():
@@ -591,6 +603,7 @@ def request_json_string(
591603
auth_type,
592604
request_binary,
593605
timeout=None,
606+
extra_headers=None,
594607
):
595608
"""
596609
See :meth:`request_json_string_with_retry` for description of
@@ -611,6 +624,10 @@ def request_json_string(
611624
url = self._get_route_url(fq_hostname, func_name)
612625

613626
headers = {"User-Agent": self._user_agent}
627+
628+
if extra_headers:
629+
headers.update(extra_headers)
630+
614631
auth_types = auth_type.replace(" ", "").split(",")
615632
if (USER_AUTH in auth_types or TEAM_AUTH in auth_types) and self._oauth2_access_token:
616633
headers["Authorization"] = "Bearer %s" % self._oauth2_access_token

generate_base_client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,17 @@ def main():
6767

6868
o = subprocess.check_output(
6969
(
70-
["python", "-m", "stone.cli", "python_client", dropbox_pkg_path]
70+
[
71+
"python",
72+
"-m",
73+
"stone.cli",
74+
os.path.join(
75+
os.path.dirname(__file__),
76+
"generator",
77+
"dropbox_python_client.stoneg.py",
78+
),
79+
dropbox_pkg_path,
80+
]
7181
+ specs
7282
+ ["-a", "host", "-a", "style", "-a", "auth", "-a", "scope"]
7383
+ [

0 commit comments

Comments
 (0)