Skip to content

Commit 46fc260

Browse files
committed
fix: bandwidth saved shows 0B
1 parent 3a111ff commit 46fc260

File tree

5 files changed

+276
-0
lines changed

5 files changed

+276
-0
lines changed

backend/internal/db/sqlite.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,24 @@ func (db *DB) GetISOByComposite(name, version, arch, edition, fileType string) (
429429
}
430430
return iso, nil
431431
}
432+
433+
// ListISOsWithMissingSize returns ISOs that are complete but have size_bytes = 0.
434+
func (db *DB) ListISOsWithMissingSize() ([]models.ISO, error) {
435+
query := fmt.Sprintf("SELECT %s FROM isos WHERE status = 'complete' AND size_bytes = 0", isoSelectFields)
436+
rows, err := db.conn.Query(query)
437+
if err != nil {
438+
return nil, fmt.Errorf("failed to list ISOs with missing size: %w", err)
439+
}
440+
defer rows.Close()
441+
442+
var isos []models.ISO
443+
for rows.Next() {
444+
iso, err := scanISO(rows)
445+
if err != nil {
446+
return nil, fmt.Errorf("failed to scan ISO row: %w", err)
447+
}
448+
isos = append(isos, *iso)
449+
}
450+
451+
return isos, rows.Err()
452+
}

backend/internal/db/sqlite_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,3 +762,112 @@ func TestConcurrentOperations(t *testing.T) {
762762
t.Error("ISO corrupted after concurrent operations")
763763
}
764764
}
765+
766+
func TestListISOsWithMissingSize(t *testing.T) {
767+
db, cleanup := setupTestDB(t)
768+
defer cleanup()
769+
770+
// Create ISOs with various states
771+
// ISO 1: complete with size_bytes = 0 (should be returned)
772+
iso1 := createTestISO()
773+
iso1.ID = "iso-missing-size-1"
774+
iso1.Name = "missing-size-1"
775+
iso1.Status = models.StatusComplete
776+
iso1.SizeBytes = 0
777+
if err := db.CreateISO(iso1); err != nil {
778+
t.Fatalf("Failed to create iso1: %v", err)
779+
}
780+
781+
// ISO 2: complete with size_bytes > 0 (should NOT be returned)
782+
iso2 := createTestISO()
783+
iso2.ID = "iso-has-size"
784+
iso2.Name = "has-size"
785+
iso2.Status = models.StatusComplete
786+
iso2.SizeBytes = 1000000
787+
if err := db.CreateISO(iso2); err != nil {
788+
t.Fatalf("Failed to create iso2: %v", err)
789+
}
790+
791+
// ISO 3: pending with size_bytes = 0 (should NOT be returned - not complete)
792+
iso3 := createTestISO()
793+
iso3.ID = "iso-pending"
794+
iso3.Name = "pending"
795+
iso3.Status = models.StatusPending
796+
iso3.SizeBytes = 0
797+
if err := db.CreateISO(iso3); err != nil {
798+
t.Fatalf("Failed to create iso3: %v", err)
799+
}
800+
801+
// ISO 4: failed with size_bytes = 0 (should NOT be returned - not complete)
802+
iso4 := createTestISO()
803+
iso4.ID = "iso-failed"
804+
iso4.Name = "failed"
805+
iso4.Status = models.StatusFailed
806+
iso4.SizeBytes = 0
807+
if err := db.CreateISO(iso4); err != nil {
808+
t.Fatalf("Failed to create iso4: %v", err)
809+
}
810+
811+
// ISO 5: complete with size_bytes = 0 (should be returned)
812+
iso5 := createTestISO()
813+
iso5.ID = "iso-missing-size-2"
814+
iso5.Name = "missing-size-2"
815+
iso5.Status = models.StatusComplete
816+
iso5.SizeBytes = 0
817+
if err := db.CreateISO(iso5); err != nil {
818+
t.Fatalf("Failed to create iso5: %v", err)
819+
}
820+
821+
// Test ListISOsWithMissingSize
822+
isos, err := db.ListISOsWithMissingSize()
823+
if err != nil {
824+
t.Fatalf("ListISOsWithMissingSize() failed: %v", err)
825+
}
826+
827+
// Should return exactly 2 ISOs (iso1 and iso5)
828+
if len(isos) != 2 {
829+
t.Errorf("Expected 2 ISOs with missing size, got %d", len(isos))
830+
}
831+
832+
// Verify the returned ISOs are the correct ones
833+
foundIDs := make(map[string]bool)
834+
for _, iso := range isos {
835+
foundIDs[iso.ID] = true
836+
// All returned ISOs should be complete with size_bytes = 0
837+
if iso.Status != models.StatusComplete {
838+
t.Errorf("Returned ISO %s has status %s, expected complete", iso.ID, iso.Status)
839+
}
840+
if iso.SizeBytes != 0 {
841+
t.Errorf("Returned ISO %s has size_bytes %d, expected 0", iso.ID, iso.SizeBytes)
842+
}
843+
}
844+
845+
if !foundIDs["iso-missing-size-1"] {
846+
t.Error("Expected iso-missing-size-1 to be returned")
847+
}
848+
if !foundIDs["iso-missing-size-2"] {
849+
t.Error("Expected iso-missing-size-2 to be returned")
850+
}
851+
}
852+
853+
func TestListISOsWithMissingSize_Empty(t *testing.T) {
854+
db, cleanup := setupTestDB(t)
855+
defer cleanup()
856+
857+
// Create only ISOs that should NOT be returned
858+
iso := createTestISO()
859+
iso.Status = models.StatusComplete
860+
iso.SizeBytes = 500000
861+
if err := db.CreateISO(iso); err != nil {
862+
t.Fatalf("Failed to create ISO: %v", err)
863+
}
864+
865+
isos, err := db.ListISOsWithMissingSize()
866+
if err != nil {
867+
t.Fatalf("ListISOsWithMissingSize() failed: %v", err)
868+
}
869+
870+
if len(isos) != 0 {
871+
t.Errorf("Expected 0 ISOs with missing size, got %d", len(isos))
872+
}
873+
}

backend/internal/download/worker.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ func (w *Worker) Process(ctx context.Context, iso *models.ISO) error {
9090
return fmt.Errorf("failed to move file to final location: %w", err)
9191
}
9292

93+
// Update size_bytes from actual file size if not set (e.g., server didn't send Content-Length)
94+
if iso.SizeBytes == 0 {
95+
if fi, err := os.Stat(finalFile); err == nil {
96+
iso.SizeBytes = fi.Size()
97+
if err := w.db.UpdateISOSize(iso.ID, iso.SizeBytes); err != nil {
98+
slog.Warn("failed to update ISO size from file", slog.Any("error", err))
99+
}
100+
}
101+
}
102+
93103
// Download and save checksum file alongside ISO (after file is moved)
94104
if iso.ChecksumURL != "" {
95105
checksumFile := pathutil.ConstructChecksumPath(finalFile, iso.ChecksumType)

backend/internal/download/worker_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,3 +516,85 @@ func TestWorkerTempFileCleanup(t *testing.T) {
516516
t.Errorf("Temp file should be cleaned up: %s", tmpFile)
517517
}
518518
}
519+
520+
// TestWorkerDownloadNoContentLength tests that size_bytes is set from actual file
521+
// when server doesn't send Content-Length header.
522+
func TestWorkerDownloadNoContentLength(t *testing.T) {
523+
worker, database, isoDir, cleanup := setupTestWorker(t)
524+
defer cleanup()
525+
526+
// Create test HTTP server WITHOUT Content-Length header
527+
testContent := []byte("test file content without content-length header")
528+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
529+
// Explicitly remove Content-Length by using chunked transfer
530+
w.Header().Set("Transfer-Encoding", "chunked")
531+
w.WriteHeader(http.StatusOK)
532+
w.Write(testContent)
533+
}))
534+
defer server.Close()
535+
536+
// Create test ISO
537+
iso := &models.ISO{
538+
ID: uuid.New().String(),
539+
Name: "test-no-cl",
540+
Version: "1.0",
541+
Arch: "x86_64",
542+
FileType: "iso",
543+
DownloadURL: server.URL,
544+
Status: models.StatusPending,
545+
Progress: 0,
546+
SizeBytes: 0, // Explicitly set to 0
547+
CreatedAt: time.Now(),
548+
}
549+
iso.ComputeFields()
550+
database.CreateISO(iso)
551+
552+
// Verify initial size is 0
553+
initialISO, _ := database.GetISO(iso.ID)
554+
if initialISO.SizeBytes != 0 {
555+
t.Errorf("Initial SizeBytes should be 0, got: %d", initialISO.SizeBytes)
556+
}
557+
558+
// Process download
559+
ctx := context.Background()
560+
err := worker.Process(ctx, iso)
561+
if err != nil {
562+
t.Fatalf("Process failed: %v", err)
563+
}
564+
565+
// Verify file was downloaded
566+
finalPath := filepath.Join(isoDir, iso.FilePath)
567+
fi, err := os.Stat(finalPath)
568+
if os.IsNotExist(err) {
569+
t.Fatalf("Downloaded file does not exist at %s", finalPath)
570+
}
571+
572+
// Verify file content matches
573+
content, err := os.ReadFile(finalPath)
574+
if err != nil {
575+
t.Fatalf("Failed to read downloaded file: %v", err)
576+
}
577+
if !bytes.Equal(content, testContent) {
578+
t.Errorf("File content mismatch: got %q, want %q", content, testContent)
579+
}
580+
581+
// THIS IS THE KEY TEST: Verify size_bytes was backfilled from actual file size
582+
updatedISO, err := database.GetISO(iso.ID)
583+
if err != nil {
584+
t.Fatalf("Failed to get updated ISO: %v", err)
585+
}
586+
587+
if updatedISO.Status != models.StatusComplete {
588+
t.Errorf("Status should be 'complete', got: %s", updatedISO.Status)
589+
}
590+
591+
// size_bytes should match actual file size even without Content-Length
592+
expectedSize := fi.Size()
593+
if updatedISO.SizeBytes != expectedSize {
594+
t.Errorf("SizeBytes should be %d (actual file size), got: %d", expectedSize, updatedISO.SizeBytes)
595+
}
596+
597+
if updatedISO.SizeBytes != int64(len(testContent)) {
598+
t.Errorf("SizeBytes should be %d (content length), got: %d", len(testContent), updatedISO.SizeBytes)
599+
}
600+
}

backend/main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ func main() {
5858
defer database.Close()
5959
log.Info("database initialized", slog.String("db_path", dbPath))
6060

61+
// Backfill missing ISO sizes from actual files on disk
62+
backfillISOSizes(database, isoDir, log)
63+
6164
// Initialize WebSocket hub
6265
wsHub := ws.NewHub()
6366
go wsHub.Run()
@@ -135,3 +138,54 @@ func main() {
135138

136139
log.Info("server stopped successfully")
137140
}
141+
142+
// backfillISOSizes updates size_bytes for complete ISOs that have size_bytes = 0
143+
// by reading the actual file size from disk. This handles ISOs that were downloaded
144+
// when the server didn't send a Content-Length header.
145+
func backfillISOSizes(database *db.DB, isoDir string, log *slog.Logger) {
146+
isos, err := database.ListISOsWithMissingSize()
147+
if err != nil {
148+
log.Warn("failed to list ISOs with missing size", slog.Any("error", err))
149+
return
150+
}
151+
152+
if len(isos) == 0 {
153+
return
154+
}
155+
156+
log.Info("backfilling missing ISO sizes", slog.Int("count", len(isos)))
157+
158+
updated := 0
159+
for _, iso := range isos {
160+
filePath := pathutil.ConstructISOPath(isoDir, iso.FilePath)
161+
fi, err := os.Stat(filePath)
162+
if err != nil {
163+
log.Warn("failed to stat ISO file for size backfill",
164+
slog.String("iso_id", iso.ID),
165+
slog.String("path", filePath),
166+
slog.Any("error", err),
167+
)
168+
continue
169+
}
170+
171+
size := fi.Size()
172+
if err := database.UpdateISOSize(iso.ID, size); err != nil {
173+
log.Warn("failed to update ISO size",
174+
slog.String("iso_id", iso.ID),
175+
slog.Any("error", err),
176+
)
177+
continue
178+
}
179+
180+
updated++
181+
log.Debug("backfilled ISO size",
182+
slog.String("iso_id", iso.ID),
183+
slog.String("name", iso.Name),
184+
slog.Int64("size_bytes", size),
185+
)
186+
}
187+
188+
if updated > 0 {
189+
log.Info("backfilled ISO sizes", slog.Int("updated", updated))
190+
}
191+
}

0 commit comments

Comments
 (0)