collective

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

diff --git a/crates/platform/src/staging.rs b/crates/platform/src/staging.rs--- a/crates/platform/src/staging.rs+++ b/crates/platform/src/staging.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) => {diff --git a/crates/platform/src/staging/tests.rs b/crates/platform/src/staging/tests.rs--- a/crates/platform/src/staging/tests.rs+++ b/crates/platform/src/staging/tests.rs@@ -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();+}