Initial commit

This commit is contained in:
2023-03-26 16:11:00 +03:00
commit 92469fa3a2
47 changed files with 5610 additions and 0 deletions

182
application/application.go Normal file
View File

@@ -0,0 +1,182 @@
package application
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/dgraph-io/badger/v4"
"github.com/ogen-go/ogen/middleware"
"go.uber.org/multierr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/derfenix/webarchive/adapters/processors"
badgerRepo "github.com/derfenix/webarchive/adapters/repository/badger"
"github.com/derfenix/webarchive/api/openapi"
"github.com/derfenix/webarchive/entity"
"github.com/derfenix/webarchive/ports/rest"
)
func NewApplication(cfg Config) (Application, error) {
log, err := newLogger(cfg.Logging)
if err != nil {
return Application{}, fmt.Errorf("new logger: %w", err)
}
db, err := badgerRepo.NewBadger(cfg.DB.Path, log.Named("db"))
if err != nil {
return Application{}, fmt.Errorf("new badger: %w", err)
}
fileRepo := badgerRepo.NewFile(db)
pageRepo, err := badgerRepo.NewPage(db, fileRepo)
if err != nil {
return Application{}, fmt.Errorf("new page repo: %w", err)
}
processor, err := processors.NewProcessors()
if err != nil {
return Application{}, fmt.Errorf("new processors: %w", err)
}
server, err := openapi.NewServer(
rest.NewService(pageRepo),
openapi.WithMiddleware(
func(r middleware.Request, next middleware.Next) (middleware.Response, error) {
start := time.Now()
log := log.With(
zap.String("operation_id", r.OperationID),
zap.String("uri", r.Raw.RequestURI),
)
var response middleware.Response
var reqErr error
response, reqErr = next(r)
log.Debug("request completed", zap.Duration("duration", time.Since(start)), zap.Error(err))
return response, reqErr
},
),
)
if err != nil {
return Application{}, fmt.Errorf("new rest server: %w", err)
}
httpServer := http.Server{
Addr: "0.0.0.0:5001",
Handler: server,
ReadTimeout: time.Second * 15,
ReadHeaderTimeout: time.Second * 5,
IdleTimeout: time.Second * 30,
MaxHeaderBytes: 1024 * 2,
}
return Application{
cfg: cfg,
log: log,
db: db,
processor: processor,
httpServer: &httpServer,
pageRepo: pageRepo,
fileRepo: fileRepo,
}, nil
}
type Application struct {
cfg Config
log *zap.Logger
db *badger.DB
processor entity.Processor
httpServer *http.Server
pageRepo *badgerRepo.Page
fileRepo *badgerRepo.File
}
func (a *Application) Log() *zap.Logger {
return a.log
}
func (a *Application) Start(ctx context.Context, wg *sync.WaitGroup) error {
wg.Add(2)
a.httpServer.BaseContext = func(net.Listener) context.Context {
return ctx
}
go func() {
defer wg.Done()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := a.httpServer.Shutdown(shutdownCtx); err != nil {
a.log.Warn("http graceful shutdown failed", zap.Error(err))
}
}()
go func() {
defer wg.Done()
a.log.Info("starting http server", zap.String("address", a.httpServer.Addr))
if err := a.httpServer.ListenAndServe(); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
a.log.Error("http serve error", zap.Error(err))
}
a.log.Info("http server stopped")
}
}()
return nil
}
func (a *Application) Stop() error {
var errs error
if err := a.db.Sync(); err != nil {
errs = multierr.Append(errs, fmt.Errorf("sync db: %w", err))
}
if err := badgerRepo.Backup(a.db, badgerRepo.BackupStop); err != nil {
errs = multierr.Append(errs, fmt.Errorf("backup on stop: %w", err))
}
if err := a.db.Close(); err != nil {
errs = multierr.Append(errs, fmt.Errorf("close db: %w", err))
}
return errs
}
func newLogger(cfg Logging) (*zap.Logger, error) {
logCfg := zap.NewProductionConfig()
logCfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder
logCfg.EncoderConfig.EncodeDuration = zapcore.NanosDurationEncoder
logCfg.DisableCaller = true
logCfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
if cfg.Debug {
logCfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
}
log, err := logCfg.Build()
if err != nil {
return nil, fmt.Errorf("build logger: %w", err)
}
return log, nil
}

38
application/config.go Normal file
View File

@@ -0,0 +1,38 @@
package application
import (
"context"
"fmt"
"github.com/sethvargo/go-envconfig"
)
const envPrefix = "WEBARCHIVE_"
func NewConfig(ctx context.Context) (Config, error) {
cfg := Config{}
lookuper := envconfig.MultiLookuper(
envconfig.PrefixLookuper(envPrefix, envconfig.OsLookuper()),
envconfig.OsLookuper(),
)
if err := envconfig.ProcessWith(ctx, &cfg, lookuper); err != nil {
return Config{}, fmt.Errorf("process env: %w", err)
}
return cfg, nil
}
type Config struct {
DB DB `env:",prefix=DB_"`
Logging Logging `env:",prefix=LOGGING_"`
}
type DB struct {
Path string `env:"PATH,default=./db"`
}
type Logging struct {
Debug bool `env:"DEBUG"`
}

View File

@@ -0,0 +1,42 @@
package application
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewConfig(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("no envs", func(t *testing.T) {
config, err := NewConfig(ctx)
require.NoError(t, err)
assert.Equal(t, "./db", config.DB.Path)
})
t.Run("env without prefix", func(t *testing.T) {
require.NoError(t, os.Setenv("DB_PATH", "./old_db"))
config, err := NewConfig(ctx)
require.NoError(t, err)
assert.Equal(t, "./old_db", config.DB.Path)
})
t.Run("prefix env override", func(t *testing.T) {
require.NoError(t, os.Setenv("WEBARCHIVE_DB_PATH", "./new_db"))
config, err := NewConfig(ctx)
require.NoError(t, err)
assert.Equal(t, "./new_db", config.DB.Path)
})
}