feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -10,9 +10,9 @@ use crate::routes::local_markdown_parser::{
|
||||
};
|
||||
use crate::routes::local_search_index;
|
||||
use crate::routes::snapshot_support::ProjectionSnapshot;
|
||||
use axum::extract::{Extension, Multipart, Path as AxumPath, Query, State};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Multipart, Path as AxumPath, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use bridge_runtime::project_legacy_content_to_block_document;
|
||||
use control_plane::AppendAuditInput;
|
||||
use control_plane::{
|
||||
@@ -24,7 +24,7 @@ use core_protocol::{
|
||||
};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Map, Value};
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
@@ -41,6 +41,9 @@ static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS: std::sync::atomic::AtomicU64 =
|
||||
#[cfg(test)]
|
||||
static LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
#[cfg(test)]
|
||||
static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT: OnceLock<Mutex<BTreeMap<String, u64>>> =
|
||||
OnceLock::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalPageTreeSnapshotCacheEntry {
|
||||
@@ -52,8 +55,8 @@ static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
|
||||
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn local_page_tree_snapshot_cache(
|
||||
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
|
||||
fn local_page_tree_snapshot_cache()
|
||||
-> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
}
|
||||
|
||||
@@ -61,6 +64,11 @@ fn local_page_tree_snapshot_cache(
|
||||
pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||
if let Some(loads) = LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT.get() {
|
||||
if let Ok(mut loads) = loads.lock() {
|
||||
loads.clear();
|
||||
}
|
||||
}
|
||||
if let Some(cache) = LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get() {
|
||||
if let Ok(mut cache) = cache.lock() {
|
||||
cache.clear();
|
||||
@@ -69,8 +77,13 @@ pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn local_page_tree_snapshot_test_loads() -> u64 {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
|
||||
pub(crate) fn local_page_tree_snapshot_test_loads_for_root(root_uri: &str) -> u64 {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT
|
||||
.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|loads| loads.get(root_uri).copied())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2784,6 +2797,15 @@ fn load_local_folder_page_tree_snapshot_for_scope(
|
||||
) -> Result<ProjectionSnapshot, WebError> {
|
||||
#[cfg(test)]
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Ok(mut loads) = LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT
|
||||
.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
.lock()
|
||||
{
|
||||
*loads.entry(root_uri.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let root_path = parse_file_root_uri(root_uri)?;
|
||||
let canonical_root = root_path.canonicalize().map_err(|error| {
|
||||
@@ -3255,10 +3277,12 @@ pub fn write_local_markdown_page_body(
|
||||
relative_path,
|
||||
&request.document_id,
|
||||
);
|
||||
store.mark_saved(
|
||||
store.mark_saved_with_operation(
|
||||
&ws_path,
|
||||
file_version.to_string(),
|
||||
format!("sha256:{file_version}"),
|
||||
request.write_intent_id.clone(),
|
||||
request.save_operation_id.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3283,6 +3307,22 @@ pub fn write_local_markdown_page_body(
|
||||
.map(Value::from)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
map.insert(
|
||||
"writeIntentId".into(),
|
||||
request
|
||||
.write_intent_id
|
||||
.as_deref()
|
||||
.map(Value::from)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
map.insert(
|
||||
"saveOperationId".into(),
|
||||
request
|
||||
.save_operation_id
|
||||
.as_deref()
|
||||
.map(Value::from)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -3818,7 +3858,7 @@ pub(crate) fn write_local_folder_file_upload(
|
||||
"上传目标必须是本地 root 内的目录",
|
||||
));
|
||||
}
|
||||
let sanitized_name = sanitize_file_name(&file.name, "附件");
|
||||
let sanitized_name = sanitize_uploaded_file_name(&file.name, "附件");
|
||||
let target = next_available_raw_path(&target_dir, &sanitized_name);
|
||||
fs::write(&target, &file.bytes).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -3908,7 +3948,7 @@ pub(crate) fn write_local_markdown_asset(
|
||||
)
|
||||
})?;
|
||||
|
||||
let sanitized_name = sanitize_file_name(&file.name, "附件");
|
||||
let sanitized_name = sanitize_uploaded_file_name(&file.name, "附件");
|
||||
let target = next_available_asset_path(&asset_dir, &sanitized_name);
|
||||
if !target.starts_with(&canonical_root) {
|
||||
return Err(WebError::bad_request_code(
|
||||
@@ -7023,11 +7063,7 @@ fn parent_key_for_relative_path(relative_path: &str) -> String {
|
||||
.map(|component| component.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
})
|
||||
.map(|value| normalize_file_order_parent_key(&value))
|
||||
.unwrap_or_else(|| ".".to_string())
|
||||
@@ -7517,11 +7553,16 @@ fn local_folder_row_to_projection_item(row: &LocalFolderRow) -> Value {
|
||||
}
|
||||
"asset" => match row.icon_hint.as_str() {
|
||||
"mindmap" => KernelObjectKind::Mindmap,
|
||||
"onlyoffice" | "office" => KernelObjectKind::OnlyOffice,
|
||||
"onlyoffice" | "office" | "word" | "ppt" | "sheet" => KernelObjectKind::OnlyOffice,
|
||||
_ => KernelObjectKind::Attachment,
|
||||
},
|
||||
_ => KernelObjectKind::Attachment,
|
||||
};
|
||||
let object_asset_id = if row.row_kind == "asset" && !row.relative_path.trim().is_empty() {
|
||||
Some(format!("local-file:{}", row.relative_path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let workspace_path = ObjectWorkspacePath {
|
||||
workspace_id: row.workspace_id.clone(),
|
||||
source_kind: WorkspaceSourceKind::LocalFolder,
|
||||
@@ -7531,7 +7572,7 @@ fn local_folder_row_to_projection_item(row: &LocalFolderRow) -> Value {
|
||||
object_kind,
|
||||
document_id: row.document_id.clone(),
|
||||
block_id: None,
|
||||
asset_id: None,
|
||||
asset_id: object_asset_id,
|
||||
},
|
||||
resource_kind: Some(row.row_kind.clone()),
|
||||
};
|
||||
@@ -7733,21 +7774,22 @@ fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String {
|
||||
"pdf"
|
||||
} else if matches!(
|
||||
extension(&lower).as_deref(),
|
||||
Some(
|
||||
"doc"
|
||||
| "docx"
|
||||
| "odt"
|
||||
| "rtf"
|
||||
| "ppt"
|
||||
| "pptx"
|
||||
| "odp"
|
||||
| "xls"
|
||||
| "xlsx"
|
||||
| "ods"
|
||||
| "csv"
|
||||
)
|
||||
Some("doc" | "docx" | "odt" | "rtf")
|
||||
) {
|
||||
"office"
|
||||
"word"
|
||||
} else if matches!(extension(&lower).as_deref(), Some("ppt" | "pptx" | "odp")) {
|
||||
"ppt"
|
||||
} else if matches!(
|
||||
extension(&lower).as_deref(),
|
||||
Some("xls" | "xlsx" | "ods" | "csv")
|
||||
) {
|
||||
"sheet"
|
||||
} else if is_web_file_name(&lower) {
|
||||
"web"
|
||||
} else if is_config_file_name(&lower) {
|
||||
"config"
|
||||
} else if is_code_file_name(&lower) {
|
||||
"code"
|
||||
} else if matches!(extension(&lower).as_deref(), Some("epub" | "mobi")) {
|
||||
"book"
|
||||
} else {
|
||||
@@ -7756,6 +7798,135 @@ fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn is_web_file_name(file_name: &str) -> bool {
|
||||
matches!(
|
||||
extension(file_name).as_deref(),
|
||||
Some(
|
||||
"html"
|
||||
| "htm"
|
||||
| "css"
|
||||
| "scss"
|
||||
| "less"
|
||||
| "js"
|
||||
| "jsx"
|
||||
| "ts"
|
||||
| "tsx"
|
||||
| "mjs"
|
||||
| "cjs"
|
||||
| "vue"
|
||||
| "svelte"
|
||||
| "astro"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_config_file_name(file_name: &str) -> bool {
|
||||
let code_file_names = [
|
||||
".dockerignore",
|
||||
".editorconfig",
|
||||
".env",
|
||||
".eslintrc",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
".npmrc",
|
||||
".prettierrc",
|
||||
"dockerfile",
|
||||
"containerfile",
|
||||
"makefile",
|
||||
"cmakelists.txt",
|
||||
"gemfile",
|
||||
"rakefile",
|
||||
"procfile",
|
||||
];
|
||||
code_file_names.contains(&file_name)
|
||||
|| matches!(
|
||||
extension(file_name).as_deref(),
|
||||
Some(
|
||||
"json"
|
||||
| "jsonc"
|
||||
| "json5"
|
||||
| "toml"
|
||||
| "yaml"
|
||||
| "yml"
|
||||
| "ini"
|
||||
| "env"
|
||||
| "xml"
|
||||
| "lock"
|
||||
| "hcl"
|
||||
| "tf"
|
||||
| "tfvars"
|
||||
| "nix"
|
||||
| "properties"
|
||||
| "conf"
|
||||
| "cfg"
|
||||
| "config"
|
||||
| "service"
|
||||
| "desktop"
|
||||
| "gitignore"
|
||||
| "gitattributes"
|
||||
| "editorconfig"
|
||||
| "npmrc"
|
||||
| "prettierrc"
|
||||
| "eslintrc"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_code_file_name(file_name: &str) -> bool {
|
||||
matches!(
|
||||
extension(file_name).as_deref(),
|
||||
Some(
|
||||
"rs" | "py"
|
||||
| "go"
|
||||
| "java"
|
||||
| "c"
|
||||
| "cpp"
|
||||
| "h"
|
||||
| "hpp"
|
||||
| "cs"
|
||||
| "php"
|
||||
| "rb"
|
||||
| "sh"
|
||||
| "bash"
|
||||
| "zsh"
|
||||
| "sql"
|
||||
| "lua"
|
||||
| "dart"
|
||||
| "kt"
|
||||
| "kts"
|
||||
| "swift"
|
||||
| "scala"
|
||||
| "gradle"
|
||||
| "groovy"
|
||||
| "clj"
|
||||
| "ex"
|
||||
| "exs"
|
||||
| "erl"
|
||||
| "hrl"
|
||||
| "fs"
|
||||
| "fsx"
|
||||
| "r"
|
||||
| "jl"
|
||||
| "m"
|
||||
| "mm"
|
||||
| "pl"
|
||||
| "pm"
|
||||
| "ps1"
|
||||
| "bat"
|
||||
| "cmd"
|
||||
| "psm1"
|
||||
| "psd1"
|
||||
| "proto"
|
||||
| "graphql"
|
||||
| "gql"
|
||||
| "prisma"
|
||||
| "cmake"
|
||||
| "bazel"
|
||||
| "bzl"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn extension(file_name: &str) -> Option<String> {
|
||||
Path::new(file_name)
|
||||
.extension()
|
||||
@@ -7850,6 +8021,15 @@ fn sanitize_file_name(value: &str, fallback: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_uploaded_file_name(value: &str, fallback: &str) -> String {
|
||||
let trimmed = value.trim();
|
||||
let leaf = trimmed
|
||||
.rsplit(['/', '\\'])
|
||||
.find(|part| !part.trim().is_empty())
|
||||
.unwrap_or(trimmed);
|
||||
sanitize_file_name(leaf, fallback)
|
||||
}
|
||||
|
||||
fn next_available_path(directory: &Path, stem: &str, extension: &str) -> PathBuf {
|
||||
let first = directory.join(format!("{stem}.{extension}"));
|
||||
if !first.exists() {
|
||||
@@ -8919,7 +9099,7 @@ fn system_time_ms(time: SystemTime) -> u128 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn local_markdown_conflict_detection_key(
|
||||
pub(crate) fn local_markdown_conflict_detection_key(
|
||||
document_id: &str,
|
||||
markdown_path: &Path,
|
||||
) -> Result<String, WebError> {
|
||||
@@ -9044,6 +9224,10 @@ fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Val
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
LocalAccessGrantRequest, LocalAccessValidateRootRequest, LocalFileOpenQuery,
|
||||
LocalResourceReadQuery, LocalResourceWriteRequest, LocalShareGrantRequest,
|
||||
LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery, SharedCacheRecordRequest,
|
||||
SyncConflictReportRequest, SyncPendingChangeRequest,
|
||||
add_sqlite_local_access_grant_for_context,
|
||||
create_default_local_workspace_for_actor_at_base, create_local_access_grant,
|
||||
create_share_grant, create_share_link, create_user_access_grant, create_user_share_grant,
|
||||
@@ -9062,18 +9246,15 @@ mod tests {
|
||||
resolve_local_markdown_page_aggregate, save_local_markdown_page,
|
||||
update_local_markdown_title, validate_local_access_root, write_local_markdown_asset,
|
||||
write_local_markdown_page_body, write_local_mindmap_data, write_local_resource,
|
||||
write_sync_conflict_report, LocalAccessGrantRequest, LocalAccessValidateRootRequest,
|
||||
LocalFileOpenQuery, LocalResourceReadQuery, LocalResourceWriteRequest,
|
||||
LocalShareGrantRequest, LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery,
|
||||
SharedCacheRecordRequest, SyncConflictReportRequest, SyncPendingChangeRequest,
|
||||
write_sync_conflict_report,
|
||||
};
|
||||
use crate::app::{AppConfig, AppState};
|
||||
use crate::context::RequestContext;
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Path as AxumPath, Query, State};
|
||||
use axum::http::{HeaderMap, Method, StatusCode};
|
||||
use axum::Json;
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
@@ -9345,28 +9526,25 @@ mod tests {
|
||||
"tableCell"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]
|
||||
["text"],
|
||||
table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]["text"],
|
||||
"左"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
|
||||
["text"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]["text"],
|
||||
"A"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
|
||||
["marks"][0]["type"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]["marks"]
|
||||
[0]["type"],
|
||||
"code"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
|
||||
["text"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]["text"],
|
||||
"B"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
|
||||
["marks"][0]["type"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]["marks"]
|
||||
[0]["type"],
|
||||
"bold"
|
||||
);
|
||||
}
|
||||
@@ -9469,10 +9647,12 @@ mod tests {
|
||||
let body = serde_json::to_value(&aggregate.body).expect("body json");
|
||||
|
||||
assert_eq!(body["fileVersion"], body["conflictDetectionKey"]);
|
||||
assert!(body["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:"));
|
||||
assert!(
|
||||
body["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:")
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -9494,10 +9674,12 @@ mod tests {
|
||||
.expect("save");
|
||||
|
||||
assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
|
||||
assert!(result["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:"));
|
||||
assert!(
|
||||
result["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:")
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -9516,6 +9698,8 @@ mod tests {
|
||||
source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
|
||||
root_uri: root_uri.clone(),
|
||||
expected_file_version: aggregate.body.file_version.as_str().map(ToOwned::to_owned),
|
||||
write_intent_id: Some("intent:test:page-body-write".into()),
|
||||
save_operation_id: Some("save:test:page-body-write".into()),
|
||||
base_content_hash: Some("sha256:test-base".into()),
|
||||
content_format: "editorBlocks".into(),
|
||||
content: serde_json::json!([
|
||||
@@ -9530,6 +9714,8 @@ mod tests {
|
||||
assert_eq!(result["compatCommand"], "page.body.save");
|
||||
assert_eq!(result["contentFormat"], "editorBlocks");
|
||||
assert_eq!(result["editorSource"], "unit-test");
|
||||
assert_eq!(result["writeIntentId"], "intent:test:page-body-write");
|
||||
assert_eq!(result["saveOperationId"], "save:test:page-body-write");
|
||||
assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
|
||||
let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
|
||||
assert!(saved.contains("# Written"));
|
||||
@@ -10614,10 +10800,12 @@ fn main() {}
|
||||
assert_eq!(created["grant"]["ownerUserId"], "user_owner");
|
||||
assert_eq!(created["grant"]["targetUserId"], "user_target");
|
||||
assert_eq!(created["grant"]["permission"], "write");
|
||||
assert!(created["grant"]["shareId"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.starts_with("folder_"));
|
||||
assert!(
|
||||
created["grant"]["shareId"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.starts_with("folder_")
|
||||
);
|
||||
|
||||
let outside_error = create_user_share_grant(
|
||||
Extension(request_context("user_owner", "user")),
|
||||
@@ -10887,11 +11075,13 @@ fn main() {}
|
||||
assert_eq!(stored.len(), 1);
|
||||
assert_eq!(stored[0].id, link_id);
|
||||
assert_ne!(stored[0].token_hash, "visible-token");
|
||||
assert!(state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve share link")
|
||||
.is_some());
|
||||
assert!(
|
||||
state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve share link")
|
||||
.is_some()
|
||||
);
|
||||
|
||||
let (_, Json(listed)) = get_share_links(
|
||||
State(state.clone()),
|
||||
@@ -10924,11 +11114,13 @@ fn main() {}
|
||||
.expect("share revoked broadcast delta");
|
||||
assert_eq!(revoked_delta["kind"], "control_plane_event");
|
||||
assert_eq!(revoked_delta["eventType"], "control.share.revoked");
|
||||
assert!(state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve revoked share link")
|
||||
.is_none());
|
||||
assert!(
|
||||
state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve revoked share link")
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.control_plane()
|
||||
@@ -11132,11 +11324,13 @@ fn main() {}
|
||||
payload["workspace"]["manifest"]["ownerId"],
|
||||
"user@example.com"
|
||||
);
|
||||
assert!(payload["workspace"]["manifest"]["capabilities"]
|
||||
.as_array()
|
||||
.expect("capabilities")
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some("markdown_edit")));
|
||||
assert!(
|
||||
payload["workspace"]["manifest"]["capabilities"]
|
||||
.as_array()
|
||||
.expect("capabilities")
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some("markdown_edit"))
|
||||
);
|
||||
ensure_local_workspace_access_for_actor("user@example.com", "user", &root_uri)
|
||||
.expect("owner can access managed workspace");
|
||||
|
||||
@@ -11387,11 +11581,12 @@ fn main() {}
|
||||
execute_local_tree_command(&root_uri, "delete", "local-md:Renamed.md", None, None)
|
||||
.expect("delete loose markdown");
|
||||
assert!(!root.join("Renamed.md").exists());
|
||||
assert!(root
|
||||
.join(".mnote")
|
||||
.join("trash")
|
||||
.join("Renamed.md")
|
||||
.is_file());
|
||||
assert!(
|
||||
root.join(".mnote")
|
||||
.join("trash")
|
||||
.join("Renamed.md")
|
||||
.is_file()
|
||||
);
|
||||
assert_eq!(deleted["resourceKind"].as_str(), Some("markdown"));
|
||||
|
||||
let restored =
|
||||
@@ -12441,6 +12636,17 @@ fn main() {}
|
||||
std::fs::write(root.join("Page").join("Page.md"), "").expect("write md");
|
||||
std::fs::write(root.join("Page").join("思维导图123456.json"), "{}").expect("write mindmap");
|
||||
std::fs::write(root.join("Page").join("report.docx"), b"docx").expect("write docx");
|
||||
std::fs::write(root.join("Page").join("sheet.xlsx"), b"xlsx").expect("write xlsx");
|
||||
std::fs::write(root.join("Page").join("slides.pptx"), b"pptx").expect("write pptx");
|
||||
std::fs::write(root.join("Page").join("main.rs"), b"fn main() {}").expect("write rs");
|
||||
std::fs::write(
|
||||
root.join("Page").join("app.tsx"),
|
||||
b"export const App = () => null;",
|
||||
)
|
||||
.expect("write tsx");
|
||||
std::fs::write(root.join("Page").join("package.json"), b"{}").expect("write package");
|
||||
std::fs::write(root.join("Page").join(".env"), b"KEY=value").expect("write env");
|
||||
std::fs::write(root.join("Page").join("config.yaml"), b"name: mnote").expect("write yaml");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let snapshot =
|
||||
@@ -12456,17 +12662,60 @@ fn main() {}
|
||||
mindmap["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(),
|
||||
Some("mindmap")
|
||||
);
|
||||
assert_eq!(
|
||||
mindmap["resourceMeta"]["workspacePath"]["objectIdentity"]["assetId"].as_str(),
|
||||
Some("local-file:Page/思维导图123456.json")
|
||||
);
|
||||
|
||||
let office = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("report.docx"))
|
||||
.expect("office row");
|
||||
assert_eq!(office["rowKind"].as_str(), Some("asset"));
|
||||
assert_eq!(office["iconHint"].as_str(), Some("office"));
|
||||
assert_eq!(office["iconHint"].as_str(), Some("word"));
|
||||
assert_eq!(
|
||||
office["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(),
|
||||
Some("only_office")
|
||||
);
|
||||
assert_eq!(
|
||||
office["resourceMeta"]["workspacePath"]["objectIdentity"]["assetId"].as_str(),
|
||||
Some("local-file:Page/report.docx")
|
||||
);
|
||||
let sheet = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("sheet.xlsx"))
|
||||
.expect("sheet row");
|
||||
assert_eq!(sheet["iconHint"].as_str(), Some("sheet"));
|
||||
let ppt = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("slides.pptx"))
|
||||
.expect("ppt row");
|
||||
assert_eq!(ppt["iconHint"].as_str(), Some("ppt"));
|
||||
let rust = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("main.rs"))
|
||||
.expect("rust row");
|
||||
assert_eq!(rust["iconHint"].as_str(), Some("code"));
|
||||
let tsx = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("app.tsx"))
|
||||
.expect("tsx row");
|
||||
assert_eq!(tsx["iconHint"].as_str(), Some("web"));
|
||||
let package_json = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("package.json"))
|
||||
.expect("package row");
|
||||
assert_eq!(package_json["iconHint"].as_str(), Some("config"));
|
||||
let env = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some(".env"))
|
||||
.expect("env row");
|
||||
assert_eq!(env["iconHint"].as_str(), Some("config"));
|
||||
let yaml = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("config.yaml"))
|
||||
.expect("yaml row");
|
||||
assert_eq!(yaml["iconHint"].as_str(), Some("config"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -12497,6 +12746,8 @@ fn main() {}
|
||||
source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
|
||||
root_uri: root_uri.clone(),
|
||||
expected_file_version: None,
|
||||
write_intent_id: Some("intent:test:buffer-save".into()),
|
||||
save_operation_id: Some("save:test:buffer-save".into()),
|
||||
base_content_hash: None,
|
||||
content_format: "editorBlocks".into(),
|
||||
content: serde_json::json!([
|
||||
@@ -12875,6 +13126,30 @@ fn main() {}
|
||||
markdown_asset["ownerDocumentId"],
|
||||
"local-md:docs~2FREADME~2FREADME.md"
|
||||
);
|
||||
let windows_path_asset = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
"local-md:docs~2FREADME~2FREADME.md",
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "C:\\Users\\liaib\\Downloads\\1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx".to_string(),
|
||||
content_type: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
.to_string(),
|
||||
bytes: b"pptx".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload windows path named pptx asset");
|
||||
assert_eq!(
|
||||
windows_path_asset["file_name"],
|
||||
"1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
);
|
||||
assert_eq!(
|
||||
windows_path_asset["sourcePath"],
|
||||
"1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
);
|
||||
assert_eq!(
|
||||
windows_path_asset["rootRelativePath"],
|
||||
"docs/README/1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
);
|
||||
let uploaded_asset_index =
|
||||
std::fs::read_to_string(root.join(".mnote").join("uploaded-assets.json"))
|
||||
.expect("uploaded asset index");
|
||||
|
||||
Reference in New Issue
Block a user