flxzt/rnote · error · anyhow::Error
Can't create-replace file that has no path.
Error message
Can't create-replace file that has no path.
What it means
This error is thrown by create_replace_file_future when the given gio::File has no filesystem path. gio::File can represent non-native locations (e.g. GVfs/remote URIs like trash://, sftp://, http://) for which path() returns None. Since the function writes bytes with async_fs::OpenOptions, which requires a native path, it fails fast instead of attempting a doomed write.
Solutions
- Check file.path() (or file.query_info for G_FILE_ATTRIBUTE_LOCAL_PATH) is Some before calling create_replace_file_future, and surface a user-facing message for non-local files.
- Use file.has_uri_scheme("file") to verify the File is native; for remote files, copy them to a local temp file (file.copy) and operate on that instead.
- If saving from Trash, restore the file to a real location first, then create-replace at the restored path.
- Normalize command-line/DBus-provided arguments with gio::File::new_for_path after validating the input is an absolute filesystem path rather than new_for_commandline_arg/URI.
Example fix
// before
let file = gio::File::for_uri("trash:///note.rnote");
create_replace_file_future(bytes, &file).await?;
// after
let file = gio::File::for_uri("trash:///note.rnote");
if file.path().is_none() {
anyhow::bail!("cannot save: {} is not a local file", file.uri());
}
create_replace_file_future(bytes, &file).await?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_local_file(file: &gio::File) -> anyhow::Result<std::path::PathBuf> {
file.path().ok_or_else(|| anyhow::anyhow!(
"'{}' is not a local file and cannot be written directly", file.uri()
))
} Type guard
fn is_writable_local_file(file: &gio::File) -> bool {
file.path().map(|p| p.is_file() || !p.exists()).unwrap_or(false)
} Try / catch
match create_replace_file_future(bytes, &file).await {
Ok(()) => {},
Err(e) if e.to_string().contains("no path") => {
// non-native gio::File: offer 'Save As' to a local location
}
Err(e) => return Err(e),
} Prevention
- Only construct gio::File via new_for_path for local saves; treat URIs as needing a copy-to-local step first
- Check file.has_uri_scheme("file") early in the open/load flow and warn the user before edits are made
- Re-check file.path() right before save in case the mount was unmounted in between
- Offer 'Save As...' as fallback whenever the source document has no native path
When it happens
Trigger: Calling create_replace_file_future with a gio::File constructed from a non-file URI (trash:///, network://, sftp://, http://), or from a File whose path has been unmounted/deleted, or a File created via gio::File::new_for_commandline_arg with a non-path argument; any call where file.path() returns None.
Common situations: Saving a document from the Trash or a recently-used remote location, drag-and-drop of a file from a remote/GVfs share into the app, launching the app with a URI instead of a local path (e.g. via DBus activation or command line), or the target file's mount having been disconnected before save.
Related errors
- Could not get a path for file
- Failed to get file stem
- Failed to get file name from output-file
- Failed to get file stem from rnote file
- Expected directory, found file
AI-assisted analysis of flxzt/rnote@bbc5354502 (2026-09-08).
Data as JSON: /api/errors/7a6a48c059c47193.
Report an issue: GitHub.
Appendix: source
Thrown at crates/rnote-ui/src/utils.rs:34
pub(crate) const FILE_DUP_SUFFIX_DELIM_REGEX: &str = r"\s-\s";
/// An asynchronous adaptation of the [`rnote_engine::utils::atomic_save_to_file`] function.
pub(crate) async fn atomic_save_to_file_future<Q>(filepath: Q, bytes: Vec<u8>) -> anyhow::Result<()>
where
Q: AsRef<std::path::Path>,
{
let filepath = filepath.as_ref().to_path_buf();
blocking::unblock(move || rnote_engine::utils::atomic_save_to_file(filepath, bytes)).await
}
/// Create a new file or replace if it already exists, asynchronously.
pub(crate) async fn create_replace_file_future(
bytes: Vec<u8>,
file: &gio::File,
) -> anyhow::Result<()> {
let Some(file_path) = file.path() else {
return Err(anyhow::anyhow!(
"Can't create-replace file that has no path."
));
};
let mut write_file = async_fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&file_path)
.await
.context(format!(
"Failed to create/open/truncate file for path '{}'",
file_path.display()
))?;
write_file.write_all(&bytes).await.context(format!(
"Failed to write bytes to file with path '{}'",
file_path.display()
))?;
write_file.sync_all().await.context(format!(View on GitHub (pinned to bbc5354502)