Browse / github.com/storytold/pdfcraft / patch
fix(platform): preserve visibility when saving files to network paths (#5001)
[Bug] Saving PDF to network path hides the target folder and file ### ๐ Bug Description When the software attempts to save a generated PDF file to a network share/path, the file is not visible to users or other applications. Furthermore, the target destination folder itself appears to become hidden or inaccessible immediately after the save operation. ### ๐ Steps to Reproduce 1. Configure the software output path to a network share (e.g., `\\server\share\folder` or mapped drive). 2. Trigger an action that generates and saves a PDF. 3. Check the network folder using File Explorer / terminal. 4. **Observe:** The PDF file is missing, and the folder where it wa
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- YusufMntm (imported from pull request #5048, open)
- Base
e9fc890a862ea2d8c895ea32ddecda98580483cd- Size
- +120 โ4 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
d1b51cbfd82a689a43cbbde0
How it went
No other operator has reported using it yet.
Diff
raw ยท crates/platform/src/staging.rs crates/platform/src/staging/tests.rs
@@ -16,6 +16,59 @@ pub enum StagingName { TagThenSuffix, } +// Thread-local storage for testing mapped network drives (#5001).+#[cfg(test)]+thread_local! {+ static MAPPED_DRIVES_FOR_TEST: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };+}++/// Registers a drive prefix (e.g. `"Z:\\"`) as a simulated mapped network drive for tests (#5001).+#[cfg(test)]+pub(crate) fn set_simulated_mapped_drive_for_test(drive_prefix: &str) {+ MAPPED_DRIVES_FOR_TEST.with(|cell| cell.borrow_mut().push(drive_prefix.to_string()));+}++/// Clears simulated mapped network drives registered for tests (#5001).+#[cfg(test)]+pub(crate) fn clear_simulated_mapped_drives_for_test() {+ MAPPED_DRIVES_FOR_TEST.with(|cell| cell.borrow_mut().clear());+}++/// Whether `path` is a Windows network/UNC path or mapped network drive (#5001).+///+/// On Windows SMB shares (direct UNC paths or drives mapped to network shares),+/// dotfiles are often automatically hidden by server configurations, which can+/// hide the target directory or file after an atomic rename.+pub fn is_network_path(path: &Path) -> bool {+ let s = path.to_string_lossy();+ if s.starts_with(r"\\") || s.starts_with("//") || s.starts_with(r"\\?\UNC\") {+ return true;+ }+ #[cfg(test)]+ {+ let matched = MAPPED_DRIVES_FOR_TEST.with(|cell| cell.borrow().iter().any(|prefix| s.starts_with(prefix)));+ if matched {+ return true;+ }+ }+ #[cfg(windows)]+ {+ // On Windows, canonicalize resolves mapped network drives (e.g. Z:\) to \\?\UNC\...+ if let Ok(canonical) = std::fs::canonicalize(path)+ && canonical.to_string_lossy().starts_with(r"\\?\UNC\")+ {+ return true;+ }+ if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty())+ && let Ok(canonical) = std::fs::canonicalize(parent)+ && canonical.to_string_lossy().starts_with(r"\\?\UNC\")+ {+ return true;+ }+ }+ false+}+ /// Create a new, empty staging file in `dir` for the file `name`, one name per suffix. It is /// opened with `create_new`, which fails if anything already has the name (a file, a hard link, /// a symbolic link even when dangling, a folder), on Windows as everywhere else; such a name is@@ -52,10 +105,13 @@ fn create_staging_as( } stem.push(c); }+ let is_unc = is_network_path(dir); for suffix in suffixes.into_iter().take(STAGING_ATTEMPTS) {- let tmp = dir.join(match form {- StagingName::SuffixThenTag => format!(".{stem}.{suffix:016x}.pdfcraft-tmp"),- StagingName::TagThenSuffix => format!(".{stem}.pdfcraft-{suffix:016x}.tmp"),+ let tmp = dir.join(match (is_unc, form) {+ (true, StagingName::SuffixThenTag) => format!("{stem}.{suffix:016x}.pdfcraft-tmp"),+ (true, StagingName::TagThenSuffix) => format!("{stem}.pdfcraft-{suffix:016x}.tmp"),+ (false, StagingName::SuffixThenTag) => format!(".{stem}.{suffix:016x}.pdfcraft-tmp"),+ (false, StagingName::TagThenSuffix) => format!(".{stem}.pdfcraft-{suffix:016x}.tmp"), }); let mut options = std::fs::OpenOptions::new(); options.write(true).create_new(true);@@ -68,7 +124,14 @@ fn create_staging_as( #[cfg(windows)] if let Some(keep) = keep { use std::os::windows::fs::{MetadataExt as _, OpenOptionsExt as _};- options.attributes(keep.file_attributes() & KEPT_ATTRIBUTES);+ let mut attrs = keep.file_attributes() & KEPT_ATTRIBUTES;+ // On network shares (UNC), do not propagate hidden/system flags (#5001).+ if is_unc {+ attrs &= !KEPT_ATTRIBUTES;+ }+ if attrs != 0 {+ options.attributes(attrs);+ } } match options.open(&tmp) { Ok(file) => {@@ -448,3 +448,56 @@ fn a_replaced_link_keeps_its_own_attributes_not_its_targets() { assert_ne!(std::fs::metadata(&referent).unwrap().file_attributes() & HIDDEN, 0, "and still hidden"); let _ = std::fs::remove_dir_all(&dir); }++/// #5001: UNC network paths are recognized properly.+#[test]+fn unc_network_paths_are_recognized() {+ assert!(is_network_path(Path::new(r"\\server\share\file.pdf")));+ assert!(is_network_path(Path::new(r"\\192.168.1.10\docs\file.pdf")));+ assert!(is_network_path(Path::new("//server/share/file.pdf")));+ assert!(is_network_path(Path::new(r"\\?\UNC\server\share\file.pdf")));+ assert!(!is_network_path(Path::new(r"C:\Users\user\file.pdf")));+ assert!(!is_network_path(Path::new(r"D:\file.pdf")));+ assert!(!is_network_path(Path::new("/tmp/file.pdf")));+ assert!(!is_network_path(Path::new("relative/file.pdf")));+}++/// #5001: staging names on network paths avoid leading dot to prevent SMB servers hiding them.+#[test]+fn network_paths_avoid_leading_dot_in_staging_names() {+ let dir = Path::new(r"\\server\share\folder");+ let name = "document.pdf";+ let is_unc = is_network_path(dir);+ assert!(is_unc);+ let suffix = 0x42u64;+ let name_suffix_tag = if is_unc { format!("{name}.{suffix:016x}.pdfcraft-tmp") } else { format!(".{name}.{suffix:016x}.pdfcraft-tmp") };+ assert!(!name_suffix_tag.starts_with('.'));+ assert_eq!(name_suffix_tag, "document.pdf.0000000000000042.pdfcraft-tmp");++ let name_tag_suffix = if is_unc { format!("{name}.pdfcraft-{suffix:016x}.tmp") } else { format!(".{name}.pdfcraft-{suffix:016x}.tmp") };+ assert!(!name_tag_suffix.starts_with('.'));+ assert_eq!(name_tag_suffix, "document.pdf.pdfcraft-0000000000000042.tmp");+}++/// #5001: mapped network drive destinations avoid dot-prefixed staging names and do not apply hidden/system flags.+#[test]+fn mapped_network_drives_avoid_leading_dot_and_hidden_attributes() {+ set_simulated_mapped_drive_for_test(r"Z:\");+ let mapped_dir = Path::new(r"Z:\documents");+ assert!(is_network_path(mapped_dir));+ assert!(is_network_path(Path::new(r"Z:\documents\contract.pdf")));++ let is_unc = is_network_path(mapped_dir);+ assert!(is_unc);+ let suffix = 0x99u64;+ let name = "contract.pdf";+ let name_suffix_tag = if is_unc { format!("{name}.{suffix:016x}.pdfcraft-tmp") } else { format!(".{name}.{suffix:016x}.pdfcraft-tmp") };+ assert!(!name_suffix_tag.starts_with('.'));+ assert_eq!(name_suffix_tag, "contract.pdf.0000000000000099.pdfcraft-tmp");++ let name_tag_suffix = if is_unc { format!("{name}.pdfcraft-{suffix:016x}.tmp") } else { format!(".{name}.pdfcraft-{suffix:016x}.tmp") };+ assert!(!name_tag_suffix.starts_with('.'));+ assert_eq!(name_tag_suffix, "contract.pdf.pdfcraft-0000000000000099.tmp");++ clear_simulated_mapped_drives_for_test();+}