Automating files upload to Microsoft OneDrive. Unexpected challenges and a success story!  
Tags: tutorial, python, microsoftgraph, onedriveapi
Published: 2020-05-14 20:35:47



In the current data era we live in, a huge number of reports are being generated every minute. Uploading and distributing these reports is an inconvenience to say the least, and a security threat if sensitive/confidential data is included in these reports.

I am currently in charge of approximately 200 analytics reports generated daily, attached to emails and sent to the according audience at different departments. While this is a time consuming task, it was not a security concern just because these emails and reports were accessed on company approved devices in a closed and highly secured network.

Now with the pandemic and the stay in home orders. The chances of downloading these reports on unsecured machines became an urgent issue.

So I decided to take advantage of Microsoft OneDrive as a central storage solution. Automating uploading these reports to a departmental folders and email just a link to the file location to my audience, reducing the chances of someone downloading the files on their machine when Microsoft OneDrive encourages web previews.

This sounds like an easy task, right? Especially if you have worked with Python and APIs before. Include the requests library, configure your Client Id and Client secret, request an AccessToken/RefreshToken, and start rolling.

Well it turned out that working with Microsoft Graph is not that easy especially if you want to schedule your application to run in the background with no human interference.

While Microsoft has a good documentation, it was still vague on many critical subjects:
<ul>
<li>Authorization and what endpoint to use</li>
<li>Authenticating using you Microsoft Account</li>
<li>An running in the background (A Daemon app) requires high privileged admin access I don't have</li>
<li>How to use delegate permission instead and still run your app in the background</li>
<li>How to set up the header for resumable large files upload</li>
</ul>

After doing what every developer does from reading documentation to looking up a solution or idea on stack overflow, I wasn't able to find a solution using python especially for the resumable large files upload.

**_So I decided to publish my solution to help any fellow developer who is currently in the same shoes I was in couple weeks ago!_**

For the complete code please visit my [GitHub](https://github.com/jsnm-repo/Python-OneDriveAPI-FileUpload)

##There are two major parts to this tutorial:
<ol>
<li>Create, set up and configure the API on Azure Portal</li>
<li>Write the python script</li>
</ol>

**Part 1 - Create, set up and configure the API on Azure Portal**

*Step 1: Register your application*
Go to https://portal.azure.com/#home 
*Azure Active Directory -> App Registration -> New Registration*
<ul>
<li>Name your API</li>
<li>Accounts in this organizational directory only (Single Tenant)</li>
<li>Redirect URI is not needed as our app running and authenticating in the        background</li>
<li>Click register</li>
</ul>
    
Once your new API is created, click on the API and Save the following two information for the code later:
<ul>
<li>Application (client) ID </li>
<li>Directory (tenant) ID</li>
</ul>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yb60xb7g5pujul4ewjiz.jpg)

Then grab the OAuth 2.0 authorization endpoint (v2)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/sfs6w6ofzo608sg1na4m.jpg)
We will use it during the authorization script to get the URL link to permissions consent

*Step 2: Configure the API permissions*

*API permissions → Add a permission → Microsoft APIs → Microsoft Graph → Delegated permissions → Select permissions Permissions needed* “Sites.ReadWrite.All” and “Files.ReadWrite.All”

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zrzcx3fhhrg79j0ebxju.jpg)

*Step 3: Expose the API*

After adding the permissions in Step 3, we have to expose the API and those permissions to the scope.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/evh39if7qi1x33ywxrz8.jpg)

Then we need to add the client ID and select the authorized scopes we just added.

*Expose an API → Add a client application → Enter Client ID → select the Authorized scopes → click add application*

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ixs1rap9aud4sk99q6fp.jpg)

*Step 4: Edit the manifest* **(Very important to allow Implicit grant)**

This is a very important step in the API set. Go to Manifest and set the Oauth2IdToken and ImplicitFlow to true

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ruozq0pj89qkv28ouvh6.png)

Now that our API is all set and configured we can start writing some Python code!!!

**Part 2- Write the python script**

Our code is 2 different python scripts:

1- *generateOneDriveAPIConsentURL-public.py*

Script to generate the consent URL.This script is basically run once after setting the permissions in the API setup to give the user's consent to these permissions.
This is the best solution to run the main app in the background without using the app permissions (which pose high security risk as it is global high privileged and requires admin approval)
Sticking to delegate permission limit the permissions to the current user privileges and in virtually all cases, it doesn't require admin approval.
<pre>
import requests
import json
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import MobileApplicationClient

client_id = "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
scopes = ['Sites.ReadWrite.All','Files.ReadWrite.All']
auth_url = 'https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/oauth2/v2.0/authorize'

#OAuth2Session is an extension to requests.Session
#used to create an authorization url using the requests.Session interface
#MobileApplicationClient is used to get the Implicit Grant

oauth = OAuth2Session(client=MobileApplicationClient(client_id=client_id), scope=scopes)
authorization_url, state = oauth.authorization_url(auth_url)
consent_link = oauth.get(authorization_url)
print(consent_link.url)</pre>
This script basically connect the Microsoft authorization V2. endpoint, send the Client ID and the scope of permission we are asking for then a URL will be generated and sent back in the terminal
<pre>c:/Users/jsnmtr/Code/onedrive/generateOneDriveAPIConsentURL-public.py
https://login.microsoftonline.com/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx/oauth2/v2.0/authorize?response_type=token&client_id=xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx&scope=Sites.ReadWrite.All+Files.ReadWrite.All&state=xxxxxxxxxxxxxxxxxxxxxxxx
</pre>

Open the link in your web browser and click to accept to accept the permissions requested

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wpt45hke2umpetcdav34.jpg)

2- *AutomatedOneDriveAPIUploadFiles-public.py*

This is the main script, it creates a public client application using the MSAL library, request a token on behalf of the user, gain access to Microsoft Graph and use the OneDrive API to upload files.

Basic flow:
-Importing libraries 
<pre>
import os
import requests
import json
import msal
</pre>

-Configuration

<pre>
CLIENT_ID = 'xxxxxxxx-xxxxxxx-xxxxxx-xxxxxxx-xxxxxxxxxx'
TENANT_ID = 'xxxxxxxx-xxxxxxx-xxxxxx-xxxxxxx-xxxxxxxxxx'
AUTHORITY_URL = 'https://login.microsoftonline.com/{}'.format(TENANT_ID)
RESOURCE_URL = 'https://graph.microsoft.com/'
API_VERSION = 'v1.0'
USERNAME = 'xxxxxxxxx@xxxxxx.xxx' #Office365 user's account username
PASSWORD = 'xxxxxxxxxxxxxxx'
SCOPES = ['Sites.ReadWrite.All','Files.ReadWrite.All'] # Add other scopes/permissions as needed.
</pre>

-Create a public client application using the Microsoft Authentication Library (MSAL)

<pre>
#Creating a public client app, Aquire a access token for the user and set the header for API calls
cognos_to_onedrive = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY_URL)
</pre>

-Acquire a token from Microsoft identity platform endpoint to access Microsoft Graph API

<pre>
token = cognos_to_onedrive.acquire_token_by_username_password(USERNAME,PASSWORD,SCOPES)
</pre>
-Set the request header with access token
<pre>
headers = {'Authorization': 'Bearer {}'.format(token['access_token'])}
</pre>
-Read all the file in source directory
so we loop into the directory, get the file path, file size and read the data in the file
<pre>

#Looping through the files inside the source directory
for root, dirs, files in os.walk(cognos_reports_source):
    for file_name in files:
        file_path = os.path.join(root,file_name)
        file_size = os.stat(file_path).st_size
        file_data = open(file_path, 'rb')
</pre>
-If the file is less than 4mb:
<pre>
if file_size < 4100000:   
</pre>
-perform a simple upload
<pre>
#Perform simple upload to the OneDrive API
            r = requests.put(onedrive_destination+"/"+file_name+":/content", data=file_data, headers=headers)
</pre>
-If the file is larger than 4mb:
Create an upload session
<pre>
upload_session = requests.post(onedrive_destination+"/"+file_name+":/createUploadSession", headers=headers).json()
</pre>
-Divide the file into byte chunks
<pre>
total_file_size = os.path.getsize(file_path)
chunk_size = 327680
chunk_number = total_file_size//chunk_size
chunk_leftover = total_file_size - chunk_size * chunk_number
chunk_data = f.read(chunk_size)
start_index = i*chunk_size
end_index = start_index + chunk_size
</pre>
-Set the header to match the starting index and end index of the byte chunk
<pre>
headers = {'Content-Length':'{}'.format(chunk_size),'Content-Range':'bytes {}-{}/{}'.format(start_index, end_index-1, total_file_size)}
</pre>
-Upload chunks
<pre>
chunk_data_upload = requests.put(upload_session['uploadUrl'], data=chunk_data, headers=headers)
</pre>

For the complete code please visit my [GitHub](https://github.com/jsnm-repo/Python-OneDriveAPI-FileUpload)

To me, anytime you achieve a goal, is a success story and by  writing this code I achieved my goal to automate my reports upload process to OneDrive! SUCCESS!
***
## Let me know your thoughts!

## Find me on [LinkedIn](https://www.linkedin.com/in/jasonmatar/) | [Twitter](https://twitter.com/Jsnmtr) | [Dev.to](https://dev.to/jsnmtr) | [StackOverflow](https://stackoverflow.com/users/8206322/jason-m)