From c44b3a118c56a3c5c77fef00cf717bc03d999a8c Mon Sep 17 00:00:00 2001 From: Ayoub Faouzi Date: Tue, 7 Apr 2026 07:45:47 +0100 Subject: [PATCH 1/2] feat: display file size in file scan results --- cmd/scan.go | 2 ++ cmd/scanui.go | 74 ++++++++++++++++++++++++++++++++++++++++----------- cmd/view.go | 8 +++--- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index 97ba6d8..3376d87 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -47,6 +47,7 @@ func init() { type scanSummary struct { SHA256 string `json:"sha256"` + Size int64 `json:"size"` Classification string `json:"classification"` FileFormat string `json:"file_format"` FileExtension string `json:"file_extension"` @@ -61,6 +62,7 @@ type avSummary struct { func buildScanSummary(file entity.File) scanSummary { s := scanSummary{ SHA256: file.SHA256, + Size: file.Size, Classification: file.Classification, FileFormat: file.Format, FileExtension: file.Extension, diff --git a/cmd/scanui.go b/cmd/scanui.go index 7d44146..269c257 100644 --- a/cmd/scanui.go +++ b/cmd/scanui.go @@ -33,13 +33,16 @@ const maxPollRetries = 120 // 120 * 5s = 10 minutes // One row in the UI. type fileRow struct { - filename string - sha256 string - state fileState - spinner spinner.Model - result *scanSummary - err error - pollCount int + filename string + sha256 string + size int64 // file size in bytes (set from upload response for archives) + state fileState + spinner spinner.Model + result *scanSummary + err error + pollCount int + isArchive bool // true for ZIP containers with multiple files + childCount int // number of extracted files } // Top-level bubbletea model. @@ -55,9 +58,12 @@ type scanModel struct { // --- Messages --- type fileUploadedMsg struct { - index int - sha256 string - err error + index int + sha256 string + size int64 + err error + isArchive bool + childHashes []string } type fileScanStatusMsg struct { @@ -94,8 +100,15 @@ func uploadFileCmd(index int, web webapi.Service, filename, token string) tea.Cm } // Use the SHA256 from the server response. For single-file ZIPs, // the server extracts the file and returns the child's hash, not - // the ZIP's hash. - return fileUploadedMsg{index: index, sha256: file.SHA256} + // the ZIP's hash. For multi-file ZIPs, the server returns the + // archive doc with child hashes so we can track them individually. + return fileUploadedMsg{ + index: index, + sha256: file.SHA256, + size: file.Size, + isArchive: file.IsArchive, + childHashes: file.ArchiveFiles, + } } else if forceRescanFlag { err = web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag) if err != nil { @@ -249,8 +262,34 @@ func (m scanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.maybeQuitOrNext() } m.files[i].sha256 = msg.sha256 - m.files[i].state = stateScanning - cmds = append(cmds, pollStatusCmd(i, m.web, msg.sha256)) + + if msg.isArchive && len(msg.childHashes) > 0 { + // Archive container: mark it as done immediately and track children. + m.files[i].state = stateDone + m.files[i].isArchive = true + m.files[i].childCount = len(msg.childHashes) + m.files[i].size = msg.size + + archiveName := filepath.Base(m.files[i].filename) + for _, childHash := range msg.childHashes { + s := spinner.New() + s.Spinner = spinner.Dot + m.files = append(m.files, fileRow{ + filename: archiveName + "/" + truncSha(childHash), + sha256: childHash, + state: stateScanning, + spinner: s, + }) + childIdx := len(m.files) - 1 + cmds = append(cmds, + pollStatusCmd(childIdx, m.web, childHash), + m.files[childIdx].spinner.Tick, + ) + } + } else { + m.files[i].state = stateScanning + cmds = append(cmds, pollStatusCmd(i, m.web, msg.sha256)) + } case fileScanStatusMsg: i := msg.index @@ -404,7 +443,11 @@ func (m scanModel) View() string { case stateDone: sha := truncSha(f.sha256) line := styleSuccess.Render("✓") + " " + name + " " + styleDim.Render(sha) - if f.result != nil { + if f.isArchive { + line += " " + styleDim.Render(formatSize(f.size)) + line += " " + styleLabel.Render(fmt.Sprintf("archive (%d files)", f.childCount)) + } else if f.result != nil { + line += " " + styleDim.Render(formatSize(f.result.Size)) fmtStr := f.result.FileFormat if f.result.FileExtension != "" { fmtStr += "/" + f.result.FileExtension @@ -454,3 +497,4 @@ func truncSha(sha string) string { } return sha } + diff --git a/cmd/view.go b/cmd/view.go index ef8c009..ca8a739 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -286,13 +286,13 @@ func printKV(key, value string) { func formatSize(size int64) string { switch { case size >= 1<<30: - return fmt.Sprintf("%.2f GB (%d bytes)", float64(size)/float64(1<<30), size) + return fmt.Sprintf("%.2f GB", float64(size)/float64(1<<30)) case size >= 1<<20: - return fmt.Sprintf("%.2f MB (%d bytes)", float64(size)/float64(1<<20), size) + return fmt.Sprintf("%.2f MB", float64(size)/float64(1<<20)) case size >= 1<<10: - return fmt.Sprintf("%.2f KB (%d bytes)", float64(size)/float64(1<<10), size) + return fmt.Sprintf("%.2f KB", float64(size)/float64(1<<10)) default: - return fmt.Sprintf("%d bytes", size) + return fmt.Sprintf("%d B", size) } } From db9518ef198a9f8c2e8e14395cfb070afaae6f33 Mon Sep 17 00:00:00 2001 From: Ayoub Faouzi Date: Tue, 7 Apr 2026 07:48:39 +0100 Subject: [PATCH 2/2] fi rescanning --- cmd/scanui.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/cmd/scanui.go b/cmd/scanui.go index 269c257..1787dcf 100644 --- a/cmd/scanui.go +++ b/cmd/scanui.go @@ -110,6 +110,28 @@ func uploadFileCmd(index int, web webapi.Service, filename, token string) tea.Cm childHashes: file.ArchiveFiles, } } else if forceRescanFlag { + // Fetch the existing file to check if it's an archive. + var file entity.File + if err := web.GetFile(sha256, &file); err != nil { + return fileUploadedMsg{index: index, err: fmt.Errorf("get file: %w", err)} + } + + if file.IsArchive && len(file.ArchiveFiles) > 0 { + // Archive: rescan each child, not the container itself. + for _, childHash := range file.ArchiveFiles { + if err := web.Rescan(childHash, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil { + return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", childHash[:12], err)} + } + } + return fileUploadedMsg{ + index: index, + sha256: sha256, + size: file.Size, + isArchive: true, + childHashes: file.ArchiveFiles, + } + } + err = web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag) if err != nil { return fileUploadedMsg{index: index, err: fmt.Errorf("rescan: %w", err)} @@ -153,6 +175,27 @@ func delayedPollCmd(index int, web webapi.Service, sha256 string) tea.Cmd { func rescanFileCmd(index int, web webapi.Service, sha256, token string) tea.Cmd { return func() tea.Msg { + // Check if the hash is an archive container. + var file entity.File + if err := web.GetFile(sha256, &file); err != nil { + return fileUploadedMsg{index: index, err: fmt.Errorf("get file: %w", err)} + } + + if file.IsArchive && len(file.ArchiveFiles) > 0 { + for _, childHash := range file.ArchiveFiles { + if err := web.Rescan(childHash, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil { + return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", childHash[:12], err)} + } + } + return fileUploadedMsg{ + index: index, + sha256: sha256, + size: file.Size, + isArchive: true, + childHashes: file.ArchiveFiles, + } + } + err := web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag) if err != nil { return fileUploadedMsg{index: index, err: fmt.Errorf("rescan: %w", err)}