@@ -33,13 +33,16 @@ const maxPollRetries = 120 // 120 * 5s = 10 minutes
3333
3434// One row in the UI.
3535type fileRow struct {
36- filename string
37- sha256 string
38- state fileState
39- spinner spinner.Model
40- result * scanSummary
41- err error
42- pollCount int
36+ filename string
37+ sha256 string
38+ size int64 // file size in bytes (set from upload response for archives)
39+ state fileState
40+ spinner spinner.Model
41+ result * scanSummary
42+ err error
43+ pollCount int
44+ isArchive bool // true for ZIP containers with multiple files
45+ childCount int // number of extracted files
4346}
4447
4548// Top-level bubbletea model.
@@ -55,9 +58,12 @@ type scanModel struct {
5558// --- Messages ---
5659
5760type fileUploadedMsg struct {
58- index int
59- sha256 string
60- err error
61+ index int
62+ sha256 string
63+ size int64
64+ err error
65+ isArchive bool
66+ childHashes []string
6167}
6268
6369type fileScanStatusMsg struct {
@@ -94,8 +100,15 @@ func uploadFileCmd(index int, web webapi.Service, filename, token string) tea.Cm
94100 }
95101 // Use the SHA256 from the server response. For single-file ZIPs,
96102 // the server extracts the file and returns the child's hash, not
97- // the ZIP's hash.
98- return fileUploadedMsg {index : index , sha256 : file .SHA256 }
103+ // the ZIP's hash. For multi-file ZIPs, the server returns the
104+ // archive doc with child hashes so we can track them individually.
105+ return fileUploadedMsg {
106+ index : index ,
107+ sha256 : file .SHA256 ,
108+ size : file .Size ,
109+ isArchive : file .IsArchive ,
110+ childHashes : file .ArchiveFiles ,
111+ }
99112 } else if forceRescanFlag {
100113 err = web .Rescan (sha256 , token , osFlag , enableDetonationFlag , timeoutFlag )
101114 if err != nil {
@@ -249,8 +262,34 @@ func (m scanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
249262 return m , m .maybeQuitOrNext ()
250263 }
251264 m .files [i ].sha256 = msg .sha256
252- m .files [i ].state = stateScanning
253- cmds = append (cmds , pollStatusCmd (i , m .web , msg .sha256 ))
265+
266+ if msg .isArchive && len (msg .childHashes ) > 0 {
267+ // Archive container: mark it as done immediately and track children.
268+ m .files [i ].state = stateDone
269+ m .files [i ].isArchive = true
270+ m .files [i ].childCount = len (msg .childHashes )
271+ m .files [i ].size = msg .size
272+
273+ archiveName := filepath .Base (m .files [i ].filename )
274+ for _ , childHash := range msg .childHashes {
275+ s := spinner .New ()
276+ s .Spinner = spinner .Dot
277+ m .files = append (m .files , fileRow {
278+ filename : archiveName + "/" + truncSha (childHash ),
279+ sha256 : childHash ,
280+ state : stateScanning ,
281+ spinner : s ,
282+ })
283+ childIdx := len (m .files ) - 1
284+ cmds = append (cmds ,
285+ pollStatusCmd (childIdx , m .web , childHash ),
286+ m .files [childIdx ].spinner .Tick ,
287+ )
288+ }
289+ } else {
290+ m .files [i ].state = stateScanning
291+ cmds = append (cmds , pollStatusCmd (i , m .web , msg .sha256 ))
292+ }
254293
255294 case fileScanStatusMsg :
256295 i := msg .index
@@ -404,7 +443,11 @@ func (m scanModel) View() string {
404443 case stateDone :
405444 sha := truncSha (f .sha256 )
406445 line := styleSuccess .Render ("✓" ) + " " + name + " " + styleDim .Render (sha )
407- if f .result != nil {
446+ if f .isArchive {
447+ line += " " + styleDim .Render (formatSize (f .size ))
448+ line += " " + styleLabel .Render (fmt .Sprintf ("archive (%d files)" , f .childCount ))
449+ } else if f .result != nil {
450+ line += " " + styleDim .Render (formatSize (f .result .Size ))
408451 fmtStr := f .result .FileFormat
409452 if f .result .FileExtension != "" {
410453 fmtStr += "/" + f .result .FileExtension
@@ -454,3 +497,4 @@ func truncSha(sha string) string {
454497 }
455498 return sha
456499}
500+
0 commit comments