From c5b8bf247fe7acaa509557129755157f5188b724 Mon Sep 17 00:00:00 2001 From: unicxrn Date: Sat, 10 Oct 2026 16:44:42 +0200 Subject: [PATCH] Handle ftpsrv reply codes --- src-tauri/src/ftp.rs | 9 +++++++-- src-tauri/src/lib.rs | 9 +++++++++ src-tauri/src/pipeline.rs | 21 ++++++++++++++++----- src/lib/Focus.svelte | 17 ++++++++++++++--- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/ftp.rs b/src-tauri/src/ftp.rs index 68433e9..f0c362b 100644 --- a/src-tauri/src/ftp.rs +++ b/src-tauri/src/ftp.rs @@ -106,11 +106,12 @@ impl Ftp { pub fn rename(&mut self, from: &str, to: &str) -> Result<(), String> { self.expect(&format!("RNFR {from}"), &[350])?; - self.expect(&format!("RNTO {to}"), &[250]).map(|_| ()) + // ftpsrv (the usual PS5 server) answers RNTO and DELE with 226 rather than 250. + self.expect(&format!("RNTO {to}"), &[250, 226]).map(|_| ()) } pub fn delete(&mut self, path: &str) -> Result<(), String> { - self.expect(&format!("DELE {path}"), &[250]).map(|_| ()) + self.expect(&format!("DELE {path}"), &[250, 226]).map(|_| ()) } fn pasv(&mut self) -> Result { @@ -224,6 +225,10 @@ impl Ftp { pub fn put_atomic(&mut self, path: &str, bytes: &[u8]) -> Result<(), String> { let (dir, name) = path.rsplit_once('/').ok_or("bad path")?; let tmp = format!("{dir}/.{name}.packer-tmp"); + // ftpsrv's STOR doesn't truncate before writing, so never write over a leftover temp file. + if self.size(&tmp)?.is_some() { + self.delete(&tmp)?; + } let never = AtomicBool::new(false); self.store(&tmp, &mut &bytes[..], 0, &never, &mut |_| {})?; self.rename(&tmp, path) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bb8e79f..7a5947f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -119,6 +119,15 @@ pub fn run() { let engine = Arc::new(engine::Engine::new(&settings::data_dir())); let _ = engine.write_verifier(); let queue = pipeline::Queue::new(emitter, engine.clone()); + // Folders passed on the command line (or via "Open with") are queued straight away. + for arg in std::env::args().skip(1).filter(|a| !a.starts_with('-')) { + let p = PathBuf::from(&arg); + if p.is_dir() { + if let Err(e) = dump::inspect(&p).and_then(|info| queue.add(info)) { + eprintln!("{arg}: {e}"); + } + } + } app.manage(App { queue, engine }); Ok(()) }) diff --git a/src-tauri/src/pipeline.rs b/src-tauri/src/pipeline.rs index f191d3c..34008a1 100644 --- a/src-tauri/src/pipeline.rs +++ b/src-tauri/src/pipeline.rs @@ -396,6 +396,7 @@ impl Queue { j.files_total = 0; j.rate = 0.0; j.rate_history.clear(); + j.rate_label = ""; j.eta_secs = None; j.stage_secs = 0; j.log("hl", format!("{} started", title_case(STAGES[stage]))); @@ -506,6 +507,11 @@ impl Queue { self.with(id, |j| { j.image_path = Some(final_path.to_string_lossy().into_owned()); j.image_bytes = Some(size); + if j.dump.apr && !j.dump.has_ampr_index && Path::new(&j.dump.path).join("ampr_emu.index").is_file() { + j.dump.has_ampr_index = true; + j.warnings.retain(|w| !w.contains("ampr_emu.index")); + j.log("", "MkPFS added ampr_emu.index to the dump folder"); + } let saved = j.dump.total_bytes.saturating_sub(size); j.stages[0].meta = format!("{} → {} · {}", gb(j.dump.total_bytes), gb(size), dur(secs)); j.log( @@ -868,14 +874,21 @@ mod e2e { let _ = std::fs::remove_file(dump.join("ampr_emu.index")); let ps5_root = tmp.path().join("ps5"); - let port = 22121; - let mut server = std::process::Command::new(scratch.join("mkvenv/bin/python")) + let port = 22000 + (std::process::id() % 1000) as u16; + struct KillOnDrop(std::process::Child); + impl Drop for KillOnDrop { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } + let _server = KillOnDrop(std::process::Command::new(scratch.join("mkvenv/bin/python")) .arg(scratch.join("fake_ps5.py")) .arg(&ps5_root) .arg(port.to_string()) .stderr(std::process::Stdio::null()) .spawn() - .unwrap(); + .unwrap()); std::thread::sleep(Duration::from_millis(1500)); let s = Settings { @@ -948,7 +961,5 @@ mod e2e { assert!(j5.log.iter().any(|l| l.text.starts_with("Resuming at")), "should resume"); assert_eq!(std::fs::read(&remote).unwrap(), good, "resumed upload must be byte-identical"); assert!(!part.exists()); - - server.kill().ok(); } } diff --git a/src/lib/Focus.svelte b/src/lib/Focus.svelte index a073af9..11abd7e 100644 --- a/src/lib/Focus.svelte +++ b/src/lib/Focus.svelte @@ -42,7 +42,11 @@ {#if job.status === "running"}
Step {job.stage + 1} of 4 · {STAGE_NAMES[stageKey]}
-
{shown.toFixed(1)}%
+ {#if stageKey === "mount"} +
{clock(job.stageSecs)}of 2:00
+ {:else} +
{shown.toFixed(1)}%
+ {/if}

{description}

@@ -73,7 +77,10 @@ {#if job.rateLabel} {:else} -
Polling the PS5 every 3 s
+
+ + Checking /user/app/{job.dump.titleId}/mount_img.lnk every 3 s +
{/if}
@@ -173,7 +180,11 @@ .rate small { font-size: 13px; color: var(--muted); margin-left: 4px; } .src { color: var(--faint); } .spark { flex: 1; min-height: 110px; margin: 14px -4px 0; position: relative; } - .idle { position: absolute; inset: 0; display: grid; place-items: center; color: var(--faint); } + .idle { position: absolute; inset: 0; display: flex; flex-direction: column; gap: 18px; align-items: center; justify-content: center; color: var(--faint); text-align: center; } + .pulse { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); position: relative; } + .pulse::before, .pulse::after { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--accent); animation: ring 2.4s var(--ease) infinite; } + .pulse::after { animation-delay: 1.2s; } + @keyframes ring { from { transform: scale(1); opacity: 0.8; } to { transform: scale(5); opacity: 0; } } .actions { display: flex; gap: 8px; align-items: center; margin-top: 22px; } .saved { margin-left: auto; color: var(--muted); font-size: 12.5px; }