Skip to content
Merged
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
110 changes: 107 additions & 3 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,7 @@ fn cmd_session_export(
includes: crate::export::parse_export_includes(include)?,
};
if let Some(path) = output {
ensure_parent_dir(&path)?;
let file = fs::File::create(&path)?;
crate::export::write_jsonl(&store, &options, file)?;
write_jsonl_file(&store, &options, &path)?;
} else {
let stdout = std::io::stdout();
let handle = stdout.lock();
Expand Down Expand Up @@ -984,6 +982,65 @@ fn ensure_parent_dir(path: &Path) -> Result<()> {
Ok(())
}

fn write_jsonl_file(store: &Store, options: &ExportOptions, path: &Path) -> Result<()> {
ensure_parent_dir(path)?;
let target = resolve_output_target(path)?;
let path = target.as_path();
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
#[cfg(unix)]
let mut builder = tempfile::Builder::new();
#[cfg(not(unix))]
let builder = tempfile::Builder::new();
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
let existing_permissions = match fs::metadata(path) {
Ok(metadata) => Some(fs::Permissions::from_mode(metadata.permissions().mode() & 0o777)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(error.into()),
};
#[cfg(unix)]
if existing_permissions.is_none() {
builder.permissions(fs::Permissions::from_mode(0o666));
}
let mut temp = builder.tempfile_in(parent)?;
crate::export::write_jsonl(store, options, temp.as_file_mut())?;
#[cfg(unix)]
if let Some(permissions) = existing_permissions {
temp.as_file().set_permissions(permissions)?;
}
temp.as_file().sync_all()?;
temp.persist(path).map_err(|error| error.error)?;
Comment thread
samzong marked this conversation as resolved.
Comment thread
samzong marked this conversation as resolved.
Ok(())
}

fn resolve_output_target(path: &Path) -> Result<PathBuf> {
let mut target = path.to_path_buf();
for _ in 0..40 {
match fs::symlink_metadata(&target) {
Ok(metadata) if metadata.file_type().is_symlink() => {
let link = fs::read_link(&target)?;
target = if link.is_absolute() {
link
} else {
target
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.join(link)
};
}
Ok(_) => return Ok(target),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(target),
Err(error) => return Err(error.into()),
}
}
anyhow::bail!("too many symbolic links in output path: {}", path.display())
}

fn read_tldr_file(path: &Path) -> Option<String> {
match fs::read_to_string(path) {
Ok(content) => {
Expand Down Expand Up @@ -1037,4 +1094,51 @@ mod tests {

let _ = fs::remove_file(path);
}

#[test]
fn jsonl_file_export_replaces_target_only_after_success() {
let dir = tempfile::tempdir().unwrap();
let target_path = dir.path().join("export.jsonl");
fs::write(&target_path, "keep").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&target_path, fs::Permissions::from_mode(0o640)).unwrap();
}
#[cfg(unix)]
let path = {
let path = dir.path().join("export-link.jsonl");
std::os::unix::fs::symlink("export.jsonl", &path).unwrap();
path
};
#[cfg(not(unix))]
let path = target_path.clone();
crate::db::schema::register_sqlite_vec();
let store = Store::open_in_memory().unwrap();
let mut options = ExportOptions {
session_ids: vec!["missing".to_string()],
sources: None,
time_range: TimeRange::All,
scope: ProjectScope::Global,
thread_role: None,
limit: None,
includes: ExportIncludes::full(),
};

assert!(write_jsonl_file(&store, &options, &path).is_err());
assert_eq!(fs::read_to_string(&target_path).unwrap(), "keep");

options.session_ids.clear();
write_jsonl_file(&store, &options, &path).unwrap();
assert_eq!(fs::read_to_string(&target_path).unwrap(), "");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert!(fs::symlink_metadata(&path).unwrap().file_type().is_symlink());
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 2);
assert_eq!(fs::metadata(target_path).unwrap().permissions().mode() & 0o777, 0o640);
}
#[cfg(not(unix))]
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
}
}