FIX - match FiveManage camera upload flow

This commit is contained in:
Leon.Schmidt
2026-08-21 00:49:47 +02:00
parent 37948e1f6d
commit 10afed3d2c
4 changed files with 46 additions and 27 deletions
@@ -484,12 +484,7 @@ async function uploadReady(ready: UploadReady): Promise<void> {
})
const form = new FormData()
form.append('path', ready.uploadPath)
form.append('file', blob, fileName)
form.append(
'metadata',
JSON.stringify({ captureToken: ready.captureToken, source: 'sky_phone' }),
)
const controller = new AbortController()
const timeout = window.setTimeout(
() => controller.abort(),
@@ -511,11 +506,10 @@ async function uploadReady(ready: UploadReady): Promise<void> {
})
const text = await response.text()
const body = JSON.parse(text) as {
data?: { id?: string; originalUrl?: string; url?: string }
data?: { id?: string; url?: string }
error?: string
id?: string
message?: string
originalUrl?: string
url?: string
}
const uploaded = body.data ?? body
@@ -529,7 +523,6 @@ async function uploadReady(ready: UploadReady): Promise<void> {
debugStage = 'completion_callback'
const completion = await nuiCall('media:completeUpload', {
correlationId: ready.correlationId,
originalUrl: uploaded.originalUrl,
remoteId: uploaded.id,
requestId: ready.requestId,
url: uploaded.url,
@@ -28,6 +28,10 @@ const mediaServer = readFileSync(
new URL('../../sky_phone/source/server/media.lua', import.meta.url),
'utf8',
)
const memoServer = readFileSync(
new URL('../../sky_phone/source/server/memos.lua', import.meta.url),
'utf8',
)
const mediaCapture = readFileSync(
new URL('./components/PhoneMediaCapture.vue', import.meta.url),
'utf8',
@@ -58,15 +62,25 @@ describe('FiveManage server configuration contract', () => {
)
})
it('resolves uploads through their server-generated FiveManage path', () => {
it('uses the direct FiveManage upload response flow for Camera media', () => {
expect(mediaConfig).not.toContain('VerificationRetryDelaysMs')
expect(mediaServer).toContain('upload_path = "sky_phone-" .. capture_token')
expect(mediaCapture).toContain("form.append('file', blob, fileName)")
expect(mediaCapture).not.toContain("form.append('path'")
expect(mediaCapture).not.toContain("form.append(\n 'metadata'")
expect(mediaServer).toContain('if state.media_type ~= "audio" then')
expect(mediaServer).toContain(
'Accepting the direct FiveManage camera upload response',
)
expect(mediaServer).toContain('remote_id = remote_id')
expect(mediaServer).toContain('url = uploaded_url')
})
it('keeps server-generated upload path verification for voice memos', () => {
expect(memoServer).toContain('upload_path = "sky_phone-" .. capture_token')
expect(mediaServer).toContain(
'"?limit=100&page=1&path=" .. SkyPhoneMediaImport.UrlEncode(state.upload_path)',
)
expect(mediaCapture).toContain("form.append('path', ready.uploadPath)")
expect(memoRecorder).toContain("form.append('path', ready.uploadPath)")
expect(mediaCapture).toContain('originalUrl: uploaded.originalUrl')
expect(memoRecorder).toContain('originalUrl: uploaded.originalUrl')
expect(mediaServer).toContain(
'local verified_url = remote.url or remote.originalUrl',
@@ -76,7 +90,7 @@ describe('FiveManage server configuration contract', () => {
it('binds metadata verification to the FiveManage host that issued the upload URL', () => {
expect(mediaServer).toContain('["api.fivemanage.com"] = true')
expect(mediaServer).toContain('["fmapi.net"] = true')
expect(mediaServer).toContain('provider_base_url = provider_base_url')
expect(memoServer).toContain('provider_base_url = provider_base_url')
expect(mediaServer).toContain('state.provider_base_url')
})
-2
View File
@@ -70,7 +70,6 @@ export type MediaImportResult = {
}
export type UploadReady = {
captureToken: string
correlationId: string
mediaType: MediaType
photo?: {
@@ -79,7 +78,6 @@ export type UploadReady = {
}
presignedUrl: string
requestId: string
uploadPath: string
uploadTimeoutMs?: number
video?: {
BitrateKbps?: number
+26 -12
View File
@@ -500,6 +500,27 @@ local function verify_remote_upload(state, remote_id, uploaded_url, original_url
)
return nil, "invalid_upload"
end
if state.media_type ~= "audio" then
local uploaded_host = uploaded_url:match("^https://([^/%?#]+)")
if not uploaded_host or uploaded_host:lower() ~= "r2.fivemanage.com" then
Bridge.Debug(
"error",
"[sky_phone][media-debug] FiveManage camera upload returned an unexpected media host."
)
return nil, "invalid_upload"
end
media_debug(
"Accepting the direct FiveManage camera upload response (type=%s).",
tostring(state.media_type)
)
return {
mime_type = state.mime_type,
remote_id = remote_id,
url = uploaded_url,
}, nil, true
end
media_debug("Verifying uploaded file with FiveManage (type=%s).", tostring(state.media_type))
local remote, remote_error, remote_path = get_remote_file(state, remote_id, uploaded_url)
if not remote then
@@ -909,7 +930,7 @@ RegisterNetEvent("sky_phone:media:request-upload", function(data)
diagnostic_text(correlation_id, 80),
owner.account_id and "account" or "device"
)
local presigned_url, presigned_error, provider_base_url = request_presigned_url()
local presigned_url, presigned_error = request_presigned_url()
if not presigned_url then
Bridge.Debug(
"error",
@@ -921,33 +942,28 @@ RegisterNetEvent("sky_phone:media:request-upload", function(data)
upload_result(src, correlation_id, false, presigned_error)
return
end
local ids = Bridge.Database.Query("SELECT UUID() AS `request_id`, UUID() AS `capture_token`", {})
local ids = Bridge.Database.Query("SELECT UUID() AS `request_id`", {})
local request_id = ids[1] and ids[1].request_id
local capture_token = ids[1] and ids[1].capture_token
if type(request_id) ~= "string" or type(capture_token) ~= "string" then
if type(request_id) ~= "string" then
Bridge.Debug(
"error",
"[sky_phone][media-debug] Database did not generate upload session identifiers (source=%s, correlation=%s, rows=%s, request-id=%s, capture-token=%s).",
"[sky_phone][media-debug] Database did not generate an upload request ID (source=%s, correlation=%s, rows=%s, request-id=%s).",
tostring(src),
diagnostic_text(correlation_id, 80),
tostring(type(ids) == "table" and #ids or 0),
type(request_id),
type(capture_token)
type(request_id)
)
upload_result(src, correlation_id, false, "request_failed")
return
end
pending_uploads[request_id] = {
capture_token = capture_token,
correlation_id = correlation_id,
media_type = media_type,
mime_type = media_type == "video" and "video/webm"
or ({ png = "image/png", webp = "image/webp" })[tostring(Config.Media.Photo.Encoding):lower()]
or "image/jpeg",
owner = owner,
provider_base_url = provider_base_url,
source = src,
upload_path = "sky_phone-" .. capture_token,
}
SetTimeout(tonumber(Config.Media.UploadSessionTimeoutMs) or 60000, function()
expire_upload(request_id)
@@ -959,13 +975,11 @@ RegisterNetEvent("sky_phone:media:request-upload", function(data)
tostring(media_type)
)
TriggerClientEvent("sky_phone:media:upload-ready", src, {
captureToken = capture_token,
correlationId = correlation_id,
mediaType = media_type,
photo = Config.Media.Photo,
presignedUrl = presigned_url,
requestId = request_id,
uploadPath = pending_uploads[request_id].upload_path,
uploadTimeoutMs = Config.Media.FiveManage.UploadTimeoutMs,
video = Config.Media.Video,
})