778 lines
22 KiB
Go
778 lines
22 KiB
Go
//-----------------------------
|
||
//Distributed under GPL
|
||
//Author Sergey Kalinin
|
||
//svk@nuk-svk.ru
|
||
//------------------------------
|
||
|
||
package main
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"io"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/go-co-op/gocron"
|
||
"github.com/gorilla/mux"
|
||
"github.com/likexian/whois"
|
||
"github.com/oschwald/geoip2-golang"
|
||
"golang.org/x/text/language"
|
||
)
|
||
|
||
type GeoData struct {
|
||
// IP адрес
|
||
IPAddress string `json:"ip_address"`
|
||
// Местоположение
|
||
Location struct {
|
||
Country string `json:"country,omitempty"`
|
||
City string `json:"city,omitempty"`
|
||
Continent string `json:"continent,omitempty"`
|
||
Region string `json:"region,omitempty"`
|
||
RegionISO string `json:"region_iso,omitempty"`
|
||
Latitude float64 `json:"latitude,omitempty"`
|
||
Longitude float64 `json:"longitude,omitempty"`
|
||
TimeZone string `json:"time_zone,omitempty"`
|
||
PostalCode string `json:"postal_code,omitempty"`
|
||
} `json:"location"`
|
||
// Данные о сети
|
||
Network struct {
|
||
ASN uint `json:"asn,omitempty"`
|
||
ASOrg string `json:"autonomous_system_organization,omitempty"`
|
||
} `json:"network"`
|
||
// ISO коды
|
||
Codes struct {
|
||
CountryISO string `json:"country,omitempty"`
|
||
ContinentISO string `json:"continent,omitempty"`
|
||
} `json:"codes"`
|
||
// Прокси
|
||
Security struct {
|
||
IsAnonymousProxy bool `json:"is_anonymous_proxy,omitempty"`
|
||
IsSatelliteProvider bool `json:"is_satellite_provider,omitempty"`
|
||
} `json:"security"`
|
||
// Локаль
|
||
Metadata struct {
|
||
LocaleUsed string `json:"locale_used,omitempty"`
|
||
DataSource string `json:"data_source,omitempty"`
|
||
} `json:"metadata"`
|
||
}
|
||
|
||
// TemplateData структура для передачи данных в шаблон
|
||
type TemplateData struct {
|
||
*GeoData
|
||
Error string
|
||
JSONData string
|
||
WhoIsData string
|
||
BaseURL string
|
||
DBVersion string
|
||
}
|
||
|
||
type Config struct {
|
||
MMDBURL string
|
||
MMDBLocalPath string
|
||
HTMLTemplatePath string
|
||
ListenPort string
|
||
}
|
||
|
||
var (
|
||
cityDB *geoip2.Reader
|
||
countryDB *geoip2.Reader
|
||
asnDB *geoip2.Reader
|
||
templates *template.Template
|
||
cfg *Config
|
||
dbMutex sync.RWMutex
|
||
dbFilesVersion string
|
||
)
|
||
|
||
func main() {
|
||
var err error
|
||
// Загрузка конфигурации
|
||
cfg = LoadConfig()
|
||
|
||
// Запускаем скачивание файлов
|
||
mmdbDownload("GeoLite2-City.mmdb")
|
||
mmdbDownload("GeoLite2-Country.mmdb")
|
||
mmdbDownload("GeoLite2-ASN.mmdb")
|
||
|
||
// Запускаем планировщик параллельно с основным процессом
|
||
go sheduler()
|
||
|
||
// Открываем БД
|
||
pathGeoLite2City := filepath.Join(cfg.MMDBLocalPath, "GeoLite2-City.mmdb")
|
||
log.Println(pathGeoLite2City)
|
||
|
||
cityDB, err = geoip2.Open(pathGeoLite2City)
|
||
if err != nil {
|
||
log.Fatal("Failed to open City database:", err)
|
||
}
|
||
defer cityDB.Close()
|
||
|
||
pathGeoLite2Country := filepath.Join(cfg.MMDBLocalPath, "GeoLite2-Country.mmdb")
|
||
countryDB, err = geoip2.Open(pathGeoLite2Country)
|
||
if err != nil {
|
||
log.Fatal("Failed to open Country database:", err)
|
||
}
|
||
defer countryDB.Close()
|
||
|
||
pathGeoLite2ASN := filepath.Join(cfg.MMDBLocalPath, "GeoLite2-ASN.mmdb")
|
||
asnDB, err = geoip2.Open(pathGeoLite2ASN)
|
||
if err != nil {
|
||
log.Fatal("Failed to open ASN database:", err)
|
||
}
|
||
defer asnDB.Close()
|
||
|
||
dbFilesVersion = getFileModTime(pathGeoLite2City)
|
||
|
||
// Загружаем шаблон
|
||
templates = template.Must(template.ParseFiles(filepath.Join(cfg.HTMLTemplatePath, "index.html")))
|
||
|
||
// Обработка ссылок
|
||
r := mux.NewRouter()
|
||
r.HandleFunc("/", homeHandler)
|
||
r.HandleFunc("/api/{ip}", apiHandler)
|
||
r.HandleFunc("/api/", apiCurrentHandler)
|
||
r.HandleFunc("/lookup/{ip}", lookupHandler)
|
||
r.HandleFunc("/{ip}", lookupHandler)
|
||
|
||
log.Println("Server starting on:", cfg.ListenPort)
|
||
log.Fatal(http.ListenAndServe(":"+cfg.ListenPort, r))
|
||
}
|
||
|
||
func getFileModTime(filePath string) string {
|
||
// Получаем информацию о файле
|
||
fileInfo, err := os.Stat(filePath)
|
||
if err != nil {
|
||
log.Println("Ошибка в getFileModTime:", err)
|
||
return "Error"
|
||
}
|
||
|
||
// Получаем время последнего изменения
|
||
modTime := fileInfo.ModTime()
|
||
// fmt.Println("Время изменения файла:", modTime)
|
||
|
||
// Форматируем вывод
|
||
// fmt.Printf("Форматированное время: %s\n", modTime.Format("02/01/2006 15:04:05"))
|
||
return modTime.Format("02/01/2006 15:04:05")
|
||
}
|
||
|
||
// reopenDBs закрывает и заново открывает нужную БД
|
||
func reopenDBs(fileName string) {
|
||
dbMutex.Lock()
|
||
defer dbMutex.Unlock()
|
||
|
||
var err error
|
||
|
||
switch fileName {
|
||
case "GeoLite2-City.mmdb":
|
||
if cityDB != nil {
|
||
cityDB.Close()
|
||
}
|
||
// Открываем базы данных заново
|
||
pathGeoLite2City := filepath.Join(cfg.MMDBLocalPath, fileName)
|
||
cityDB, err = geoip2.Open(pathGeoLite2City)
|
||
if err != nil {
|
||
log.Printf("Ошибка открытия базы данных %v: %v", fileName, err)
|
||
return
|
||
}
|
||
case "GeoLite2-Country.mmdb":
|
||
if countryDB != nil {
|
||
countryDB.Close()
|
||
}
|
||
pathGeoLite2Country := filepath.Join(cfg.MMDBLocalPath, fileName)
|
||
countryDB, err = geoip2.Open(pathGeoLite2Country)
|
||
if err != nil {
|
||
log.Printf("Ошибка открытия базы данных %v: %v", fileName, err)
|
||
return
|
||
}
|
||
case "GeoLite2-ASN.mmdb":
|
||
if asnDB != nil {
|
||
asnDB.Close()
|
||
}
|
||
pathGeoLite2ASN := filepath.Join(cfg.MMDBLocalPath, fileName)
|
||
asnDB, err = geoip2.Open(pathGeoLite2ASN)
|
||
if err != nil {
|
||
log.Printf("Ошибка открытия базы данных %v: %v", fileName, err)
|
||
return
|
||
}
|
||
}
|
||
log.Println("Открыта база данных:", fileName)
|
||
dbFilesVersion = getFileModTime(filepath.Join(cfg.MMDBLocalPath, fileName))
|
||
}
|
||
|
||
func homeHandler(w http.ResponseWriter, r *http.Request) {
|
||
baseURL := getBaseURL(r)
|
||
|
||
// Обрабатываем параметр ip из GET запроса
|
||
ipStr := r.URL.Query().Get("ip")
|
||
var geoData *GeoData
|
||
var errMsg string
|
||
var whoisData string
|
||
|
||
if ipStr != "" {
|
||
ip := net.ParseIP(ipStr)
|
||
if ip == nil {
|
||
errMsg = "Неверный IP адрес: " + ipStr
|
||
} else {
|
||
preferredLocale := getPreferredLocale(r)
|
||
var err error
|
||
geoData, err = getGeoData(ip, preferredLocale)
|
||
if err != nil {
|
||
errMsg = err.Error()
|
||
}
|
||
whoisData = getWhoIsInfo(ipStr)
|
||
}
|
||
}
|
||
|
||
data := TemplateData{
|
||
GeoData: geoData,
|
||
WhoIsData: whoisData,
|
||
Error: errMsg,
|
||
BaseURL: baseURL,
|
||
DBVersion: dbFilesVersion,
|
||
}
|
||
|
||
err := templates.ExecuteTemplate(w, "index.html", data)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
}
|
||
}
|
||
|
||
func apiCurrentHandler(w http.ResponseWriter, r *http.Request) {
|
||
// Определяем IP клиента
|
||
ipStr := getClientIP(r)
|
||
ip := net.ParseIP(ipStr)
|
||
if ip == nil {
|
||
sendJSONError(w, "Unable to determine client IP", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
// Определяем локаль
|
||
preferredLocale := getPreferredLocale(r)
|
||
geoData, err := getGeoData(ip, preferredLocale)
|
||
if err != nil {
|
||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
sendJSONResponse(w, geoData)
|
||
}
|
||
|
||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
ipStr := vars["ip"]
|
||
|
||
// Проверяем IP
|
||
ip := net.ParseIP(ipStr)
|
||
if ip == nil {
|
||
sendJSONError(w, "Invalid IP address", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
// Определяем локаль
|
||
preferredLocale := getPreferredLocale(r)
|
||
geoData, err := getGeoData(ip, preferredLocale)
|
||
if err != nil {
|
||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
sendJSONResponse(w, geoData)
|
||
}
|
||
|
||
// Обрабатываем запросы вида /lookup/{ip} и возвращает HTML страницу с результатами
|
||
func lookupHandler(w http.ResponseWriter, r *http.Request) {
|
||
vars := mux.Vars(r)
|
||
ipStr := vars["ip"]
|
||
baseURL := getBaseURL(r)
|
||
|
||
// Проверка IP
|
||
ip := net.ParseIP(ipStr)
|
||
if ip == nil {
|
||
// Если IP невалидный, показываем ошибку на странице
|
||
templates.ExecuteTemplate(w, "index.html", TemplateData{
|
||
Error: "Invalid IP address: " + ipStr,
|
||
BaseURL: baseURL,
|
||
})
|
||
return
|
||
}
|
||
|
||
// Определяем локаль
|
||
preferredLocale := getPreferredLocale(r)
|
||
geoData, err := getGeoData(ip, preferredLocale)
|
||
if err != nil {
|
||
templates.ExecuteTemplate(w, "index.html", TemplateData{
|
||
Error: err.Error(),
|
||
BaseURL: baseURL,
|
||
})
|
||
return
|
||
}
|
||
|
||
jsonData, err := json.MarshalIndent(geoData, "", " ")
|
||
if err != nil {
|
||
jsonData = []byte("{\"error\": \"Failed to generate JSON\"}")
|
||
}
|
||
|
||
whoisData := getWhoIsInfo(ipStr)
|
||
|
||
// Передаем данные в шаблон
|
||
templates.ExecuteTemplate(w, "index.html", TemplateData{
|
||
GeoData: geoData,
|
||
JSONData: string(jsonData),
|
||
WhoIsData: whoisData,
|
||
BaseURL: baseURL,
|
||
DBVersion: dbFilesVersion,
|
||
})
|
||
}
|
||
|
||
// Определяем локаль броузера
|
||
func getPreferredLocale(r *http.Request) string {
|
||
acceptLang := r.Header.Get("Accept-Language")
|
||
if acceptLang == "" {
|
||
return "en"
|
||
}
|
||
|
||
tags, _, err := language.ParseAcceptLanguage(acceptLang)
|
||
if err != nil || len(tags) == 0 {
|
||
return "en"
|
||
}
|
||
|
||
preferred := tags[0]
|
||
base, _ := preferred.Base()
|
||
|
||
locale := base.String()
|
||
|
||
supportedLocales := []string{"en", "de", "es", "fr", "ja", "pt-BR", "ru", "zh-CN"}
|
||
|
||
for _, supported := range supportedLocales {
|
||
if locale == supported {
|
||
return locale
|
||
}
|
||
}
|
||
|
||
// Возвращем en если ничего другого не найдено
|
||
return "en"
|
||
}
|
||
|
||
func sendJSONResponse(w http.ResponseWriter, data interface{}) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||
|
||
// Pretty print JSON with indentation
|
||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||
if err != nil {
|
||
sendJSONError(w, "Failed to encode JSON response", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
w.Write(jsonData)
|
||
}
|
||
|
||
func sendJSONError(w http.ResponseWriter, message string, statusCode int) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||
w.WriteHeader(statusCode)
|
||
|
||
errorResponse := map[string]interface{}{
|
||
"error": map[string]string{
|
||
"message": message,
|
||
"code": http.StatusText(statusCode),
|
||
},
|
||
"success": false,
|
||
}
|
||
|
||
jsonData, _ := json.MarshalIndent(errorResponse, "", " ")
|
||
w.Write(jsonData)
|
||
}
|
||
|
||
// Определяем адрес клиента
|
||
func getClientIP(r *http.Request) string {
|
||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
||
return forwarded
|
||
}
|
||
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
|
||
return realIP
|
||
}
|
||
|
||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||
if err != nil {
|
||
return r.RemoteAddr
|
||
}
|
||
return host
|
||
}
|
||
|
||
// Проверяем доступную локаль
|
||
func getLocalizedString(names map[string]string, preferredLocale string) string {
|
||
if names == nil {
|
||
return ""
|
||
}
|
||
|
||
// Определяем локаль
|
||
if name, exists := names[preferredLocale]; exists && name != "" {
|
||
return name
|
||
}
|
||
|
||
// Переключаем на en
|
||
if name, exists := names["en"]; exists {
|
||
return name
|
||
}
|
||
|
||
// Если нет Английского возвращем пустоту
|
||
for _, name := range names {
|
||
if name != "" {
|
||
return name
|
||
}
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
func hasRegisteredCountryData(registeredCountry struct {
|
||
Names map[string]string `maxminddb:"names"`
|
||
IsoCode string `maxminddb:"iso_code"`
|
||
GeoNameID uint `maxminddb:"geoname_id"`
|
||
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
|
||
}) bool {
|
||
return registeredCountry.IsoCode != "" || len(registeredCountry.Names) > 0
|
||
}
|
||
|
||
// Получаем данные из БД MaxMind
|
||
func getGeoData(ip net.IP, preferredLocale string) (*GeoData, error) {
|
||
dbMutex.RLock()
|
||
defer dbMutex.RUnlock()
|
||
|
||
data := &GeoData{}
|
||
data.IPAddress = ip.String()
|
||
data.Metadata.LocaleUsed = preferredLocale
|
||
|
||
// Получаем данные из БД City
|
||
if city, err := cityDB.City(ip); err == nil {
|
||
data.Metadata.DataSource = "city"
|
||
|
||
// Сперва определяем страну (main country)
|
||
data.Location.Country = getLocalizedString(city.Country.Names, preferredLocale)
|
||
data.Codes.CountryISO = city.Country.IsoCode
|
||
|
||
// Если (main country) пустое то пробуем (registered country)
|
||
if data.Location.Country == "" && hasRegisteredCountryData(city.RegisteredCountry) {
|
||
data.Location.Country = getLocalizedString(city.RegisteredCountry.Names, preferredLocale)
|
||
if data.Codes.CountryISO == "" {
|
||
data.Codes.CountryISO = city.RegisteredCountry.IsoCode
|
||
}
|
||
}
|
||
|
||
// Определяем континет (main continent)
|
||
data.Location.Continent = getLocalizedString(city.Continent.Names, preferredLocale)
|
||
data.Codes.ContinentISO = city.Continent.Code
|
||
|
||
// Город и данные местоположения
|
||
data.Location.City = getLocalizedString(city.City.Names, preferredLocale)
|
||
data.Location.Latitude = city.Location.Latitude
|
||
data.Location.Longitude = city.Location.Longitude
|
||
data.Location.TimeZone = city.Location.TimeZone
|
||
data.Location.PostalCode = city.Postal.Code
|
||
data.Security.IsAnonymousProxy = city.Traits.IsAnonymousProxy
|
||
data.Security.IsSatelliteProvider = city.Traits.IsSatelliteProvider
|
||
|
||
// Регион
|
||
if len(city.Subdivisions) > 0 {
|
||
subdivision := city.Subdivisions[0]
|
||
data.Location.Region = getLocalizedString(subdivision.Names, preferredLocale)
|
||
data.Location.RegionISO = subdivision.IsoCode
|
||
}
|
||
}
|
||
|
||
// Определяем страну (fallback for country data)
|
||
if data.Location.Country == "" {
|
||
if country, err := countryDB.Country(ip); err == nil {
|
||
if data.Metadata.DataSource == "" {
|
||
data.Metadata.DataSource = "country"
|
||
}
|
||
|
||
// Сперва определяем страну (main country)
|
||
countryName := getLocalizedString(country.Country.Names, preferredLocale)
|
||
countryISO := country.Country.IsoCode
|
||
|
||
// Если (main country) пустое то пробуем (registered country)
|
||
if countryName == "" && hasRegisteredCountryData(country.RegisteredCountry) {
|
||
countryName = getLocalizedString(country.RegisteredCountry.Names, preferredLocale)
|
||
if countryISO == "" {
|
||
countryISO = country.RegisteredCountry.IsoCode
|
||
}
|
||
}
|
||
|
||
// Обновлем данные если что нашли
|
||
if countryName != "" {
|
||
data.Location.Country = countryName
|
||
}
|
||
if countryISO != "" {
|
||
data.Codes.CountryISO = countryISO
|
||
}
|
||
|
||
// Континент
|
||
continentName := getLocalizedString(country.Continent.Names, preferredLocale)
|
||
continentISO := country.Continent.Code
|
||
|
||
if continentName != "" {
|
||
data.Location.Continent = continentName
|
||
}
|
||
if continentISO != "" {
|
||
data.Codes.ContinentISO = continentISO
|
||
}
|
||
|
||
// Прокси
|
||
data.Security.IsAnonymousProxy = country.Traits.IsAnonymousProxy
|
||
data.Security.IsSatelliteProvider = country.Traits.IsSatelliteProvider
|
||
}
|
||
}
|
||
|
||
// Читаем данные из бд ASN
|
||
if asn, err := asnDB.ASN(ip); err == nil {
|
||
data.Network.ASN = asn.AutonomousSystemNumber
|
||
data.Network.ASOrg = asn.AutonomousSystemOrganization
|
||
}
|
||
|
||
return data, nil
|
||
}
|
||
|
||
// Определяем базовый URL сайта где запуцщен проект
|
||
func getBaseURL(r *http.Request) string {
|
||
scheme := "http"
|
||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||
scheme = "https"
|
||
}
|
||
|
||
host := r.Host
|
||
// Убираем порт если это стандартный порт
|
||
if strings.HasSuffix(host, ":80") && scheme == "http" {
|
||
host = strings.TrimSuffix(host, ":80")
|
||
} else if strings.HasSuffix(host, ":443") && scheme == "https" {
|
||
host = strings.TrimSuffix(host, ":443")
|
||
}
|
||
|
||
return scheme + "://" + host
|
||
}
|
||
|
||
// Запрос информации с серверов whois
|
||
func getWhoIsInfo(address string) string {
|
||
result, err := whois.Whois(address)
|
||
|
||
if err != nil {
|
||
return "WhoIs request error"
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// Загрузка конфигурации
|
||
func LoadConfig() *Config {
|
||
return &Config{
|
||
MMDBURL: getEnv("MMDB_URL", "localhost"),
|
||
MMDBLocalPath: getEnv("MMDB_LOCAL_PATH", "/usr/local/share/geoip/db"),
|
||
HTMLTemplatePath: getEnv("HTML_TEMPLATE_PATH", "/usr/local/share/geoip/templates"),
|
||
ListenPort: getEnv("LISTEN_PORT", "8080"),
|
||
}
|
||
}
|
||
|
||
// Получение знаячений переменных окружения
|
||
func getEnv(key, defaultValue string) string {
|
||
if value := os.Getenv(key); value != "" {
|
||
return value
|
||
}
|
||
return defaultValue
|
||
}
|
||
|
||
// Запуск задания скачивания файлов
|
||
func sheduler() {
|
||
// инициализируем объект планировщика
|
||
s := gocron.NewScheduler(time.UTC)
|
||
// добавляем одну задачу на каждую минуту
|
||
s.Cron("30 2 * * *").Do(startDownload)
|
||
// s.Cron("*/1 * * * *").Do(startDownload)
|
||
// запускаем планировщик с блокировкой текущего потока
|
||
s.StartBlocking()
|
||
}
|
||
|
||
// Отслеживает прогресс загрузки
|
||
type WriteCounter struct {
|
||
Total uint64
|
||
}
|
||
|
||
// Параллельный запуск загрузки БД
|
||
func startDownload() {
|
||
go mmdbDownload("GeoLite2-City.mmdb")
|
||
go mmdbDownload("GeoLite2-Country.mmdb")
|
||
go mmdbDownload("GeoLite2-ASN.mmdb")
|
||
|
||
}
|
||
|
||
func (wc *WriteCounter) Write(p []byte) (int, error) {
|
||
n := len(p)
|
||
wc.Total += uint64(n)
|
||
wc.PrintProgress()
|
||
return n, nil
|
||
}
|
||
|
||
func (wc WriteCounter) PrintProgress() {
|
||
fmt.Printf("\rЗагружено %d байт...", wc.Total)
|
||
}
|
||
|
||
// Проверка целостности файла.
|
||
func isValidMMDB(filePath string) bool {
|
||
db, err := geoip2.Open(filePath)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
db.Close()
|
||
return true
|
||
}
|
||
|
||
// Проверяем контрольную сумму локального файла
|
||
func getFileChecksum(filePath string) (string, error) {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer file.Close()
|
||
|
||
hash := sha256.New()
|
||
if _, err := io.Copy(hash, file); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||
}
|
||
|
||
// Проверяем контрольную сумму удаленного файла (скачиваемого)
|
||
func getRemoteChecksum(url string) string {
|
||
resp, err := http.Get(url)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return ""
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
|
||
// Предполагаем, что файл содержит только хеш
|
||
return strings.TrimSpace(string(body))
|
||
}
|
||
|
||
func shouldDownload(fileName string) bool {
|
||
log.Printf("Проверяем контрольную сумму файла %s", fileName)
|
||
fileURL := cfg.MMDBURL + fileName
|
||
filePath := filepath.Join(cfg.MMDBLocalPath, fileName)
|
||
|
||
// Если файла нет, нужно скачивать
|
||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||
return true
|
||
}
|
||
|
||
// Проверяем возраст файла (скачиваем если старше 7 дней)
|
||
/*
|
||
if info, err := os.Stat(filePath); err == nil {
|
||
if time.Since(info.ModTime()) > 7*24*time.Hour {
|
||
return true
|
||
}
|
||
}
|
||
|
||
*/
|
||
// Проверка по контрольной сумме
|
||
if checksumURL := fileURL + ".sha256"; checksumURL != "" {
|
||
remoteFileChecksum := getRemoteChecksum(checksumURL)
|
||
// fmt.Println("Remote", remoteFileChecksum)
|
||
|
||
if remoteFileChecksum != "" {
|
||
localFileChecksum, error := getFileChecksum(filePath)
|
||
if error == nil {
|
||
if localFileChecksum != "" {
|
||
fmt.Println(filePath, "Remote:", remoteFileChecksum, "Local:", localFileChecksum)
|
||
fmt.Println(remoteFileChecksum)
|
||
fmt.Println(localFileChecksum)
|
||
if localFileChecksum != remoteFileChecksum {
|
||
return false
|
||
}
|
||
}
|
||
} else {
|
||
fmt.Println("Error", filePath, error)
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// Загрузка файлов БД с ВЭБ-сервера
|
||
func mmdbDownload(fileName string) {
|
||
fileURL := cfg.MMDBURL + fileName
|
||
filePath := filepath.Join(cfg.MMDBLocalPath, fileName)
|
||
|
||
if !shouldDownload(fileName) {
|
||
log.Printf("Файл %s актуален, пропускаем загрузку", fileName)
|
||
return
|
||
}
|
||
|
||
// Скачиваем во временный файл
|
||
tempPath := filePath + ".tmp"
|
||
|
||
log.Println("Загружается файл:", fileURL)
|
||
// Создаём файл
|
||
file, err := os.Create(tempPath)
|
||
if err != nil {
|
||
log.Printf("Ошибка создания файла %s: %v", tempPath, err)
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
// Загружаем данные с отслеживанием прогресса
|
||
counter := &WriteCounter{}
|
||
response, err := http.Get(fileURL)
|
||
if err != nil {
|
||
log.Printf("Ошибка загрузки файла %s: %v", fileURL, err)
|
||
return
|
||
}
|
||
defer response.Body.Close()
|
||
|
||
if response.StatusCode != http.StatusOK {
|
||
log.Printf("Ошибка загрузки: %s", response.Status)
|
||
return
|
||
}
|
||
|
||
// Копируем через буфер с отслеживанием прогресса
|
||
_, err = io.Copy(file, io.TeeReader(response.Body, counter))
|
||
if err != nil {
|
||
log.Printf("Ошибка копирования данных: %v", err)
|
||
return
|
||
}
|
||
|
||
log.Println("Загрузка завершена! Файл сохранен:", tempPath)
|
||
|
||
// Переоткрываем базы данных после успешной загрузки
|
||
// reopenDBs(fileName)
|
||
// Проверяем валидность перед заменой
|
||
if isValidMMDB(tempPath) {
|
||
// Заменяем старый файл
|
||
os.Rename(tempPath, filePath)
|
||
// Переоткрываем только эту базу
|
||
reopenDBs(fileName)
|
||
} else {
|
||
log.Printf("Скачанный файл %s поврежден, удаляем", fileName)
|
||
os.Remove(tempPath)
|
||
}
|
||
}
|