Skip to content
This repository was archived by the owner on Oct 2, 2024. It is now read-only.

Commit 81aa7ed

Browse files
committed
Introduce fragment_upload
Introduce new function that is handling all the hassles of multipart-upload. Creates upload session, uploads the file in recommended 10MB parts, refreshes authentication token if needed. Sample usage: client.item(drive="me", path="foo/bar.txt").fragment_upload("/path/to/file")
1 parent 556b8fb commit 81aa7ed

File tree

2 files changed

+185
-1
lines changed

2 files changed

+185
-1
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# -*- coding: utf-8 -*-
2+
'''
3+
The MIT License (MIT)
4+
5+
Copyright (c) 2015 Wiktor Niesiobędzki
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of
8+
this software and associated documentation files (the "Software"), to deal in
9+
the Software without restriction, including without limitation the rights to
10+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11+
the Software, and to permit persons to whom the Software is furnished to do so,
12+
subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
'''
24+
25+
from ..error import OneDriveError
26+
from ..model.upload_session import UploadSession
27+
from ..options import HeaderOption
28+
from ..request.item_request_builder import ItemRequestBuilder
29+
from ..request_builder_base import RequestBuilderBase
30+
from ..request_base import RequestBase
31+
from ..helpers.file_slice import FileSlice
32+
import asyncio
33+
import json
34+
import math
35+
import os
36+
import time
37+
38+
__PART_SIZE = 10 * 1024 * 1024 # recommended file size. Should be multiple of 320 * 1024
39+
__MAX_SINGLE_FILE_UPLOAD = 100 * 1024 * 1024
40+
41+
class ItemUploadFragment(RequestBase):
42+
def __init__(self, request_url, client, options, file_handle):
43+
super(ItemUploadFragment, self).__init__(request_url, client, options)
44+
self.method = "PUT"
45+
self._file_handle = file_handle
46+
47+
def post(self):
48+
"""Sends the POST request
49+
50+
Returns:
51+
:class:`UploadSession<onedrivesdk.model.upload_session.UploadSession>`:
52+
The resulting entity from the operation
53+
"""
54+
entity = UploadSession(json.loads(self.send(data=self._file_handle).content))
55+
return entity
56+
57+
@asyncio.coroutine
58+
def post_async(self):
59+
"""Sends the POST request using an asyncio coroutine
60+
61+
Yields:
62+
:class:`UploadedSession<onedrivesdk.model.upload_session.UploadedSession>`:
63+
The resulting entity from the operation
64+
"""
65+
future = self._client._loop.run_in_executor(None,
66+
self.post)
67+
entity = yield from future
68+
return entity
69+
70+
class ItemUploadFragmentBuilder(RequestBuilderBase):
71+
def __init__(self, request_url, client, content_local_path):
72+
super(ItemUploadFragmentBuilder, self).__init__(request_url, client)
73+
self._method_options = {}
74+
self._file_handle = open(content_local_path, "rb")
75+
self._total_length = os.stat(content_local_path).st_size
76+
77+
def __enter__(self):
78+
return self
79+
80+
def __exit__(self, type, value, traceback):
81+
self._file_handle.close()
82+
83+
def request(self, begin, length, options=None):
84+
"""Builds the request for the ItemUploadFragment
85+
86+
Args:
87+
options (list of :class:`Option<onedrivesdk.options.Option>`):
88+
Default to None, list of options to include in the request
89+
90+
Returns:
91+
:class:`ItemUploadFragment<onedrivesdk.request.item_upload_fragment.ItemUploadFragment>`:
92+
The request
93+
"""
94+
opts = None
95+
if not (options is None or len(options) == 0):
96+
opts = options.copy()
97+
else:
98+
opts = []
99+
100+
self.content_type = "application/octet-stream"
101+
102+
opts.append(HeaderOption("Content-Range", "bytes %d-%d/%d" % (begin, begin + length - 1, self._total_length)))
103+
opts.append(HeaderOption("Content-Length", length))
104+
105+
file_slice = FileSlice(self._file_handle, begin, length=length)
106+
req = ItemUploadFragment(self._request_url, self._client, opts, file_slice)
107+
return req
108+
109+
def post(self, begin, length, options=None):
110+
"""Sends the POST request
111+
112+
Returns:
113+
:class:`UploadedFragment<onedrivesdk.model.uploaded_fragment.UploadedFragment>`:
114+
The resulting UploadSession from the operation
115+
"""
116+
return self.request(begin, length, options).post()
117+
118+
@asyncio.coroutine
119+
def post_async(self, begin, length, options=None):
120+
"""Sends the POST request using an asyncio coroutine
121+
122+
Yields:
123+
:class:`UploadedFragment<onedrivesdk.model.uploaded_fragment.UploadedFragment>`:
124+
The resulting UploadSession from the operation
125+
"""
126+
entity = yield from self.request(begin, length, options).post_async()
127+
return entity
128+
129+
130+
def fragment_upload(self, local_path, upload_status=None):
131+
"""Uploads file using PUT using multipart upload if needed.
132+
133+
Args:
134+
local_path (str): The path to the local file to upload.
135+
upload_status (func): function(current_part, total_parts) to be called
136+
with upload status for each 10MB part
137+
138+
Returns:
139+
Created entity.
140+
"""
141+
file_size = os.stat(local_path).st_size
142+
if file_size <= __MAX_SINGLE_FILE_UPLOAD:
143+
# fallback to single shot upload if file is small enough
144+
return self.content.request().upload(local_path)
145+
else:
146+
# multipart upload needed for larger files
147+
session = self.create_session().post()
148+
149+
with ItemUploadFragmentBuilder(session.upload_url, self._client, local_path) as upload_builder:
150+
total_parts = math.ceil(file_size / __PART_SIZE)
151+
for i in range(total_parts):
152+
if upload_status:
153+
upload_status(i, total_parts)
154+
155+
length = min(__PART_SIZE, file_size - i * __PART_SIZE)
156+
tries = 0
157+
while True:
158+
try:
159+
tries += 1
160+
resp = upload_builder.post(i * __PART_SIZE, length)
161+
except OneDriveError as exc:
162+
if exc.status_code in (500, 502, 503, 504) and tries < 5:
163+
time.sleep(5)
164+
continue
165+
elif exc.status_code == 401:
166+
self._client.auth_provider.refresh_token()
167+
continue
168+
else:
169+
raise exc
170+
break # while True
171+
if upload_status:
172+
upload_status(total_parts, total_parts) # job completed
173+
# return last response
174+
return resp
175+
176+
ItemRequestBuilder.fragment_upload = fragment_upload

src/onedrivesdk/request_base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def append_option(self, option):
113113
elif isinstance(option, QueryOption):
114114
self._query_options[option.key] = option.value
115115

116-
def send(self, content=None, path=None):
116+
def send(self, content=None, path=None, data=None):
117117
"""Send the request using the client specified
118118
at request initialization
119119
@@ -122,6 +122,8 @@ def send(self, content=None, path=None):
122122
that will be sent
123123
path (str): Defaults to None, the local path of the file which
124124
will be sent
125+
data (file object): Defaults to none, the file object of the
126+
file which will be sent
125127
126128
Returns:
127129
:class:`HttpResponse<onedrivesdk.http_response.HttpResponse>`:
@@ -141,6 +143,12 @@ def send(self, content=None, path=None):
141143
self._headers,
142144
self.request_url,
143145
path=path)
146+
elif data:
147+
response = self._client.http_provider.send(
148+
self.method,
149+
self._headers,
150+
self.request_url,
151+
data=data)
144152
else:
145153
content_dict = None
146154

0 commit comments

Comments
 (0)