fix local folder upload intent contract

This commit is contained in:
lix-2026
2026-05-24 02:34:52 +08:00
parent 6d9d8ce9cd
commit ae6bbc9656
5 changed files with 136 additions and 29 deletions
@@ -211,6 +211,7 @@ struct LocalAssetUploadFields {
root_uri: String,
document_id: String,
target_relative_path: Option<String>,
upload_intent: String,
kind: String,
}
@@ -3077,20 +3078,41 @@ pub async fn upload_local_markdown_asset(
let fields = read_local_asset_upload_multipart(multipart).await?;
ensure_local_workspace_write_access_with_state(&state, &context, &fields.root_uri)
.map_err(|error| error.with_context(&context))?;
let asset = if fields.target_relative_path.is_some() || fields.document_id.trim().is_empty() {
write_local_folder_file_upload(
&fields.root_uri,
fields.target_relative_path.as_deref().unwrap_or(""),
&fields.kind,
fields.file,
)?
} else {
write_local_markdown_asset(
&fields.root_uri,
&fields.document_id,
&fields.kind,
fields.file,
)?
let asset = match fields.upload_intent.as_str() {
"editor.markdown.attach" => {
if fields.document_id.trim().is_empty() || fields.target_relative_path.is_some() {
return Err(WebError::bad_request_code(
"local_asset_upload_intent_invalid",
"editor.markdown.attach 必须提供 documentId 且不能提供 targetRelativePath",
));
}
write_local_markdown_asset(
&fields.root_uri,
&fields.document_id,
&fields.kind,
fields.file,
)?
}
"filetree.folder.drop" => {
let Some(target_relative_path) = fields.target_relative_path.as_deref() else {
return Err(WebError::bad_request_code(
"local_asset_upload_intent_invalid",
"filetree.folder.drop 必须提供 targetRelativePath",
));
};
write_local_folder_file_upload(
&fields.root_uri,
target_relative_path,
&fields.kind,
fields.file,
)?
}
_ => {
return Err(WebError::bad_request_code(
"local_asset_upload_intent_unsupported",
"不支持的本地上传 intent",
));
}
};
Ok((StatusCode::OK, Json(json!({ "ok": true, "asset": asset }))))
}
@@ -3361,6 +3383,7 @@ async fn read_local_asset_upload_multipart(
let mut root_uri = String::new();
let mut document_id = String::new();
let mut target_relative_path: Option<String> = None;
let mut upload_intent = String::new();
let mut kind = String::new();
while let Some(field) = multipart.next_field().await.map_err(|error| {
@@ -3413,6 +3436,7 @@ async fn read_local_asset_upload_multipart(
"targetRelativePath" | "targetDirectoryPath" => {
target_relative_path = Some(value.trim().to_string())
}
"uploadIntent" | "upload_intent" => upload_intent = value.trim().to_string(),
"kind" => kind = value.trim().to_string(),
_ => {}
}
@@ -3421,10 +3445,7 @@ async fn read_local_asset_upload_multipart(
let file = file.ok_or_else(|| {
WebError::bad_request_code("local_asset_upload_file_missing", "缺少 file")
})?;
if file.bytes.is_empty()
|| root_uri.is_empty()
|| (document_id.is_empty() && target_relative_path.is_none())
{
if file.bytes.is_empty() || root_uri.is_empty() || upload_intent.is_empty() {
return Err(WebError::bad_request_code(
"local_asset_upload_required_missing",
"缺少必要参数",
@@ -3435,6 +3456,7 @@ async fn read_local_asset_upload_multipart(
root_uri,
document_id,
target_relative_path,
upload_intent,
kind,
})
}
@@ -3501,6 +3523,7 @@ pub(crate) fn write_local_folder_file_upload(
let asset_type = local_upload_asset_type(kind, &file.content_type);
Ok(json!({
"id": format!("local-file:{root_relative_path}"),
"uploadIntent": "filetree.folder.drop",
"asset_type": asset_type,
"file_name": target.file_name().and_then(|value| value.to_str()).unwrap_or(&sanitized_name),
"mime_type": file.content_type,
@@ -3510,6 +3533,9 @@ pub(crate) fn write_local_folder_file_upload(
"sourceKind": "local_folder",
"rootUri": file_uri_for_path(&canonical_root),
"rootRelativePath": root_relative_path,
"targetRelativePath": target_relative_path,
"markdownRelativePath": Value::Null,
"ownerDocumentId": Value::Null,
}))
}
@@ -3610,6 +3636,7 @@ pub(crate) fn write_local_markdown_asset(
write_uploaded_asset_index_metadata(&canonical_root, &uploaded_assets)?;
Ok(json!({
"id": format!("local:asset:{root_relative_path}"),
"uploadIntent": "editor.markdown.attach",
"asset_type": asset_type,
"file_name": target.file_name().and_then(|value| value.to_str()).unwrap_or(&sanitized_name),
"mime_type": file.content_type,
@@ -3618,9 +3645,11 @@ pub(crate) fn write_local_markdown_asset(
"sourcePath": markdown_relative_path,
"document_id": document_id,
"documentId": document_id,
"ownerDocumentId": document_id,
"sourceKind": "local_folder",
"rootUri": file_uri_for_path(&canonical_root),
"rootRelativePath": root_relative_path,
"markdownRelativePath": markdown_relative_path,
}))
}
@@ -11509,6 +11538,13 @@ fn main() {}
assert_eq!(asset["file_url"], "photo-1.png");
assert_eq!(asset["asset_type"], "image");
assert_eq!(asset["sourceKind"], "local_folder");
assert_eq!(asset["uploadIntent"], "editor.markdown.attach");
assert_eq!(asset["rootRelativePath"], "docs/README/photo-1.png");
assert_eq!(asset["markdownRelativePath"], "photo-1.png");
assert_eq!(
asset["ownerDocumentId"],
"local-md:docs~2FREADME~2FREADME.md"
);
assert_eq!(
std::fs::read(root.join("docs").join("README").join("photo-1.png"))
.expect("read copied asset"),
@@ -11526,6 +11562,13 @@ fn main() {}
)
.expect("upload markdown asset");
assert_eq!(markdown_asset["sourcePath"], "notes.md");
assert_eq!(markdown_asset["uploadIntent"], "editor.markdown.attach");
assert_eq!(markdown_asset["rootRelativePath"], "docs/README/notes.md");
assert_eq!(markdown_asset["markdownRelativePath"], "notes.md");
assert_eq!(
markdown_asset["ownerDocumentId"],
"local-md:docs~2FREADME~2FREADME.md"
);
let uploaded_asset_index =
std::fs::read_to_string(root.join(".mnote").join("uploaded-assets.json"))
.expect("uploaded asset index");
+23 -6
View File
@@ -3073,7 +3073,8 @@ const SIDEBAR_TREE_JS: &str = r##"
targetDocumentId: documentId,
targetMindmapId: null,
targetSubPath: null,
targetRelativePath: String(detail.targetRelativePath || '')
targetRelativePath: String(detail.targetRelativePath || ''),
uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')
};
}
if (!workspaceId || !documentId) {
@@ -3083,7 +3084,8 @@ const SIDEBAR_TREE_JS: &str = r##"
workspaceId: workspaceId,
targetDocumentId: documentId,
targetMindmapId: null,
targetSubPath: null
targetSubPath: null,
uploadIntent: String(detail && detail.uploadIntent || 'editor.markdown.attach')
};
}
@@ -3096,6 +3098,13 @@ const SIDEBAR_TREE_JS: &str = r##"
if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
plan.targetRelativePath = String(detail.targetRelativePath || '');
}
if (detail && detail.uploadIntent) {
plan.uploadIntent = String(detail.uploadIntent);
} else if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
plan.uploadIntent = 'filetree.folder.drop';
} else if (!plan.uploadIntent) {
plan.uploadIntent = 'editor.markdown.attach';
}
return plan;
} catch (error) {
console.warn('[mnote upload] upload target preflight fallback', error);
@@ -3878,12 +3887,14 @@ const SIDEBAR_TREE_JS: &str = r##"
var rootUri = currentRootUri();
var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim();
var hasFolderTarget = plan && Object.prototype.hasOwnProperty.call(plan, 'targetRelativePath');
if (!rootUri || (!documentId && !hasFolderTarget)) {
var uploadIntent = String(plan && plan.uploadIntent || (hasFolderTarget ? 'filetree.folder.drop' : 'editor.markdown.attach')).trim();
if (!rootUri || !uploadIntent) {
throw new Error(' Markdown rootUri documentId');
}
var localForm = new FormData();
localForm.append('file', file);
localForm.append('rootUri', rootUri);
localForm.append('uploadIntent', uploadIntent);
if (documentId) localForm.append('documentId', documentId);
if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || ''));
localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment');
@@ -3969,7 +3980,8 @@ const SIDEBAR_TREE_JS: &str = r##"
void uploadFilesWithResolvedTarget(files, {
workspaceId: uploadContext.workspaceId || resolveWorkspaceId(document.body),
documentId: uploadContext.documentId || currentDocumentId(),
targetRowId: null
targetRowId: null,
uploadIntent: 'editor.markdown.attach'
}, {
insertIntoEditor: detail && detail.insertIntoEditor !== false,
editorRoot: uploadContext.root
@@ -4007,7 +4019,8 @@ const SIDEBAR_TREE_JS: &str = r##"
void uploadFilesWithResolvedTarget(files, {
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
targetRowId: null
targetRowId: null,
uploadIntent: 'editor.markdown.attach'
}, {
insertIntoEditor: true,
editorRoot: editorUploadRootFromElement(editorTarget)
@@ -10026,7 +10039,8 @@ const SIDEBAR_TREE_JS: &str = r##"
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null,
targetRelativePath: targetRow && (targetRow.getAttribute('data-row-kind') === 'folder' || targetRow.getAttribute('data-row-kind') === 'directory')
? fileTreeRowLocalRelativePath(targetRow)
: null
: null,
uploadIntent: 'filetree.folder.drop'
};
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = null;
@@ -10871,6 +10885,9 @@ mod tests {
"主编辑器上传应能从当前文档 DOM 回退解析 documentId"
);
assert!(SIDEBAR_TREE_JS.contains("localForm.append('rootUri', rootUri)"));
assert!(SIDEBAR_TREE_JS.contains("localForm.append('uploadIntent', uploadIntent)"));
assert!(SIDEBAR_TREE_JS.contains("uploadIntent: 'editor.markdown.attach'"));
assert!(SIDEBAR_TREE_JS.contains("uploadIntent: 'filetree.folder.drop'"));
assert!(SIDEBAR_TREE_JS.contains("localForm.append('documentId', documentId)"));
assert!(SIDEBAR_TREE_JS.contains("isLocalUploadedAsset(asset)"));
assert!(SIDEBAR_TREE_JS.contains("buildLocalOnlyOfficeOpenUrl"));