Skip to content

Instantly share code, notes, and snippets.

@petelacey
Created February 24, 2024 23:02
Show Gist options
  • Save petelacey/a4661d2dde576eb892796b317877aba7 to your computer and use it in GitHub Desktop.
Save petelacey/a4661d2dde576eb892796b317877aba7 to your computer and use it in GitHub Desktop.
Sample Elixir code to upload large documents to OneDrive via the MS Graph API
defmodule OneDrive
require Logger
@graph_api_url "https://graph.microsoft.com/v1.0/"
# The parent_drive_item is a Graph API driveItem object representing the root folder the file it to be uploaded to.
# There are many other representations left as an exercise to the reader
#
# The remote_file_path variable has the complete path to the file on the OneDrive side, e.g. /foo/bar/baz.txt
# If either the /foo or /bar folders are not already there, they will be created. They do not have to be created
# ahead of time
#
# So that it can be used in Multis, this will return:
# {:ok, nil}
# {:error, reason}
#
# I don't need the success value, but you may, so you can change it from nil to whatever.
def upload_file(access_token, parent_drive_item, local_file_path, remote_file_path) do
with {:ok, upload_url} <- create_upload_session(access_token, parent_drive_item, remote_file_path),
{:ok, file_size} <- File.stat(local_file_path) do
upload_file_in_chunks(access_token, upload_url, local_file_path, file_size.size)
end
end
defp create_upload_session(token, parent_drive_item, file_path) do
# Change as necessary
path = "/me/drives/#{parent_drive_item["parentReference"]["driveId"]}/items/#{parent_drive_item["id"]}:/#{file_path}:/createUploadSession"
url =
@graph_api_url
|> URI.new!()
|> URI.append_path(path)
|> URI.to_string
headers = [Authorization: "Bearer #{token}", "Content-Type": "application/json"]
# conflictBehavior can be rename, replace, or fail. Must be in the body, not the URL query string
body = %{item: %{"@microsoft.graph.conflictBehavior" => "rename"}} |> Jason.encode!()
case HTTPoison.post(url, body, headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
upload_url = Jason.decode!(body)["uploadUrl"]
{:ok, upload_url}
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error("Failed to create upload session: HTTP #{code} - #{inspect(body)}")
{:error, :failed_to_create_session}
{:error, %HTTPoison.Error{reason: reason}} ->
Logger.error("HTTP request failed while creating upload session: #{reason}")
{:error, :request_failed}
end
end
defp upload_file_in_chunks(token, upload_url, file_path, file_size) do
# Per MS docs
#
# https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online
#
# * A byte range size of 10 MiB for stable high speed connections is optimal.
# For slower or less reliable connections you may get better results from a
# smaller fragment size. The recommended fragment size is between 5-10 MiB.
#
# * Use a byte range size that is a multiple of 320 KiB (327,680 bytes).
# Failing to use a fragment size that is a multiple of 320 KiB can result
# in large file transfers failing after the last byte range is uploaded.
chunk_size = 320 * 1024 * 32 # 10 Mib
File.stream!(file_path, [], chunk_size)
|> Enum.with_index()
# We're not really accumumating anything, we just need to return the final response
|> Enum.reduce_while(nil, fn {chunk, index}, _acc ->
start_byte = index * chunk_size
end_byte = start_byte + byte_size(chunk) - 1
content_range = "bytes #{start_byte}-#{end_byte}/#{file_size}"
headers = [
{"Authorization", "Bearer #{token}"},
{"Content-Range", content_range}
]
case HTTPoison.put(upload_url, chunk, headers) do
{:ok, %HTTPoison.Response{status_code: 202, body: _body}} ->
Logger.info("Chunk #{index + 1} uploaded successfully")
{:cont, {:ok, nil}}
{:ok, %HTTPoison.Response{status_code: code, body: body}} when code in 200..299 ->
Logger.info("Upload complete: HTTP #{code} - #{body}")
{:cont, {:ok, nil}}
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error("Failed to upload chunk: HTTP #{code} - #{inspect(body)}")
{:halt, {:error, "Graph API interaction failed"}}
{:error, %HTTPoison.Error{reason: reason}} ->
Logger.error("HTTP request failed: #{reason}")
{:halt, {:error, "HTTP Request to Graph API failed"}}
end
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment