fix: open local uploaded resources
This commit is contained in:
@@ -42,6 +42,7 @@ struct LocalFolderEntry {
|
||||
struct LocalFolderMetadata {
|
||||
page_options: BTreeMap<String, Value>,
|
||||
trash_entries: BTreeMap<String, LocalTrashEntry>,
|
||||
uploaded_assets: BTreeMap<String, LocalUploadedAssetEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -156,6 +157,19 @@ pub(crate) struct LocalTrashEntry {
|
||||
pub(crate) purged_at: Option<u128>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LocalUploadedAssetEntry {
|
||||
#[serde(default, alias = "documentId")]
|
||||
document_id: String,
|
||||
#[serde(default, alias = "relativePath")]
|
||||
relative_path: String,
|
||||
#[serde(default, alias = "fileName")]
|
||||
file_name: String,
|
||||
#[serde(default, alias = "createdAtMs")]
|
||||
created_at_ms: u128,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalFolderRow {
|
||||
node_id: String,
|
||||
@@ -2523,6 +2537,21 @@ pub(crate) fn write_local_markdown_asset(
|
||||
let root_relative_path = normalize_relative_path(&canonical_root, &target)?;
|
||||
let markdown_relative_path = normalize_markdown_relative_asset_path(markdown_dir, &target)?;
|
||||
let asset_type = local_upload_asset_type(kind, &file.content_type);
|
||||
let mut uploaded_assets = metadata.uploaded_assets;
|
||||
uploaded_assets.insert(
|
||||
root_relative_path.clone(),
|
||||
LocalUploadedAssetEntry {
|
||||
document_id: document_id.to_string(),
|
||||
relative_path: root_relative_path.clone(),
|
||||
file_name: target
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or(&sanitized_name)
|
||||
.to_string(),
|
||||
created_at_ms: now_ms(),
|
||||
},
|
||||
);
|
||||
write_uploaded_asset_index_metadata(&canonical_root, &uploaded_assets)?;
|
||||
Ok(json!({
|
||||
"id": format!("local:asset:{root_relative_path}"),
|
||||
"asset_type": asset_type,
|
||||
@@ -4506,6 +4535,9 @@ fn load_local_folder_metadata(root: &Path) -> Result<LocalFolderMetadata, WebErr
|
||||
Ok(LocalFolderMetadata {
|
||||
page_options: load_metadata_value_map(&root.join(".mnote").join("page-options.json"))?,
|
||||
trash_entries: load_trash_index_map(&root.join(".mnote").join("trash-index.json"))?,
|
||||
uploaded_assets: load_uploaded_asset_index_map(
|
||||
&root.join(".mnote").join("uploaded-assets.json"),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4568,6 +4600,19 @@ fn load_trash_index_map(path: &Path) -> Result<BTreeMap<String, LocalTrashEntry>
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn load_uploaded_asset_index_map(
|
||||
path: &Path,
|
||||
) -> Result<BTreeMap<String, LocalUploadedAssetEntry>, WebError> {
|
||||
if !path.exists() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
let value = read_metadata_json(path)?;
|
||||
let source = value.get("entries").unwrap_or(&value);
|
||||
let entries: BTreeMap<String, LocalUploadedAssetEntry> = serde_json::from_value(source.clone())
|
||||
.map_err(|error| metadata_invalid(path, format!("上传资源索引损坏: {error}")))?;
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn metadata_invalid(path: &Path, message: impl Into<String>) -> WebError {
|
||||
WebError::bad_request_code(
|
||||
"local_metadata_invalid",
|
||||
@@ -4641,6 +4686,25 @@ fn write_page_options_metadata(
|
||||
write_json_atomic(&path, &value)
|
||||
}
|
||||
|
||||
fn write_uploaded_asset_index_metadata(
|
||||
root: &Path,
|
||||
entries: &BTreeMap<String, LocalUploadedAssetEntry>,
|
||||
) -> Result<(), WebError> {
|
||||
let mnote_dir = root.join(".mnote");
|
||||
fs::create_dir_all(&mnote_dir).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"local_metadata_write_failed",
|
||||
format!("无法创建本地元数据目录 {}: {error}", mnote_dir.display()),
|
||||
)
|
||||
})?;
|
||||
let path = mnote_dir.join("uploaded-assets.json");
|
||||
let value = json!({
|
||||
"version": 1,
|
||||
"entries": entries,
|
||||
});
|
||||
write_json_atomic(&path, &value)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn write_json_atomic(path: &Path, value: &Value) -> Result<(), WebError> {
|
||||
let tmp_path = path.with_extension("json.tmp");
|
||||
@@ -4684,8 +4748,11 @@ fn scan_directory(
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let uploaded_asset = metadata.uploaded_assets.get(&entry.relative_path);
|
||||
let row_kind = if entry.is_dir {
|
||||
"folder".to_string()
|
||||
} else if uploaded_asset.is_some() {
|
||||
"asset".to_string()
|
||||
} else if is_markdown_file(&entry.file_name) {
|
||||
"markdown".to_string()
|
||||
} else {
|
||||
@@ -4706,7 +4773,9 @@ fn scan_directory(
|
||||
child_count,
|
||||
expandable: entry.is_dir && child_count > 0,
|
||||
expanded_by_default: depth < 1 && entry.is_dir && entry_count <= 80,
|
||||
document_id: if is_markdown_file(&entry.file_name) {
|
||||
document_id: if let Some(asset) = uploaded_asset {
|
||||
Some(asset.document_id.clone())
|
||||
} else if is_markdown_file(&entry.file_name) {
|
||||
Some(local_markdown_path_page_id(&entry.relative_path))
|
||||
} else {
|
||||
None
|
||||
@@ -4968,6 +5037,29 @@ fn scan_markdown_page_tree(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let uploaded_asset = metadata.uploaded_assets.get(&entry.relative_path);
|
||||
if let Some(asset) = uploaded_asset {
|
||||
directory_rows.push(LocalFolderRow {
|
||||
node_id: local_node_id(&entry.relative_path),
|
||||
row_id: format!("local:asset:{}", entry.relative_path),
|
||||
parent_node_id: parent_node_id.clone(),
|
||||
title: entry.file_name.clone(),
|
||||
depth,
|
||||
position: position as u32,
|
||||
row_kind: "asset".to_string(),
|
||||
icon_hint: icon_hint_for_entry(&entry),
|
||||
relative_path: entry.relative_path.clone(),
|
||||
source_uri: file_uri_for_path(&entry.path),
|
||||
child_count: 0,
|
||||
expandable: false,
|
||||
expanded_by_default: false,
|
||||
document_id: Some(asset.document_id.clone()),
|
||||
capabilities: local_entry_capabilities(&entry),
|
||||
workspace_id: workspace_id.to_string(),
|
||||
root_source_uri: root_source_uri.to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if is_markdown_file(&entry.file_name) {
|
||||
let markdown = fs::read_to_string(&entry.path).unwrap_or_default();
|
||||
let parsed = parse_markdown_page(&markdown, &entry.file_name);
|
||||
@@ -8284,6 +8376,49 @@ fn main() {}
|
||||
.expect("read copied asset"),
|
||||
b"new-image"
|
||||
);
|
||||
let markdown_asset = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
"local-md:docs~2FREADME~2FREADME.md",
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Uploaded notes\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload markdown asset");
|
||||
assert_eq!(markdown_asset["sourcePath"], "notes.md");
|
||||
let uploaded_asset_index =
|
||||
std::fs::read_to_string(root.join(".mnote").join("uploaded-assets.json"))
|
||||
.expect("uploaded asset index");
|
||||
assert!(uploaded_asset_index.contains("docs/README/notes.md"));
|
||||
|
||||
let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("file tree");
|
||||
let items = snapshot.projection["items"].as_array().expect("items");
|
||||
let notes_row = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("notes.md"))
|
||||
.expect("uploaded markdown asset row");
|
||||
assert_eq!(notes_row["rowKind"].as_str(), Some("asset"));
|
||||
assert_eq!(notes_row["iconHint"].as_str(), Some("markdown"));
|
||||
assert_eq!(
|
||||
notes_row["resourceMeta"]["assetId"].as_str(),
|
||||
Some("local-file:docs/README/notes.md")
|
||||
);
|
||||
let page_tree = load_local_folder_page_tree_snapshot(&root_uri).expect("page tree");
|
||||
let page_items = page_tree.projection["items"]
|
||||
.as_array()
|
||||
.expect("page items");
|
||||
let page_notes_row = page_items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("notes.md"))
|
||||
.expect("uploaded markdown asset page row");
|
||||
assert_eq!(page_notes_row["rowKind"].as_str(), Some("asset"));
|
||||
assert_eq!(page_notes_row["iconHint"].as_str(), Some("markdown"));
|
||||
assert_eq!(
|
||||
page_notes_row["resourceMeta"]["assetId"].as_str(),
|
||||
Some("local-file:docs/README/notes.md")
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ use axum::extract::{Path, Query, State};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, Method, Request, Uri};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use base64::Engine;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path as FsPath, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
const ONLYOFFICE_PROBE_PATH: &str = "/web-apps/apps/api/documents/api.js";
|
||||
@@ -729,6 +731,9 @@ pub async fn proxy(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| WebError::bad_request_code("onlyoffice_proxy_url_missing", "缺少 u"))?;
|
||||
if let Some(response) = proxy_local_folder_file_open(encoded_url, &method)? {
|
||||
return Ok(response);
|
||||
}
|
||||
let prepared = prepare_proxy_request(OnlyOfficeProxyPreparationInput {
|
||||
encoded_url: encoded_url.to_string(),
|
||||
method: method.as_str().to_string(),
|
||||
@@ -789,6 +794,173 @@ pub async fn proxy(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn decode_onlyoffice_proxy_url(encoded_url: &str) -> Option<String> {
|
||||
[
|
||||
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||
&base64::engine::general_purpose::URL_SAFE,
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|engine| {
|
||||
engine
|
||||
.decode(encoded_url)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
})
|
||||
}
|
||||
|
||||
fn is_local_mnote_proxy_host(host: &str) -> bool {
|
||||
matches!(host, "localhost" | "127.0.0.1" | "host.docker.internal")
|
||||
}
|
||||
|
||||
fn parse_local_file_root_uri(root_uri: &str) -> Result<PathBuf, WebError> {
|
||||
let trimmed = root_uri.trim();
|
||||
let Some(path) = trimmed.strip_prefix("file://") else {
|
||||
return Err(WebError::bad_request_code(
|
||||
"onlyoffice_local_file_root_invalid",
|
||||
"本地文件 rootUri 必须使用 file://",
|
||||
));
|
||||
};
|
||||
if path.trim().is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"onlyoffice_local_file_root_invalid",
|
||||
"本地文件 rootUri 不能为空",
|
||||
));
|
||||
}
|
||||
Ok(PathBuf::from(path))
|
||||
}
|
||||
|
||||
fn resolve_onlyoffice_local_file_path(
|
||||
root_uri: &str,
|
||||
relative_path: &str,
|
||||
) -> Result<PathBuf, WebError> {
|
||||
let root = parse_local_file_root_uri(root_uri)?;
|
||||
let canonical_root = root.canonicalize().map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"onlyoffice_local_file_root_unavailable",
|
||||
format!("无法访问本地文件夹: {error}"),
|
||||
)
|
||||
})?;
|
||||
let requested = FsPath::new(relative_path);
|
||||
if requested.is_absolute()
|
||||
|| requested
|
||||
.components()
|
||||
.any(|component| matches!(component, std::path::Component::ParentDir))
|
||||
{
|
||||
return Err(WebError::bad_request_code(
|
||||
"onlyoffice_local_file_root_escape",
|
||||
"本地文件路径不能越过 root",
|
||||
));
|
||||
}
|
||||
let target = canonical_root
|
||||
.join(requested)
|
||||
.canonicalize()
|
||||
.map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"onlyoffice_local_file_not_found",
|
||||
format!("找不到本地文件: {error}"),
|
||||
)
|
||||
})?;
|
||||
if !target.starts_with(&canonical_root) || !target.is_file() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"onlyoffice_local_file_root_escape",
|
||||
"本地文件路径不能越过 root",
|
||||
));
|
||||
}
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
fn onlyoffice_content_type_for_path(path: &FsPath) -> HeaderValue {
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase();
|
||||
HeaderValue::from_static(match extension.as_str() {
|
||||
"doc" => "application/msword",
|
||||
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"ppt" => "application/vnd.ms-powerpoint",
|
||||
"pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"xls" => "application/vnd.ms-excel",
|
||||
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"odt" => "application/vnd.oasis.opendocument.text",
|
||||
"odp" => "application/vnd.oasis.opendocument.presentation",
|
||||
"ods" => "application/vnd.oasis.opendocument.spreadsheet",
|
||||
"csv" => "text/csv; charset=utf-8",
|
||||
"md" | "markdown" => "text/markdown; charset=utf-8",
|
||||
"txt" | "log" => "text/plain; charset=utf-8",
|
||||
"pdf" => "application/pdf",
|
||||
_ => "application/octet-stream",
|
||||
})
|
||||
}
|
||||
|
||||
fn proxy_local_folder_file_open(
|
||||
encoded_url: &str,
|
||||
method: &Method,
|
||||
) -> Result<Option<Response>, WebError> {
|
||||
if *method != Method::GET && *method != Method::HEAD {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(raw_url) = decode_onlyoffice_proxy_url(encoded_url) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(url) = reqwest::Url::parse(&raw_url) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(host) = url.host_str() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !is_local_mnote_proxy_host(host) || url.path() != "/api/local-folder/files/open" {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut root_uri = String::new();
|
||||
let mut relative_path = String::new();
|
||||
for (key, value) in url.query_pairs() {
|
||||
match key.as_ref() {
|
||||
"rootUri" => root_uri = value.into_owned(),
|
||||
"path" => relative_path = value.into_owned(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if root_uri.trim().is_empty() || relative_path.trim().is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"onlyoffice_local_file_query_missing",
|
||||
"本地文件代理缺少 rootUri 或 path",
|
||||
));
|
||||
}
|
||||
let target = resolve_onlyoffice_local_file_path(&root_uri, &relative_path)?;
|
||||
let metadata = fs::metadata(&target).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"onlyoffice_local_file_metadata_failed",
|
||||
format!("无法读取本地文件元数据: {error}"),
|
||||
)
|
||||
})?;
|
||||
let mut response = if *method == Method::HEAD {
|
||||
Response::new(Body::empty())
|
||||
} else {
|
||||
let bytes = fs::read(&target).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"onlyoffice_local_file_read_failed",
|
||||
format!("无法读取本地文件: {error}"),
|
||||
)
|
||||
})?;
|
||||
Response::new(Body::from(bytes))
|
||||
};
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
onlyoffice_content_type_for_path(&target),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&metadata.len().to_string())
|
||||
.unwrap_or_else(|_| HeaderValue::from_static("0")),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
|
||||
Ok(Some(response))
|
||||
}
|
||||
|
||||
pub async fn callback(
|
||||
State(state): State<AppState>,
|
||||
uri: Uri,
|
||||
@@ -1222,6 +1394,47 @@ mod tests {
|
||||
assert!(html.contains("documentId=doc_1"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_proxy_serves_local_folder_file_open_url() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-onlyoffice-local-proxy-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||
fs::write(root.join("Page").join("report.docx"), b"docx").expect("write docx");
|
||||
let local_url = format!(
|
||||
"http://localhost:3000/api/local-folder/files/open?rootUri=file://{}&path=Page/report.docx",
|
||||
root.display()
|
||||
);
|
||||
let encoded = base64::Engine::encode(
|
||||
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||
local_url.as_bytes(),
|
||||
);
|
||||
|
||||
let response = proxy(
|
||||
Query(OnlyOfficeProxyQuery { u: Some(encoded) }),
|
||||
HeaderMap::new(),
|
||||
Method::GET,
|
||||
)
|
||||
.await
|
||||
.expect("proxy local file");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
||||
);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
assert_eq!(&body[..], b"docx");
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onlyoffice_internal_candidates_keep_default_first_after_env() {
|
||||
let candidates = onlyoffice_internal_candidates();
|
||||
|
||||
@@ -2137,7 +2137,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
|
||||
function localFilePathFromAssetId(assetId) {
|
||||
var value = String(assetId || '').trim();
|
||||
return value.indexOf('local-file:') === 0 ? value.slice('local-file:'.length) : '';
|
||||
return value.indexOf('local-file:') === 0 ? value.slice('local-file:'.length) : value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length) : '';
|
||||
}
|
||||
|
||||
function buildLocalFileOpenUrl(relativePath, download) {
|
||||
@@ -2350,6 +2350,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
|
||||
}
|
||||
|
||||
function fileTreeIconKindForFileName(fileName) {
|
||||
var name = String(fileName || '').trim().toLowerCase();
|
||||
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
|
||||
if (['doc', 'docx', 'odt', 'rtf', 'ppt', 'pptx', 'odp', 'xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'office';
|
||||
if (ext === 'md' || ext === 'markdown') return 'markdown';
|
||||
if (ext === 'pdf') return 'pdf';
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].indexOf(ext) >= 0) return 'image';
|
||||
return '';
|
||||
}
|
||||
|
||||
function isLocalUploadedAsset(asset) {
|
||||
var id = String(asset && asset.id || '').trim();
|
||||
return String(asset && asset.sourceKind || '').trim() === 'local_folder' || id.indexOf('local:asset:') === 0;
|
||||
@@ -2688,7 +2698,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
};
|
||||
li.setAttribute('data-node-id', 'asset:' + assetId);
|
||||
var title = uploadedAssetTitle(asset);
|
||||
var iconKind = uploadedAssetType(asset) || 'file';
|
||||
var iconKind = fileTreeIconKindForFileName(title) || uploadedAssetType(asset) || 'file';
|
||||
if (objectKind === 'mindmap') iconKind = 'mindmap';
|
||||
li.innerHTML =
|
||||
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(objectKind || 'attachment') + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
|
||||
@@ -7221,6 +7231,19 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
|
||||
async function resolveEditorAttachmentUrl(detail) {
|
||||
var assetId = String(detail && detail.assetId || '').trim();
|
||||
var localFilePath = localFilePathFromAssetId(assetId);
|
||||
if (localFilePath) {
|
||||
var localUrl = buildLocalFileOpenUrl(localFilePath, false);
|
||||
if (localUrl) {
|
||||
return {
|
||||
url: localUrl,
|
||||
asset: {
|
||||
file_name: String(detail && detail.fileName || '').trim(),
|
||||
fileSize: String(detail && detail.fileSize || '').trim()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if (assetId) {
|
||||
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
|
||||
method: 'GET',
|
||||
@@ -7303,6 +7326,14 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
|
||||
async function openEditorAttachmentDownload(detail) {
|
||||
if (!detail) return;
|
||||
var localFilePath = localFilePathFromAssetId(detail.assetId);
|
||||
if (localFilePath) {
|
||||
var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true);
|
||||
if (localDownloadUrl) {
|
||||
window.open(localDownloadUrl, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (detail.assetId) {
|
||||
try {
|
||||
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(detail.assetId), {
|
||||
@@ -8740,6 +8771,8 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("function localFilePathFromAssetId"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("/api/local-folder/files/open"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("local-file:"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length)"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("fetchCurrentOnlyOfficeUserId"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("/api/auth/whoami"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("buildOnlyOfficeOpenUrl"));
|
||||
@@ -8908,6 +8941,7 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("isLocalUploadedAsset(asset)"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("buildLocalOnlyOfficeOpenUrl"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("localOfficeUrl"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("fileTreeIconKindForFileName(title)"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("wolai:local-assets-changed"));
|
||||
}
|
||||
@@ -9051,6 +9085,9 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("function openPdfEditorAttachment"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("function openCodeEditorAttachment"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("function resolveEditorAttachmentUrl"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("type: 'codeBlock'"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("attrs: { language: language }"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("inferCodeAttachmentLanguage"));
|
||||
|
||||
@@ -1819,6 +1819,34 @@ body {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-row::before {
|
||||
content: "" !important;
|
||||
display: inline-block !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
flex: 0 0 18px !important;
|
||||
border-radius: 4px !important;
|
||||
background: #6b7280;
|
||||
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12) !important;
|
||||
}
|
||||
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-word::before {
|
||||
background: #4f7df3;
|
||||
}
|
||||
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-ppt::before,
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-pdf::before {
|
||||
background: #d94841;
|
||||
}
|
||||
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-sheet::before {
|
||||
background: #2f9e44;
|
||||
}
|
||||
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-code::before {
|
||||
background: #111111;
|
||||
}
|
||||
|
||||
.document-shell .editor-surface .ProseMirror a.mnote-uploaded-attachment-row::after {
|
||||
content: "" !important;
|
||||
display: none !important;
|
||||
@@ -4044,6 +4072,19 @@ mod tests {
|
||||
assert!(MNOTE_CSS.contains("#mnote-mindmap-island"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uploaded_attachment_rows_use_colored_file_badges() {
|
||||
assert!(MNOTE_CSS.contains("a.mnote-uploaded-attachment-row::before"));
|
||||
assert!(MNOTE_CSS.contains("a.mnote-uploaded-attachment-word::before"));
|
||||
assert!(MNOTE_CSS.contains("background: #4f7df3"));
|
||||
assert!(MNOTE_CSS.contains("a.mnote-uploaded-attachment-ppt::before"));
|
||||
assert!(MNOTE_CSS.contains("background: #d94841"));
|
||||
assert!(MNOTE_CSS.contains("a.mnote-uploaded-attachment-sheet::before"));
|
||||
assert!(MNOTE_CSS.contains("background: #2f9e44"));
|
||||
assert!(MNOTE_CSS.contains("a.mnote-uploaded-attachment-code::before"));
|
||||
assert!(MNOTE_CSS.contains("background: #111111"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnote_css_contains_responsive_breakpoints() {
|
||||
assert!(MNOTE_CSS.contains("@media (max-width: 768px)"));
|
||||
|
||||
Reference in New Issue
Block a user