refactor: restructure project
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
Tom Neuber 2024-11-26 13:35:54 +01:00
parent 1bde2041e1
commit 8215e1f13a
Signed by: tom
GPG key ID: F17EFE4272D89FF6
21 changed files with 850 additions and 269 deletions

30
internal/cache/cache.go vendored Normal file
View file

@ -0,0 +1,30 @@
package cache
import (
"sync"
"time"
)
type Cache struct {
ttl time.Duration
store sync.Map
}
func NewCache(ttl time.Duration) *Cache {
return &Cache{
ttl: ttl,
}
}
func (c *Cache) Set(key, value uint) {
c.store.Store(key, value)
time.AfterFunc(c.ttl, func() {
c.store.Delete(key)
})
}
func (c *Cache) Get(key uint) (uint, bool) {
output, _ := c.store.Load(key)
value, ok := output.(uint)
return value, ok
}

View file

@ -0,0 +1,81 @@
package csvimporter
import (
"encoding/csv"
"os"
"strconv"
"git.ar21.de/yolokube/country-geo-locations/internal/database"
"git.ar21.de/yolokube/country-geo-locations/pkg/geoloc"
)
func parseCSV(filePath string) ([]geoloc.IPInfo, error) {
file, fileErr := os.Open(filePath)
if fileErr != nil {
return nil, fileErr
}
defer file.Close()
reader := csv.NewReader(file)
var data []geoloc.IPInfo
for {
record, err := reader.Read()
if err != nil {
break
}
ipnumfrom, err := strconv.ParseUint(record[0], 10, 64)
if err != nil {
continue
}
ipnumto, err := strconv.ParseUint(record[1], 10, 64)
if err != nil {
continue
}
if record[2] == "-" {
record[2] = ""
}
if record[3] == "-" {
record[3] = ""
}
if record[4] == "-" {
record[4] = ""
}
if record[5] == "-" {
record[5] = ""
}
latitude, err := strconv.ParseFloat(record[6], 32)
if err != nil {
latitude = 0
}
longitude, err := strconv.ParseFloat(record[7], 32)
if err != nil {
longitude = 0
}
ipinfo := geoloc.IPInfo{
IPNumFrom: uint(ipnumfrom),
IPNumTo: uint(ipnumto),
Code: record[2],
Country: record[3],
State: record[4],
City: record[5],
Latitude: float32(latitude),
Longitude: float32(longitude),
}
data = append(data, ipinfo)
}
return data, nil
}
func ImportCSV(filePath string, db *database.Database) error {
ipinfos, err := parseCSV(filePath)
if err != nil {
return err
}
return db.Load(ipinfos)
}

View file

@ -0,0 +1,89 @@
package database
import (
"errors"
"git.ar21.de/yolokube/country-geo-locations/pkg/geoloc"
"github.com/hashicorp/go-memdb"
)
var (
ErrUnknownInterface = errors.New("unknown interface structure")
ErrIPNetNotFound = errors.New("IP net not found")
)
type Database struct {
db *memdb.MemDB
}
func NewDatabase() (*Database, error) {
database, err := memdb.NewMemDB(
&memdb.DBSchema{
Tables: map[string]*memdb.TableSchema{
"ipinfo": {
Name: "ipinfo",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
Unique: true,
Indexer: &memdb.UintFieldIndex{Field: "IPNumFrom"},
},
},
},
},
},
)
if err != nil {
return nil, err
}
return &Database{
db: database,
}, nil
}
func (d *Database) Load(ipinfos []geoloc.IPInfo) error {
txn := d.db.Txn(true)
defer txn.Abort()
for _, ipinfo := range ipinfos {
if err := txn.Insert("ipinfo", ipinfo); err != nil {
return err
}
}
txn.Commit()
return nil
}
func (d *Database) SearchIPNet(ipnetnum uint) (*geoloc.IPInfo, error) {
txn := d.db.Txn(false)
defer txn.Abort()
var (
ipinfo geoloc.IPInfo
ok bool
)
for {
raw, err := txn.First("ipinfo", "id", ipnetnum)
if err != nil {
return nil, err
}
if raw != nil {
ipinfo, ok = raw.(geoloc.IPInfo)
if !ok {
return nil, ErrUnknownInterface
}
break
}
if ipnetnum == 0 {
return nil, ErrIPNetNotFound
}
ipnetnum -= geoloc.CalculationValue
}
return &ipinfo, nil
}

View file

@ -0,0 +1,87 @@
package downloader
import (
"errors"
"io"
"net/http"
"os"
"strings"
req "github.com/levigross/grequests"
"github.com/schollz/progressbar/v3"
)
var (
ErrAccessDenied = errors.New("restricted access (credentials required)")
ErrInvalidURL = errors.New("invalid url")
ErrUnknownAuthority = errors.New("certificate from unknown authority")
)
type Context struct {
Filename string
Link string
}
func NewContext(filename, link string) *Context {
return &Context{
Filename: filename,
Link: link,
}
}
func (c *Context) Download() error {
resp, err := req.Head(c.Link, nil)
if err != nil {
if strings.Contains(err.Error(), "no such host") {
return ErrInvalidURL
}
if strings.Contains(err.Error(), "certificate signed by unknown authority") {
return ErrUnknownAuthority
}
return err
}
defer resp.Close()
if resp.StatusCode == http.StatusNotFound {
return ErrInvalidURL
}
if resp.StatusCode == http.StatusUnauthorized {
return ErrAccessDenied
}
if resp.StatusCode != http.StatusOK {
return errors.New(resp.RawResponse.Status)
}
resp.Close()
resp, err = req.Get(c.Link, nil)
if err != nil {
return err
}
var filesize int64
if resp.RawResponse.ContentLength > -1 {
filesize = resp.RawResponse.ContentLength
}
destFile, err := os.OpenFile(c.Filename, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer destFile.Close()
bar := progressbar.DefaultBytes(
filesize,
"downloading",
)
_, err = io.Copy(io.MultiWriter(destFile, bar), resp.RawResponse.Body)
if err != nil {
return err
}
return nil
}
func (c *Context) FileExists() bool {
_, err := os.Stat(c.Filename)
return !errors.Is(err, os.ErrNotExist)
}

View file

@ -0,0 +1,118 @@
package downloader_test
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"git.ar21.de/yolokube/country-geo-locations/internal/downloader"
)
// TestNewContext tests the creation of a new Context.
func TestNewContext(t *testing.T) {
filename := "testfile"
link := "http://example.com"
ctx := downloader.NewContext(filename, link)
if ctx.Filename != filename {
t.Errorf("expected %s, got %s", filename, ctx.Filename)
}
if ctx.Link != link {
t.Errorf("expected %s, got %s", link, ctx.Link)
}
}
// TestDownload tests the Download function with different scenarios.
func TestDownload(t *testing.T) {
t.Run("ValidDownload", func(t *testing.T) {
// Mock server to simulate a valid download
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("This is the file content"))
}))
defer server.Close()
filename := "test_valid_download.txt"
ctx := downloader.NewContext(filename, server.URL)
defer os.Remove(filename)
err := ctx.Download()
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if !ctx.FileExists() {
t.Errorf("expected file to exist, but it does not")
}
})
t.Run("InvalidURL", func(t *testing.T) {
ctx := downloader.NewContext("test_invalid_url.txt", "http://invalid.url")
err := ctx.Download()
if err == nil || err.Error() != "invalid url" {
t.Errorf("expected invalid url error, got %v", err)
}
})
t.Run("CertificateError", func(t *testing.T) {
// This test assumes a self-signed certificate or similar issue. This is hard to simulate in a unit test.
ctx := downloader.NewContext("test_cert_error.txt", "https://self-signed.badssl.com/") // Example URL that can be used
if err := ctx.Download(); err == nil || err.Error() != "certificate from unknown authority" {
t.Errorf("expected certificate from unknown authority error, got %v", err)
}
})
t.Run("FileNotFound", func(t *testing.T) {
// Mock server to simulate a 404 response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
ctx := downloader.NewContext("test_not_found.txt", server.URL)
if err := ctx.Download(); err == nil || err.Error() != "invalid url" {
t.Errorf("expected invalid url error, got %v", err)
}
})
t.Run("RestrictedAccess", func(t *testing.T) {
// Mock server to simulate a 401 response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
ctx := downloader.NewContext("test_restricted_access.txt", server.URL)
if err := ctx.Download(); err == nil || err.Error() != "restricted access (credentials required)" {
t.Errorf("expected restricted access error, got %v", err)
}
})
}
// TestFileExists tests the FileExists function.
func TestFileExists(t *testing.T) {
filename := "testfile_exists.txt"
ctx := downloader.NewContext(filename, "http://example.com")
// Ensure file does not exist initially
if ctx.FileExists() {
t.Errorf("expected file to not exist, but it does")
}
// Create a file and check again
file, err := os.Create(filename)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
file.Close()
defer os.Remove(filename)
if !ctx.FileExists() {
t.Errorf("expected file to exist, but it does not")
}
}