feat: integrate pi rust lab runtime
This commit is contained in:
@@ -56,6 +56,11 @@ static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
|
||||
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
#[cfg(not(test))]
|
||||
static LOCAL_SEARCH_INDEX_CONTROL_PLANE: OnceLock<
|
||||
std::sync::Arc<dyn control_plane::ControlPlaneStore>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn local_page_tree_snapshot_cache(
|
||||
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
@@ -3363,13 +3368,26 @@ fn save_local_markdown_page_inner(
|
||||
}
|
||||
|
||||
fn refresh_local_search_index_best_effort(root: &Path, root_uri: &str, workspace_id: &str) {
|
||||
let control_plane = open_control_plane_store();
|
||||
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
|
||||
control_plane.as_ref(),
|
||||
root,
|
||||
root_uri,
|
||||
workspace_id,
|
||||
);
|
||||
#[cfg(test)]
|
||||
{
|
||||
let control_plane = open_control_plane_store();
|
||||
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
|
||||
control_plane.as_ref(),
|
||||
root,
|
||||
root_uri,
|
||||
workspace_id,
|
||||
);
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let control_plane = LOCAL_SEARCH_INDEX_CONTROL_PLANE.get_or_init(open_control_plane_store);
|
||||
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
|
||||
control_plane.as_ref(),
|
||||
root,
|
||||
root_uri,
|
||||
workspace_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn local_markdown_conflict_error(
|
||||
@@ -4605,13 +4623,20 @@ fn add_local_file_operation_contract(root_uri: &str, action: &str, mut result: V
|
||||
let previous_relative_path = local_tree_previous_relative_path(&result);
|
||||
let document_id = local_tree_result_string(&result, "documentId")
|
||||
.or_else(|| local_tree_result_string(&result, "id"));
|
||||
let resource_kind = local_tree_result_string(&result, "resourceKind");
|
||||
let previous_document_id =
|
||||
local_tree_result_string(&result, "previousDocumentId").or_else(|| {
|
||||
previous_relative_path
|
||||
.as_deref()
|
||||
.map(local_markdown_path_page_id)
|
||||
if matches!(
|
||||
resource_kind.as_deref(),
|
||||
Some("markdown") | Some("markdown_bundle")
|
||||
) {
|
||||
document_id.clone()
|
||||
} else {
|
||||
previous_relative_path
|
||||
.as_deref()
|
||||
.map(local_markdown_path_page_id)
|
||||
}
|
||||
});
|
||||
let resource_kind = local_tree_result_string(&result, "resourceKind");
|
||||
let affected_parents = add_local_tree_command_affected_parents(&result, &resolved_action);
|
||||
let resource = relative_path.as_deref().and_then(|path| {
|
||||
local_file_operation_resource(path, document_id.clone(), resource_kind.as_deref())
|
||||
@@ -5529,6 +5554,15 @@ fn move_local_directory(
|
||||
}
|
||||
|
||||
fn trash_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
|
||||
if let Some(idempotent) = idempotent_deleted_local_directory(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
if let Some(idempotent) = idempotent_deleted_local_markdown_bundle_directory(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
if let Some(idempotent) = idempotent_deleted_local_raw_file(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
if let Some(directory) = resolve_local_directory_id(root, entry_id)? {
|
||||
// 若目录是页面 bundle(包含同名 .md),按 Markdown 页面生命周期入 trash
|
||||
if let Some(main_md) = nested_bundle_main_markdown(&directory) {
|
||||
@@ -5552,6 +5586,12 @@ fn restore_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
|
||||
if find_local_file_trash_entry_key(&metadata, entry_id).is_some() {
|
||||
return restore_local_raw_file(root, entry_id);
|
||||
}
|
||||
if let Some(idempotent) = idempotent_restored_local_directory(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
if let Some(idempotent) = idempotent_restored_local_raw_file(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
restore_local_markdown_page(root, entry_id)
|
||||
}
|
||||
|
||||
@@ -5563,6 +5603,12 @@ fn purge_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
|
||||
if find_local_file_trash_entry_key(&metadata, entry_id).is_some() {
|
||||
return purge_local_raw_file(root, entry_id);
|
||||
}
|
||||
if let Some(idempotent) = idempotent_purged_local_directory(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
if let Some(idempotent) = idempotent_purged_local_raw_file(root, entry_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
if entry_id.starts_with("local:asset:") || entry_id.starts_with("local:node:") {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_trash_entry_not_found",
|
||||
@@ -5574,13 +5620,20 @@ fn purge_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
|
||||
|
||||
fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
let markdown_file =
|
||||
find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
let markdown_file = match find_markdown_by_page_id(root, &metadata, document_id)? {
|
||||
Some(markdown_file) => markdown_file,
|
||||
None => {
|
||||
if let Some(idempotent) =
|
||||
idempotent_deleted_local_markdown_page(root, &metadata, document_id)?
|
||||
{
|
||||
return Ok(idempotent);
|
||||
}
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_markdown_not_found",
|
||||
"找不到要删除的本地 Markdown 页面",
|
||||
)
|
||||
})?;
|
||||
));
|
||||
}
|
||||
};
|
||||
let trash_dir = root.join(".mnote").join("trash");
|
||||
fs::create_dir_all(&trash_dir).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -5660,6 +5713,95 @@ fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
}))
|
||||
}
|
||||
|
||||
fn idempotent_deleted_local_markdown_page(
|
||||
root: &Path,
|
||||
metadata: &LocalFolderMetadata,
|
||||
document_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(trash_entry) = metadata.trash_entries.get(document_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if trash_entry.resource_kind != "markdown" && trash_entry.resource_kind != "markdown_bundle" {
|
||||
return Ok(None);
|
||||
}
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
|
||||
let trash_exists = if trash_entry.resource_kind == "markdown_bundle" {
|
||||
trash_path.is_dir()
|
||||
} else {
|
||||
trash_path.is_file()
|
||||
};
|
||||
if !trash_exists {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": document_id,
|
||||
"documentId": document_id,
|
||||
"resourceKind": trash_entry.resource_kind,
|
||||
"originalRelativePath": trash_entry.original_relative_path,
|
||||
"trashPath": trash_entry.trash_relative_path,
|
||||
"action": "delete",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn idempotent_deleted_local_markdown_bundle_directory(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_directory_relative_path_from_entry_id(entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let metadata = load_local_folder_metadata(root)?;
|
||||
let Some((_, trash_entry)) = metadata.trash_entries.iter().find(|(_, entry)| {
|
||||
entry.resource_kind == "markdown_bundle" && entry.original_relative_path == relative_path
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
|
||||
if !trash_path.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": trash_entry.document_id,
|
||||
"resourceKind": "markdown_bundle",
|
||||
"resourceScope": "local_folder",
|
||||
"originalRelativePath": trash_entry.original_relative_path,
|
||||
"originalFilePath": local_trash_original_path(trash_entry),
|
||||
"trashEntryId": trash_entry.trash_entry_id,
|
||||
"trashPath": trash_entry.trash_relative_path,
|
||||
"action": "delete",
|
||||
"canonicalCommand": "tree.resource.archive",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn idempotent_purged_local_markdown_page(
|
||||
root: &Path,
|
||||
document_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_markdown_relative_path_from_document_id(document_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let markdown_path = resolve_metadata_relative_path(root, &relative_path)?;
|
||||
if markdown_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": document_id,
|
||||
"documentId": document_id,
|
||||
"originalRelativePath": relative_path,
|
||||
"action": "purge",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
let relative_path = normalize_relative_path(root, file)?;
|
||||
@@ -5725,6 +5867,39 @@ fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Valu
|
||||
}))
|
||||
}
|
||||
|
||||
fn idempotent_deleted_local_raw_file(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let metadata = load_local_folder_metadata(root)?;
|
||||
let Some(trash_key) = find_local_file_trash_entry_key(&metadata, entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(trash_entry) = metadata.trash_entries.get(&trash_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let trash_relative_path = local_trash_file_path(trash_entry);
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
|
||||
if !trash_path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": entry_id,
|
||||
"resourceKind": "local_file",
|
||||
"resourceScope": "local_folder",
|
||||
"originalRelativePath": trash_entry.original_relative_path,
|
||||
"originalFilePath": local_trash_original_path(trash_entry),
|
||||
"trashEntryId": trash_key,
|
||||
"trashPath": trash_relative_path,
|
||||
"action": "delete",
|
||||
"canonicalCommand": "tree.resource.archive",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Result<Value, WebError> {
|
||||
if directory == root {
|
||||
return Err(WebError::bad_request_code(
|
||||
@@ -5801,16 +5976,18 @@ fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Resul
|
||||
|
||||
fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
let trash_entry = metadata
|
||||
.trash_entries
|
||||
.get(document_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
let trash_entry = match metadata.trash_entries.get(document_id).cloned() {
|
||||
Some(trash_entry) => trash_entry,
|
||||
None => {
|
||||
if let Some(idempotent) = idempotent_restored_local_markdown_page(root, document_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_trash_entry_not_found",
|
||||
"找不到要恢复的本地回收站记录",
|
||||
)
|
||||
})?;
|
||||
));
|
||||
}
|
||||
};
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
|
||||
if trash_entry.resource_kind == "markdown_bundle" {
|
||||
return restore_local_markdown_bundle(
|
||||
@@ -5868,6 +6045,33 @@ fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result<Value,
|
||||
}))
|
||||
}
|
||||
|
||||
fn idempotent_restored_local_markdown_page(
|
||||
root: &Path,
|
||||
document_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_markdown_relative_path_from_document_id(document_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let markdown_path = resolve_metadata_relative_path(root, &relative_path)?;
|
||||
if !markdown_path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
let restored_document_id = local_markdown_path_page_id(&relative_path);
|
||||
if restored_document_id != document_id {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": restored_document_id,
|
||||
"documentId": restored_document_id,
|
||||
"relativePath": relative_path,
|
||||
"previousDocumentId": document_id,
|
||||
"action": "restore",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn restore_local_markdown_bundle(
|
||||
root: &Path,
|
||||
document_id: &str,
|
||||
@@ -6004,6 +6208,34 @@ fn restore_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError
|
||||
}))
|
||||
}
|
||||
|
||||
fn idempotent_restored_local_raw_file(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_file_relative_path_from_entry_id(entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
|
||||
if !original_path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
let trash_entry_id = local_file_trash_entry_id(&relative_path);
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": entry_id,
|
||||
"resourceKind": "local_file",
|
||||
"resourceScope": "local_folder",
|
||||
"originalFilePath": relative_path,
|
||||
"relativePath": normalize_relative_path(root, &original_path)?,
|
||||
"trashEntryId": trash_entry_id,
|
||||
"action": "restore",
|
||||
"canonicalCommand": "tree.resource.restore",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn restore_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
let trash_key = find_local_directory_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
|
||||
@@ -6073,6 +6305,116 @@ fn restore_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebErro
|
||||
}))
|
||||
}
|
||||
|
||||
fn idempotent_deleted_local_directory(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let metadata = load_local_folder_metadata(root)?;
|
||||
let Some(trash_key) = find_local_directory_trash_entry_key(&metadata, entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(trash_entry) = metadata.trash_entries.get(&trash_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let trash_relative_path = local_trash_file_path(trash_entry);
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
|
||||
if !trash_path.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": entry_id,
|
||||
"resourceKind": "local_directory",
|
||||
"resourceScope": "local_folder",
|
||||
"originalRelativePath": trash_entry.original_relative_path,
|
||||
"originalFilePath": local_trash_original_path(trash_entry),
|
||||
"trashEntryId": trash_key,
|
||||
"trashPath": trash_relative_path,
|
||||
"action": "delete",
|
||||
"canonicalCommand": "tree.resource.archive",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn local_directory_relative_path_from_entry_id(entry_id: &str) -> Option<String> {
|
||||
let trimmed = entry_id.trim();
|
||||
if let Some(relative_path) = trimmed.strip_prefix("local-dir-trash:") {
|
||||
return Some(relative_path.to_string());
|
||||
}
|
||||
if let Some(encoded) = trimmed.strip_prefix("local-dir:") {
|
||||
return decode_local_id_segment(encoded).ok();
|
||||
}
|
||||
trimmed
|
||||
.strip_prefix("local:node:")
|
||||
.or_else(|| trimmed.strip_prefix("local:folder:"))
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn local_file_relative_path_from_entry_id(entry_id: &str) -> Option<String> {
|
||||
let trimmed = entry_id.trim();
|
||||
trimmed
|
||||
.strip_prefix("local-file:")
|
||||
.or_else(|| trimmed.strip_prefix("local:asset:"))
|
||||
.or_else(|| trimmed.strip_prefix("local:node:"))
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn idempotent_restored_local_directory(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_directory_relative_path_from_entry_id(entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
|
||||
if !original_path.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
let trash_entry_id = local_directory_trash_entry_id(&relative_path);
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": entry_id,
|
||||
"resourceKind": "local_directory",
|
||||
"resourceScope": "local_folder",
|
||||
"relativePath": relative_path,
|
||||
"trashEntryId": trash_entry_id,
|
||||
"action": "restore",
|
||||
"canonicalCommand": "tree.resource.restore",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn idempotent_purged_local_directory(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_directory_relative_path_from_entry_id(entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
|
||||
if original_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let trash_entry_id = local_directory_trash_entry_id(&relative_path);
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": entry_id,
|
||||
"resourceKind": "local_directory",
|
||||
"resourceScope": "local_folder",
|
||||
"originalRelativePath": relative_path,
|
||||
"trashEntryId": trash_entry_id,
|
||||
"action": "purge",
|
||||
"canonicalCommand": "tree.resource.purge",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
if let Some(markdown_file) = find_markdown_by_page_id(root, &metadata, document_id)? {
|
||||
@@ -6108,12 +6450,18 @@ fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
"sourceKind": "local_folder",
|
||||
}));
|
||||
}
|
||||
let trash_entry = metadata.trash_entries.remove(document_id).ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
"local_markdown_not_found",
|
||||
"找不到要永久删除的本地 Markdown 页面",
|
||||
)
|
||||
})?;
|
||||
let trash_entry = match metadata.trash_entries.remove(document_id) {
|
||||
Some(trash_entry) => trash_entry,
|
||||
None => {
|
||||
if let Some(idempotent) = idempotent_purged_local_markdown_page(root, document_id)? {
|
||||
return Ok(idempotent);
|
||||
}
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_markdown_not_found",
|
||||
"找不到要永久删除的本地 Markdown 页面",
|
||||
));
|
||||
}
|
||||
};
|
||||
remove_file_order_path(
|
||||
&mut metadata.file_order,
|
||||
&trash_entry.original_relative_path,
|
||||
@@ -6194,6 +6542,33 @@ fn purge_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError>
|
||||
}))
|
||||
}
|
||||
|
||||
fn idempotent_purged_local_raw_file(
|
||||
root: &Path,
|
||||
entry_id: &str,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
let Some(relative_path) = local_file_relative_path_from_entry_id(entry_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
|
||||
if original_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let trash_entry_id = local_file_trash_entry_id(&relative_path);
|
||||
Ok(Some(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
"documentId": entry_id,
|
||||
"resourceKind": "local_file",
|
||||
"resourceScope": "local_folder",
|
||||
"originalRelativePath": relative_path,
|
||||
"trashEntryId": trash_entry_id,
|
||||
"action": "purge",
|
||||
"canonicalCommand": "tree.resource.purge",
|
||||
"sourceKind": "local_folder",
|
||||
"idempotent": true,
|
||||
})))
|
||||
}
|
||||
|
||||
fn purge_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
let trash_key = find_local_file_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
|
||||
@@ -12223,6 +12598,50 @@ fn main() {}
|
||||
Some("local-dir-trash:Folder")
|
||||
);
|
||||
|
||||
let repeated_delete =
|
||||
execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
|
||||
.expect("repeated folder delete should be idempotent");
|
||||
assert_eq!(repeated_delete["idempotent"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
repeated_delete["trashEntryId"].as_str(),
|
||||
Some("local-dir-trash:Folder")
|
||||
);
|
||||
|
||||
let restored = execute_local_tree_command(&root_uri, "restore", &folder_id, None, None)
|
||||
.expect("restore folder");
|
||||
assert_eq!(restored["resourceKind"].as_str(), Some("local_directory"));
|
||||
assert!(root
|
||||
.join("Folder")
|
||||
.join("Nested")
|
||||
.join("note.txt")
|
||||
.is_file());
|
||||
|
||||
let repeated_restore =
|
||||
execute_local_tree_command(&root_uri, "restore", "local-dir-trash:Folder", None, None)
|
||||
.expect("repeated folder restore should be idempotent");
|
||||
assert_eq!(repeated_restore["idempotent"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
repeated_restore["resourceKind"].as_str(),
|
||||
Some("local_directory")
|
||||
);
|
||||
assert!(root
|
||||
.join("Folder")
|
||||
.join("Nested")
|
||||
.join("note.txt")
|
||||
.is_file());
|
||||
|
||||
execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
|
||||
.expect("re-delete folder");
|
||||
let purged =
|
||||
execute_local_tree_command(&root_uri, "purge", "local-dir-trash:Folder", None, None)
|
||||
.expect("purge folder");
|
||||
assert_eq!(purged["resourceKind"].as_str(), Some("local_directory"));
|
||||
assert!(!root.join(".mnote").join("trash").join("Folder").exists());
|
||||
let repeated_purge =
|
||||
execute_local_tree_command(&root_uri, "purge", "local-dir-trash:Folder", None, None)
|
||||
.expect("repeated folder purge should be idempotent");
|
||||
assert_eq!(repeated_purge["idempotent"].as_bool(), Some(true));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -12256,6 +12675,45 @@ fn main() {}
|
||||
assert!(root.join(trash_path).join("Page.md").is_file());
|
||||
assert!(root.join(trash_path).join("asset.txt").is_file());
|
||||
|
||||
let repeated_delete =
|
||||
execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
|
||||
.expect("repeated folder-row delete should be idempotent");
|
||||
assert_eq!(repeated_delete["idempotent"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
repeated_delete["resourceKind"].as_str(),
|
||||
Some("markdown_bundle")
|
||||
);
|
||||
assert!(
|
||||
root.join(trash_path).join("Page.md").is_file(),
|
||||
"重复删除不应破坏回收站中的页面 bundle"
|
||||
);
|
||||
|
||||
let restored = execute_local_tree_command(&root_uri, "restore", &page_id, None, None)
|
||||
.expect("restore page bundle");
|
||||
assert_eq!(restored["documentId"].as_str(), Some(page_id.as_str()));
|
||||
assert!(root.join("Page").join("Page.md").is_file());
|
||||
assert!(root.join("Page").join("asset.txt").is_file());
|
||||
|
||||
let repeated_restore =
|
||||
execute_local_tree_command(&root_uri, "restore", &page_id, None, None)
|
||||
.expect("repeated restore should be idempotent");
|
||||
assert_eq!(repeated_restore["idempotent"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
repeated_restore["documentId"].as_str(),
|
||||
Some(page_id.as_str())
|
||||
);
|
||||
assert!(root.join("Page").join("Page.md").is_file());
|
||||
|
||||
execute_local_tree_command(&root_uri, "delete", &page_id, None, None)
|
||||
.expect("re-delete page bundle");
|
||||
let purged = execute_local_tree_command(&root_uri, "purge", &page_id, None, None)
|
||||
.expect("purge page bundle");
|
||||
assert_eq!(purged["documentId"].as_str(), Some(page_id.as_str()));
|
||||
assert!(!root.join(".mnote").join("trash").join("Page").exists());
|
||||
let repeated_purge = execute_local_tree_command(&root_uri, "purge", &page_id, None, None)
|
||||
.expect("repeated page purge should be idempotent");
|
||||
assert_eq!(repeated_purge["idempotent"].as_bool(), Some(true));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -12287,6 +12745,37 @@ fn main() {}
|
||||
.expect("trash index");
|
||||
assert!(trash_index.contains("local-file:docs/photo.png"));
|
||||
|
||||
let repeated_delete = execute_local_tree_command(
|
||||
&root_uri,
|
||||
"delete",
|
||||
"local-file:docs/photo.png",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("repeated local-file delete should be idempotent");
|
||||
assert_eq!(repeated_delete["idempotent"].as_bool(), Some(true));
|
||||
|
||||
let restored = execute_local_tree_command(
|
||||
&root_uri,
|
||||
"restore",
|
||||
"local-file:docs/photo.png",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("restore local-file asset");
|
||||
assert_eq!(restored["resourceKind"].as_str(), Some("local_file"));
|
||||
assert!(root.join("docs").join("photo.png").is_file());
|
||||
|
||||
let repeated_restore = execute_local_tree_command(
|
||||
&root_uri,
|
||||
"restore",
|
||||
"local-file:docs/photo.png",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("repeated local-file restore should be idempotent");
|
||||
assert_eq!(repeated_restore["idempotent"].as_bool(), Some(true));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -12325,6 +12814,9 @@ fn main() {}
|
||||
.expect("purge chinese-named asset");
|
||||
assert_eq!(purged["ok"].as_bool(), Some(true));
|
||||
assert!(!root.join(".mnote").join("trash").join("资源.ext").exists());
|
||||
let repeated_purge = execute_local_tree_command(&root_uri, "purge", asset_id, None, None)
|
||||
.expect("repeated asset purge should be idempotent");
|
||||
assert_eq!(repeated_purge["idempotent"].as_bool(), Some(true));
|
||||
let trash_index_after_purge =
|
||||
std::fs::read_to_string(root.join(".mnote").join("trash-index.json"))
|
||||
.expect("trash index after purge");
|
||||
|
||||
Reference in New Issue
Block a user