use super::{PendingMediaPut, UploadSuccess, UploadTarget};
use crate::ImError;
use serde_json::{json, Value};
pub fn parse_upload_success_reply(bytes: &[u8]) -> Result<UploadSuccess, String> {
let parsed: Value =
serde_json::from_slice(bytes).map_err(|error| format!("upload reply: {error}"))?;
let object = parsed
.as_object()
.ok_or_else(|| "upload reply must be object".to_string())?;
let required = |key: &str| {
object
.get(key)
.and_then(Value::as_str)
.filter(|text| !text.is_empty())
.map(str::to_string)
.ok_or_else(|| format!("upload reply field {key} must be non-empty string"))
};
let etag = object
.get("etag")
.map(|value| {
value
.as_str()
.map(str::to_string)
.ok_or_else(|| "upload reply field etag must be string".to_string())
})
.transpose()?;
Ok(UploadSuccess {
object_key: required("objectKey")?,
public_url: required("url")?,
etag,
})
}
pub fn mark_upload_success(
props: &mut Value,
target: &UploadTarget,
result: &UploadSuccess,
) -> Result<(), ImError> {
let node = target_node_mut(props, target)?;
let upload = node
.get_mut("upload")
.and_then(Value::as_object_mut)
.ok_or_else(|| ImError::Parse("upload must be object".to_string()))?;
upload.insert("status".to_string(), json!("success"));
upload.remove("url");
upload.remove("headers");
upload.insert("objectKey".to_string(), json!(&result.object_key));
upload.insert("publicUrl".to_string(), json!(&result.public_url));
if let Some(etag) = &result.etag {
upload.insert("etag".to_string(), json!(etag));
}
target.apply_upload_state(props, "success", Some(FULL_PERCENT))
}
pub fn mark_upload_failed(
props: &mut Value,
target: &UploadTarget,
_error: &str,
) -> Result<(), ImError> {
let node = target_node_mut(props, target)?;
let upload = node
.get_mut("upload")
.and_then(Value::as_object_mut)
.ok_or_else(|| ImError::Parse("upload must be object".to_string()))?;
upload.insert("status".to_string(), json!("failed"));
target.apply_upload_state(props, "failed", None)
}
pub fn mark_media_complete(props: &mut Value, pending: &PendingMediaPut) -> Result<(), ImError> {
let node = target_node_mut(props, &pending.target)?;
let object = node
.as_object_mut()
.ok_or_else(|| ImError::Parse("media target must be object".to_string()))?;
for field in [
"mediaInput",
"localPath",
"localUrl",
"local_url",
"local_path",
"objectKey",
"uploadId",
"fileId",
"headers",
"userId",
"teamId",
"etag",
"method",
"state",
"publicUrl",
"url",
"mediaHandle",
"media_handle",
] {
object.remove(field);
}
object.insert("bucket".to_string(), json!(&pending.prepared.bucket));
object.insert("id".to_string(), json!(&pending.prepared.file_id));
object.insert("name".to_string(), json!(&pending.input.file_name));
object.insert("uri".to_string(), json!(&pending.prepared.uri));
object.insert(
"contentType".to_string(),
json!(&pending.input.content_type),
);
object.insert("size".to_string(), json!(pending.input.size));
object.insert("sha256".to_string(), json!(&pending.input.sha256));
object.insert("upload".to_string(), json!({"status": "completed"}));
pending
.target
.apply_upload_state(props, "completed", Some(FULL_PERCENT))?;
let weighted = UploadTarget::message_upload_progress(props)?;
props
.as_object_mut()
.ok_or_else(|| ImError::Parse("props must be object".to_string()))?
.insert("uploadProgress".to_string(), json!(weighted));
Ok(())
}
pub fn mark_media_stage(
props: &mut Value,
target: &UploadTarget,
status: &str,
) -> Result<(), ImError> {
let node = target_node_mut(props, target)?;
node.as_object_mut()
.ok_or_else(|| ImError::Parse("media target must be object".to_string()))?
.insert("upload".to_string(), json!({"status": status}));
target.apply_upload_state(props, status, None)
}
const FULL_PERCENT: u64 = 100;
pub(super) const PENDING_STATUS: &str = "pending";
pub(super) fn init_upload_state(object: &mut serde_json::Map<String, Value>) {
object.insert("uploadStatus".to_string(), json!(PENDING_STATUS));
object.insert("uploadProgress".to_string(), json!(0u64));
}
impl UploadTarget {
pub fn apply_upload_state(
&self,
props: &mut Value,
status: &str,
percent: Option<u64>,
) -> Result<(), ImError> {
let node = target_node_mut(props, self)?;
let object = node
.as_object_mut()
.ok_or_else(|| ImError::Parse("media target must be object".to_string()))?;
object.insert("uploadStatus".to_string(), json!(status));
match percent {
Some(percent) => {
object.insert(
"uploadProgress".to_string(),
json!(percent.min(FULL_PERCENT)),
);
}
None => {
object
.entry("uploadProgress".to_string())
.or_insert_with(|| json!(0));
}
}
Ok(())
}
pub fn message_upload_progress(props: &Value) -> Result<u64, ImError> {
let mut weighted = 0u128;
let mut total = 0u128;
let mut accumulate = |node: &Value| -> Result<(), ImError> {
let object = node
.as_object()
.ok_or_else(|| ImError::Parse("media target must be object".to_string()))?;
let size = object.get("size").and_then(Value::as_u64).unwrap_or(0);
let percent = match object.get("uploadProgress").and_then(Value::as_u64) {
Some(percent) => percent.min(FULL_PERCENT),
None if is_settled_reference(object) => FULL_PERCENT,
None => 0,
};
weighted += u128::from(size) * u128::from(percent);
total += u128::from(size);
Ok(())
};
let template_file = props
.get("template")
.and_then(Value::as_object)
.and_then(|template| template.get("file"));
match (props.get("files"), props.get("file"), template_file) {
(Some(files), _, _) => {
let files = files
.as_array()
.ok_or_else(|| ImError::Parse("props.files must be array".to_string()))?;
for node in files {
accumulate(node)?;
}
}
(None, Some(file), _) => accumulate(file)?,
(None, None, Some(file)) => accumulate(file)?,
(None, None, None) => return Ok(0),
}
if total == 0 {
return Ok(0);
}
Ok((weighted / total) as u64)
}
}
fn is_settled_reference(object: &serde_json::Map<String, Value>) -> bool {
["bucket", "id", "uri"]
.iter()
.all(|field| object.get(*field).and_then(Value::as_str).is_some())
}
fn target_node_mut<'a>(
props: &'a mut Value,
target: &UploadTarget,
) -> Result<&'a mut Value, ImError> {
match target {
UploadTarget::RichImage { index } | UploadTarget::RichVideo { index } => props
.get_mut("files")
.and_then(Value::as_array_mut)
.and_then(|files| files.get_mut(*index))
.ok_or_else(|| ImError::Parse("missing rich media target".to_string())),
UploadTarget::File => props
.get_mut("file")
.ok_or_else(|| ImError::Parse("missing file target".to_string())),
UploadTarget::TemplateImage => props
.get_mut("template")
.and_then(Value::as_object_mut)
.and_then(|template| template.get_mut("file"))
.ok_or_else(|| ImError::Parse("missing template image target".to_string())),
}
}