Created
February 24, 2024 23:02
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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