Fix review findings: hostile snapshots, stale async results, trash safety
- snapshot: depth limit (stack overflow in exporters), decompressed-size limit (gzip bombs), per-node size cap and saturating aggregation - export: sanitise exclusion patterns from snapshot metadata - tui: drop duplicate/trash/filter results that belong to a replaced tree; cancel duplicate search on rescan - trash: refuse symlinked or foreign shared-filesystem trash dirs; never overwrite an existing trashed item - scan: no symlink following without inode identity; correct unscanned count and keep real read errors after cancellation
This commit is contained in:
+12
-5
@@ -17,8 +17,9 @@ import (
|
||||
// ---- Trash ---------------------------------------------------------------
|
||||
|
||||
type trashDoneMsg struct {
|
||||
id inventory.NodeID
|
||||
err error
|
||||
tree *inventory.Tree // the tree id belongs to; results for a replaced tree are dropped
|
||||
id inventory.NodeID
|
||||
err error
|
||||
}
|
||||
|
||||
// requestTrash asks for confirmation before moving the selection to the
|
||||
@@ -49,7 +50,8 @@ func (m *Model) requestTrash() tea.Cmd {
|
||||
"", "It will be moved to the " + platform.TrashName + ", not permanently deleted."},
|
||||
yes: "move to " + platform.TrashName,
|
||||
onYes: func(m *Model) tea.Cmd {
|
||||
return func() tea.Msg { return trashDoneMsg{id, platform.Trash(path)} }
|
||||
t := m.tree
|
||||
return func() tea.Msg { return trashDoneMsg{t, id, platform.Trash(path)} }
|
||||
},
|
||||
})
|
||||
return nil
|
||||
@@ -59,6 +61,10 @@ func (m *Model) trashDone(msg trashDoneMsg) tea.Cmd {
|
||||
if msg.err != nil {
|
||||
return m.warn("Trash failed: " + textutil.Sanitize(msg.err.Error()))
|
||||
}
|
||||
if msg.tree != m.tree || m.scanning {
|
||||
// A rescan replaced the inventory; the new scan reflects the change.
|
||||
return m.info("Moved to " + platform.TrashName)
|
||||
}
|
||||
// The scan has finished, so the UI goroutine is the tree's only writer.
|
||||
t := m.tree
|
||||
t.Lock()
|
||||
@@ -96,6 +102,7 @@ type dupState struct {
|
||||
type (
|
||||
dupProgressMsg struct{}
|
||||
dupDoneMsg struct {
|
||||
finder *duplicate.Finder // identifies the search; stale results are dropped
|
||||
groups []duplicate.Group
|
||||
err error
|
||||
}
|
||||
@@ -128,7 +135,7 @@ func (m *Model) startDuplicates() tea.Cmd {
|
||||
t := m.tree
|
||||
return tea.Batch(m.startTicking(), func() tea.Msg {
|
||||
g, err := f.Find(ctx, t, duplicate.Options{MinSize: 1, Workers: 4})
|
||||
return dupDoneMsg{g, err}
|
||||
return dupDoneMsg{f, g, err}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -147,7 +154,7 @@ func (m *Model) ensureDupView() {
|
||||
}
|
||||
|
||||
func (m *Model) dupMsg(msg tea.Msg) tea.Cmd {
|
||||
if d, ok := msg.(dupDoneMsg); ok && m.dups != nil {
|
||||
if d, ok := msg.(dupDoneMsg); ok && m.dups != nil && d.finder == m.dups.finder {
|
||||
m.dups.running = false
|
||||
m.dups.groups, m.dups.err = d.groups, d.err
|
||||
m.dirty = true
|
||||
|
||||
+14
-2
@@ -378,10 +378,22 @@ func (m *Model) rescan() tea.Cmd {
|
||||
m.scanner, m.tree, m.cancel, m.scanning, m.ctrlC = s, t, cancel, s != nil, false
|
||||
m.zoom, m.sel = t.Root(), noSel
|
||||
m.kidsCache = map[inventory.NodeID]*kids{}
|
||||
m.dups = nil
|
||||
if m.dups != nil {
|
||||
m.dups.cancel()
|
||||
m.dups = nil
|
||||
// Drop the duplicates view: its rows referred to the old tree.
|
||||
for i, v := range m.views {
|
||||
if _, ok := v.(*dupView); ok {
|
||||
m.views = append(m.views[:i], m.views[i+1:]...)
|
||||
m.view = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
m.search.result = nil
|
||||
m.search.gen++ // results computed against the old tree are discarded
|
||||
m.invalidate()
|
||||
return tea.Batch(waitScan(s), m.startTicking(), m.info("Rescanning…"))
|
||||
return tea.Batch(waitScan(s), m.startTicking(), m.info("Rescanning…"), m.applyFilter())
|
||||
}
|
||||
|
||||
// resolvePendingZoom re-zooms into the remembered directory once the new
|
||||
|
||||
Reference in New Issue
Block a user