Skip to content
Open
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
6 changes: 3 additions & 3 deletions core/src/location/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ impl LocationManager {
accessed_at: Set(None),
indexed_at: Set(Some(now)), // Record when location root was created
permissions: Set(None),
inode: Set(inode.map(|i| i as i64)), // Use extracted inode
parent_id: Set(None), // Location root has no parent
volume_id: Set(Some(volume_id)), // Volume is required for all locations
inode: Set(inode.map(|i: i64| i as i64)), // Use extracted inode
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like inode is Option<u64> above; |i: i64| i as i64 seems like it won't compile (and a plain as cast here can truncate on overflow).

Suggested change
inode: Set(inode.map(|i: i64| i as i64)), // Use extracted inode
inode: Set(inode.and_then(|i| i.try_into().ok())), // Use extracted inode

parent_id: Set(None), // Location root has no parent
volume_id: Set(Some(volume_id)), // Volume is required for all locations
..Default::default()
};

Expand Down
21 changes: 17 additions & 4 deletions core/src/ops/indexing/phases/processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
//! entries. Processes entries in depth-first order (parents before children) within database
//! transactions, preserving ephemeral UUIDs from prior browsing sessions and validating that
//! indexing paths stay within location boundaries to prevent cross-location data corruption.

use crate::{
infra::{
db::entities::{self, directory_paths, entry_closure},
Expand Down Expand Up @@ -519,9 +518,23 @@ pub async fn run_processing_phase(
};

#[cfg(windows)]
let inode = {
use std::os::windows::fs::MetadataExt;
metadata.file_index()
let inode: Option<u64> = {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
};

// Open file to get handle for GetFileInformationByHandle
std::fs::File::open(location_root_path).ok().and_then(|file| {
Copy link
Contributor

Choose a reason for hiding this comment

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

location_root_path is a directory; File::open can fail on Windows unless you open with FILE_FLAG_BACKUP_SEMANTICS, which would make this always return None.

Suggested change
std::fs::File::open(location_root_path).ok().and_then(|file| {
// Open directory to get handle for GetFileInformationByHandle
use std::os::windows::fs::OpenOptionsExt;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(location_root_path)
.ok()
.and_then(|file| {
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let success = unsafe {
GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info)
};
if success != 0 {
Some(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
} else {
None
}
})

let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let success =
unsafe { GetFileInformationByHandle(file.as_raw_handle() as isize, &mut info) };
if success != 0 {
Some(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
} else {
None
}
})
};

#[cfg(not(any(unix, windows)))]
Expand Down
3 changes: 2 additions & 1 deletion core/src/volume/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,8 +733,9 @@ impl VolumeManager {
|| old_info.total_bytes_available != new_info.total_bytes_available
|| old_info.error_status != new_info.error_status
{
// Update the volume
// Update the volume - preserve existing ID for cache stability
let mut updated_volume = detected.clone();
updated_volume.id = existing.id;
updated_volume.update_info(new_info.clone());
current_volumes.insert(fingerprint.clone(), updated_volume.clone());

Expand Down
97 changes: 74 additions & 23 deletions core/src/volume/platform/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@ use crate::volume::{
types::{DiskType, FileSystem, MountType, Volume, VolumeDetectionConfig, VolumeFingerprint},
utils,
};
use serde::Deserialize;
use std::path::PathBuf;
use std::process::Command;
use tokio::task;
use tracing::warn;
use tracing::{debug, warn};
use uuid::Uuid;

/// Windows volume information from PowerShell/WMI
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct WindowsVolumeInfo {
pub drive_letter: Option<String>,
#[serde(rename = "FileSystemLabel")]
pub label: Option<String>,
#[serde(default)]
pub size: u64,
#[serde(default)]
pub size_remaining: u64,
#[serde(rename = "FileSystem", default)]
pub filesystem: String,
#[serde(rename = "UniqueId")]
pub volume_guid: Option<String>,
}

Expand All @@ -34,7 +41,7 @@ pub async fn detect_volumes(
let output = Command::new("powershell")
.args([
"-Command",
"Get-Volume | Select-Object DriveLetter,FileSystemLabel,Size,SizeRemaining,FileSystem | ConvertTo-Json"
"Get-Volume | Select-Object DriveLetter,FileSystemLabel,Size,SizeRemaining,FileSystem,UniqueId | ConvertTo-Json"
])
.output()
.map_err(|e| VolumeError::platform(format!("Failed to run PowerShell: {}", e)))?;
Expand All @@ -57,10 +64,55 @@ fn parse_powershell_volumes(
device_id: Uuid,
config: &VolumeDetectionConfig,
) -> VolumeResult<Vec<Volume>> {
// For now, return empty until we implement full JSON parsing
// This would require adding serde_json dependency
warn!("PowerShell JSON parsing not fully implemented yet");
Ok(Vec::new())
let trimmed = json_output.trim();
Copy link
Contributor

Choose a reason for hiding this comment

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

ConvertTo-Json can emit null (eg when no objects are returned), which would currently hard-error on the object parse path. Treating that as empty keeps volume detection resilient.

Suggested change
let trimmed = json_output.trim();
let trimmed = json_output.trim();
if trimmed.is_empty() || trimmed == "null" {
debug!("PowerShell returned empty/null output");
return Ok(Vec::new());
}

if trimmed.is_empty() {
debug!("PowerShell returned empty output");
return Ok(Vec::new());
}

// PowerShell returns a single object (not array) when there's only one volume
let volume_infos: Vec<WindowsVolumeInfo> = if trimmed.starts_with('[') {
serde_json::from_str(trimmed).map_err(|e| {
VolumeError::platform(format!("Failed to parse PowerShell JSON array: {}", e))
})?
} else {
let single: WindowsVolumeInfo = serde_json::from_str(trimmed).map_err(|e| {
VolumeError::platform(format!("Failed to parse PowerShell JSON object: {}", e))
})?;
vec![single]
};

debug!("Parsed {} volumes from PowerShell", volume_infos.len());

let mut volumes = Vec::new();
for info in volume_infos {
// Skip volumes without drive letters or with zero size (unless they have a label)
if info.drive_letter.is_none() {
debug!(
"Skipping volume without drive letter: label={:?}. guid={:?}",
info.label, info.volume_guid
);
continue;
}

if info.size == 0 {
debug!("Skipping volume with zero size: {:?}", info.drive_letter);
continue;
}

match create_volume_from_windows_info(info, device_id) {
Ok(volume) => {
if should_include_volume(&volume, config) {
volumes.push(volume);
}
}
Err(e) => {
warn!("Failed to create volume from Windows info: {}", e);
}
}
}

Ok(volumes)
}

/// Fallback method using wmic or fsutil
Expand Down Expand Up @@ -217,19 +269,20 @@ pub fn create_volume_from_windows_info(
info: WindowsVolumeInfo,
device_id: Uuid,
) -> VolumeResult<Volume> {
let mount_path = if let Some(drive_letter) = &info.drive_letter {
PathBuf::from(format!("{}:\\", drive_letter))
} else {
PathBuf::from("C:\\") // Default fallback
let mount_path = match &info.drive_letter {
Some(drive_letter) => PathBuf::from(format!("{}:\\", drive_letter)),
None => {
return Err(VolumeError::platform(format!(
"Volume without drive letter reached create_volume_from_windows_info: {:?}",
info.label
)))
}
};

let name = info.label.unwrap_or_else(|| {
if let Some(drive) = &info.drive_letter {
format!("Local Disk ({}:)", drive)
} else {
"Unknown Drive".to_string()
}
});
let name = match &info.label {
Some(label) if !label.is_empty() => label.clone(),
_ => format!("Local Disk ({}:)", info.drive_letter.as_ref().unwrap()),
};

let file_system = utils::parse_filesystem_type(&info.filesystem);
let mount_type = if let Some(drive) = &info.drive_letter {
Expand All @@ -254,11 +307,9 @@ pub fn create_volume_from_windows_info(
}
crate::volume::types::VolumeType::Network => {
// Use mount path as backend identifier for network volumes
let backend_id = info
.volume_guid
.as_deref()
.unwrap_or(&mount_path.to_string_lossy());
VolumeFingerprint::from_network_volume(backend_id, &mount_path.to_string_lossy())
let path_lossy = mount_path.to_string_lossy();
let backend_id = info.volume_guid.as_deref().unwrap_or(&path_lossy);
VolumeFingerprint::from_network_volume(backend_id, &path_lossy)
}
_ => {
// Primary, UserData, Secondary, System, Virtual, Unknown
Expand Down
3 changes: 2 additions & 1 deletion xtask/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ pub fn generate_cargo_config(
.unwrap_or_else(|_| {
println!(" ⚠️ Android NDK not found. Android builds will not work.");
String::new()
});
})
.replace('\\', "\\\\");

// Build context for mustache
let context = ConfigContext {
Expand Down