This repository has been archived on 2026-08-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
ip-info/internal/data/ipdb.go
claudecodeandClaude 80aa464e09 Address codex review (#22): client IP validation, updated_at, merge robustness
1. /json client IP (codex #1, Medium): HandleJson now validates
   c.ClientIP() and returns 400 on a malformed value instead of letting
   Query fail into a 500. HandleIpInfo gained a selfLookup flag so a valid
   but DB-absent self-IP preserves the 9d07639 graceful 200 {"ip": ip}
   default (private/loopback addresses are never in a public GeoIP DB),
   while the explicit /:ip route still returns 404 on not-found.

2. updated_at=0 on initial load (codex #2, Low): stampDbUpdatedAt() now
   records the canonical mmdb's mtime both on initial load (existing
   files) and after each successful update, so /metrics never reports 0
   while serving real data.

3. Merge corruption -> permanent outage: mmdbmeld.WriteMMDB truncates the
   canonical mmdb before writing; a failed merge previously left it empty
   and the saved etag made the next daily run 304-skip the retry, so a
   restart failed to open the file and the service went 503 permanently.
   fetchDbs now backs up the canonical file before merge, restores it on
   merge/reload failure, and drops the etags so the next run retries.

4. Temp-file leaks: Reload removes the temp clone and restores d.dbFile
   when reader.Open fails (it previously assigned d.dbFile before Open);
   cloneDBFile removes the temp on a failed copy.

Verified: go build, go vet, gofmt -l, go test -race ./... all pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 17:11:58 +07:00

141 lines
2.7 KiB
Go

package data
import (
"fmt"
"github.com/rs/zerolog/log"
"io"
"os"
"path/filepath"
"sync"
"thuanle.me/ip-info/configs"
)
import reader "github.com/oschwald/maxminddb-golang/v2"
type IpDb struct {
r *reader.Reader
mu sync.RWMutex
dbFile string
}
var (
ins *IpDb
once sync.Once
)
func Ins() *IpDb {
once.Do(func() {
ins = &IpDb{}
_ = ins.Reload()
})
return ins
}
func CleanupDataDir() error {
if _, err := os.Stat(configs.GeoDbFolder); os.IsNotExist(err) {
log.Info().Str("dir", configs.GeoDbFolder).Msg("Creating data folder")
err := os.MkdirAll(configs.GeoDbFolder, os.ModePerm)
if err != nil {
return err
}
}
log.Info().Str("dir", configs.GeoDbFolder).Msg("Cleaning temp files")
dir, err := os.Open(configs.GeoDbFolder)
if err != nil {
return err
}
defer dir.Close()
// List all files in the directory
files, err := dir.Readdir(-1) // -1 means read all files
if err != nil {
return err
}
// Delete each file
for _, file := range files {
if !file.IsDir() && file.Name() != configs.MmdbDbFileName {
filePath := filepath.Join(configs.GeoDbFolder, file.Name())
err := os.Remove(filePath)
if err != nil {
return fmt.Errorf("failed to delete file %s: %w", filePath, err)
}
log.Info().Str("file", filePath).Msg("Deleted temp file")
}
}
return nil
}
func (d *IpDb) Reload() error {
d.mu.Lock()
defer d.mu.Unlock()
wilDeleteFile := d.dbFile
tmpFile, err := cloneDBFile()
if err != nil {
log.Err(err).Msg("Failed to clone db file")
return err
}
d.dbFile = tmpFile
r, err := reader.Open(tmpFile)
if err != nil {
log.Err(err).Str("file", tmpFile).Msg("Failed to open mmdb")
// Open failed: drop the orphaned temp clone and leave d.dbFile/d.r
// pointing at the previously loaded DB.
_ = os.Remove(tmpFile)
d.dbFile = wilDeleteFile
return err
}
log.Info().Str("file", tmpFile).Msg("MMDB reloaded")
tmpR := d.r
d.r = r
if tmpR != nil {
_ = tmpR.Close()
if wilDeleteFile != "" {
log.Info().Str("file", wilDeleteFile).Msg("Deleting old mmdb")
_ = os.Remove(wilDeleteFile)
}
}
return nil
}
func (d *IpDb) IsLoaded() bool {
d.mu.RLock()
defer d.mu.RUnlock()
return d.r != nil
}
func cloneDBFile() (string, error) {
srcFile, err := os.Open(configs.MmdbDbFile)
if err != nil {
return "", err
}
defer srcFile.Close()
tmpFile, err := os.CreateTemp(configs.GeoDbFolder, "geoip-v4-*.mmdb")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %v", err)
}
defer tmpFile.Close()
buf := make([]byte, 1024*1024) // 1 MB buffer
// Copy the data from srcFile to dstFile
_, err = io.CopyBuffer(tmpFile, srcFile, buf)
if err != nil {
// Leave nothing behind on a failed copy.
_ = os.Remove(tmpFile.Name())
return "", err
}
return tmpFile.Name(), nil
}