Merge pull request 'v0.1.0' (#1) from develop into main

Reviewed-on: #1
This commit is contained in:
2026-04-09 22:15:30 +05:00
9 changed files with 922 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
# geoip2
Получение данных с сервиса maxmind.com. Изначально пакет пишется для сервиса https://git.kor-elf.net/kor-elf-shield/kor-elf-shield.
## Установка
```sh
go get git.kor-elf.net/kor-elf-shield/geoip2
```
## Примеры использования
```go
package main
import (
"fmt"
"net/netip"
"git.kor-elf.net/kor-elf-shield/geoip2/service/maxmind/mmdb"
)
type Logger struct{}
func (l *Logger) Error(err error) {
fmt.Println(err)
}
func main() {
// Для получения username и password ознакомьтесь тут: https://support.maxmind.com/knowledge-base/articles/create-a-maxmind-account
username := "username"
password := "password"
download, err := mmdb.NewDownload(mmdb.DownloadURLCityLite, username, password, mmdb.DefaultDownloadConfig())
if err != nil {
panic(err)
}
city := mmdb.NewCity(download, &Logger{}, "tmp", mmdb.LanguageRussian)
defer func() {
_ = city.Close()
}()
/*
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Если хотите обновить данные
if err := city.Refresh(ctx); err != nil {
panic(err)
}
*/
// Вместо 127.0.0.1 укажите интересующиеся IP адрес
ip, err := netip.ParseAddr("127.0.0.1")
if err != nil {
panic(err)
}
info, err := city.Info(ip)
if err != nil {
panic(err)
}
fmt.Println(info.ToString())
}
```
## Лицензия
[MIT](https://git.kor-elf.net/kor-elf-shield/geoip2/src/branch/main/LICENSE)
+78
View File
@@ -0,0 +1,78 @@
package geoip2
import (
"context"
"fmt"
"net/netip"
"strings"
)
// ErrNotFound is returned when the database does not contain the IP address.
const ErrNotFound = "not found"
// Info is a structure that contains information about the IP address.
type Info struct {
// IP is the IP address.
IP netip.Addr
// ISOCode is the ISO code of the country.
ISOCode string
// Continent is the continent.
Continent string
// Country is the country.
Country string
// City is the city.
City string
// CitySubdivisions is the list of subdivisions.
CitySubdivisions []string
// TimeZone is the time zone.
TimeZone string
}
// ToString returns a string representation of the Info structure.
func (i Info) ToString() string {
var data []string
if i.Continent != "" {
data = append(data, i.Continent)
}
if i.ISOCode != "" {
data = append(data, i.ISOCode)
}
if i.Country != "" {
data = append(data, i.Country)
}
if len(i.CitySubdivisions) > 0 {
data = append(data, strings.Join(i.CitySubdivisions, ", "))
}
if i.City != "" {
data = append(data, i.City)
}
return fmt.Sprintf("%s (%s) time zone: %s",
i.IP.String(),
strings.Join(data, "/"),
i.TimeZone,
)
}
// GeoIP2 is an interface for geoip2.
type GeoIP2 interface {
Info(ip netip.Addr) (Info, error)
}
// RefreshableGeoIP2 is an interface for geoip2 with refresh and close functionality.
type RefreshableGeoIP2 interface {
GeoIP2
Refresh(ctx context.Context) error
Close() error
}
// Logger is an interface for logging.
type Logger interface {
// Error logs an error.
Error(err error)
}
+10
View File
@@ -0,0 +1,10 @@
module git.kor-elf.net/kor-elf-shield/geoip2
go 1.25
require github.com/oschwald/geoip2-golang/v2 v2.1.0
require (
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
golang.org/x/sys v0.38.0 // indirect
)
+14
View File
@@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+124
View File
@@ -0,0 +1,124 @@
package pkg
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"time"
)
type Dir struct {
Path string
PathCurrentDir string
PathBackupDir string
}
// NewDir creates a new Dir instance.
func NewDir(dir, nameCurrentDir string) *Dir {
return &Dir{
Path: dir,
PathCurrentDir: filepath.Join(dir, nameCurrentDir),
PathBackupDir: filepath.Join(dir, nameCurrentDir+".bak"),
}
}
// CreateRandomTmpDir creates a temporary directory in the specified directory with a random name.
// The directory is created with permissions 0750.
// If the directory already exists, an error is returned.
// @return string - path to the temporary directory
// @return error - an error if the directory could not be created
func (d *Dir) CreateRandomTmpDir() (string, error) {
tmpDir := filepath.Join(d.Path, time.Now().Format(time.RFC3339)+"_"+rand.Text()+".tmp")
if _, err := os.Stat(tmpDir); err == nil {
return "", fmt.Errorf("directory already exists: %s", tmpDir)
}
if err := os.MkdirAll(tmpDir, 0750); err != nil {
return "", err
}
return tmpDir, nil
}
// ReplaceDirToCurrent replaces the current directory with a new directory.
// If the new directory already exists, it is renamed to a backup directory.
// If the backup directory already exists, it is removed.
// @return error - an error if the directory could not be replaced
func (d *Dir) ReplaceDirToCurrent(newDir string) error {
if err := os.RemoveAll(d.PathBackupDir); err != nil {
return err
}
if _, err := os.Stat(d.PathCurrentDir); err == nil {
if err := os.Rename(d.PathCurrentDir, d.PathBackupDir); err != nil {
return err
}
}
if err := os.Rename(newDir, d.PathCurrentDir); err != nil {
if _, statErr := os.Stat(d.PathBackupDir); statErr == nil {
_ = os.Rename(d.PathBackupDir, d.PathCurrentDir)
}
return err
}
if err := os.RemoveAll(d.PathBackupDir); err != nil {
return err
}
return nil
}
// MovingFiles moves all files from dirFrom to dirTo.
// If dirTo does not exist, it is created.
// @param dirFrom - path to the directory containing the files to move
// @param dirTo - path to the directory where the files will be moved
// @return error - an error if the files could not be moved
func MovingFiles(dirFrom, dirTo string) error {
if _, err := os.Stat(dirTo); err != nil {
if err := os.MkdirAll(dirTo, 0750); err != nil {
return err
}
}
entries, err := os.ReadDir(dirFrom)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
if err := MovingFiles(filepath.Join(dirFrom, entry.Name()), dirTo); err != nil {
return err
}
continue
}
src := filepath.Join(dirFrom, entry.Name())
dst := filepath.Join(dirTo, entry.Name())
if err := os.Rename(src, dst); err != nil {
return err
}
}
return nil
}
// FindMMDBFile finds the .mmdb file in the specified directory.
// @param dir - path to the directory to search
// @return string - path to the .mmdb file
// @return error - an error if the .mmdb file could not be found
func FindMMDBFile(dir string) (string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if filepath.Ext(entry.Name()) == ".mmdb" {
return filepath.Join(dir, entry.Name()), nil
}
}
return "", fmt.Errorf("no .mmdb file found in %s", dir)
}
+187
View File
@@ -0,0 +1,187 @@
package mmdb
import (
"context"
"errors"
"fmt"
"net/netip"
"os"
"sync"
"git.kor-elf.net/kor-elf-shield/geoip2"
"git.kor-elf.net/kor-elf-shield/geoip2/internal/pkg"
oschwaldGeoip2 "github.com/oschwald/geoip2-golang/v2"
)
// download is an interface for downloading MaxMind GeoIP2 database.
type download interface {
// Download downloads MaxMind GeoIP2 database.
Download(dir string, ctx context.Context) error
}
type service struct {
// download is an interface for downloading MaxMind GeoIP2 database.
download download
// info is a function that returns information about the IP address.
info info
logger geoip2.Logger
// dir is a directory for storing MaxMind GeoIP2 database.
dir *pkg.Dir
// isRefreshing is a flag that indicates whether the database is being refreshed.
isRefreshing bool
// mu is a mutex for synchronizing access to the reader.
mu sync.RWMutex
// reader is a MaxMind GeoIP2 database reader.
reader *oschwaldGeoip2.Reader
}
// info is a function that returns information about the IP address.
// @param ip - IP address
// @param reader - MaxMind GeoIP2 database reader
// @return geoip2.Info - information about the IP address
type info func(ip netip.Addr, reader *oschwaldGeoip2.Reader) (geoip2.Info, error)
// NewMMDB creates a new MaxMind GeoIP2 database service.
// @param download - an interface for downloading MaxMind GeoIP2 database
// @param info - a function that returns information about the IP address
// @param logger - a logger
// @param dir - a directory for storing MaxMind GeoIP2 database
// @return geoip2.RefreshableGeoIP2 - a MaxMind GeoIP2 database service
func NewMMDB(download download, info info, logger geoip2.Logger, dir *pkg.Dir) geoip2.RefreshableGeoIP2 {
s := &service{
download: download,
logger: logger,
dir: dir,
info: info,
mu: sync.RWMutex{},
}
s.init()
return s
}
// Info returns information about the IP address.
// @param ip - IP address
// @return geoip2.Info - information about the IP address
// @return error - an error if the IP address could not be found
func (s *service) Info(ip netip.Addr) (geoip2.Info, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.reader == nil {
return geoip2.Info{}, errors.New("geoip reader is not ready")
}
return s.info(ip, s.reader)
}
// Refresh refreshes the MaxMind GeoIP2 database.
// @param ctx - a context
// @return error - an error if the database could not be refreshed
func (s *service) Refresh(ctx context.Context) error {
if s.isRefreshing {
return fmt.Errorf("process is refreshing")
}
s.isRefreshing = true
defer func() {
s.isRefreshing = false
}()
if err := s.fetch(ctx); err != nil {
return err
}
newReader, err := s.openReader()
if err != nil {
return err
}
s.mu.Lock()
oldReader := s.reader
s.reader = newReader
s.mu.Unlock()
if oldReader != nil {
_ = oldReader.Close()
}
return nil
}
// Close closes the MaxMind GeoIP2 database.
// @return error - an error if the database could not be closed
func (s *service) Close() error {
if s.reader != nil {
return s.reader.Close()
}
return nil
}
// init initializes the MaxMind GeoIP2 database service.
func (s *service) init() {
reader, err := s.openReader()
if err != nil {
go func() {
// If there is no data yet, then we try to get the data.
if err := s.Refresh(context.Background()); err != nil {
s.logger.Error(err)
}
}()
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.reader = reader
}
// openReader opens the MaxMind GeoIP2 database reader.
// @return (*oschwaldGeoip2.Reader, error) - a MaxMind GeoIP2 database reader and an error if the database could not be opened
// 1. If the database is already open, then it is returned.
// 2. If the database is not open, then it is opened and returned.
func (s *service) openReader() (*oschwaldGeoip2.Reader, error) {
path, err := pkg.FindMMDBFile(s.dir.PathCurrentDir)
if err != nil {
return nil, err
}
return oschwaldGeoip2.Open(path)
}
// fetch downloads the MaxMind GeoIP2 database.
// @param ctx - a context
// @return error - an error if the database could not be downloaded
func (s *service) fetch(ctx context.Context) error {
tmpDir, err := s.dir.CreateRandomTmpDir()
if err != nil {
return err
}
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
s.logger.Error(err)
}
}()
if err := s.download.Download(tmpDir, ctx); err != nil {
return err
}
tmpDirForFiles, err := s.dir.CreateRandomTmpDir()
if err != nil {
return err
}
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
s.logger.Error(err)
}
}()
if err := pkg.MovingFiles(tmpDir, tmpDirForFiles); err != nil {
return err
}
if err := s.dir.ReplaceDirToCurrent(tmpDirForFiles); err != nil {
return err
}
return nil
}
+189
View File
@@ -0,0 +1,189 @@
package mmdb
import (
"errors"
"net/netip"
"git.kor-elf.net/kor-elf-shield/geoip2"
"git.kor-elf.net/kor-elf-shield/geoip2/internal/pkg"
"git.kor-elf.net/kor-elf-shield/geoip2/internal/service/maxmind/mmdb"
oschwaldGeoip2 "github.com/oschwald/geoip2-golang/v2"
)
const (
DownloadURLCityLite = "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
NameDirNameCityLite = "GeoLite2-City"
)
// City is a structure that contains information about the IP address.
type City struct {
// lang is a language.
lang language
}
// NewCity creates a new City instance.
// @param download - an interface for downloading MaxMind GeoIP2 database
// @param logger - a logger
// @param dir - a directory for storing MaxMind GeoIP2 database
// @return geoip2.RefreshableGeoIP2 - a MaxMind GeoIP2 database service
func NewCity(download *Download, logger geoip2.Logger, dir string, language language) geoip2.RefreshableGeoIP2 {
city := &City{
lang: language,
}
newDir := pkg.NewDir(dir, NameDirNameCityLite)
return mmdb.NewMMDB(download, city.infoCity, logger, newDir)
}
// infoCity returns information about the IP address.
// @param ip - IP address
// @param reader - MaxMind GeoIP2 database reader
// @return geoip2.Info - information about the IP address
// @return error - an error if the IP address could not be found
func (c *City) infoCity(ip netip.Addr, reader *oschwaldGeoip2.Reader) (geoip2.Info, error) {
record, err := reader.City(ip)
if err != nil {
return geoip2.Info{}, err
}
if !record.HasData() {
return geoip2.Info{}, errors.New(geoip2.ErrNotFound)
}
var timeZone string
if record.Location.HasData() {
timeZone = record.Location.TimeZone
}
return geoip2.Info{
IP: ip,
ISOCode: record.Country.ISOCode,
Country: c.country(record.Country),
City: c.city(record.City),
CitySubdivisions: c.subdivisions(record.Subdivisions),
TimeZone: timeZone,
Continent: c.continent(record.Continent),
}, nil
}
func (c *City) continent(continent oschwaldGeoip2.Continent) string {
if !continent.HasData() {
return ""
}
switch c.lang {
case LanguageRussian:
return continent.Names.Russian
case LanguageEnglish:
return continent.Names.English
case LanguageBrazilianPortuguese:
return continent.Names.BrazilianPortuguese
case LanguageSimplifiedChinese:
return continent.Names.SimplifiedChinese
case LanguageJapanese:
return continent.Names.Japanese
case LanguageGerman:
return continent.Names.German
case LanguageFrench:
return continent.Names.French
case LanguageSpanish:
return continent.Names.Spanish
default:
return continent.Names.English
}
}
func (c *City) country(country oschwaldGeoip2.CountryRecord) string {
if !country.HasData() {
return ""
}
switch c.lang {
case LanguageRussian:
return country.Names.Russian
case LanguageEnglish:
return country.Names.English
case LanguageBrazilianPortuguese:
return country.Names.BrazilianPortuguese
case LanguageSimplifiedChinese:
return country.Names.SimplifiedChinese
case LanguageJapanese:
return country.Names.Japanese
case LanguageGerman:
return country.Names.German
case LanguageFrench:
return country.Names.French
case LanguageSpanish:
return country.Names.Spanish
default:
return country.Names.English
}
}
func (c *City) city(city oschwaldGeoip2.CityRecord) string {
if !city.HasData() {
return ""
}
switch c.lang {
case LanguageRussian:
return city.Names.Russian
case LanguageEnglish:
return city.Names.English
case LanguageBrazilianPortuguese:
return city.Names.BrazilianPortuguese
case LanguageSimplifiedChinese:
return city.Names.SimplifiedChinese
case LanguageJapanese:
return city.Names.Japanese
case LanguageGerman:
return city.Names.German
case LanguageFrench:
return city.Names.French
case LanguageSpanish:
return city.Names.Spanish
default:
return city.Names.English
}
}
func (c *City) subdivisions(subdivisions []oschwaldGeoip2.CitySubdivision) []string {
var citySubdivisions []string
if len(subdivisions) == 0 {
return citySubdivisions
}
for _, subdivision := range subdivisions {
citySubdivisions = append(citySubdivisions, c.subdivision(subdivision))
}
return citySubdivisions
}
func (c *City) subdivision(subdivision oschwaldGeoip2.CitySubdivision) string {
if !subdivision.HasData() {
return ""
}
switch c.lang {
case LanguageRussian:
return subdivision.Names.Russian
case LanguageEnglish:
return subdivision.Names.English
case LanguageBrazilianPortuguese:
return subdivision.Names.BrazilianPortuguese
case LanguageSimplifiedChinese:
return subdivision.Names.SimplifiedChinese
case LanguageJapanese:
return subdivision.Names.Japanese
case LanguageGerman:
return subdivision.Names.German
case LanguageFrench:
return subdivision.Names.French
case LanguageSpanish:
return subdivision.Names.Spanish
default:
return subdivision.Names.English
}
}
+240
View File
@@ -0,0 +1,240 @@
package mmdb
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/rand"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
)
const (
// requestTimeout defines the maximum duration for request operations before timing out.
requestTimeout = 60 * time.Second
// maxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
maxDownloadSize int64 = 200 << 20 // 200 MiB
// maxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
maxArchiveFileSize uint64 = 500 << 20 // 500 MiB
)
// DownloadConfig defines the configuration for downloading MaxMind GeoIP2 database.
type DownloadConfig struct {
// RequestTimeout defines the maximum duration for request operations before timing out.
RequestTimeout time.Duration
// MaxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
MaxDownloadSize int64
// MaxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
MaxArchiveFileSize uint64
}
// DefaultDownloadConfig returns the default configuration for downloading MaxMind GeoIP2 database.
func DefaultDownloadConfig() DownloadConfig {
return DownloadConfig{
RequestTimeout: requestTimeout,
MaxDownloadSize: maxDownloadSize,
MaxArchiveFileSize: maxArchiveFileSize,
}
}
// Download is a struct for downloading MaxMind GeoIP2 database.
type Download struct {
// url is a URL for downloading MaxMind GeoIP2 database.
url string
// username is a username for downloading MaxMind GeoIP2 database.
username string
// password is a password for downloading MaxMind GeoIP2 database.
password string
// Cfg is a configuration for downloading MaxMind GeoIP2 database.
Cfg DownloadConfig
}
// NewDownload creates a new Download struct.
// @param downloadURL - a URL for downloading MaxMind GeoIP2 database
// @param username - a username for downloading MaxMind GeoIP2 database
// @param password - a password for downloading MaxMind GeoIP2 database
// @param cfg - a configuration for downloading MaxMind GeoIP2 database
// @return (*Download, error) - a Download struct and an error if the download URL is invalid
func NewDownload(downloadURL, username, password string, cfg DownloadConfig) (*Download, error) {
parsedURL, err := url.Parse(downloadURL)
if err != nil {
return nil, fmt.Errorf("invalid url: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return nil, fmt.Errorf("invalid url scheme: %s", parsedURL.Scheme)
}
return &Download{
url: downloadURL,
username: username,
password: password,
Cfg: cfg,
}, nil
}
// Download downloads MaxMind GeoIP2 database.
// @param dir - a directory to save the downloaded database
// @param ctx - a context for cancelling the download
// @return error - an error if the download failed
func (d *Download) Download(dir string, ctx context.Context) error {
if err := createDir(dir); err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.url, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.SetBasicAuth(d.username, d.password)
client := &http.Client{
Timeout: d.Cfg.RequestTimeout,
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("download archive: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download archive: unexpected status %s", resp.Status)
}
filename := rand.Text() + ".tar.gz"
archivePath := filepath.Join(dir, filename)
file, err := os.Create(archivePath)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("archive file already exists: %w", err)
}
return fmt.Errorf("create archive file: %w", err)
}
limitedReader := io.LimitReader(resp.Body, d.Cfg.MaxDownloadSize+1)
written, copyErr := io.Copy(file, limitedReader)
closeErr := file.Close()
if copyErr != nil {
if removeErr := os.Remove(archivePath); removeErr != nil {
return fmt.Errorf("remove archive: %w", removeErr)
}
return fmt.Errorf("save archive: %w", copyErr)
}
if closeErr != nil {
if removeErr := os.Remove(archivePath); removeErr != nil {
return fmt.Errorf("remove archive: %w", removeErr)
}
return fmt.Errorf("close archive file: %w", closeErr)
}
if written > d.Cfg.MaxDownloadSize {
if removeErr := os.Remove(archivePath); removeErr != nil {
return fmt.Errorf("remove archive: %w", removeErr)
}
return fmt.Errorf("download archive too large: %d bytes", written)
}
if err := extractTarGz(archivePath, dir, d.Cfg.MaxArchiveFileSize); err != nil {
if removeErr := os.Remove(archivePath); removeErr != nil {
return fmt.Errorf("remove archive: %w", removeErr)
}
return err
}
if removeErr := os.Remove(archivePath); removeErr != nil {
return fmt.Errorf("remove archive: %w", removeErr)
}
return nil
}
// createDir creates a directory if it does not exist.
// @param dir - a directory to create
// @return error - an error if the directory could not be created
func createDir(dir string) error {
if err := os.MkdirAll(dir, 0750); err != nil {
return err
}
return nil
}
// extractTarGz extracts a tar.gz archive to a specified directory.
// @param archivePath - path to the archive to extract
// @param dir - path to the directory to extract the archive to
// @param maxExtractSize - maximum size of the extracted archive in bytes
// @return error - an error if the archive could not be extracted
func extractTarGz(archivePath string, dir string, maxExtractSize uint64) error {
file, err := os.Open(archivePath)
if err != nil {
return fmt.Errorf("open archive: %w", err)
}
defer func() {
_ = file.Close()
}()
gzr, err := gzip.NewReader(file)
if err != nil {
return fmt.Errorf("create gzip reader: %w", err)
}
defer func() {
_ = gzr.Close()
}()
tr := tar.NewReader(gzr)
var extractedSize uint64
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("read tar archive: %w", err)
}
targetPath := filepath.Join(dir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("create directory %q: %w", targetPath, err)
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(targetPath), 0750); err != nil {
return fmt.Errorf("create parent directory %q: %w", targetPath, err)
}
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return fmt.Errorf("create file %q: %w", targetPath, err)
}
n, err := io.Copy(outFile, tr)
closeErr := outFile.Close()
if err != nil {
return fmt.Errorf("write file %q: %w", targetPath, err)
}
if closeErr != nil {
return fmt.Errorf("close file %q: %w", targetPath, closeErr)
}
extractedSize += uint64(n)
if extractedSize > maxExtractSize {
return fmt.Errorf("extracted archive too large: %d bytes", extractedSize)
}
}
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
package mmdb
type language string
const (
LanguageRussian = language("Russian")
LanguageEnglish = language("English")
LanguageSpanish = language("Spanish")
LanguageFrench = language("French")
LanguageGerman = language("German")
LanguageJapanese = language("Japanese")
LanguageBrazilianPortuguese = language("BrazilianPortuguese")
LanguageSimplifiedChinese = language("SimplifiedChinese")
)