Microsoft Graph in Practice #5: Files in OneDrive and SharePoint — Upload Sessions and Sharing
This part is files: auto-uploading build artifacts to SharePoint, backing up daily logs, generating sharing links for reports. The good news about the file APIs is that OneDrive and SharePoint share one model — learn it once and the same code runs against a personal drive or a team site.
Permissions: delegated starts with Files.ReadWrite (your own files); application defaults to Sites.ReadWrite.All — but since that opens every site in the organization, narrowing with Sites.Selected (covered at the end) is today’s standard practice.
The model — drive and driveItem, and that’s all #
- drive — one file container. A user’s OneDrive is a drive; a SharePoint site’s document library is a drive; what’s behind a Teams team’s Files tab is, in the end, a SharePoint drive too.
- driveItem — an item in a drive. Files and folders are both driveItems; a
folderfacet means folder, afilefacet means file.
Only the path to reach a drive differs by container type.
# a user's OneDrive
GET /users/{user-id}/drive
# a SharePoint site's default document library
GET /sites/{site-id}/drive
# finding the site ID by URL is the practical route
GET /sites/contoso.sharepoint.com:/sites/DevTeam
→ the id in the response is the site-id
# items within a drive — path-based addressing is convenient
GET /drives/{drive-id}/root:/reports/2026/weekly.xlsxThat last line — path-based addressing (the colon syntax) — is the workhorse. root:/folder/filename reaches items by the paths humans know, no IDs needed. The clean automation pattern: resolve “site URL → site-id → drive-id” once, pin them in config, then travel by path from there on.
Downloads and simple uploads #
Reading, and writing small files, are one call each.
async def download(client, drive_id: str, path: str) -> bytes:
return await (
client.drives.by_drive_id(drive_id)
.root.item_with_path(path)
.content.get()
)
async def upload_small(client, drive_id: str, path: str, data: bytes):
# PUT .../root:/{path}:/content — up to 250MB in one shot
return await (
client.drives.by_drive_id(drive_id)
.root.item_with_path(path)
.content.put(data)
)Simple upload (PUT /content) supports files up to 250MB. The official recommendation, though, is to switch to upload sessions beyond 10MiB: a single large request restarts from zero if the network blips once, and resumability wins in operations.
Also: uploading to an existing path overwrites. If that’s not the intent, state the conflict behavior explicitly with @microsoft.graph.conflictBehavior (rename / fail / replace).
Large files — the upload session #
The standard for large files has three steps: ① create an upload session, ② PUT chunks in order, ③ the final chunk completes it automatically.
import httpx
from msgraph.generated.drives.item.items.item.create_upload_session.create_upload_session_post_request_body import (
CreateUploadSessionPostRequestBody,
)
from msgraph.generated.models.drive_item_uploadable_properties import (
DriveItemUploadableProperties,
)
CHUNK = 5 * 1024 * 1024 # 5MiB — must be a multiple of 320KiB (327,680)
async def upload_large(client, drive_id: str, path: str, filepath: str):
body = CreateUploadSessionPostRequestBody(
item=DriveItemUploadableProperties(
additional_data={"@microsoft.graph.conflictBehavior": "replace"},
),
)
session = await (
client.drives.by_drive_id(drive_id)
.root.item_with_path(path)
.create_upload_session.post(body)
)
# the session URL needs no auth token (it's pre-authenticated)
import os
total = os.path.getsize(filepath)
async with httpx.AsyncClient() as http:
with open(filepath, "rb") as f:
offset = 0
while chunk := f.read(CHUNK):
end = offset + len(chunk) - 1
resp = await http.put(
session.upload_url,
content=chunk,
headers={
"Content-Range": f"bytes {offset}-{end}/{total}",
"Content-Length": str(len(chunk)),
},
)
resp.raise_for_status()
offset = end + 1A few rules — break them and the upload fails in the nastiest way, on the final chunk:
- Chunk sizes must be multiples of 320KiB (327,680 bytes). On stable connections, 5–10MiB is the recommended range; the per-request cap is 60MiB.
- Chunks go up in order (for OneDrive/SharePoint).
- On 5xx or a dropped connection, GET the session URL, read
nextExpectedRanges(what the server is still missing), and resume there. A 404 means the session expired — start over. - Sessions have an expiry, but every uploaded chunk extends it, so an in-progress upload stays alive.
Note that the language SDKs ship large-file upload task helpers wrapping this procedure (e.g., LargeFileUploadTask); use them where available. The code above shows the raw procedure so it works in any environment.
Sharing links — createLink #
The last piece delivers the uploaded report to people: createLink mints a link you can drop into part 4’s mail or part 6’s Teams message, closing the automation loop.
from msgraph.generated.drives.item.items.item.create_link.create_link_post_request_body import (
CreateLinkPostRequestBody,
)
async def share_link(client, drive_id: str, item_id: str) -> str:
body = CreateLinkPostRequestBody(
type="view", # view / edit
scope="organization", # organization / anonymous / users
)
perm = await (
client.drives.by_drive_id(drive_id)
.items.by_drive_item_id(item_id)
.create_link.post(body)
)
return perm.link.web_urlThe scope choice is a security decision. For internal automation the default should be organization (viewable inside the org only); anonymous (anyone with the link) is often blocked by org policy — and arguably should be. Automation mints links at scale, so a habit of wide scopes becomes a leak surface.
Narrowing application permissions — Sites.Selected #
This part closes with permissions. Application-side Sites.ReadWrite.All opens every SharePoint site, which invites the objection “we can’t open everything for one automation.” The answer is Sites.Selected.
Its behavior is unusual enough to spell out: the Sites.Selected permission by itself opens no sites at all. An admin must separately grant the app read/write on specific sites (site permissions — granted via Graph’s site permission APIs or PnP PowerShell) before any access exists. In other words: one permission in the app’s list, and the real scope is a per-site allowlist. For production file automation, make this the default and reserve .All for the cases that genuinely span all sites.
Summary #
- The model is two nouns — drive (container) and driveItem (file/folder) — shared by OneDrive and SharePoint. Path-based (colon) addressing is the working default.
- Simple upload covers up to 250MB, but beyond 10MiB the upload session is the standard: chunks in 320KiB multiples (5–10MiB recommended), resume via nextExpectedRanges.
- Always state conflictBehavior on uploads; the default will eventually overwrite something you care about.
- Sharing links default to organization scope. Anonymous links minted by automation are a leak surface.
- The production permission pattern for application file automation is Sites.Selected plus per-site grants; treat
.Allas the exception.
Next is Teams — the area where reading and writing have very different permission terrain, organized around the three routes for sending notifications.