Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend-panel/src/i18n/locales/en/admin/settings-common.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"settings_item_webdav_enabled_desc": "Controls whether the WebDAV entrypoint is exposed. When disabled, clients can no longer access files through WebDAV.",
"settings_item_webdav_max_active_locks_per_user_label": "WebDAV active lock limit",
"settings_item_webdav_max_active_locks_per_user_desc": "Maximum active locks each WebDAV user can hold. New LOCK requests are rejected after this limit to prevent resource abuse.",
"settings_item_webdav_download_audit_coalesce_window_secs_label": "WebDAV download audit window",
"settings_item_webdav_download_audit_coalesce_window_secs_desc": "Seconds to coalesce repeated WebDAV download audit records for the same account, file, request type, and client. Set to 0 to record every read.",
"settings_item_webdav_block_system_files_enabled_label": "Block WebDAV system files",
"settings_item_webdav_block_system_files_enabled_desc": "Prevents WebDAV clients from creating common operating-system metadata files and folders such as .DS_Store, Thumbs.db, and desktop.ini.",
"settings_item_webdav_block_system_file_patterns_label": "Blocked WebDAV system-file patterns",
Expand Down
2 changes: 2 additions & 0 deletions frontend-panel/src/i18n/locales/zh/admin/settings-common.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"settings_item_webdav_enabled_desc": "控制 WebDAV 入口是否对外提供服务。关闭后客户端将无法通过 WebDAV 访问文件。",
"settings_item_webdav_max_active_locks_per_user_label": "WebDAV 活跃锁上限",
"settings_item_webdav_max_active_locks_per_user_desc": "每个 WebDAV 用户可持有的最大活跃锁数量。超过后拒绝新的 LOCK 请求,以防止资源滥用。",
"settings_item_webdav_download_audit_coalesce_window_secs_label": "WebDAV 下载审计合并窗口",
"settings_item_webdav_download_audit_coalesce_window_secs_desc": "同一账号、文件、请求类型和客户端在该秒数内的重复 WebDAV 下载审计会合并记录。设为 0 则每次读取都记录。",
"settings_item_webdav_block_system_files_enabled_label": "阻止 WebDAV 系统文件",
"settings_item_webdav_block_system_files_enabled_desc": "阻止 WebDAV 客户端创建常见操作系统元数据文件和目录,例如 .DS_Store、Thumbs.db、desktop.ini。",
"settings_item_webdav_block_system_file_patterns_label": "WebDAV 系统文件拦截规则",
Expand Down
14 changes: 14 additions & 0 deletions src/config/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ pub const AUDIT_LOG_RECORDED_ACTIONS_KEY: &str = "audit_log_recorded_actions";
pub const WEBDAV_ENABLED_KEY: &str = "webdav_enabled";
pub const WEBDAV_MAX_ACTIVE_LOCKS_PER_USER_KEY: &str = "webdav_max_active_locks_per_user";
pub const DEFAULT_WEBDAV_MAX_ACTIVE_LOCKS_PER_USER: u64 = 1024;
pub const WEBDAV_DOWNLOAD_AUDIT_COALESCE_WINDOW_SECS_KEY: &str =
"webdav_download_audit_coalesce_window_secs";
pub const DEFAULT_WEBDAV_DOWNLOAD_AUDIT_COALESCE_WINDOW_SECS: u64 = 30;
pub const WEBDAV_BLOCK_SYSTEM_FILES_ENABLED_KEY: &str = "webdav_block_system_files_enabled";
pub const WEBDAV_BLOCK_SYSTEM_FILE_PATTERNS_KEY: &str = "webdav_block_system_file_patterns";
pub const DEFAULT_WEBDAV_SYSTEM_FILE_PATTERNS: &[&str] = &[
Expand Down Expand Up @@ -447,6 +450,17 @@ pub static ALL_CONFIGS: &[ConfigDef] = &[
category: CONFIG_CATEGORY_WEBDAV,
description: "Maximum active WebDAV locks a single user can hold before new LOCK requests are rejected",
},
ConfigDef {
key: WEBDAV_DOWNLOAD_AUDIT_COALESCE_WINDOW_SECS_KEY,
label_i18n_key: "settings_item_webdav_download_audit_coalesce_window_secs_label",
description_i18n_key: "settings_item_webdav_download_audit_coalesce_window_secs_desc",
value_type: SystemConfigValueType::Number,
default_fn: || DEFAULT_WEBDAV_DOWNLOAD_AUDIT_COALESCE_WINDOW_SECS.to_string(),
requires_restart: false,
is_sensitive: false,
category: CONFIG_CATEGORY_WEBDAV,
description: "Seconds to coalesce repeated WebDAV download audit records for the same account, file, request type, and client fingerprint; 0 records every read",
},
ConfigDef {
key: WEBDAV_BLOCK_SYSTEM_FILES_ENABLED_KEY,
label_i18n_key: "settings_item_webdav_block_system_files_enabled_label",
Expand Down
45 changes: 44 additions & 1 deletion src/db/repository/property_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use sea_orm::{
ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, FromQueryResult, PaginatorTrait,
QueryFilter, QuerySelect, Set, TryInsertResult, sea_query::Expr,
QueryFilter, QueryOrder, QuerySelect, Set, TryInsertResult, sea_query::Expr,
};

use crate::entities::entity_property::{self, Entity as EntityProperty};
Expand All @@ -25,6 +25,49 @@ pub async fn find_by_entity(
.map_err(AsterError::from)
}

/// 查询多个实体的所有属性。
pub async fn find_by_entities<C: ConnectionTrait>(
db: &C,
targets: &[(EntityType, i64)],
) -> Result<Vec<entity_property::Model>> {
if targets.is_empty() {
return Ok(Vec::new());
}

let mut files = Vec::new();
let mut folders = Vec::new();
for (entity_type, entity_id) in targets {
match entity_type {
EntityType::File => files.push(*entity_id),
EntityType::Folder => folders.push(*entity_id),
}
}
files.sort_unstable();
files.dedup();
folders.sort_unstable();
folders.dedup();

let mut props = Vec::new();
for (entity_type, ids) in [(EntityType::File, files), (EntityType::Folder, folders)] {
for chunk in ids.chunks(ENTITY_PROPERTY_BATCH_CHUNK_SIZE) {
props.extend(
EntityProperty::find()
.filter(entity_property::Column::EntityType.eq(entity_type))
.filter(entity_property::Column::EntityId.is_in(chunk.iter().copied()))
.order_by_asc(entity_property::Column::EntityType)
.order_by_asc(entity_property::Column::EntityId)
.order_by_asc(entity_property::Column::Namespace)
.order_by_asc(entity_property::Column::Name)
.all(db)
.await
.map_err(AsterError::from)?,
);
}
}

Ok(props)
}

/// 查询实体的单个属性
pub async fn find_by_key(
db: &DatabaseConnection,
Expand Down
31 changes: 26 additions & 5 deletions src/services/file_service/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::entities::file;
use crate::errors::Result;
use crate::services::workspace_storage_service;
use crate::utils::http_validators;

const INLINE_SANDBOX_CSP: &str = "sandbox";

Expand All @@ -29,10 +30,14 @@ pub(crate) fn ensure_personal_file_scope(file: &file::Model) -> Result<()> {
}

pub(crate) fn if_none_match_matches_value(if_none_match: &str, etag_value: &str) -> bool {
if_none_match.split(',').any(|value| {
let candidate = value.trim();
candidate == "*" || candidate.trim_matches('"').eq_ignore_ascii_case(etag_value)
})
http_validators::if_none_match_header_matches(if_none_match, true, Some(etag_value))
.unwrap_or(false)
|| if_none_match.split(',').any(|candidate| {
candidate
.trim()
.trim_matches('"')
.eq_ignore_ascii_case(etag_value)
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub(crate) fn if_none_match_matches(if_none_match: &str, blob_hash: &str) -> bool {
Expand All @@ -41,7 +46,7 @@ pub(crate) fn if_none_match_matches(if_none_match: &str, blob_hash: &str) -> boo

#[cfg(test)]
mod tests {
use super::requires_inline_sandbox;
use super::{if_none_match_matches_value, requires_inline_sandbox};

#[test]
fn dangerous_same_origin_inline_mime_types_require_sandbox() {
Expand All @@ -52,4 +57,20 @@ mod tests {
assert!(!requires_inline_sandbox("text/plain"));
assert!(!requires_inline_sandbox("application/pdf"));
}

#[test]
fn if_none_match_matches_value_preserves_case_insensitive_blob_hash_matching() {
assert!(if_none_match_matches_value(
r#""ABCDEF123456""#,
"abcdef123456"
));
assert!(if_none_match_matches_value(
r#""other", "ABCDEF123456""#,
"abcdef123456"
));
assert!(!if_none_match_matches_value(
r#"W/"ABCDEF123456""#,
"abcdef123456"
));
}
}
29 changes: 28 additions & 1 deletion src/services/folder_service/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub(super) struct CachedFolderPathChain {
pub(super) chain_ids: Vec<i64>,
}

fn folder_path_cache_key(folder_id: i64) -> String {
pub(crate) fn folder_path_cache_key(folder_id: i64) -> String {
format!("{FOLDER_PATH_CACHE_PREFIX}{folder_id}")
}

Expand Down Expand Up @@ -49,6 +49,18 @@ pub(super) async fn invalidate_folder_path_chain(state: &impl SharedRuntimeState
.await;
}

pub(super) async fn invalidate_folder_path_chains(
state: &impl SharedRuntimeState,
folder_ids: &[i64],
) {
let keys = folder_ids
.iter()
.copied()
.map(folder_path_cache_key)
.collect::<Vec<_>>();
state.cache().delete_many(&keys).await;
}

pub(crate) async fn invalidate_all_folder_path_chains(state: &impl SharedRuntimeState) {
state
.cache()
Expand Down Expand Up @@ -98,4 +110,19 @@ mod tests {

assert!(load_folder_path_chain(&state, 11).await.is_none());
}

#[tokio::test]
async fn folder_path_chain_supports_batch_invalidation() {
let state = CacheOnlyState::new().await;

store_folder_path_chain(&state, 10, vec![10]).await;
store_folder_path_chain(&state, 11, vec![11]).await;
store_folder_path_chain(&state, 12, vec![12]).await;

invalidate_folder_path_chains(&state, &[10, 12]).await;

assert!(load_folder_path_chain(&state, 10).await.is_none());
assert!(load_folder_path_chain(&state, 11).await.is_some());
assert!(load_folder_path_chain(&state, 12).await.is_none());
}
}
1 change: 0 additions & 1 deletion src/services/folder_service/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ pub(crate) async fn copy_folder_in_scope(
)
.with_storage_delta(storage_delta),
);
super::invalidate_folder_path_cache(state).await;
tracing::debug!(
scope = ?scope,
src_folder_id = src_id,
Expand Down
7 changes: 7 additions & 0 deletions src/services/folder_service/hierarchy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub(crate) async fn invalidate_folder_path_cache(state: &impl SharedRuntimeState
cache::invalidate_all_folder_path_chains(state).await;
}

pub(crate) async fn invalidate_folder_path_cache_for_ids(
state: &impl SharedRuntimeState,
folder_ids: &[i64],
) {
cache::invalidate_folder_path_chains(state, folder_ids).await;
}

pub(super) async fn load_folder_chain_map<C: ConnectionTrait>(
db: &C,
folder_ids: &[i64],
Expand Down
6 changes: 4 additions & 2 deletions src/services/folder_service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ pub use mutation::{create, delete, move_folder, set_lock, update};
pub(crate) use access::{
ensure_folder_model_in_scope, ensure_personal_folder_scope, verify_folder_in_scope,
};
pub(crate) use cache::FOLDER_PATH_CACHE_PREFIX;
pub(crate) use cache::{FOLDER_PATH_CACHE_PREFIX, folder_path_cache_key};
pub(crate) use copy::{copy_folder_in_scope, copy_folder_tree_in_scope};
pub(crate) use hierarchy::{get_ancestors_in_scope, invalidate_folder_path_cache};
pub(crate) use hierarchy::{
get_ancestors_in_scope, invalidate_folder_path_cache, invalidate_folder_path_cache_for_ids,
};
pub(crate) use listing::list_in_scope;
pub(crate) use mutation::{
admin_set_policy, create_in_scope, delete_in_scope, get_info_in_scope,
Expand Down
5 changes: 2 additions & 3 deletions src/services/folder_service/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ pub(crate) async fn create_in_scope(
vec![created.parent_id],
),
);
super::invalidate_folder_path_cache(state).await;
tracing::debug!(
scope = ?scope,
folder_id = created.id,
Expand Down Expand Up @@ -291,7 +290,7 @@ pub(crate) async fn delete_in_scope(
vec![folder.parent_id],
),
);
super::invalidate_folder_path_cache(state).await;
super::invalidate_folder_path_cache_for_ids(state, &[folder.id]).await;
tracing::debug!(
scope = ?scope,
folder_id = folder.id,
Expand Down Expand Up @@ -444,7 +443,7 @@ pub(crate) async fn update_in_scope(
),
);
if name.is_some() || parent_id.is_present() {
super::invalidate_folder_path_cache(state).await;
super::invalidate_folder_path_cache_for_ids(state, &[updated.id]).await;
}
tracing::debug!(
scope = ?scope,
Expand Down
Loading
Loading