Skip to content
Open
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
93 changes: 84 additions & 9 deletions src-tauri/src/services/skill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,18 @@ impl SkillService {

// 保存到数据库
db.save_skill(&skill)?;

// 按用户配置的同步方式(symlink/copy)同步到各已启用的应用目录
for app in skill.apps.enabled_apps() {
if let Err(err) = Self::sync_to_app_dir(&skill.directory, &app) {
log::warn!(
"导入 Skill '{}' 后同步到 {:?} 失败: {err:#}",
skill.directory,
app
);
}
}

imported.push(skill);
}

Expand All @@ -1029,15 +1041,78 @@ impl SkillService {

#[cfg(windows)]
fn create_symlink(src: &Path, dest: &Path) -> Result<()> {
std::os::windows::fs::symlink_dir(src, dest)
.with_context(|| format!("创建符号链接失败: {} -> {}", src.display(), dest.display()))
// 优先尝试真正的 symlink(需要开发者模式或管理员权限)
match std::os::windows::fs::symlink_dir(src, dest) {
Ok(()) => return Ok(()),
Err(err) => {
log::debug!(
"Windows symlink 创建失败(需要开发者模式或管理员权限),尝试 junction: {err}"
);
}
}

// 回退到 junction(无需特殊权限,同卷目录链接)
Self::create_junction(src, dest)
}

/// 检查路径是否为符号链接
/// 在 Windows 上创建目录联接(junction),不需要管理员权限
#[cfg(windows)]
fn create_junction(src: &Path, dest: &Path) -> Result<()> {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;

let output = std::process::Command::new("cmd")
.arg("/c")
.arg("mklink")
.arg("/J")
.arg(dest.as_os_str())
.arg(src.as_os_str())
.creation_flags(CREATE_NO_WINDOW)
.output()
.with_context(|| {
format!("创建目录联接失败: {} -> {}", dest.display(), src.display())
})?;

if output.status.success() {
log::debug!(
"已通过 junction 创建目录链接: {} -> {}",
dest.display(),
src.display()
);
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(anyhow!(
"创建目录联接失败: {} -> {}. {}",
dest.display(),
src.display(),
stderr.trim()
))
}
}

/// 检查路径是否为符号链接或目录联接(junction)
///
/// Windows 上 `FileType::is_symlink()` 仅检测 IO_REPARSE_TAG_SYMLINK,
/// 不包括 junction(IO_REPARSE_TAG_MOUNT_POINT)。这里统一检查
/// FILE_ATTRIBUTE_REPARSE_POINT 属性,以同时涵盖 symlink 和 junction,
/// 避免 `remove_dir_all` 穿透 junction 删除 SSOT 源文件。
fn is_symlink(path: &Path) -> bool {
path.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
return path
.symlink_metadata()
.map(|m| m.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0)
.unwrap_or(false);
Comment on lines +1104 to +1108
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict reparse-point detection to link tags

is_symlink now treats any FILE_ATTRIBUTE_REPARSE_POINT as a symlink/junction, but that flag is also set on non-link reparse types (for example cloud placeholder directories). In those environments, remove_path will take the symlink branch and call fs::remove_dir instead of recursive deletion, which can fail on non-empty directories and break sync/remove flows on Windows. Please narrow this check to actual link-like reparse tags (symlink/mount-point) rather than all reparse points.

Useful? React with 👍 / 👎.

}
#[cfg(not(windows))]
{
path.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
}

/// 获取当前同步方式配置
Expand Down Expand Up @@ -1110,14 +1185,14 @@ impl SkillService {
Self::sync_to_app_dir(directory, app)
}

/// 删除路径(支持 symlink 和真实目录)
/// 删除路径(支持 symlink、junction 和真实目录)
fn remove_path(path: &Path) -> Result<()> {
if Self::is_symlink(path) {
// 符号链接:仅删除链接本身,不影响源文件
// symlink / junction:仅删除链接本身,不影响源文件
#[cfg(unix)]
fs::remove_file(path)?;
#[cfg(windows)]
fs::remove_dir(path)?; // Windows 的目录 symlink 需要用 remove_dir
fs::remove_dir(path)?; // Windows 的目录 symlink 和 junction 都用 remove_dir
} else if path.is_dir() {
// 真实目录:递归删除
fs::remove_dir_all(path)?;
Expand Down
Loading