package geoip import ( "compress/gzip" "context" "fmt" "io" "net/http" "os" "path/filepath" "strings" ) // DownloadMMDB downloads an MMDB URL to destPath. If the URL ends with `.gz`, the body is gunzipped. // Otherwise the response body is written as-is (raw `.mmdb`, e.g. GitHub raw). func DownloadMMDB(ctx context.Context, client *http.Client, url, destPath string) error { if strings.HasSuffix(strings.ToLower(strings.TrimSpace(url)), ".gz") { return downloadGzippedMMDB(ctx, client, url, destPath) } return downloadRawMMDB(ctx, client, url, destPath) } func downloadRawMMDB(ctx context.Context, client *http.Client, url, destPath string) error { if client == nil { client = http.DefaultClient } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return fmt.Errorf("geoip download: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("geoip download: http %s", resp.Status) } dir := filepath.Dir(destPath) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("geoip mkdir %q: %w (directory must exist or be creatable by the process)", dir, err) } tmp, err := os.CreateTemp(dir, "geolite-*.mmdb.part") if err != nil { return fmt.Errorf("geoip temp file in %q: %w", dir, err) } tmpPath := tmp.Name() _, copyErr := io.Copy(tmp, resp.Body) closeErr := tmp.Close() if copyErr != nil { _ = os.Remove(tmpPath) return copyErr } if closeErr != nil { _ = os.Remove(tmpPath) return closeErr } _ = os.Remove(destPath) if err := os.Rename(tmpPath, destPath); err != nil { _ = os.Remove(tmpPath) return fmt.Errorf("geoip rename: %w", err) } return nil } func downloadGzippedMMDB(ctx context.Context, client *http.Client, url, destPath string) error { if client == nil { client = http.DefaultClient } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return fmt.Errorf("geoip download: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("geoip download: http %s", resp.Status) } gzr, err := gzip.NewReader(resp.Body) if err != nil { return fmt.Errorf("geoip gzip: %w", err) } defer gzr.Close() dir := filepath.Dir(destPath) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("geoip mkdir %q: %w (directory must exist or be creatable by the process)", dir, err) } tmp, err := os.CreateTemp(dir, "geolite-*.mmdb.part") if err != nil { return fmt.Errorf("geoip temp file in %q: %w", dir, err) } tmpPath := tmp.Name() _, copyErr := io.Copy(tmp, gzr) closeErr := tmp.Close() if copyErr != nil { _ = os.Remove(tmpPath) return copyErr } if closeErr != nil { _ = os.Remove(tmpPath) return closeErr } _ = os.Remove(destPath) if err := os.Rename(tmpPath, destPath); err != nil { _ = os.Remove(tmpPath) return fmt.Errorf("geoip rename: %w", err) } return nil }