diff --git a/.gitignore b/.gitignore index 4c49bd7..5bfcd1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .env +dist diff --git a/internal/pages/clean_all.go b/internal/pages/clean_all.go new file mode 100644 index 0000000..62d3525 --- /dev/null +++ b/internal/pages/clean_all.go @@ -0,0 +1,5 @@ +package pages + +type CleanAllPageMsg struct { + ServerName string +} diff --git a/internal/services/storage.go b/internal/services/storage.go index 466204d..68b75ea 100644 --- a/internal/services/storage.go +++ b/internal/services/storage.go @@ -77,6 +77,14 @@ func (s *StorageService) Send(localPaths []string) error { // CleanAll removes all files from remote storage. func (s *StorageService) CleanAll() error { - // TODO: implement - return fmt.Errorf("clean all: not yet implemented") + cmd := SSHCmd(s.server, + "rm -f "+defaultStoragePath+"/*", + ) + debugLog("CleanAll | args: %v", cmd.Args) + out, err := cmd.CombinedOutput() + debugLog("CleanAll | exit_err: %v | output: %q", err, strings.TrimSpace(string(out))) + if err != nil { + return fmt.Errorf("clean all failed: %w\n%s", err, strings.TrimSpace(string(out))) + } + return nil } diff --git a/internal/styles/styles.go b/internal/styles/styles.go index ba99429..f87fe6f 100644 --- a/internal/styles/styles.go +++ b/internal/styles/styles.go @@ -86,6 +86,12 @@ var ( Foreground(lipgloss.Color("203")). MarginTop(1) + CleanWarningStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("203")). + Bold(true). + MarginBottom(1). + Width(44) + // Footer FooterStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). diff --git a/internal/tui/tui.go b/internal/tui/tui.go index a2ead9e..4023d67 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -3,6 +3,8 @@ package tui import ( "filepass/internal/pages" "filepass/internal/services" + + "charm.land/bubbles/v2/textinput" ) type page int @@ -15,6 +17,7 @@ const ( pageServerActions pageFileAction pageSend + pageCleanAll ) type TUIInterface struct { @@ -45,6 +48,10 @@ type TUIInterface struct { FileOpLoading bool FileOpErr error FileOpSuccess string + // clean all confirmation page + CleanInput textinput.Model + CleanOpLoading bool + CleanOpErr error // send / file picker page Picker picker } diff --git a/internal/tui/update.go b/internal/tui/update.go index b801c78..d15f761 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -8,6 +8,7 @@ import ( "filepass/internal/pages" "filepass/internal/services" + "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" ) @@ -60,6 +61,30 @@ func deleteFileCmd(store *services.ServicesStore, serverName, filename string) t } } +type cleanAllMsg struct { + err error +} + +func cleanAllCmd(store *services.ServicesStore, serverName string) tea.Cmd { + return func() tea.Msg { + storage, err := store.NewStorageService(serverName) + if err != nil { + return cleanAllMsg{err: err} + } + return cleanAllMsg{err: storage.CleanAll()} + } +} + +func newCleanInput() textinput.Model { + ti := textinput.New() + ti.Placeholder = "yes" + ti.Prompt = "" + ti.CharLimit = 3 + ti.SetWidth(10) + ti.Focus() + return ti +} + func checkStorageCmd(store *services.ServicesStore, serverName string) tea.Cmd { return func() tea.Msg { storage, err := store.NewStorageService(serverName) @@ -152,6 +177,22 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.FileOpSuccess = "" return m, nil + case pages.CleanAllPageMsg: + m.Page = pageCleanAll + m.CleanInput = newCleanInput() + m.CleanOpLoading = false + m.CleanOpErr = nil + return m, nil + + case cleanAllMsg: + m.CleanOpLoading = false + if msg.err != nil { + m.CleanOpErr = msg.err + return m, nil + } + server := m.ActiveServer + return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} } + case pages.SendPageMsg: m.Page = pageSend m.Picker = newPicker(m.LocalDir) @@ -222,6 +263,9 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.Page == pageSend { return m.updateSend(msg) } + if m.Page == pageCleanAll { + return m.updateCleanAll(msg) + } switch msg.String() { case "up", "k": @@ -313,7 +357,7 @@ func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.C case "send": return m, func() tea.Msg { return pages.SendPageMsg{ServerName: server} } case "clean": - // TODO: navigate to clean all page + return m, func() tea.Msg { return pages.CleanAllPageMsg{ServerName: server} } } case "ctrl+c": @@ -429,6 +473,36 @@ func (m TUIInterface) updateSend(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { return m, nil } +func (m TUIInterface) updateCleanAll(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + if m.CleanOpLoading { + if msg.String() == "ctrl+c" { + m.Quitting = true + return m, tea.Quit + } + return m, nil + } + + switch msg.String() { + case "enter": + if m.CleanInput.Value() == "yes" { + m.CleanOpLoading = true + m.CleanOpErr = nil + return m, cleanAllCmd(m.Services, m.ActiveServer) + } + case "ctrl+c": + m.Quitting = true + return m, tea.Quit + case "esc": + server := m.ActiveServer + return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} } + } + + // route all other keys to the text input + var cmd tea.Cmd + m.CleanInput, cmd = m.CleanInput.Update(msg) + return m, cmd +} + func (m TUIInterface) updateSelectServer(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { last := len(m.ServerNames) - 1 switch msg.String() { diff --git a/internal/tui/view.go b/internal/tui/view.go index bda3dc0..8c14196 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -1,6 +1,8 @@ package tui import ( + "fmt" + "filepass/internal/styles" tea "charm.land/bubbletea/v2" @@ -34,6 +36,8 @@ func (m TUIInterface) subtitle() string { return m.ActiveFile case pageSend: return "Send File" + case pageCleanAll: + return "Clean All" default: return "Secure file transfer" } @@ -65,6 +69,8 @@ func (m TUIInterface) View() tea.View { body = m.viewFileAction() case pageSend: body = m.viewSend() + case pageCleanAll: + body = m.viewCleanAll() default: body = m.viewMenu() } @@ -108,6 +114,10 @@ func (m TUIInterface) View() tea.View { footerHint("enter", "confirm") + footerSep() + footerHint("esc", "back") + case pageCleanAll: + footerStr = footerHint("enter", "confirm") + + footerSep() + + footerHint("esc", "back") case pageSend: footerStr = footerHint("↑↓", "navigate") + footerSep() + @@ -252,6 +262,30 @@ func (m TUIInterface) viewSend() string { return lipgloss.JoinVertical(lipgloss.Left, crumb, queryLine, list) } +func (m TUIInterface) viewCleanAll() string { + fileCount := len(m.StorageFiles) + warning := styles.CleanWarningStyle.Render( + fmt.Sprintf("This will permanently delete all %d file(s) from remote storage.", fileCount), + ) + + promptLabel := styles.FieldLabelStyle(true).Render("Type \"yes\" to confirm") + input := m.CleanInput.View() + + var statusLine string + switch { + case m.CleanOpLoading: + statusLine = styles.StatusWarnStyle.Render(" deleting…") + case m.CleanOpErr != nil: + statusLine = styles.StatusErrStyle.Render("✗ " + m.CleanOpErr.Error()) + } + + parts := []string{warning, promptLabel, input} + if statusLine != "" { + parts = append(parts, statusLine) + } + return lipgloss.JoinVertical(lipgloss.Left, parts...) +} + func (m TUIInterface) viewSelectServer() string { if len(m.ServerNames) == 0 { return styles.StatusWarnStyle.Render("⚠ No servers configured.")