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

Commit e2acdda

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 ce50805 commit e2acdda

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
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

0 commit comments

Comments
 (0)