aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/installers/symlinks.rs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-08 03:30:04 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-08 03:30:04 -0500
commit3de4056df4ea5d0afbcea388ad8d681daea1aac3 (patch)
treefc5978451f9f51b84268f45bd2fa6f5d4a17baaf /libs/nak/src/installers/symlinks.rs
parent5ba23a6d7a40052d9e1d4bd0cf018cfe7814d03a (diff)
Remove NaK/Rust dependency, port remaining functionality to native C++
Replace NaK Rust crate and nak_ffi with native C++ implementations: - Game detection (Steam, Heroic, Bottles), VDF parser, icon extraction - Prefix symlinks, SLR manager, Steam path detection - Known games database Add FUSE VFS optimizations: zero-copy reads via fuse_reply_data, default_permissions mount option, FUSE_CAP_ASYNC_DIO, FUSE_CAP_EXPIRE_ONLY, lookup cache parent index, cached mode bits, increased I/O buffer sizes. Update build system, prefix setup, and various fixes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/nak/src/installers/symlinks.rs')
-rw-r--r--libs/nak/src/installers/symlinks.rs377
1 files changed, 0 insertions, 377 deletions
diff --git a/libs/nak/src/installers/symlinks.rs b/libs/nak/src/installers/symlinks.rs
deleted file mode 100644
index e8e477b..0000000
--- a/libs/nak/src/installers/symlinks.rs
+++ /dev/null
@@ -1,377 +0,0 @@
-//! Symlink management for NaK prefixes
-//!
-//! Creates symlinks FROM NaK prefix TO game prefixes.
-//! Data stays in the game prefix, NaK just has links pointing to it.
-//!
-//! This inverted approach means:
-//! - Game saves remain in their original location (Steam cloud sync works)
-//! - NaK prefix provides unified access to all game data
-//! - No data duplication or sync issues
-//!
-//! NaK Tools folder contains convenience symlinks pointing INTO the prefix
-//! for easy access to Documents, AppData, etc.
-
-// Allow unused items - some functions are public API for future use
-#![allow(dead_code)]
-
-use std::fs;
-use std::path::Path;
-
-use crate::game_finder::{detect_all_games, Game, GameScanResult};
-use crate::logging::{log_info, log_warning};
-
-// ============================================================================
-// Public API
-// ============================================================================
-
-/// Create symlinks from NaK prefix to game prefixes for all detected games
-///
-/// This creates symlinks in the NaK prefix pointing to the actual save/config
-/// folders in each game's own prefix. The symlink direction is:
-///
-/// NaK/Documents/My Games/Skyrim -> game_prefix/Documents/My Games/Skyrim
-/// NaK/AppData/Local/Skyrim -> game_prefix/AppData/Local/Skyrim
-///
-/// Data stays in the game prefix (preserving Steam Cloud sync), while NaK
-/// provides unified access through symlinks.
-pub fn create_game_symlinks(nak_prefix: &Path, games: &[Game]) {
- let users_dir = nak_prefix.join("drive_c/users");
- let username = find_prefix_username(&users_dir);
- let user_dir = users_dir.join(&username);
-
- let documents = user_dir.join("Documents");
- let my_games = documents.join("My Games");
- let appdata_local = user_dir.join("AppData/Local");
- let appdata_roaming = user_dir.join("AppData/Roaming");
-
- // Ensure base directories exist (real folders in NaK prefix)
- let _ = fs::create_dir_all(&my_games);
- let _ = fs::create_dir_all(&appdata_local);
- let _ = fs::create_dir_all(&appdata_roaming);
-
- let mut linked_count = 0;
-
- for game in games {
- // Skip games without prefixes
- let Some(game_prefix) = &game.prefix_path else {
- log_info(&format!(
- "Symlink skip '{}' (app_id {}): no prefix_path",
- game.name, game.app_id
- ));
- continue;
- };
-
- // Discover the game prefix's user directory
- let game_users_dir = game_prefix.join("drive_c/users");
- let game_username = find_prefix_username(&game_users_dir);
- let game_user_dir = game_users_dir.join(&game_username);
-
- let my_games_dir = game_user_dir.join("Documents/My Games");
- log_info(&format!(
- "Symlink checking '{}' (app_id {}): prefix={}, user='{}', my_games_exists={}",
- game.name, game.app_id, game_prefix.display(), game_username, my_games_dir.is_dir()
- ));
-
- // Scan and symlink ALL folders in the game prefix's Documents/My Games/
- linked_count += scan_and_link_all(
- &my_games,
- &game_user_dir.join("Documents/My Games"),
- "Documents/My Games",
- &game.name,
- game_prefix,
- );
-
- // Also link the Documents folder itself for non-My Games entries
- // (some games put saves directly in Documents/<GameName>)
- linked_count += scan_and_link_all(
- &documents,
- &game_user_dir.join("Documents"),
- "Documents",
- &game.name,
- game_prefix,
- );
-
- // Scan and symlink ALL folders in AppData/Local/
- linked_count += scan_and_link_all(
- &appdata_local,
- &game_user_dir.join("AppData/Local"),
- "AppData/Local",
- &game.name,
- game_prefix,
- );
-
- // Scan and symlink ALL folders in AppData/Roaming/
- linked_count += scan_and_link_all(
- &appdata_roaming,
- &game_user_dir.join("AppData/Roaming"),
- "AppData/Roaming",
- &game.name,
- game_prefix,
- );
- }
-
- if linked_count > 0 {
- log_info(&format!(
- "Created {} symlinks to game prefixes",
- linked_count
- ));
- }
-
- // Create "My Documents" symlink for compatibility
- let my_documents = user_dir.join("My Documents");
- if !my_documents.exists() && fs::symlink_metadata(&my_documents).is_err() {
- if let Err(e) = std::os::unix::fs::symlink("Documents", &my_documents) {
- log_warning(&format!("Failed to create My Documents symlink: {}", e));
- }
- }
-}
-
-/// Create NaK Tools convenience symlinks pointing INTO the prefix
-///
-/// Creates symlinks in NaK Tools folder for easy access:
-/// - NaK Tools/Prefix Documents -> prefix/drive_c/users/<user>/Documents
-/// - NaK Tools/Prefix AppData Local -> prefix/drive_c/users/<user>/AppData/Local
-/// - NaK Tools/Prefix AppData Roaming -> prefix/drive_c/users/<user>/AppData/Roaming
-pub fn create_nak_tools_symlinks(tools_dir: &Path, prefix_path: &Path) {
- let users_dir = prefix_path.join("drive_c/users");
- let username = find_prefix_username(&users_dir);
- let user_dir = users_dir.join(&username);
-
- // Symlink: NaK Tools/Prefix Documents -> prefix Documents
- let documents_link = tools_dir.join("Prefix Documents");
- let documents_target = user_dir.join("Documents");
- create_or_update_symlink(&documents_link, &documents_target, "Prefix Documents");
-
- // Symlink: NaK Tools/Prefix AppData Local -> prefix AppData/Local
- let appdata_local_link = tools_dir.join("Prefix AppData Local");
- let appdata_local_target = user_dir.join("AppData/Local");
- create_or_update_symlink(&appdata_local_link, &appdata_local_target, "Prefix AppData Local");
-
- // Symlink: NaK Tools/Prefix AppData Roaming -> prefix AppData/Roaming
- let appdata_roaming_link = tools_dir.join("Prefix AppData Roaming");
- let appdata_roaming_target = user_dir.join("AppData/Roaming");
- create_or_update_symlink(&appdata_roaming_link, &appdata_roaming_target, "Prefix AppData Roaming");
-
- log_info("Created NaK Tools convenience symlinks to prefix folders");
-}
-
-/// Create or update a symlink
-fn create_or_update_symlink(link_path: &Path, target: &Path, name: &str) {
- // Remove existing symlink or file
- if link_path.exists() || fs::symlink_metadata(link_path).is_ok() {
- let _ = fs::remove_file(link_path);
- let _ = fs::remove_dir_all(link_path);
- }
-
- // Create symlink
- if let Err(e) = std::os::unix::fs::symlink(target, link_path) {
- log_warning(&format!("Failed to create {} symlink: {}", name, e));
- }
-}
-
-/// Create symlinks for all detected games
-///
-/// Convenience function that detects games and creates symlinks in one call.
-pub fn create_game_symlinks_auto(nak_prefix: &Path) -> GameScanResult {
- let result = detect_all_games();
- create_game_symlinks(nak_prefix, &result.games);
- result
-}
-
-/// Ensure only the Temp directory exists in AppData/Local
-///
-/// MO2 and other tools require AppData/Local/Temp to exist.
-/// We create only this essential directory, leaving other game-specific
-/// folders to be symlinked from game prefixes.
-pub fn ensure_temp_directory(prefix_path: &Path) {
- let users_dir = prefix_path.join("drive_c/users");
- let username = find_prefix_username(&users_dir);
- let user_dir = users_dir.join(&username);
-
- let temp_dir = user_dir.join("AppData/Local/Temp");
- if let Err(e) = fs::create_dir_all(&temp_dir) {
- log_warning(&format!("Failed to create Temp directory: {}", e));
- } else {
- log_info("Ensured AppData/Local/Temp directory exists");
- }
-}
-
-// ============================================================================
-// Internal Functions
-// ============================================================================
-
-/// Directories to skip when scanning prefix folders for symlinking.
-/// These are Wine/Proton internal or system dirs, not game data.
-const SKIP_DIRS: &[&str] = &[
- "Temp", "Microsoft", "wine", "Public", "root",
- "Application Data", "Cookies", "Local Settings",
- "NetHood", "PrintHood", "Recent", "SendTo",
- "Start Menu", "Templates", "My Documents", "My Music",
- "My Pictures", "My Videos", "Desktop", "Downloads",
- "Favorites", "Links", "Searches",
- "Contacts", "3D Objects",
-];
-
-/// Scan all subdirectories in a game prefix folder and create symlinks
-/// for each one in the corresponding NaK prefix folder.
-///
-/// Returns the number of symlinks created.
-fn scan_and_link_all(
- nak_base: &Path,
- game_base: &Path,
- label: &str,
- game_name: &str,
- _game_prefix: &Path,
-) -> usize {
- if !game_base.is_dir() {
- log_info(&format!(
- " scan_and_link_all: {} not a dir for '{}', skipping",
- game_base.display(), game_name
- ));
- return 0;
- }
-
- let Ok(entries) = fs::read_dir(game_base) else {
- log_info(&format!(
- " scan_and_link_all: cannot read {} for '{}'",
- game_base.display(), game_name
- ));
- return 0;
- };
-
- let mut count = 0;
- for entry in entries.flatten() {
- // Only symlink directories (game folders), not loose files
- if !entry.path().is_dir() {
- continue;
- }
-
- let folder_name = entry.file_name().to_string_lossy().to_string();
-
- // Skip Wine/system internal directories
- if SKIP_DIRS.iter().any(|&s| s.eq_ignore_ascii_case(&folder_name)) {
- continue;
- }
-
- // Skip if it's "My Games" and we're scanning Documents (handled separately)
- if label == "Documents" && folder_name == "My Games" {
- continue;
- }
-
- let nak_path = nak_base.join(&folder_name);
- let source_path = entry.path();
-
- if create_symlink_if_needed(&nak_path, &source_path, game_name, label, &folder_name) {
- count += 1;
- }
- }
-
- count
-}
-
-/// Create a symlink if the target doesn't already exist or is already correct.
-///
-/// Returns true if a symlink was created (or already existed correctly).
-fn create_symlink_if_needed(
- nak_path: &Path,
- source_path: &Path,
- game_name: &str,
- label: &str,
- folder_name: &str,
-) -> bool {
- // Check if target already exists
- if nak_path.exists() || fs::symlink_metadata(nak_path).is_ok() {
- // Check if it's already a symlink to the correct location
- if let Ok(target) = fs::read_link(nak_path) {
- if target == source_path {
- return true; // Already correctly linked
- }
- }
- // Something else exists here, don't overwrite
- return false;
- }
-
- // Ensure parent directory exists
- if let Some(parent) = nak_path.parent() {
- let _ = fs::create_dir_all(parent);
- }
-
- // Create the symlink
- match std::os::unix::fs::symlink(source_path, nak_path) {
- Ok(()) => {
- log_info(&format!(
- "Linked {}/{} -> {} ({})",
- label,
- folder_name,
- source_path.display(),
- game_name,
- ));
- true
- }
- Err(e) => {
- log_warning(&format!(
- "Failed to create symlink for {} ({}/{}): {}",
- game_name, label, folder_name, e
- ));
- false
- }
- }
-}
-
-/// Find the username from a Wine prefix users directory
-fn find_prefix_username(users_dir: &Path) -> String {
- if let Ok(entries) = fs::read_dir(users_dir) {
- for entry in entries.flatten() {
- let name = entry.file_name().to_string_lossy().to_string();
- if name != "Public" && name != "root" {
- return name;
- }
- }
- }
- "steamuser".to_string()
-}
-
-// ============================================================================
-// Oblivion Lowercase INI Symlinks
-// ============================================================================
-
-/// Create lowercase INI symlinks for Oblivion (some tools expect lowercase)
-pub fn create_oblivion_ini_symlinks(prefix_path: &Path) {
- let users_dir = prefix_path.join("drive_c/users");
- let username = find_prefix_username(&users_dir);
- let oblivion_dir = users_dir
- .join(&username)
- .join("Documents/My Games/Oblivion");
-
- if !oblivion_dir.exists() {
- return;
- }
-
- create_lowercase_ini_symlink(&oblivion_dir, "Oblivion.ini", "oblivion.ini");
- create_lowercase_ini_symlink(&oblivion_dir, "OblivionPrefs.ini", "oblivionprefs.ini");
-}
-
-/// Create a lowercase symlink for an INI file
-fn create_lowercase_ini_symlink(dir: &Path, original: &str, lowercase: &str) {
- let original_path = dir.join(original);
- let lowercase_path = dir.join(lowercase);
-
- // Only create if original exists and lowercase doesn't
- if original_path.exists()
- && !lowercase_path.exists()
- && fs::symlink_metadata(&lowercase_path).is_err()
- {
- // Create relative symlink (just the filename)
- if let Err(e) = std::os::unix::fs::symlink(original, &lowercase_path) {
- log_warning(&format!(
- "Failed to create lowercase symlink {} -> {}: {}",
- lowercase, original, e
- ));
- } else {
- log_info(&format!(
- "Created lowercase INI symlink: {} -> {}",
- lowercase, original
- ));
- }
- }
-}