Skip to content

Commit 68e7e22

Browse files
committed
Introduced support for resumable upload in OneDrive API, chunk upload in SharePoint API
1 parent 1db87d2 commit 68e7e22

12 files changed

+168
-96
lines changed

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: python
22
python:
33
- "2.7"
4-
- "3.3"
54
- "3.4"
65
- "3.5"
76
- "3.6"

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pip install Office365-REST-Python-Client
2626
# Working with SharePoint API
2727

2828
The list of supported API versions:
29-
- [SharePoint 2013 REST API or above](https://msdn.microsoft.com/en-us/library/office/jj860569.aspx) and above
29+
- [SharePoint 2013 REST API](https://msdn.microsoft.com/en-us/library/office/jj860569.aspx) and above
3030
- SharePoint Online & OneDrive for Business REST API
3131

3232
#### Authentication

examples/onedrive/export_files.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ def get_file_content(name):
4444
for file_name in get_file_names():
4545
print("Reading local file: {0}".format(file_name))
4646
file_content = get_file_content(file_name)
47-
uploaded_file = target_drive.root.upload(file_name, file_content)
47+
uploaded_file = target_drive.root.execute(file_name, file_content)
4848
client.execute_query()
4949
print("File has been uploaded into: {0}".format(uploaded_file.webUrl))
+11-46
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,10 @@
1-
import os
2-
import uuid
3-
4-
from office365.sharepoint.file_creation_information import FileCreationInformation
51
from settings import settings
62
from office365.runtime.auth.authentication_context import AuthenticationContext
73
from office365.sharepoint.client_context import ClientContext
84

95

10-
def read_in_chunks(file_object, size=1024):
11-
"""Lazy function (generator) to read a file piece by piece.
12-
Default chunk size: 1k."""
13-
while True:
14-
data = file_object.read(size)
15-
if not data:
16-
break
17-
yield data
18-
19-
20-
def upload_file_session(context, local_path, target_folder_url,chunk_size):
21-
upload_id = str(uuid.uuid4())
22-
f = open(local_path, 'rb')
23-
st = os.stat(local_path)
24-
25-
# 1. create an empty file first
26-
info = FileCreationInformation()
27-
info.content = ""
28-
info.url = os.path.basename(local_path)
29-
info.overwrite = True
30-
target_folder = context.web.get_folder_by_server_relative_url(target_folder_url)
31-
target_file = target_folder.files.add(info)
32-
context.execute_query()
33-
34-
# 2. upload a file via session
35-
target_file_url = os.path.basename(local_path)
36-
f_pos = 0
37-
for piece in read_in_chunks(f, size=chunk_size):
38-
if f_pos == 0:
39-
upload_result = target_folder.files.get_by_url(target_file_url).start_upload(upload_id, piece)
40-
context.execute_query()
41-
elif f_pos + len(piece) < st.st_size:
42-
upload_result = target_folder.files.get_by_url(target_file_url).continue_upload(upload_id, f_pos,
43-
piece)
44-
context.execute_query()
45-
else:
46-
upload_result = target_folder.files.get_by_url(target_file_url).finish_upload(upload_id, f_pos, piece)
47-
context.execute_query()
48-
f_pos += len(piece)
6+
def print_upload_progress(offset):
7+
print ("Uploaded '{0}' bytes...".format(offset))
498

509

5110
if __name__ == '__main__':
@@ -55,7 +14,13 @@ def upload_file_session(context, local_path, target_folder_url,chunk_size):
5514
password=settings['user_credentials']['password']):
5615
ctx = ClientContext(site_url, ctx_auth)
5716

58-
size_4k = 1024 * 4
59-
path = "../data/SharePoint User Guide.docx"
17+
# size_4k = 1024 * 4
18+
size_1Mb = 1000000
19+
local_path = "../../tests/data/big_buck_bunny.mp4"
6020
target_url = "/Shared Documents"
61-
upload_file_session(ctx, path, target_url, size_4k)
21+
# result_file = upload_file_session(ctx, target_url, local_path, size_1Mb)
22+
23+
result_file = ctx.web.get_folder_by_server_relative_url(target_url)\
24+
.files.create_upload_session(local_path, size_1Mb, print_upload_progress)
25+
ctx.execute_query()
26+
print ('File {0} has been uploaded successfully'.format(result_file.properties['ServerRelativeUrl']))
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from office365.onedrive.baseItem import BaseItem
2+
3+
4+
class ColumnDefinition(BaseItem):
5+
""""""

office365/onedrive/file_upload.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
3+
from office365.onedrive.driveItemUploadableProperties import DriveItemUploadableProperties
4+
from office365.runtime.utilities.http_method import HttpMethod
5+
from office365.runtime.utilities.request_options import RequestOptions
6+
7+
8+
def read_in_chunks(file_object, chunk_size=1024):
9+
"""Lazy function (generator) to read a file piece by piece.
10+
Default chunk size: 1k."""
11+
while True:
12+
data = file_object.read(chunk_size)
13+
if not data:
14+
break
15+
yield data
16+
17+
18+
class ResumableFileUpload(object):
19+
"""Create an upload session to allow your app to upload files up to the maximum file size. An upload session
20+
allows your app to upload ranges of the file in sequential API requests, which allows the transfer to be resumed
21+
if a connection is dropped while the upload is in progress. """
22+
def __init__(self, target_folder, source_path, chunk_size):
23+
self._target_folder = target_folder
24+
self._chunk_size = chunk_size
25+
self._source_path = source_path
26+
self._uploaded_bytes = 0
27+
28+
def execute(self, chunk_uploaded=None):
29+
ctx = self._target_folder.context
30+
file_name = os.path.basename(self._source_path)
31+
# 1. create an empty file
32+
target_item = self._target_folder.upload(file_name, None)
33+
ctx.execute_query()
34+
35+
# 2. create upload session
36+
item = DriveItemUploadableProperties()
37+
item.name = file_name
38+
session_result = target_item.create_upload_session(item)
39+
ctx.execute_query()
40+
41+
# 3. start upload
42+
fh = open(self._source_path, 'rb')
43+
st = os.stat(self._source_path)
44+
f_pos = 0
45+
for piece in read_in_chunks(fh, chunk_size=self._chunk_size):
46+
req = RequestOptions(session_result.value.uploadUrl)
47+
req.method = HttpMethod.Put
48+
req.set_header('Content-Length', str(len(piece)))
49+
req.set_header('Content-Range', 'bytes {0}-{1}/{2}'.format(f_pos, (f_pos + len(piece) - 1), st.st_size))
50+
req.set_header('Accept', '*/*')
51+
req.data = piece
52+
resp = ctx.execute_request_direct(req)
53+
f_pos += len(piece)
54+
if chunk_uploaded is not None:
55+
chunk_uploaded(f_pos)
56+
57+
return target_item

office365/runtime/client_request.py

+12
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ def __init__(self, context):
1616
self.context = context
1717
self.__queries = []
1818
self.__resultObjects = {}
19+
self.__events_list = {}
1920

2021
def clear(self):
2122
self.__queries = []
2223
self.__resultObjects = {}
24+
self.__events_list = {}
2325

2426
def build_request(self, query):
2527
request = RequestOptions(query.url)
@@ -50,9 +52,13 @@ def execute_query(self):
5052
try:
5153
for query in self.__queries:
5254
request = self.build_request(query)
55+
if 'BeforeExecuteQuery' in self.__events_list:
56+
self.__events_list['BeforeExecuteQuery'](request)
5357
response = self.execute_request_direct(request)
5458
result_object = self.__resultObjects.get(query)
5559
self.process_response(response, result_object)
60+
if 'AfterExecuteQuery' in self.__events_list:
61+
self.__events_list['AfterExecuteQuery'](result_object)
5662
finally:
5763
self.clear()
5864

@@ -122,6 +128,12 @@ def add_query(self, query, result_object=None):
122128
if result_object is not None:
123129
self.__resultObjects[query] = result_object
124130

131+
def before_execute_query(self, event):
132+
self.__events_list['BeforeExecuteQuery'] = event
133+
134+
def after_execute_query(self, event):
135+
self.__events_list['AfterExecuteQuery'] = event
136+
125137
def validate_response(self, response):
126138
try:
127139
response.raise_for_status()

office365/sharepoint/client_context.py

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from office365.runtime.context_web_information import ContextWebInformation
55
from office365.runtime.odata.json_light_format import JsonLightFormat
66
from office365.runtime.odata.odata_metadata_level import ODataMetadataLevel
7-
from office365.runtime.utilities.http_method import HttpMethod
87
from office365.runtime.utilities.request_options import RequestOptions
98
from office365.sharepoint.site import Site
109
from office365.sharepoint.web import Web

office365/sharepoint/file.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,7 @@ def finish_upload(self, upload_id, file_offset, content):
196196

197197
@staticmethod
198198
def save_binary(ctx, server_relative_url, content):
199-
try:
200-
from urllib import quote # Python 2.X
201-
except ImportError:
202-
from urllib.parse import quote # Python 3+
203-
server_relative_url = quote(server_relative_url)
199+
"""Uploads a file"""
204200
url = r"{0}web/getfilebyserverrelativeurl('{1}')/\$value".format(
205201
ctx.serviceRootUrl, server_relative_url)
206202
request = RequestOptions(url)

office365/sharepoint/file_collection.py

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from office365.runtime.resource_path_service_operation import ResourcePathServiceOperation
44
from office365.runtime.utilities.http_method import HttpMethod
55
from office365.sharepoint.file import File
6+
from office365.sharepoint.upload_session import UploadSession
67

78

89
class FileCollection(ClientObjectCollection):
@@ -11,6 +12,12 @@ class FileCollection(ClientObjectCollection):
1112
def __init__(self, context, resource_path=None):
1213
super(FileCollection, self).__init__(context, File, resource_path)
1314

15+
def create_upload_session(self, source_path, chunk_size, chunk_uploaded=None):
16+
"""Upload a file as multiple chunks"""
17+
session = UploadSession(source_path, chunk_size, chunk_uploaded)
18+
session.build_query(self)
19+
return session.file
20+
1421
def add(self, file_creation_information):
1522
"""Creates a File resource"""
1623
file_new = File(self.context)
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import uuid
3+
4+
from office365.runtime.client_result import ClientResult
5+
from office365.sharepoint.file import File
6+
from office365.sharepoint.file_creation_information import FileCreationInformation
7+
8+
9+
def read_in_chunks(file_object, size=1024):
10+
"""Lazy function (generator) to read a file piece by piece.
11+
Default chunk size: 1k."""
12+
while True:
13+
data = file_object.read(size)
14+
if not data:
15+
break
16+
yield data
17+
18+
19+
class UploadSession(object):
20+
def __init__(self, source_path, chunk_size, chunk_uploaded):
21+
self._chunk_size = chunk_size
22+
self._upload_id = str(uuid.uuid4())
23+
self._source_path = source_path
24+
self._chunk_uploaded = chunk_uploaded
25+
self._target_file = None
26+
self._uploaded_bytes = 0
27+
28+
def build_query(self, files):
29+
st = os.stat(self._source_path)
30+
file_name = os.path.basename(self._source_path)
31+
32+
# 1. create an empty target file
33+
info = FileCreationInformation()
34+
info.url = file_name
35+
info.overwrite = True
36+
self._target_file = files.add(info)
37+
38+
# 2. upload a file in chunks
39+
f_pos = 0
40+
fh = open(self._source_path, 'rb')
41+
for piece in read_in_chunks(fh, size=self._chunk_size):
42+
if f_pos == 0:
43+
upload_result = files.get_by_url(file_name).start_upload(self._upload_id, piece)
44+
elif f_pos + len(piece) < st.st_size:
45+
upload_result = files.get_by_url(file_name).continue_upload(self._upload_id, f_pos, piece)
46+
else:
47+
self._target_file = files.get_by_url(file_name).finish_upload(self._upload_id, f_pos, piece)
48+
f_pos += len(piece)
49+
50+
if self._chunk_uploaded is not None:
51+
files.context.pending_request.after_execute_query(self._process_chunk_upload)
52+
53+
def _process_chunk_upload(self, result_object):
54+
if isinstance(result_object, ClientResult):
55+
if 'StartUpload' in result_object.value:
56+
self._uploaded_bytes = int(result_object.value['StartUpload'])
57+
elif 'ContinueUpload' in result_object.value:
58+
self._uploaded_bytes = int(result_object.value['ContinueUpload'])
59+
elif isinstance(result_object, File):
60+
self._uploaded_bytes = int(result_object.properties['Length'])
61+
self._chunk_uploaded(self._uploaded_bytes)
62+
63+
@property
64+
def file(self):
65+
return self._target_file

tests/test_onedrive_driveItem.py

+8-41
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import os
22
import uuid
33
from unittest import TestCase
4-
from office365.onedrive.driveItemUploadableProperties import DriveItemUploadableProperties
5-
from office365.runtime.utilities.http_method import HttpMethod
6-
from office365.runtime.utilities.request_options import RequestOptions
4+
from office365.onedrive.file_upload import ResumableFileUpload
75
from settings import settings
86

97
from office365.graphClient import GraphClient
@@ -18,16 +16,6 @@ def get_token(auth_ctx):
1816
return token
1917

2018

21-
def read_in_chunks(file_object, chunk_size=1024):
22-
"""Lazy function (generator) to read a file piece by piece.
23-
Default chunk size: 1k."""
24-
while True:
25-
data = file_object.read(chunk_size)
26-
if not data:
27-
break
28-
yield data
29-
30-
3119
def create_listDrive(client):
3220
list_info = {
3321
"displayName": "Lib_" + uuid.uuid4().hex,
@@ -70,6 +58,13 @@ def test2_upload_file(self):
7058
self.client.execute_query()
7159
self.assertIsNotNone(self.target_file.webUrl)
7260

61+
def test21_upload_file_session(self):
62+
file_name = "big_buck_bunny.mp4"
63+
local_path = "{0}/data/{1}".format(os.path.dirname(__file__), file_name)
64+
uploader = ResumableFileUpload(self.target_drive.root, local_path, 1000000)
65+
uploader.execute()
66+
print("{0} bytes has been uploaded".format(0))
67+
7368
def test3_download_file(self):
7469
result = self.__class__.target_file.download()
7570
self.client.execute_query()
@@ -97,31 +92,3 @@ def test6_delete_file(self):
9792
self.client.execute_query()
9893

9994
self.assertEqual(before_count - 1, len(items))
100-
101-
def test7_upload_file_session(self):
102-
file_name = "big_buck_bunny.mp4"
103-
path = "{0}/data/{1}".format(os.path.dirname(__file__), file_name)
104-
# 1. create a file
105-
target_item = self.target_drive.root.upload(file_name, None)
106-
self.client.execute_query()
107-
self.assertIsNotNone(target_item.properties['id'])
108-
# 2. create upload session
109-
item = DriveItemUploadableProperties()
110-
item.name = file_name
111-
session_result = target_item.create_upload_session(item)
112-
self.client.execute_query()
113-
self.assertIsNotNone(session_result.value)
114-
# 3. start upload
115-
f = open(path, 'rb')
116-
st = os.stat(path)
117-
f_pos = 0
118-
for piece in read_in_chunks(f, chunk_size=1000000):
119-
req = RequestOptions(session_result.value.uploadUrl)
120-
req.method = HttpMethod.Put
121-
req.set_header('Content-Length', str(len(piece)))
122-
req.set_header('Content-Range', 'bytes {0}-{1}/{2}'.format(f_pos, (f_pos + len(piece) - 1), st.st_size))
123-
req.set_header('Accept', '*/*')
124-
req.data = piece
125-
resp = self.client.execute_request_direct(req)
126-
self.assertTrue(resp.ok)
127-
f_pos += len(piece)

0 commit comments

Comments
 (0)