@@ -3,10 +3,11 @@ package cmd
33import (
44 "bytes"
55 "context"
6- "crypto/md5"
6+ "crypto/md5" //nolint:gosec // MD5 used for checksums, not cryptography
77 "database/sql"
88 "encoding/hex"
99 "encoding/json"
10+ "errors"
1011 "fmt"
1112 "os"
1213 "strings"
@@ -20,7 +21,12 @@ import (
2021 tea "github.com/charmbracelet/bubbletea"
2122 "github.com/charmbracelet/lipgloss"
2223 "github.com/klauspost/compress/zstd"
23- _ "github.com/lib/pq"
24+ "github.com/lib/pq"
25+ )
26+
27+ // Stage constants
28+ const (
29+ StageSkipped = "Skipped"
2430)
2531
2632type Archiver struct {
@@ -175,12 +181,12 @@ func (a *Archiver) checkTablePermissions(ctx context.Context) error {
175181 `
176182
177183 err := a .db .QueryRowContext (ctx , checkPermissionQuery , a .config .Table , a .config .Table ).Scan (& hasPermission )
178- if err != nil && err != sql .ErrNoRows {
184+ if err != nil && ! errors . Is ( err , sql .ErrNoRows ) {
179185 return fmt .Errorf ("failed to check table permissions: %w" , err )
180186 }
181187
182188 // If the base table exists and we don't have permission, fail
183- if err != sql .ErrNoRows && ! hasPermission {
189+ if ! errors . Is ( err , sql .ErrNoRows ) && ! hasPermission {
184190 return fmt .Errorf ("insufficient permissions to read table '%s'" , a .config .Table )
185191 }
186192
@@ -197,13 +203,13 @@ func (a *Archiver) checkTablePermissions(ctx context.Context) error {
197203
198204 var samplePartition string
199205 err = a .db .QueryRowContext (ctx , partitionCheckQuery , pattern ).Scan (& samplePartition )
200- if err != nil && err != sql .ErrNoRows {
206+ if err != nil && ! errors . Is ( err , sql .ErrNoRows ) {
201207 // Only fail if it's not a "no rows" error
202208 return fmt .Errorf ("failed to check partition table permissions: %w" , err )
203209 }
204210
205211 // Check if we found any partitions at all (with or without permissions)
206- if err == sql .ErrNoRows {
212+ if errors . Is ( err , sql .ErrNoRows ) {
207213 // Let's see if partitions exist but we can't access them
208214 var partitionExists bool
209215 existsQuery := `
@@ -311,7 +317,7 @@ func (a *Archiver) discoverPartitionsWithUI(program *tea.Program) ([]PartitionIn
311317 // Update count progress
312318 program.Send(updateCount(i+1, len(matchingTables), table.name))
313319
314- countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", table.name)
320+ countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", pq.QuoteIdentifier( table.name) )
315321 var count int64
316322 if err := a.db.QueryRow(countQuery).Scan(&count); err == nil {
317323 partitions = append(partitions, PartitionInfo{
@@ -420,7 +426,7 @@ func (a *Archiver) discoverPartitions() ([]PartitionInfo, error) {
420426 len(matchingTables),
421427 table.name)
422428
423- countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", table.name)
429+ countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", pq.QuoteIdentifier( table.name) )
424430 var count int64
425431 if err := a.db.QueryRow(countQuery).Scan(&count); err == nil {
426432 partitions = append(partitions, PartitionInfo{
@@ -551,7 +557,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
551557 // Cached metadata matches S3 - skip without extraction
552558 result .Skipped = true
553559 result .SkipReason = fmt .Sprintf ("Cached metadata matches S3 (size=%d, md5=%s)" , cachedSize , cachedMD5 )
554- result .Stage = "Skipped"
560+ result .Stage = StageSkipped
555561 result .BytesWritten = cachedSize
556562 if a .config .Debug {
557563 fmt .Printf (" ✅ Skipping based on cache: Size and MD5 match\n " )
@@ -620,7 +626,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
620626 }
621627
622628 // Calculate MD5 hash of compressed data
623- hasher := md5 .New ()
629+ hasher := md5 .New () //nolint:gosec // MD5 used for checksums, not cryptography
624630 hasher .Write (compressed )
625631 localMD5 := hex .EncodeToString (hasher .Sum (nil ))
626632 localSize := int64 (len (compressed ))
@@ -647,7 +653,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
647653 // Single-part upload with matching MD5
648654 result .Skipped = true
649655 result .SkipReason = fmt .Sprintf ("Already exists with matching size (%d bytes) and MD5 (%s)" , s3Size , s3ETag )
650- result .Stage = "Skipped"
656+ result .Stage = StageSkipped
651657 if a .config .Debug {
652658 fmt .Printf (" ✅ Skipping: Size and MD5 match\n " )
653659 }
@@ -664,7 +670,7 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
664670 if s3ETag == localMultipartETag {
665671 result .Skipped = true
666672 result .SkipReason = fmt .Sprintf ("Already exists with matching size (%d bytes) and multipart ETag (%s)" , s3Size , s3ETag )
667- result .Stage = "Skipped"
673+ result .Stage = StageSkipped
668674 if a .config .Debug {
669675 fmt .Printf (" ✅ Skipping: Size and multipart ETag match\n " )
670676 }
@@ -718,7 +724,9 @@ func (a *Archiver) ProcessPartitionWithProgress(partition PartitionInfo, index i
718724}
719725
720726func (a * Archiver ) extractDataWithProgress (partition PartitionInfo , program * tea.Program ) ([]byte , error ) {
721- query := fmt .Sprintf ("SELECT row_to_json(t) FROM %s t" , partition .TableName )
727+ // Use pq.QuoteIdentifier to safely quote the table name
728+ quotedTable := pq .QuoteIdentifier (partition .TableName )
729+ query := fmt .Sprintf ("SELECT row_to_json(t) FROM %s t" , quotedTable ) //nolint:gosec // Table name is quoted with pq.QuoteIdentifier
722730
723731 rows , err := a .db .Query (query )
724732 if err != nil {
@@ -763,6 +771,11 @@ func (a *Archiver) extractDataWithProgress(partition PartitionInfo, program *tea
763771 }
764772 }
765773
774+ // Check for errors from iterating over rows
775+ if err := rows .Err (); err != nil {
776+ return nil , err
777+ }
778+
766779 // Final update
767780 if program != nil {
768781 if partition .RowCount > 0 {
@@ -848,7 +861,7 @@ func (a *Archiver) calculateMultipartETag(data []byte) string {
848861
849862 // If it would be a single part, just return regular MD5
850863 if numParts == 1 {
851- hasher := md5 .New ()
864+ hasher := md5 .New () //nolint:gosec // MD5 used for checksums, not cryptography
852865 hasher .Write (data )
853866 return hex .EncodeToString (hasher .Sum (nil ))
854867 }
@@ -862,13 +875,13 @@ func (a *Archiver) calculateMultipartETag(data []byte) string {
862875 end = len (data )
863876 }
864877
865- partHasher := md5 .New ()
878+ partHasher := md5 .New () //nolint:gosec // MD5 used for checksums, not cryptography
866879 partHasher .Write (data [start :end ])
867880 partMD5s = append (partMD5s , partHasher .Sum (nil )... )
868881 }
869882
870883 // Calculate MD5 of concatenated MD5s
871- finalHasher := md5 .New ()
884+ finalHasher := md5 .New () //nolint:gosec // MD5 used for checksums, not cryptography
872885 finalHasher .Write (partMD5s )
873886 finalMD5 := hex .EncodeToString (finalHasher .Sum (nil ))
874887
@@ -894,18 +907,18 @@ func (a *Archiver) uploadToS3(key string, data []byte) error {
894907
895908 _ , err := a .s3Uploader .Upload (uploadInput )
896909 return err
897- } else {
898- // Use simple PutObject for smaller files
899- putInput := & s3.PutObjectInput {
900- Bucket : aws .String (a .config .S3 .Bucket ),
901- Key : aws .String (key ),
902- Body : bytes .NewReader (data ),
903- ContentType : aws .String ("application/zstd" ),
904- }
910+ }
905911
906- _ , err := a .s3Client .PutObject (putInput )
907- return err
912+ // Use simple PutObject for smaller files
913+ putInput := & s3.PutObjectInput {
914+ Bucket : aws .String (a .config .S3 .Bucket ),
915+ Key : aws .String (key ),
916+ Body : bytes .NewReader (data ),
917+ ContentType : aws .String ("application/zstd" ),
908918 }
919+
920+ _ , err := a .s3Client .PutObject (putInput )
921+ return err
909922}
910923
911924func (a * Archiver ) printSummary (results []ProcessResult ) {
0 commit comments