Handle ftpsrv reply codes

This commit is contained in:
unicxrn committed 2026-10-10 16:44:42 +02:00
1 parent d88425bf13
commit c5b8bf247f
4 files changed
+46 -10

No files matched your search

+7 -2
View File
@@ -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<TcpStream, String> {
@@ -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)
+9
View File
@@ -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(())
})
+16 -5
View File
@@ -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();
}
}
+14 -3
View File
@@ -42,7 +42,11 @@
{#if job.status === "running"}
<div class="left">
<div class="kicker">Step {job.stage + 1} of 4 · {STAGE_NAMES[stageKey]}</div>
<div class="big num">{shown.toFixed(1)}<small>%</small></div>
{#if stageKey === "mount"}
<div class="big num">{clock(job.stageSecs)}<small>of 2:00</small></div>
{:else}
<div class="big num">{shown.toFixed(1)}<small>%</small></div>
{/if}
<p class="desc">{description}</p>
<div class="bigbar"><i style:width="{job.pct}%"></i></div>
<dl class="stats">
@@ -73,7 +77,10 @@
{#if job.rateLabel}
<Sparkline values={job.rateHistory} color={job.dump.accent} />
{:else}
<div class="idle mono">Polling the PS5 every 3 s</div>
<div class="idle">
<span class="pulse"></span>
<span class="mono">Checking /user/app/{job.dump.titleId}/mount_img.lnk every 3 s</span>
</div>
{/if}
</div>
<div class="actions">
@@ -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; }