Posting a feedback file to a dropbox via API

I have been trying to write code either via

POST /d2l/api/le/(version)/(orgUnitId)/dropbox/folders/(folderId)/feedback/(entityType)/(entityId)/attach or

POST /d2l/api/le/(version)/(orgUnitId)/dropbox/folders/(folderId)/feedback/(entityType)/(entityId)/upload

I've been trying to upload a specific small file as a test, and have been receiving a 400, 404, or 416 error when trying to upload. In contrast, I have been able to use my Python script to post grades, dropbox score, and text/html feedback. Would you be able to provide some pointers as to the form of the payload required? Also, my admin has enabled the current scopes:

SCOPE = "core:*.* : dropbox:access:read dropbox:folders:read,write enrollment:orgunit:read grades:access:read grades:gradecategories:read grades:gradeobjects:read grades:gradevalues:read,write quizzing:access:read quizzing:attempts:read quizzing:quizzes:read managefiles:*.*"

The function call below this post is what I attempted for file uploading in Python (spacing messed up in cut/paste). Do you have any suggestions? Thanks in advance for any help you can provide.


def attach_feedback_file_prototype(access_token, org_unit_id, folder_id, user_id, file_path):
"""
Attaches a feedback file by POSTing the raw file data.This function implements:
POST /d2l/api/le/(version)/(orgUnitId)/dropbox/folders/(folderId)/feedback/(entityType)/(entityId)/attach?filename=...
"""

print(f"Preparing to attach file '{file_path}' to User ID {user_id}...")

# 1. Get file details
if not os.path.exists(file_path):
print(f"❌ ERROR: File not found: {file_path}")
return

file_name = os.path.basename(file_path)
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = 'application/octet-stream' # Default

entity_type = "user"

# 2. Build API URL, Headers, and Params
api_url = (
f"{API_BASE_URL}/d2l/api/le/{API_VERSION}/{org_unit_id}/dropbox/folders/"
f"{folder_id}/feedback/{entity_type}/{user_id}/attach"
)

# We pass the filename as a URL parameter
params = {
'filename': file_name
}

headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': mime_type # Set content-type to the file's mime type
}

# 3. Read the raw file data
try:
with open(file_path, 'rb') as f:
file_data = f.read()

# 4. Print the POST call to the console
print("\n--- Printing POST Call Details ---")
print(f"METHOD: POST")
print(f"URL: {api_url}")
print(f"PARAMS: {json.dumps(params, indent=4)}")
print(f"HEADERS:")
print(json.dumps(headers, indent=4))
print(f"BODY: (Raw bytes of {file_name}, {len(file_data)} bytes)")
print("------------------------------------")

# 5. Attempt the posting
print("\nAttempting to POST raw file data...")

# We use the 'data' param to send the raw bytes
response = requests.post(
api_url,
headers=headers,
params=params,
data=file_data
)

response.raise_for_status()

print(f"✅ SUCCESS: {response.status_code} {response.reason}")
print("File attached successfully.")

response_data = response.json()
print("\n--- Server Response Data ---")
print(json.dumps(response_data, indent=2))
print("------------------------------")

except requests.exceptions.HTTPError as err:
print(f"❌ API CALL FAILED: {err.response.status_code} - {err.response.reason}")
print("--- SERVER ERROR RESPONSE ---")
print(err.response.text)
print("-----------------------------")
except IOError as e:
print(f"❌ File Error: Could not read file {file_path}. {e}")
except Exception as e:
print(f"❌ An unexpected error occurred: {e}")

Comments

  • Joshua.G.520
    Joshua.G.520 Posts: 10 🌱

    Hi Sean,

    I haven't used those specific calls before but they look like the same style as other file upload API calls that I have used, e.g., to upload and save a file to manage files. They need first the /upload call to upload the file, which returns a file key. Then follow that with a /save call, using the key from the /upload, to save the file. At a quick glance your /upload and /attach calls look like a similar structure, but I don't see the /upload step in your code. I think you'll need that. Hope this helps!

    Josh.

  • Sean.R.636
    Sean.R.636 Posts: 6 🌱

    Josh,
    Thanks for the feedback. Is there any chance that you could share a working script? Also, what scopes do you have enabled? I'm trying to work with my admin at RIT to only enable what is needed. I was able to make some progress (fixed a 416 error and now get a 404 when trying to upload to dropbox). I appreciate the feedback and help!

    Sincerely,
    Sean

  • Joshua.G.520
    Joshua.G.520 Posts: 10 🌱

    Hi Sean,

    This may be less helpful as I write web apps in Javascript, not Python. So I have no application registered and no scopes to configure. I will share some more though in case it helps. What may be more useful is the requests I am sending, so I'll include those (these are just copied-as-fetch from the DevTools console - these should be more of a standard form to interpret and I've added CAPS to highlight some key elements like the file key). Way below is my save_files wrapper, which calls another wrapper api_action to make the actual API calls. So code like this:

    await save_files([{
    content_type: 'text/plain',
    data: 'Hello world',
    name: 'helloworld.txt',
    ou: 'OU',
    overwrite: false,
    path: '/'
    }])

    Will generate a request like this:

    fetch("https://HOSTNAME/d2l/api/lp/1.46/OU/managefiles/file/upload", {
    "headers": {
    "accept": "/",
    "accept-language": "en-US,en;q=0.9",
    "content-range": "bytes 0-10/11",
    "content-type": "text/plain",
    "priority": "u=1, i",
    "sec-ch-ua": ""Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": ""Windows"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-csrf-token": "TOKEN",
    "x-upload-content-length": "11",
    "x-upload-content-type": "text/plain",
    "x-upload-file-name": "helloworld.txt"
    },
    "referrer": "https://HOSTNAME/content/enforced/PATH/Expectation%20Tracker%20Code/Expectation.Tracker%20-%20Copy%20(589).html",
    "body": "Hello world",
    "method": "POST",
    "mode": "cors",
    "credentials": "include"
    });

    Which gets re-directed to this:

    fetch("https://HOSTNAME/d2l/upload/FILEKEY", {
    "headers": {
    "accept": "/",
    "accept-language": "en-US,en;q=0.9",
    "content-range": "bytes 0-10/11",
    "content-type": "text/plain",
    "priority": "u=1, i",
    "sec-ch-ua": ""Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": ""Windows"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-csrf-token": "TOKEN",
    "x-upload-content-length": "11",
    "x-upload-content-type": "text/plain",
    "x-upload-file-name": "helloworld.txt"
    },
    "referrer": "https://HOSTNAME/content/enforced/PATH/Expectation%20Tracker%20Code/Expectation.Tracker%20-%20Copy%20(589).html",
    "body": "Hello world",
    "method": "POST",
    "mode": "cors",
    "credentials": "include"
    });

    After that, the file is uploaded, and another request is sent to save to manage files:

    fetch("https://HOSTNAME/d2l/api/lp/1.46/OU/managefiles/file/save?overwriteFile=false", {
    "headers": {
    "accept": "/",
    "accept-language": "en-US,en;q=0.9",
    "content-type": "application/x-www-form-urlencoded",
    "priority": "u=1, i",
    "sec-ch-ua": ""Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": ""Windows"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-csrf-token": "TOKEN"
    },
    "referrer": "https://HOSTNAME/content/enforced/PATH/Expectation%20Tracker%20Code/Expectation.Tracker%20-%20Copy%20(589).html",
    "body": "fileKey=FILEKEY&relativePath=",
    "method": "POST",
    "mode": "cors",
    "credentials": "include"
    });

    And the file is saved:

    image.png

    Here's the save_files wrapper - not sure it is as useful as the requests above:

    // files = [{
    // content_type:
    // data:
    // name:
    // ou:
    // overwrite:
    // path:
    // }]
    async function save_files(files, sandbox) {

    if (Array.isArray(files) == false)
    throw 'files must be an array';

    let requests = [];

    for (let i = 0; i < files.length; i++)

    requests.push({

    tag: files[i].ou + '_' + files[i].path + '/' + files[i].name,
    type: 'post',
    url: LP + '/' + files[i].ou + '/managefiles/file/upload',

    custom_headers: {

    'Content-Type': files[i].content_type,
    'Content-Range': 'bytes 0-' + (files[i].data.length - 1) + '/' + files[i].data.length,
    'X-Upload-Content-Type': files[i].content_type,
    'X-Upload-Content-Length': files[i].data.length,
    'X-Upload-File-Name': files[i].name

    },

    data: files[i].data

    });

    let result = await api_action(requests, { throw_on_error: false });

    requests = [];

    for (let i = 0; i < files.length; i++)

    requests.push({

    tag: files[i].ou + '_' + files[i].path + '/' + files[i].name,
    type: 'post',
    url: LP + '/' + files[i].ou + '/managefiles/file/save' + '?overwriteFile=' + (files[i].overwrite === true),
    overwriteFile: files[i].overwrite === true,

    custom_headers: {

    'Content-Type': 'application/x-www-form-urlencoded'

    },

    data: 'fileKey=' + result[i].response.split('/').pop() + '&relativePath=' + apply_sandbox(files[i].path, sandbox)

    });

    result = await api_action(requests, { throw_on_error: false });

    return result;

    }