From 2f42c743b903f59950111fdf496ecbe9195c0c23 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:29:59 +0200 Subject: [PATCH] Issue #157: the Windows portable zip keeps settings, logs and recovery beside pdfcraft.exe (#325) * Issue #157: the Windows portable zip keeps settings, logs and recovery beside pdfcraft.exe The portable zip had no portable.txt, and the app had no portable mode: it always wrote %APPDATA%\PdfCraft and %LOCALAPPDATA%\PdfCraft. Now portable.txt (or PdfCraft.portable) beside the executable puts the settings, log files, crash recovery and new digital IDs in PdfCraftData next to it, as PhotoCraft does, and the zip ships portable.txt. An unwritable PdfCraftData falls back to the per-user folders and says so. * Issue #157: update ATTRIBUTION.toml SHA-256 for the translation catalogs this PR changes --------- Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 14 +-- README.md | 4 + apps/pdfcraft/src/main.rs | 21 +++- crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/portable.rs | 171 ++++++++++++++++++++++++++++ crates/ui-egui/src/recovery.rs | 6 +- crates/ui-egui/src/sign_ui.rs | 3 +- docs/development.md | 9 +- docs/releasing.md | 6 +- packaging/windows/package.ps1 | 6 +- packaging/windows/portable.txt | 9 ++ 18 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 crates/ui-egui/src/portable.rs create mode 100644 packaging/windows/portable.txt diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index b70f6b798..4cc867d72 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8792b00c7e9a12a247cbf6d4758d8583972f861010f4e4ed52949978e5a380e7" +sha256 = "e14c411ac69fd96279de51aaaa9589b8810e2970c3c212585bf47104f7c2cbd7" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9429421177790a0492b1513102823a9aa5dbd34c7a850a737792602d4d19b8d9" +sha256 = "27715eb6c29938938a39880f5ab6f3b0aa60ba6d0940609af1bfb96f1fd124de" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9a655daa584e3a5d049dbaaa30229ae7a931984b0f1c7f0177fde2ccdd6804ca" +sha256 = "3bc4e3c1d1a50c50d6be907f62e1fa2fd85ea719f1b65af21eeadcbbc7db5e44" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "777a7f441950714a61d41e5a44405209aa85bfdd4b46e27090544eb9122d715f" +sha256 = "3a46800299b34558642676d0d67ff31619d8e9bbeb2ba83e713a108f544f2d82" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cb3759b57445a3948a707aaf9423338053c7e7ad68f19f52583527dae3dfabee" +sha256 = "dad2013f25795cafc93d89c9ff4c0cc510ba844628b27469fe9b38c2fce5e0ea" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b131025cb6c14135440968f08ab6334d70dd3b20271539471878d3825ecc9629" +sha256 = "c41079a024a360951e0f935b14b6d18a16c8478daac21bc7a1c57eb73b6b3590" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c9879375eed52003bb16790e687808fe0d50dd9405390be383eab66cd513291c" +sha256 = "f42c77a513e83f4d48e1792bcba8dfe60b7453daf1b240d0a7ffb6d276ce98e3" [[asset]] path = "assets/icons/accessibility.svg" diff --git a/README.md b/README.md index a80469bcd..24db83c6f 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,10 @@ The honest assessment by area, what's lacking and where we're going are in **[RO Installers and executables are code-signed. +The portable zip runs from any folder, a USB stick included. Its `portable.txt` keeps the settings, +logs and crash recovery in a `PdfCraftData` folder next to `pdfcraft.exe`, so nothing is written to +`%APPDATA%`; delete that file to use the normal per-user folders. + The MSI installs for all users and requires administrator privileges. For unattended deployment without a desktop shortcut, run from an elevated terminal: diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 9bbf2f5ac..bf3b48833 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -39,16 +39,24 @@ const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256 /// The app was called PrintCraft before; settings saved then are under this key. const LEGACY_STORAGE_KEY: &str = "printcraft"; -/// The settings folder: `app.ron` and the `logs` folder (docs/development.md). eframe would -/// otherwise derive it from the app id; keep it under "PdfCraft". +/// The settings folder: `app.ron` and the `logs` folder (docs/development.md). In portable mode +/// it is `PdfCraftData` beside the executable (#157). eframe would otherwise derive it from the +/// app id; keep it under "PdfCraft". fn settings_dir() -> Option { + if let Some(dir) = pdfcraft_ui_egui::portable::data_dir() { + return Some(dir.to_path_buf()); + } eframe::storage_dir("PdfCraft") } /// Move the settings and crash-recovery folders of the app's former name, PrintCraft, to the new /// name once, so an upgrade keeps recent files, preferences and unsaved work. Best effort: a -/// folder is left alone when the new one already exists or the move fails. +/// folder is left alone when the new one already exists or the move fails. A portable copy leaves +/// the per-user folders alone. fn migrate_legacy_folders() { + if pdfcraft_ui_egui::portable::data_dir().is_some() { + return; + } let mut moves = vec![(eframe::storage_dir("PrintCraft"), settings_dir())]; // Recovery lives in the settings folder except on Windows, where it is under %LOCALAPPDATA%. if cfg!(windows) { @@ -173,6 +181,13 @@ fn main() -> eframe::Result { if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); } + // A portable marker whose data folder can't be written (#157): say where settings went. + if let Some(w) = &pdfcraft_ui_egui::portable::current().unwritable { + app.notify_fmt( + "Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead.", + &[("folder", &w.folder.display().to_string()), ("error", &w.error)], + ); + } if create_images { if let Err(e) = app.begin_image_import_paths(&files) { app.notify(e); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 1f37708f3..704645f63 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -108,3 +108,4 @@ Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Používá se, když PDF nepožaduje rozložení ani zvětšení. Plynulé posouvání mezi stránkami neskáče. Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Vyberte obrázek PNG nebo JPEG (nejvýše 4 MiB). Průhlednost bude zachována. Couldn't import the signature image: {e} Obrázek podpisu nelze importovat: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Přenosný režim je vypnutý: do složky {folder} nelze zapisovat ({error}). Nastavení se místo toho ukládá do vaší uživatelské složky. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f4a560c08..334c552a4 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1619,6 +1619,7 @@ Created a comment summary Se ha creado un resumen de comentarios Couldn't open {name}: {e} No se pudo abrir {name}: {e} Couldn't read {name}: {e} No se pudo leer {name}: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. El modo portátil está desactivado: no se puede escribir en {folder} ({error}). La configuración se guarda en tu carpeta de usuario. Couldn't save: {e} No se pudo guardar: {e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) La descarga de archivos adjuntos en la versión web llegará con M3.10 ({n} bytes listos) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 28391f9ac..01ef070d1 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1560,6 +1560,7 @@ Created a comment summary Un résumé des commentaires a été créé Couldn't open {name}: {e} Impossible d'ouvrir {name} : {e} Couldn't read {name}: {e} Impossible de lire {name} : {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Le mode portable est désactivé : impossible d'écrire dans {folder} ({error}). Les réglages sont conservés dans votre dossier utilisateur. Couldn't save: {e} Impossible d'enregistrer : {e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Le téléchargement des pièces jointes sur le web arrivera avec M3.10 ({n} octets prêts) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 342955dc2..0e2fb0869 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1625,6 +1625,7 @@ Created a comment summary 注釈の一覧を作成しました Couldn't open {name}: {e} {name} を開けませんでした: {e} Couldn't read {name}: {e} {name} を読み取れませんでした: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. ポータブルモードは無効です: {folder} に書き込めません ({error})。設定は代わりにユーザーフォルダーに保存されます。 Couldn't save: {e} 保存できませんでした: {e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web 版の添付ファイルのダウンロードは M3.10 で対応予定です({n} バイト準備済み) `{command}` {when} `{command}`: {when} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 0cbc9a887..7637345c1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -62,3 +62,4 @@ Auto Automático Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Escolha uma imagem PNG ou JPEG (até 4 MiB). A transparência é preservada. Couldn't import the signature image: {e} Não foi possível importar a imagem da assinatura: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. O modo portátil está desativado: não é possível gravar em {folder} ({error}). As configurações ficam na sua pasta de usuário. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 583acd5f8..e7b99f878 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1576,6 +1576,7 @@ Created a comment summary 已创建注释摘要 Couldn't open {name}: {e} 无法打开{name}:{e} Couldn't read {name}: {e} 无法读取{name}:{e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. 便携模式已关闭:无法写入{folder}({error})。设置改为保存在你的用户文件夹中。 Couldn't save: {e} 无法保存:{e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web 端附件下载将于 M3.10 到来({n} 字节已就绪) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index adea61bba..d1762eb78 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1534,6 +1534,7 @@ Created a comment summary 已建立註解摘要 Couldn't open {name}: {e} 無法開啟{name}:{e} Couldn't read {name}: {e} 無法讀取{name}:{e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. 可攜模式已關閉:無法寫入{folder}({error})。設定改為儲存在你的使用者資料夾中。 Couldn't save: {e} 無法儲存:{e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web 端附件下載將於 M3.10 到來({n} 位元組已就緒) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f1e857f5a..9e29b3c0c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -84,6 +84,7 @@ pub mod i18n; /// The longest author name kept (Preferences ▸ Identity, restored settings). pub(crate) const MAX_AUTHOR_CHARS: usize = 200; +pub mod portable; mod protect; mod recovery; #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/ui-egui/src/portable.rs b/crates/ui-egui/src/portable.rs new file mode 100644 index 000000000..fefa268e0 --- /dev/null +++ b/crates/ui-egui/src/portable.rs @@ -0,0 +1,171 @@ +//! Portable mode (#157): a marker file beside the executable ([`MARKERS`]) keeps the settings, +//! log files, crash-recovery autosaves and new digital IDs in `/PdfCraftData`, so a +//! portable copy (on a USB stick, say) writes nothing to `%APPDATA%` or `%LOCALAPPDATA%`. The +//! Windows portable zip ships with `portable.txt`; the MSI doesn't. PhotoCraft uses the same +//! scheme (`photocraft/apps/photocraft/src/app_dirs.rs`). +//! +//! If that folder can't be written (read-only medium, Program Files), portable mode stays off, the +//! per-user folders are used and the app says why. + +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +/// Files beside the executable that switch on portable mode (either one; contents are ignored). +pub const MARKERS: [&str; 2] = ["portable.txt", "PdfCraft.portable"]; +/// The data folder created beside the executable in portable mode. +pub const DATA_DIR: &str = "PdfCraftData"; + +/// The outcome of looking for a portable marker. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Portable { + /// `/PdfCraftData` when portable mode is on, `None` otherwise. + pub dir: Option, + /// Set when a marker was found but its data folder can't be written. + pub unwritable: Option, +} + +/// A marker was found, but the data folder beside it can't be created or written. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Unwritable { + pub marker: PathBuf, + pub folder: PathBuf, + /// The operating system's error text. + pub error: String, +} + +impl std::fmt::Display for Unwritable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "portable mode is off: {} was found, but {} can't be written ({}); settings are kept in the user folder instead", + self.marker.display(), + self.folder.display(), + self.error + ) + } +} + +/// Portable mode for this process, resolved once (the check probes the disk). +pub fn current() -> &'static Portable { + static CURRENT: OnceLock = OnceLock::new(); + CURRENT.get_or_init(|| { + let exe = std::env::current_exe().ok(); + let portable = resolve(exe.as_deref().and_then(Path::parent)); + if let Some(dir) = &portable.dir { + log::info!("portable mode: settings are kept in {}", dir.display()); + } + if let Some(unwritable) = &portable.unwritable { + log::warn!("{unwritable}"); + } + portable + }) +} + +/// The portable data folder, when portable mode is on. +pub fn data_dir() -> Option<&'static Path> { + current().dir.as_deref() +} + +/// Look for a marker in `exe_dir` and, when one is there, create its data folder beside it. +pub fn resolve(exe_dir: Option<&Path>) -> Portable { + let Some((exe_dir, marker)) = exe_dir.and_then(|d| find_marker(d).map(|m| (d, m))) else { + return Portable::default(); + }; + let folder = exe_dir.join(DATA_DIR); + match ensure_writable(&folder) { + Ok(()) => Portable { dir: Some(folder), unwritable: None }, + Err(e) => Portable { dir: None, unwritable: Some(Unwritable { marker, folder, error: e.to_string() }) }, + } +} + +/// The first marker file in `exe_dir`, if any. A folder with a marker's name is not a marker. +pub fn find_marker(exe_dir: &Path) -> Option { + MARKERS.iter().map(|m| exe_dir.join(m)).find(|p| p.is_file()) +} + +/// Create `dir` and prove a file can be written in it (a folder can exist yet be read-only). +fn ensure_writable(dir: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dir)?; + let probe = dir.join(".write-test"); + std::fs::write(&probe, b"")?; + // A leftover probe is harmless; don't fail portable mode over it. + let _ = std::fs::remove_file(&probe); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn temp(tag: &str) -> PathBuf { + static N: AtomicUsize = AtomicUsize::new(0); + let d = std::env::temp_dir().join(format!("pdfcraft-portable-{tag}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed))); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + #[test] + fn no_marker_leaves_portable_mode_off() { + let exe = temp("off"); + assert_eq!(resolve(Some(&exe)), Portable::default()); + assert!(!exe.join(DATA_DIR).exists(), "no data folder without a marker"); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn either_marker_keeps_the_data_beside_the_exe() { + for marker in MARKERS { + let exe = temp("on"); + std::fs::write(exe.join(marker), b"").unwrap(); + let p = resolve(Some(&exe)); + assert_eq!(p.unwritable, None, "{marker}"); + let dir = p.dir.unwrap(); + assert_eq!(dir, exe.join(DATA_DIR)); + assert!(dir.is_dir(), "the data folder is created"); + assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "the write probe is removed"); + let _ = std::fs::remove_dir_all(&exe); + } + } + + #[test] + fn an_existing_data_folder_is_reused() { + let exe = temp("reuse"); + std::fs::write(exe.join("portable.txt"), b"PdfCraft portable mode").unwrap(); + std::fs::create_dir_all(exe.join(DATA_DIR)).unwrap(); + std::fs::write(exe.join(DATA_DIR).join("app.ron"), b"()").unwrap(); + assert_eq!(resolve(Some(&exe)).dir, Some(exe.join(DATA_DIR))); + assert_eq!(std::fs::read(exe.join(DATA_DIR).join("app.ron")).unwrap(), b"()", "existing settings are kept"); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn a_marker_folder_is_not_a_marker() { + let exe = temp("dir-marker"); + std::fs::create_dir_all(exe.join("portable.txt")).unwrap(); + assert_eq!(resolve(Some(&exe)), Portable::default()); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn unwritable_data_folder_turns_portable_mode_off_and_says_why() { + let exe = temp("unwritable"); + std::fs::write(exe.join("portable.txt"), b"").unwrap(); + // A file where the data folder should go: it can't be created, on every platform. + std::fs::write(exe.join(DATA_DIR), b"in the way").unwrap(); + let p = resolve(Some(&exe)); + assert_eq!(p.dir, None); + let unwritable = p.unwritable.unwrap(); + assert_eq!(unwritable.marker, exe.join("portable.txt")); + assert_eq!(unwritable.folder, exe.join(DATA_DIR)); + let message = unwritable.to_string(); + assert!(message.contains("portable mode is off") && message.contains("can't be written"), "{message}"); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn no_exe_dir_leaves_portable_mode_off() { + assert_eq!(resolve(None), Portable::default()); + } +} diff --git a/crates/ui-egui/src/recovery.rs b/crates/ui-egui/src/recovery.rs index 8c2217e7b..df6b8bddb 100644 --- a/crates/ui-egui/src/recovery.rs +++ b/crates/ui-egui/src/recovery.rs @@ -40,10 +40,14 @@ impl RecoveryStore { Self { dir: dir.into() } } - /// The platform's per-user data folder: `~/Library/Application Support/PdfCraft/Recovery` + /// `Recovery` in the portable data folder in portable mode ([`crate::portable`]), otherwise + /// the platform's per-user data folder: `~/Library/Application Support/PdfCraft/Recovery` /// (macOS), `%LOCALAPPDATA%\PdfCraft\Recovery` (Windows), or /// `$XDG_DATA_HOME/pdfcraft/recovery` / `~/.local/share/pdfcraft/recovery` (others). pub fn default_dir() -> Option { + if let Some(dir) = crate::portable::data_dir() { + return Some(dir.join("Recovery")); + } let env = |k: &str| std::env::var_os(k).filter(|v| !v.is_empty()).map(PathBuf::from); if cfg!(target_os = "macos") { env("HOME").map(|h| h.join("Library/Application Support/PdfCraft/Recovery")) diff --git a/crates/ui-egui/src/sign_ui.rs b/crates/ui-egui/src/sign_ui.rs index a43ff478a..a7dc7fb4c 100644 --- a/crates/ui-egui/src/sign_ui.rs +++ b/crates/ui-egui/src/sign_ui.rs @@ -157,7 +157,8 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, xf: &PageXform, p } } -/// Where new digital IDs are saved: next to the recovery folder (`…/PdfCraft/Digital IDs`). +/// Where new digital IDs are saved: next to the recovery folder (`…/PdfCraft/Digital IDs`, or +/// `PdfCraftData/Digital IDs` in portable mode). fn id_dir() -> Option { crate::recovery::RecoveryStore::default_dir().and_then(|d| d.parent().map(|p| p.join("Digital IDs"))) } diff --git a/docs/development.md b/docs/development.md index adcfd9732..aa46e0cd1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -8,9 +8,12 @@ collects what the desktop app reads from its environment and where it writes its The desktop app writes its `log` records to standard error and to `logs/pdfcraft.log` in its settings folder, next to `app.ron`: Linux and FreeBSD `~/.local/share/pdfcraft/logs/` (or `$XDG_DATA_HOME/pdfcraft/logs/`), macOS `~/Library/Application Support/PdfCraft/logs/`, Windows -`%APPDATA%\PdfCraft\data\logs\`. A start from a desktop menu, Finder or the Start menu has no -terminal, so this file is what to attach to a bug report: a failed autosave, a page that would not -render, a render worker that could not start and the report of an internal error all land there. +`%APPDATA%\PdfCraft\data\logs\`. In portable mode (a `portable.txt` file next to the executable, as +the Windows portable zip ships) the settings folder is `PdfCraftData\` beside the executable, and +crash recovery and new digital IDs go there too. A start from a desktop menu, Finder or the Start +menu has no terminal, so this file is what to attach to a bug report: a failed autosave, a page that +would not render, a render worker that could not start and the report of an internal error all land +there. Each launch moves the previous log to `pdfcraft.1.log` (and that one to `pdfcraft.2.log`), so the log of a run that crashed survives the next start. The file stops growing at 16 MiB. `--version` writes no file. diff --git a/docs/releasing.md b/docs/releasing.md index 1b5616440..ec5e3b1d0 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -113,8 +113,10 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable. child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install smoke test checks that both all-users shortcuts point at the installed `pdfcraft.exe` and are removed on uninstall, and that `INSTALLDESKTOPSHORTCUT=0` skips the desktop one. -- The portable zip holds both executables, the README, the licences, and the OFL licence of each - embedded craft-fonts family. +- The portable zip holds both executables, the README, the licences, the OFL licence of each + embedded craft-fonts family, and `portable.txt`. That marker beside `pdfcraft.exe` keeps the + settings, logs, crash recovery and new digital IDs in `PdfCraftData\` next to the exe instead of + `%APPDATA%` and `%LOCALAPPDATA%` (`crates/ui-egui/src/portable.rs`). - **Signing:** `packaging/windows/sign.ps1` signs both executables and the MSI with `signtool` (SHA-256, RFC 3161 timestamp), from a `.pfx` (`WINDOWS_CERTIFICATE`) or Azure Trusted Signing (`AZURE_*`), whichever is configured. It is the one place to change when signing changes. diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index f6fc6a470..14588668d 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION Produces, in $env:DIST (default: dist/release): pdfcraft--windows-.msi per-machine installer (WiX v5) - pdfcraft--windows--portable.zip pdfcraft.exe + pdfcraft-cli.exe + pdfcraft--windows--portable.zip pdfcraft.exe + pdfcraft-cli.exe + portable.txt The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a @@ -122,6 +122,10 @@ if ($env:CRAFT_FONTS_DIR) { if (Test-Path $ofl) { Copy-Item $ofl (Join-Path $Portable "OFL-$($_.Name).txt") } } } +# portable.txt beside pdfcraft.exe switches on portable mode: settings, logs, recovery files and new +# digital IDs go to PdfCraftData\ next to the exe instead of %APPDATA% (#157; see +# crates/ui-egui/src/portable.rs). +Copy-Item (Join-Path $PSScriptRoot 'portable.txt') $Portable $Zip = Join-Path $Dist "pdfcraft-$Version-windows-$Arch-portable.zip" Remove-Item -Force $Zip -ErrorAction SilentlyContinue Compress-Archive -Path $Portable -DestinationPath $Zip diff --git a/packaging/windows/portable.txt b/packaging/windows/portable.txt new file mode 100644 index 000000000..9586f9306 --- /dev/null +++ b/packaging/windows/portable.txt @@ -0,0 +1,9 @@ +PdfCraft portable mode + +While this file sits next to pdfcraft.exe, PdfCraft keeps its settings, log files, crash-recovery +autosaves and new digital IDs in the PdfCraftData folder beside it, and writes nothing to +%APPDATA% or %LOCALAPPDATA%. Copy the whole folder to move PdfCraft with its settings. + +Delete this file to use the normal per-user folders (%APPDATA%\PdfCraft and +%LOCALAPPDATA%\PdfCraft) instead. If PdfCraftData can't be written (for example on a read-only +drive), PdfCraft says so and uses the per-user folders instead.