# Function to acquire an access token and return the token and its expiration time
function Get-GraphAccessToken {
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
client_id = $clientId
scope = "https://graph.microsoft.com/.default"
client_secret = $clientSecret
grant_type = "client_credentials"
}
$tokenResponse = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
$accessToken = $tokenResponse.access_token
$expiresIn = $tokenResponse.expires_in
$tokenExpiration = (Get-Date).AddSeconds($expiresIn - 300) # Refresh 5 minutes before expiration
return $accessToken, $tokenExpiration
}
# Acquire the token
$accessToken, $tokenExpiration = Get-GraphAccessToken
$headers = @{
Authorization = "Bearer $accessToken"
"Content-Type" = "application/json"
}
# Target SharePoint site
$siteUrl = "https://taco.sharepoint.com/sites/test-site"
# Extract the hostname and site path
$uri = [System.Uri]::new($siteUrl)
$hostname = $uri.Host
$sitePath = $uri.AbsolutePath.TrimStart('/')
# Define the endpoint URL to get the SharePoint site ID
$graphSiteUrl = "https://graph.microsoft.com/v1.0/sites/${hostname}:/${sitePath}"
# Make the request to get the site ID
$siteResponse = Invoke-RestMethod -Uri $graphSiteUrl -Headers $headers -Method Get
# Extract the relevant part of the site ID
$siteIdParts = $siteResponse.id.Split(',')
$siteId = "$($siteIdParts[1]),$($siteIdParts[2])"
# Output the site ID
Write-Output "Site Collection ID: $($siteResponse.siteCollection.hostname)"
Write-Output "Site ID--------> $siteId"
Write-Output "Site Display Name: $($siteResponse.displayName)"
Write-Output "Site Web URL: $($siteResponse.webUrl)"
This will return a value like 2480e89d-303a-4f38-b4fe-27f824ff88ac,d605ce5c-f356-422a-84fe-1d7820bc9e6d , which represents the site collection ID and the site ID.
Site collection ID: 2480e89d-303a-4f38-b4fe-27f824ff88ac Site ID: d605ce5c-f356-422a-84fe-1d7820bc9e6d
If you try to run the PnP PowerShell command New-PnPSite using a managed identity or App Registration, in a multi-geo tenant, it will create the site in the default geo. To get around this, you can use the PreferredDataLocation parameter to set the desired location, but you’ll also need to update your MS Graph permissions.
If you run the New-PnPSite command with the -PreferredDataLocationparameter and your permission are not correct, you will receive this error:
Code: Authorization_RequestDenied Message: The requesting principal is not authorized to set group preferred data location.
Open your App Registration and add the following MS Graph application permissions: Group.Create, Group.ReadWrite.All, Directory.ReadWrite.All
New-PnPSite -Type TeamSite -PreferredDataLocation NAM -Title "Test" -Alias "Test0001" -Description "my test site" -Owners email@domain.com -Wait
What started as a simple question from a co-worker turned into a rabbit hole exploration session that lasted a bit longer than anticipated. ‘Hey, I need to upload a report to SharePoint using Python.’
In the past, I’ve used SharePoint Add-in permissions to create credentials allowing an external service, app, or script to write to a site, library, list, or all of the above. However, the environment I’m currently working in does not allow Add-in permissions, and Microsoft has been slowly depreciating the service for a long time.
As of today (March 18, 2024) this is the only way I could find to upload a large file to SharePoint. Using the MS Graph SDK, you can upload files smaller than 4mb, but that is useless in most cases.
For the script below, the following items are needed: Azure App Registration: Microsoft Graph application permissions: Files.ReadWrite.All Sites.ReadWrite.All SharePoint site SharePoint library (aka drive) File to test with
import requests
import msal
import atexit
import os.path
import urllib.parse
import os
TENANT_ID = '19a6096e-3456-7890-abcd-19taco8cdedd'
CLIENT_ID = '0cd0453d-cdef-xyz1-1234-532burrito98'
CLIENT_SECRET = '.i.need.tacos-and.queso'
SHAREPOINT_HOST_NAME = 'tacoranch.sharepoint.com'
SITE_NAME = 'python'
TARGET_LIBRARY = 'reports'
UPLOAD_FILE = 'C:\\code\\test files\\LargeExcel.xlsx'
UPLOAD_FILE_NAME = 'LargeExcel.xlsx'
UPLOAD_FILE_DESCRIPTION = 'A large excel file' #not required
AUTHORITY = 'https://login.microsoftonline.com/' + TENANT_ID
ENDPOINT = 'https://graph.microsoft.com/v1.0'
SCOPES = [
'Files.ReadWrite.All',
'Sites.ReadWrite.All'
]
cache = msal.SerializableTokenCache()
if os.path.exists('token_cache.bin'):
cache.deserialize(open('token_cache.bin', 'r').read())
atexit.register(lambda: open('token_cache.bin', 'w').write(cache.serialize()) if cache.has_state_changed else None)
SCOPES = ["https://graph.microsoft.com/.default"]
app = msal.ConfidentialClientApplication(CLIENT_ID, authority=AUTHORITY, client_credential=CLIENT_SECRET, token_cache=cache)
result = None
result = app.acquire_token_silent(SCOPES, account=None)
drive_id = None
if result is None:
result = app.acquire_token_for_client(SCOPES)
if 'access_token' in result:
print('Token acquired')
else:
print(result.get('error'))
print(result.get('error_description'))
print(result.get('correlation_id'))
if 'access_token' in result:
access_token = result['access_token']
headers={'Authorization': 'Bearer ' + access_token}
# get the site id
result = requests.get(f'{ENDPOINT}/sites/{SHAREPOINT_HOST_NAME}:/sites/{SITE_NAME}', headers=headers)
result.raise_for_status()
site_info = result.json()
site_id = site_info['id']
# get the drive / library id
result = requests.get(f'{ENDPOINT}/sites/{site_id}/drives', headers=headers)
result.raise_for_status()
drives_info = result.json()
for drive in drives_info['value']:
if drive['name'] == TARGET_LIBRARY:
drive_id = drive['id']
break
if drive_id is None:
print(f'No drive named "{TARGET_LIBRARY}" found')
# upload a large file to
file_url = urllib.parse.quote(UPLOAD_FILE_NAME)
result = requests.post(
f'{ENDPOINT}/drives/{drive_id}/root:/{file_url}:/createUploadSession',
headers=headers,
json={
'@microsoft.graph.conflictBehavior': 'replace',
'description': UPLOAD_FILE_DESCRIPTION,
'fileSystemInfo': {'@odata.type': 'microsoft.graph.fileSystemInfo'},
'name': UPLOAD_FILE_NAME
}
)
result.raise_for_status()
upload_session = result.json()
upload_url = upload_session['uploadUrl']
st = os.stat(UPLOAD_FILE)
size = st.st_size
CHUNK_SIZE = 10485760
chunks = int(size / CHUNK_SIZE) + 1 if size % CHUNK_SIZE > 0 else 0
with open(UPLOAD_FILE, 'rb') as fd:
start = 0
for chunk_num in range(chunks):
chunk = fd.read(CHUNK_SIZE)
bytes_read = len(chunk)
upload_range = f'bytes {start}-{start + bytes_read - 1}/{size}'
print(f'chunk: {chunk_num} bytes read: {bytes_read} upload range: {upload_range}')
result = requests.put(
upload_url,
headers={
'Content-Length': str(bytes_read),
'Content-Range': upload_range
},
data=chunk
)
result.raise_for_status()
start += bytes_read
else:
raise Exception('no access token')
In the script, I’m uploading the LargeExcel file to a library named reports in the python site. It is important to note that the words drive and library are used interchangeably when working with MS Graph. If you see a script example that does not specify a target library but only uses root, it will write the files to the default Documents / Shared Documents library.