There’s an option to trigger a script after the end of a render in the Delivery page in resolve studio for Mac OS.
Copy the file onto the Mac running DaVinci Resolve Studio. It should go in ~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Deliver/Bunny_Upload.lua
It’s a lua script and not python for whatever reason this Mac does not like running the python script but runs lua fine so lua it is.
We use Bunny CDN for previewing and usually the AIVU gets uploaded there right after manually. Now this script runs and uploads it. Make sure to change “YOUR_USER_NAME” to get the logs written so you can debug and the values for STORAGE_ZONE, BUNNY_PASSWORD and REMOTE_FOLDER.
cat ~/Library/Logs/ResolvedBunnyUpload.log
for reading the logs in case it errors.
And here’s the script for your inspiration. Note if you have multiple renders this only uploads the first one. See after the scroll for a full downloadable one.
-- Bunny_Upload.lua — DaVinci Resolve Deliver trigger for macOS
-- ----- EDIT THESE FOUR VALUES -----
local BUNNY_ENDPOINT = "ny.storage.bunnycdn.com" -- Exact host from Bunny Storage Zone > FTP & API Access
local STORAGE_ZONE = "xxxxxxxxx"
local BUNNY_PASSWORD = "xxxxxxxxx"
local REMOTE_FOLDER = "xxxxxxxxx"
-- ----------------------------------
local LOG_FILE = "/Users/YOUR_USER_NAME/Library/Logs/ResolveBunnyUpload.log"
local function log(message)
local file = io.open(LOG_FILE, "a")
if file then
file:write(os.date("%Y-%m-%d %H:%M:%S") .. " " .. message .. "\n")
file:close()
end
print(message)
end
-- Safely wraps text used in a shell command, including filenames with spaces.
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function is_file(path)
local file = io.open(path, "rb")
if file then
file:close()
return true
end
return false
end
local function file_mtime(path)
local handle = io.popen("/usr/bin/stat -f %m " .. shell_quote(path))
if not handle then
return 0
end
local value = handle:read("*l")
handle:close()
return tonumber(value) or 0
end
local function url_encode(value)
return (tostring(value):gsub("([^%w%-_%.~])", function(character)
return string.format("%%%02X", string.byte(character))
end))
end
local function url_encode_path(path)
local parts = {}
for part in tostring(path):gmatch("[^/]+") do
table.insert(parts, url_encode(part))
end
return table.concat(parts, "/")
end
local function filename(path)
return path:match("([^/]+)$") or path
end
local function find_finished_export()
local resolve = app:GetResolve()
local project = resolve:GetProjectManager():GetCurrentProject()
local jobs = project:GetRenderJobList() or {}
local selected_file = nil
local matching_files = 0
for index, job in pairs(jobs) do
-- Resolve includes non-job metadata in this table. Ignore it.
if type(job) == "table" then
local target_dir = job["TargetDir"]
local output_name = job["OutputFilename"]
log("Job " .. tostring(index) ..
" | TargetDir=" .. tostring(target_dir) ..
" | OutputFilename=" .. tostring(output_name))
if target_dir and output_name then
local candidate = target_dir .. "/" .. output_name
local exists = is_file(candidate)
log("Candidate: " .. candidate .. " | exists=" .. tostring(exists))
if exists then
matching_files = matching_files + 1
if not selected_file then
selected_file = candidate
log("Selected export: " .. selected_file)
end
end
end
else
log(
"Ignoring non-job render-list metadata" ..
" | key=" .. tostring(index) ..
" | value=" .. tostring(job)
)
end
end
if not selected_file then
return nil, "Could not find a completed local single-file render."
end
if matching_files > 1 then
log("WARNING: Multiple rendered files were found. Uploading: " .. selected_file)
end
log("find_finished_export: returning selected path to main")
return selected_file, nil
end
local function upload_file(source)
local path_parts = { url_encode(STORAGE_ZONE) }
if REMOTE_FOLDER ~= "" then
table.insert(path_parts, url_encode_path(REMOTE_FOLDER))
end
table.insert(path_parts, url_encode(filename(source)))
local url = "https://" .. BUNNY_ENDPOINT .. "/" .. table.concat(path_parts, "/")
local command =
"/usr/bin/curl --fail --silent --show-error --retry 3 --request PUT" ..
" -H " .. shell_quote("AccessKey: " .. BUNNY_PASSWORD) ..
" -H " .. shell_quote("Content-Type: application/octet-stream") ..
" --connect-timeout 30" ..
" --upload-file " .. shell_quote(source) ..
" " .. shell_quote(url) ..
" --write-out " .. shell_quote(
"\nCURL RESULT: http=%{http_code} uploaded=%{size_upload}B time=%{time_total}s\n"
) ..
" >> " .. shell_quote(LOG_FILE) .. " 2>&1"
log("Uploading: " .. source)
log("Destination: " .. url)
local result = os.execute(command)
if result == true or result == 0 then
log("UPLOADED: " .. source)
return true, nil
end
return false, "Bunny upload failed; curl exit status: " .. tostring(result)
end
local function traceback_handler(err)
local message = "UNHANDLED LUA ERROR: " .. tostring(err)
if type(debug) == "table" and type(debug.traceback) == "function" then
message = debug.traceback(message, 2)
end
log(message)
return message
end
local function main()
log("MAIN: entered")
log("MAIN: about to call find_finished_export")
local find_call_ok, source, find_error = pcall(find_finished_export)
log(
"MAIN: find_finished_export returned" ..
" | pcall_ok=" .. tostring(find_call_ok) ..
" | source=" .. tostring(source) ..
" | detail=" .. tostring(find_error)
)
if not find_call_ok then
log("ERROR: find_finished_export threw a Lua error: " .. tostring(source))
return false
end
if type(source) ~= "string" or source == "" then
log("ERROR: no usable render path returned: " .. tostring(find_error))
return false
end
log("MAIN: confirmed source file exists in Resolve: " .. source)
log("MAIN: about to call upload_file")
local upload_call_ok, uploaded, upload_error = pcall(upload_file, source)
log(
"MAIN: upload_file returned" ..
" | pcall_ok=" .. tostring(upload_call_ok) ..
" | uploaded=" .. tostring(uploaded) ..
" | detail=" .. tostring(upload_error)
)
if not upload_call_ok then
log("ERROR: upload_file threw a Lua error: " .. tostring(uploaded))
return false
end
if not uploaded then
log("ERROR: Bunny upload did not succeed: " .. tostring(upload_error))
return false
end
log("MAIN: upload completed successfully")
return true
end
log("TOP: starting Bunny_Upload through xpcall")
local top_ok, top_result = xpcall(main, traceback_handler)
log(
"TOP: xpcall returned" ..
" | success=" .. tostring(top_ok) ..
" | result=" .. tostring(top_result)
)
here’s the upload all processed files in the render script:
-- Bunny_Upload.lua — DaVinci Resolve Deliver trigger for macOS
-- ----- EDIT THESE FOUR VALUES -----
local BUNNY_ENDPOINT = "ny.storage.bunnycdn.com" -- Exact host from Bunny Storage Zone > FTP & API Access
local STORAGE_ZONE = "xxxxxxxxx"
local BUNNY_PASSWORD = "xxxxxxxxx"
local REMOTE_FOLDER = "xxxxxxxxx"
-- ----------------------------------
local LOG_FILE = "/Users/itunes/Library/Logs/ResolveBunnyUpload.log"
local function log(message)
local file = io.open(LOG_FILE, "a")
if file then
file:write(os.date("%Y-%m-%d %H:%M:%S") .. " " .. message .. "\n")
file:close()
end
print(message)
end
-- Safely wraps text used in a shell command, including filenames with spaces.
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function is_file(path)
local file = io.open(path, "rb")
if file then
file:close()
return true
end
return false
end
local function file_mtime(path)
local handle = io.popen("/usr/bin/stat -f %m " .. shell_quote(path))
if not handle then
return 0
end
local value = handle:read("*l")
handle:close()
return tonumber(value) or 0
end
local function url_encode(value)
return (tostring(value):gsub("([^%w%-_%.~])", function(character)
return string.format("%%%02X", string.byte(character))
end))
end
local function url_encode_path(path)
local parts = {}
for part in tostring(path):gmatch("[^/]+") do
table.insert(parts, url_encode(part))
end
return table.concat(parts, "/")
end
local function filename(path)
return path:match("([^/]+)$") or path
end
-- Returns every existing single-file output referenced by the current render
-- job list. Resolve may run this delivery trigger once after a queue finishes,
-- so selecting only one output here would leave the other completed jobs out.
local function find_finished_exports()
local resolve = app:GetResolve()
local project = resolve:GetProjectManager():GetCurrentProject()
local jobs = project:GetRenderJobList() or {}
local exports = {}
local seen = {}
for index, job in pairs(jobs) do
-- Resolve includes non-job metadata in this table. Ignore it.
if type(job) == "table" then
local target_dir = job["TargetDir"]
local output_name = job["OutputFilename"]
log("Job " .. tostring(index) ..
" | TargetDir=" .. tostring(target_dir) ..
" | OutputFilename=" .. tostring(output_name))
if target_dir and output_name then
local candidate = target_dir .. "/" .. output_name
local exists = is_file(candidate)
log("Candidate: " .. candidate .. " | exists=" .. tostring(exists))
if exists then
-- Do not upload a file twice if two queue jobs reference
-- the same destination and output filename.
if not seen[candidate] then
seen[candidate] = true
table.insert(exports, candidate)
log("Queued export " .. tostring(#exports) .. ": " .. candidate)
else
log("Skipping duplicate export: " .. candidate)
end
end
end
else
log(
"Ignoring non-job render-list metadata" ..
" | key=" .. tostring(index) ..
" | value=" .. tostring(job)
)
end
end
if #exports == 0 then
return nil, "Could not find any completed local single-file renders."
end
log("find_finished_exports: returning " .. tostring(#exports) .. " path(s) to main")
return exports, nil
end
local function upload_file(source)
local path_parts = { url_encode(STORAGE_ZONE) }
if REMOTE_FOLDER ~= "" then
table.insert(path_parts, url_encode_path(REMOTE_FOLDER))
end
table.insert(path_parts, url_encode(filename(source)))
local url = "https://" .. BUNNY_ENDPOINT .. "/" .. table.concat(path_parts, "/")
local command =
"/usr/bin/curl --fail --silent --show-error --retry 3 --request PUT" ..
" -H " .. shell_quote("AccessKey: " .. BUNNY_PASSWORD) ..
" -H " .. shell_quote("Content-Type: application/octet-stream") ..
" --connect-timeout 30" ..
" --upload-file " .. shell_quote(source) ..
" " .. shell_quote(url) ..
" --write-out " .. shell_quote(
"\nCURL RESULT: http=%{http_code} uploaded=%{size_upload}B time=%{time_total}s\n"
) ..
" >> " .. shell_quote(LOG_FILE) .. " 2>&1"
log("Uploading: " .. source)
log("Destination: " .. url)
local result = os.execute(command)
if result == true or result == 0 then
log("UPLOADED: " .. source)
return true, nil
end
return false, "Bunny upload failed; curl exit status: " .. tostring(result)
end
local function traceback_handler(err)
local message = "UNHANDLED LUA ERROR: " .. tostring(err)
if type(debug) == "table" and type(debug.traceback) == "function" then
message = debug.traceback(message, 2)
end
log(message)
return message
end
local function main()
log("MAIN: entered")
log("MAIN: about to call find_finished_exports")
local find_call_ok, sources, find_error = pcall(find_finished_exports)
log(
"MAIN: find_finished_exports returned" ..
" | pcall_ok=" .. tostring(find_call_ok) ..
" | source_count=" .. tostring(type(sources) == "table" and #sources or sources) ..
" | detail=" .. tostring(find_error)
)
if not find_call_ok then
log("ERROR: find_finished_exports threw a Lua error: " .. tostring(sources))
return false
end
if type(sources) ~= "table" or #sources == 0 then
log("ERROR: no usable render paths returned: " .. tostring(find_error))
return false
end
local failed_uploads = 0
for index, source in ipairs(sources) do
log(
"MAIN: processing export " .. tostring(index) ..
" of " .. tostring(#sources) ..
" | source=" .. source
)
log("MAIN: about to call upload_file")
local upload_call_ok, uploaded, upload_error = pcall(upload_file, source)
log(
"MAIN: upload_file returned" ..
" | pcall_ok=" .. tostring(upload_call_ok) ..
" | uploaded=" .. tostring(uploaded) ..
" | detail=" .. tostring(upload_error)
)
if not upload_call_ok then
failed_uploads = failed_uploads + 1
log("ERROR: upload_file threw a Lua error: " .. tostring(uploaded))
elseif not uploaded then
failed_uploads = failed_uploads + 1
log("ERROR: Bunny upload did not succeed: " .. tostring(upload_error))
end
end
if failed_uploads > 0 then
log(
"MAIN: upload batch finished with " .. tostring(failed_uploads) ..
" failed file(s) out of " .. tostring(#sources)
)
return false
end
log("MAIN: upload batch completed successfully | files=" .. tostring(#sources))
return true
end
log("TOP: starting Bunny_Upload through xpcall")
local top_ok, top_result = xpcall(main, traceback_handler)
log(
"TOP: xpcall returned" ..
" | success=" .. tostring(top_ok) ..
" | result=" .. tostring(top_result)
)