Skip to content

Commit 1a272cf

Browse files
authored
feat: display file size in file scan results (#21)
Co-authored-by: Ayoub Faouzi <ayoubfaouzi@users.noreply.github.com>
1 parent ffe48f3 commit 1a272cf

3 files changed

Lines changed: 108 additions & 19 deletions

File tree

cmd/scan.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func init() {
4747

4848
type scanSummary struct {
4949
SHA256 string `json:"sha256"`
50+
Size int64 `json:"size"`
5051
Classification string `json:"classification"`
5152
FileFormat string `json:"file_format"`
5253
FileExtension string `json:"file_extension"`
@@ -61,6 +62,7 @@ type avSummary struct {
6162
func buildScanSummary(file entity.File) scanSummary {
6263
s := scanSummary{
6364
SHA256: file.SHA256,
65+
Size: file.Size,
6466
Classification: file.Classification,
6567
FileFormat: file.Format,
6668
FileExtension: file.Extension,

cmd/scanui.go

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ const maxPollRetries = 120 // 120 * 5s = 10 minutes
3333

3434
// One row in the UI.
3535
type 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

5760
type 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

6369
type fileScanStatusMsg struct {
@@ -94,9 +100,38 @@ 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 {
113+
// Fetch the existing file to check if it's an archive.
114+
var file entity.File
115+
if err := web.GetFile(sha256, &file); err != nil {
116+
return fileUploadedMsg{index: index, err: fmt.Errorf("get file: %w", err)}
117+
}
118+
119+
if file.IsArchive && len(file.ArchiveFiles) > 0 {
120+
// Archive: rescan each child, not the container itself.
121+
for _, childHash := range file.ArchiveFiles {
122+
if err := web.Rescan(childHash, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil {
123+
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", childHash[:12], err)}
124+
}
125+
}
126+
return fileUploadedMsg{
127+
index: index,
128+
sha256: sha256,
129+
size: file.Size,
130+
isArchive: true,
131+
childHashes: file.ArchiveFiles,
132+
}
133+
}
134+
100135
err = web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
101136
if err != nil {
102137
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan: %w", err)}
@@ -140,6 +175,27 @@ func delayedPollCmd(index int, web webapi.Service, sha256 string) tea.Cmd {
140175

141176
func rescanFileCmd(index int, web webapi.Service, sha256, token string) tea.Cmd {
142177
return func() tea.Msg {
178+
// Check if the hash is an archive container.
179+
var file entity.File
180+
if err := web.GetFile(sha256, &file); err != nil {
181+
return fileUploadedMsg{index: index, err: fmt.Errorf("get file: %w", err)}
182+
}
183+
184+
if file.IsArchive && len(file.ArchiveFiles) > 0 {
185+
for _, childHash := range file.ArchiveFiles {
186+
if err := web.Rescan(childHash, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil {
187+
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", childHash[:12], err)}
188+
}
189+
}
190+
return fileUploadedMsg{
191+
index: index,
192+
sha256: sha256,
193+
size: file.Size,
194+
isArchive: true,
195+
childHashes: file.ArchiveFiles,
196+
}
197+
}
198+
143199
err := web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
144200
if err != nil {
145201
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan: %w", err)}
@@ -249,8 +305,34 @@ func (m scanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
249305
return m, m.maybeQuitOrNext()
250306
}
251307
m.files[i].sha256 = msg.sha256
252-
m.files[i].state = stateScanning
253-
cmds = append(cmds, pollStatusCmd(i, m.web, msg.sha256))
308+
309+
if msg.isArchive && len(msg.childHashes) > 0 {
310+
// Archive container: mark it as done immediately and track children.
311+
m.files[i].state = stateDone
312+
m.files[i].isArchive = true
313+
m.files[i].childCount = len(msg.childHashes)
314+
m.files[i].size = msg.size
315+
316+
archiveName := filepath.Base(m.files[i].filename)
317+
for _, childHash := range msg.childHashes {
318+
s := spinner.New()
319+
s.Spinner = spinner.Dot
320+
m.files = append(m.files, fileRow{
321+
filename: archiveName + "/" + truncSha(childHash),
322+
sha256: childHash,
323+
state: stateScanning,
324+
spinner: s,
325+
})
326+
childIdx := len(m.files) - 1
327+
cmds = append(cmds,
328+
pollStatusCmd(childIdx, m.web, childHash),
329+
m.files[childIdx].spinner.Tick,
330+
)
331+
}
332+
} else {
333+
m.files[i].state = stateScanning
334+
cmds = append(cmds, pollStatusCmd(i, m.web, msg.sha256))
335+
}
254336

255337
case fileScanStatusMsg:
256338
i := msg.index
@@ -404,7 +486,11 @@ func (m scanModel) View() string {
404486
case stateDone:
405487
sha := truncSha(f.sha256)
406488
line := styleSuccess.Render("✓") + " " + name + " " + styleDim.Render(sha)
407-
if f.result != nil {
489+
if f.isArchive {
490+
line += " " + styleDim.Render(formatSize(f.size))
491+
line += " " + styleLabel.Render(fmt.Sprintf("archive (%d files)", f.childCount))
492+
} else if f.result != nil {
493+
line += " " + styleDim.Render(formatSize(f.result.Size))
408494
fmtStr := f.result.FileFormat
409495
if f.result.FileExtension != "" {
410496
fmtStr += "/" + f.result.FileExtension
@@ -454,3 +540,4 @@ func truncSha(sha string) string {
454540
}
455541
return sha
456542
}
543+

cmd/view.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,13 @@ func printKV(key, value string) {
286286
func formatSize(size int64) string {
287287
switch {
288288
case size >= 1<<30:
289-
return fmt.Sprintf("%.2f GB (%d bytes)", float64(size)/float64(1<<30), size)
289+
return fmt.Sprintf("%.2f GB", float64(size)/float64(1<<30))
290290
case size >= 1<<20:
291-
return fmt.Sprintf("%.2f MB (%d bytes)", float64(size)/float64(1<<20), size)
291+
return fmt.Sprintf("%.2f MB", float64(size)/float64(1<<20))
292292
case size >= 1<<10:
293-
return fmt.Sprintf("%.2f KB (%d bytes)", float64(size)/float64(1<<10), size)
293+
return fmt.Sprintf("%.2f KB", float64(size)/float64(1<<10))
294294
default:
295-
return fmt.Sprintf("%d bytes", size)
295+
return fmt.Sprintf("%d B", size)
296296
}
297297
}
298298

0 commit comments

Comments
 (0)