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

Commit 66088c6

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 510e007 commit 66088c6

File tree

2 files changed

+186
-1
lines changed

2 files changed

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