Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fb6-wp11-devops/webservice
  • maha1056/devops-pipeline
  • ludo8147/webservice
  • s52888/webservice
  • masi9606/webservice
  • kibu5600/webservice
  • s78689/webservice
  • s50860/webservice
  • s92604/devops-webservice
  • s76867/webservice-devops
  • s92274/webservice
  • s80066/webservice
  • masa1998/webservice
  • s91190/app-service
  • s84985/webservice
  • s75359/webservice
  • ouch4861/webservice-ws-24-oc
  • s92274/webservice-msws-24
  • ewbo4360/webservice
19 results
Show changes
Commits on Source (20)
workflow:
rules:
- if: >-
$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"
when: 'always'
- when: 'never'
job_trigger-pipeline:
trigger:
project: 'fb6-wp11-devops/webservice-build-and-publish'
...@@ -37,6 +37,7 @@ $(BIN_DIR)/artifact.bin: ...@@ -37,6 +37,7 @@ $(BIN_DIR)/artifact.bin:
cd $(SRC_DIR) \ cd $(SRC_DIR) \
&& go build \ && go build \
-o $(@) \ -o $(@) \
-ldflags "-X webservice/configuration.version=0.0.1" \
$(SRC_DIR)/*.go $(SRC_DIR)/*.go
.PHONY: build-linux .PHONY: build-linux
...@@ -44,12 +45,14 @@ build-linux: export GOOS := linux ...@@ -44,12 +45,14 @@ build-linux: export GOOS := linux
build-linux: export GOARCH := amd64 build-linux: export GOARCH := amd64
build-linux: export CGO_ENABLED := 0 build-linux: export CGO_ENABLED := 0
build-linux: $(BIN_DIR)/artifact.bin build-linux: $(BIN_DIR)/artifact.bin
sha256sum $(BIN_DIR)/artifact.bin
.PHONY: test .PHONY: test
.SILENT: test .SILENT: test
test: test:
cd $(SRC_DIR) \ cd $(SRC_DIR) \
&& go clean -testcache \
&& go test \ && go test \
-race \ -race \
-v \ -v \
......
Webservice Webservice
========== ==========
A Go-based simple web service meant to be the subject of any tutorial A Go-based simple web service meant to be the subject of any exercise
or even used the project work. or even used in the project work.
#### Prerequisites: #### Prerequisites:
* Go toolchain (install via system package manager or [by hand](https://go.dev/doc/install)) * minimal [required version](./go.mod#L3) of the Go toolchain (install via
system package manager or [by hand](https://go.dev/doc/install))
* [optional] [Redis](https://redis.io/docs/install/) to persist state * [optional] [Redis](https://redis.io/docs/install/) to persist state
...@@ -24,9 +25,11 @@ information checkout the [configuration code](./configuration/config.go). ...@@ -24,9 +25,11 @@ information checkout the [configuration code](./configuration/config.go).
3. Execute unit tests: `go test -race -v ./...` 3. Execute unit tests: `go test -race -v ./...`
4. Build artifact: `go build -o ./artifact.bin ./*.go` 4. Build artifact: `go build -o ./artifact.bin ./*.go`
To build for another platform, set `GOOS` and `GOARCH`. To yield a static binary (fully To build for another platform, set `GOOS` and `GOARCH`. To yield a static
self-contained, no dynamic linking) set `CGO_ENABLED=0`. For more details, please refer binary (fully self-contained, no dynamic linking) set `CGO_ENABLED=0`.
to the [Makefile](./Makefile). To set a version during build time, add the following CLI option
`-ldflags "-X webservice/configuration.version=${VERSION}"`.
For more information, please refer to the [Makefile](./Makefile).
#### Run: #### Run:
...@@ -47,7 +50,7 @@ curl http://localhost:8080 ...@@ -47,7 +50,7 @@ curl http://localhost:8080
HTML: HTML:
```bash ```bash
curl --header 'Content-Type: text/html; charset=utf-8' http://localhost:8080 curl --header 'Accept: text/html; charset=utf-8' http://localhost:8080
# or just open in a browser # or just open in a browser
``` ```
...@@ -84,6 +87,13 @@ curl \ ...@@ -84,6 +87,13 @@ curl \
http://localhost:8080/state/bar http://localhost:8080/state/bar
``` ```
Find out MIME type and size of an entry:
```bash
curl \
-X HEAD \
http://localhost:8080/state/bar
```
Obtain an entry: Obtain an entry:
```bash ```bash
curl \ curl \
...@@ -102,8 +112,8 @@ curl \ ...@@ -102,8 +112,8 @@ curl \
List all existing entries (returns JSON or plain text, depending on the `Accept` header): List all existing entries (returns JSON or plain text, depending on the `Accept` header):
```bash ```bash
curl \ curl \
-X GET -X GET \
--header 'Accept: text/plain'\ --header 'Accept: text/plain' \
http://localhost:8080/states http://localhost:8080/states
``` ```
......
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"log/slog"
"unicode"
fp "path/filepath" fp "path/filepath"
configParser "github.com/caarlos0/env/v9" configParser "github.com/caarlos0/env/v9"
...@@ -12,9 +14,13 @@ import ( ...@@ -12,9 +14,13 @@ import (
const BODY_SIZE_LIMIT = 32 * 1024 * 1024 // 32 MB, in bytes const BODY_SIZE_LIMIT = 32 * 1024 * 1024 // 32 MB, in bytes
var version string = "n/a"
type Config struct { type Config struct {
Version string `env:"VERSION" envDefault:"N/A"` Version string
FontColor string `env:"FONT_COLOR" envDefault:""`
LogLevel string `env:"LOG_LEVE" envDefault:"error"` LogLevel string `env:"LOG_LEVE" envDefault:"error"`
...@@ -31,7 +37,9 @@ type Config struct { ...@@ -31,7 +37,9 @@ type Config struct {
func New() ( *Config, error ){ func New() ( *Config, error ){
cfg := Config{} cfg := Config{
Version: version,
}
if err := configParser.Parse( &cfg ); err != nil { if err := configParser.Parse( &cfg ); err != nil {
return nil, err return nil, err
...@@ -51,24 +59,17 @@ func New() ( *Config, error ){ ...@@ -51,24 +59,17 @@ func New() ( *Config, error ){
if cfg.Environment == "development" { cfg.LogLevel = "debug" } if cfg.Environment == "development" { cfg.LogLevel = "debug" }
possibleLogLevels := map[ string ] bool { if _, err := cfg.GetLogLevel(); err != nil {
"error": true, return nil, err
"debug": true,
}
if _, ok := possibleLogLevels[ cfg.LogLevel ]; !ok {
return nil, errors.New(
fmt.Sprintf( "Invalid log level: %s", cfg.LogLevel ),
)
} }
if len( cfg.DatabaseHost ) >= 1 && len( cfg.DatabasePassword ) >= 2 { if len( cfg.DatabaseHost ) >= 1 && len( cfg.DatabasePassword ) >= 2 {
if ! fp.IsLocal( cfg.DatabasePassword ) && ! fp.IsAbs( cfg.DatabasePassword ) { if ! fp.IsLocal( cfg.DatabasePassword ) && ! fp.IsAbs( cfg.DatabasePassword ) {
return nil, errors.New( return nil, errors.New(
fmt.Sprintln( "Database password must be a file path" ), fmt.Sprintln( "Database password must be a file path" ),
) )
} }
_, err := os.Stat( cfg.DatabasePassword ) fileInfo, err := os.Stat( cfg.DatabasePassword )
if err != nil { if err != nil {
if errors.Is( err, os.ErrNotExist ){ if errors.Is( err, os.ErrNotExist ){
return nil, errors.New( return nil, errors.New(
...@@ -79,8 +80,45 @@ func New() ( *Config, error ){ ...@@ -79,8 +80,45 @@ func New() ( *Config, error ){
fmt.Sprintln( "Database password file not accessible" ), fmt.Sprintln( "Database password file not accessible" ),
) )
} }
if fileInfo.Size() <= 0 {
return nil, errors.New(
fmt.Sprintln( "Database password file is empty" ),
)
}
}
if len( cfg.FontColor ) >= 1 {
if len( cfg.FontColor ) >= 21 {
return nil, errors.New(
fmt.Sprintln( "Font color too long" ),
)
}
for _, r := range cfg.FontColor {
if ! unicode.IsLetter( r ) {
return nil, errors.New(
fmt.Sprintln( "Invalid character in font color" ),
)
}
}
} }
return &cfg, nil return &cfg, nil
} }
func ( cfg *Config ) GetLogLevel() ( slog.Level, error ){
possibleLogLevels := map[ string ] slog.Level {
"error": slog.LevelError,
"debug": slog.LevelDebug,
}
level, ok := possibleLogLevels[ cfg.LogLevel ]
if !ok {
return slog.LevelError, errors.New(
fmt.Sprintf( "Invalid log level: %s", cfg.LogLevel ),
)
}else{
return level, nil
}
}
\ No newline at end of file
module webservice module webservice
go 1.21 go 1.24
require ( require (
github.com/caarlos0/env/v9 v9.0.0 github.com/caarlos0/env/v9 v9.0.0
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"log/slog"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
...@@ -20,9 +21,22 @@ import ( ...@@ -20,9 +21,22 @@ import (
func main() { func main() {
config, err := configuration.New() config, err := configuration.New()
if err != nil { if err != nil {
log.Fatalf( "HTTP server failed to start: %v", err ) slog.Error( fmt.Sprintf( "HTTP server failed to start: %v", err ) )
os.Exit( 1 )
} }
level, _ := config.GetLogLevel()
slog.SetDefault(
slog.New(
slog.NewTextHandler(
os.Stdout,
&slog.HandlerOptions{
Level: level,
},
),
),
)
server := fiber.New( fiber.Config{ server := fiber.New( fiber.Config{
AppName: "webservice", AppName: "webservice",
DisableStartupMessage: config.Environment != "development", DisableStartupMessage: config.Environment != "development",
...@@ -40,13 +54,15 @@ func main() { ...@@ -40,13 +54,15 @@ func main() {
err = routing.SetRoutes( server, config, store, &isHealthy ) err = routing.SetRoutes( server, config, store, &isHealthy )
if err != nil { if err != nil {
log.Fatalf( "HTTP server failed to start: %v", err ) slog.Error( fmt.Sprintf( "HTTP server failed to start: %v", err ) )
os.Exit( 1 )
} }
go func(){ go func(){
err := server.Listen( fmt.Sprintf( "%s:%d", config.Host, config.Port ) ) err := server.Listen( fmt.Sprintf( "%s:%d", config.Host, config.Port ) )
if err != nil { if err != nil {
log.Fatalf( "HTTP server failed to start: %v", err ) slog.Error( fmt.Sprintf( "HTTP server failed to start: %v", err ) )
os.Exit( 1 )
} }
}() }()
......
...@@ -7,7 +7,7 @@ import ( ...@@ -7,7 +7,7 @@ import (
"strings" "strings"
"net/http" "net/http"
"html/template" "html/template"
"log" log "log/slog"
"bytes" "bytes"
"mime" "mime"
...@@ -25,13 +25,20 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -25,13 +25,20 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
return err return err
} }
metricsTextTemplate, err := template.New( "metrics" ).Parse( metricsText )
if err != nil {
return err
}
if config.LogLevel == "debug" { if config.LogLevel == "debug" {
router.All( "*", func( c *f.Ctx ) error { router.All( "*", func( c *f.Ctx ) error {
log.Printf( "%s %s mime:%s agent:%s", log.Debug(
c.Method(), fmt.Sprintf( "%s %s mime:%s agent:%s",
c.Path(), c.Method(),
c.Get( f.HeaderContentType ), c.Path(),
c.Get( f.HeaderUserAgent ), c.Get( f.HeaderContentType, c.Get( f.HeaderAccept, "" ) ),
c.Get( f.HeaderUserAgent ),
),
) )
return c.Next() return c.Next()
}) })
...@@ -48,7 +55,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -48,7 +55,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
data := indexHtmlData{ data := indexHtmlData{
Version: config.Version, Version: config.Version,
Color: "", Color: config.FontColor,
} }
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
...@@ -90,6 +97,36 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -90,6 +97,36 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
}) })
router.Get( "/metrics", func( c *f.Ctx ) error {
headers := c.GetReqHeaders()
acceptHeader := strings.Join( headers[ "Accept" ], " " )
buffer := &bytes.Buffer{}
if strings.Contains( acceptHeader , "json" ) {
// FUTUREWORK: implement https://opentelemetry.io/docs/specs/otlp/#otlphttp
return c.SendStatus( http.StatusNotAcceptable )
} else {
names, err := store.List()
if err != nil {
log.Debug( err.Error() )
return c.SendStatus( http.StatusInternalServerError )
}
data := metricsTextData{
Count: len( names ),
}
err = metricsTextTemplate.Execute( buffer, data )
if err != nil {
return err
}
c.Set( "Content-Type", "text/plain; charset=utf-8" )
return c.Send( buffer.Bytes() )
}
})
router.Get( "/env", func( c *f.Ctx ) error { router.Get( "/env", func( c *f.Ctx ) error {
c.Type( "txt", "utf-8" ) c.Type( "txt", "utf-8" )
...@@ -101,6 +138,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -101,6 +138,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
for _, envVar := range os.Environ() { for _, envVar := range os.Environ() {
_, err := c.WriteString( fmt.Sprintln( envVar ) ) _, err := c.WriteString( fmt.Sprintln( envVar ) )
if err != nil { if err != nil {
log.Debug( err.Error() )
c.Status( http.StatusInternalServerError ) c.Status( http.StatusInternalServerError )
return err return err
} }
...@@ -114,9 +152,27 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -114,9 +152,27 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
statePathGroup := router.Group( "/state" ) statePathGroup := router.Group( "/state" )
statePathGroup.Options( "/:name", func( c *f.Ctx ) error {
name := strings.Clone( c.Params( "name" ) )
existingItem, err := store.Fetch( name )
if err != nil {
log.Debug( err.Error() )
return c.SendStatus( http.StatusInternalServerError )
}
if existingItem == nil {
return c.SendStatus( http.StatusNotFound )
}
c.Set( "Allow", "OPTIONS, GET, PUT, DELETE, HEAD" )
return c.SendStatus( http.StatusNoContent )
})
statePathGroup.Get( "/:name", func( c *f.Ctx ) error { statePathGroup.Get( "/:name", func( c *f.Ctx ) error {
existingItem, err := store.Fetch( c.Params( "name" ) ) existingItem, err := store.Fetch( c.Params( "name" ) )
if err != nil { if err != nil {
log.Debug( err.Error() )
c.Status( http.StatusInternalServerError ) c.Status( http.StatusInternalServerError )
return c.Send( nil ) return c.Send( nil )
} }
...@@ -143,6 +199,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -143,6 +199,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
name := strings.Clone( c.Params( "name" ) ) name := strings.Clone( c.Params( "name" ) )
existingItem, err := store.Fetch( name ) existingItem, err := store.Fetch( name )
if err != nil { if err != nil {
log.Debug( err.Error() )
c.Status( http.StatusInternalServerError ) c.Status( http.StatusInternalServerError )
return c.Send( nil ) return c.Send( nil )
} }
...@@ -166,6 +223,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -166,6 +223,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
) )
if err = store.Add( newItem ); err != nil { if err = store.Add( newItem ); err != nil {
log.Debug( err.Error() )
c.Status( http.StatusInternalServerError ) c.Status( http.StatusInternalServerError )
return c.Send( nil ) return c.Send( nil )
} }
...@@ -179,6 +237,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -179,6 +237,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
name := strings.Clone( c.Params( "name" ) ) name := strings.Clone( c.Params( "name" ) )
existingItem, err := store.Fetch( name ) existingItem, err := store.Fetch( name )
if err != nil { if err != nil {
log.Debug( err.Error() )
return c.SendStatus( http.StatusInternalServerError ) return c.SendStatus( http.StatusInternalServerError )
} }
...@@ -187,6 +246,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -187,6 +246,7 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
} }
if err = store.Remove( name ); err != nil { if err = store.Remove( name ); err != nil {
log.Debug( err.Error() )
return c.SendStatus( http.StatusInternalServerError ) return c.SendStatus( http.StatusInternalServerError )
} }
...@@ -194,26 +254,40 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store, ...@@ -194,26 +254,40 @@ func SetRoutes( router *f.App, config *configuration.Config, store state.Store,
}) })
statePathGroup.Use( "*", func( c *f.Ctx ) error { statePathGroup.Head( "/:name", func( c *f.Ctx ) error {
if method := c.Method(); method == "OPTIONS" { name := strings.Clone( c.Params( "name" ) )
c.Set( "Allow", "GET, PUT, DELETE, OPTIONS" ) existingItem, err := store.Fetch( name )
return c.SendStatus( http.StatusNoContent ) if err != nil {
log.Debug( err.Error() )
return c.SendStatus( http.StatusInternalServerError )
}
if existingItem == nil {
return c.SendStatus( http.StatusNotFound )
} }
c.Set( "Content-Type", existingItem.MimeType() )
c.Set( "Content-Length", fmt.Sprintf( "%d", len( existingItem.Data() ) ) )
return c.SendStatus( http.StatusOK )
})
statePathGroup.Use( "*", func( c *f.Ctx ) error {
return c.SendStatus( http.StatusNotFound ) return c.SendStatus( http.StatusNotFound )
}) })
router.Get( "/states", func( c *f.Ctx ) error { router.Get( "/states", func( c *f.Ctx ) error {
states, err := store.Show() names, err := store.List()
if err != nil { if err != nil {
log.Debug( err.Error() )
return c.SendStatus( http.StatusInternalServerError ) return c.SendStatus( http.StatusInternalServerError )
} }
const pathPrefix string = "/state" const pathPrefix string = "/state"
paths := make ( []string, len( states ) ) paths := make( []string, len( names ) )
for i, state := range states { for i, name := range names {
paths[ i ] = fmt.Sprintf( "%s/%s", pathPrefix, state ) paths[ i ] = fmt.Sprintf( "%s/%s", pathPrefix, name )
} }
headers := c.GetReqHeaders() headers := c.GetReqHeaders()
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"math/rand" "math/rand"
...@@ -167,7 +168,7 @@ func TestState( t *testing.T ){ ...@@ -167,7 +168,7 @@ func TestState( t *testing.T ){
const statePath2 = "/state/another-test" const statePath2 = "/state/another-test"
const statePath2Mime = "application/octet-stream" const statePath2Mime = "application/octet-stream"
const statePath2BodySize = 64 const statePath2BodySize = 128
statePath2Body := generateRandomBytes( statePath2BodySize ) statePath2Body := generateRandomBytes( statePath2BodySize )
req := ht.NewRequest( "GET", statePath1, nil ) req := ht.NewRequest( "GET", statePath1, nil )
...@@ -211,6 +212,15 @@ func TestState( t *testing.T ){ ...@@ -211,6 +212,15 @@ func TestState( t *testing.T ){
res, _ = router.Test( req, -1 ) res, _ = router.Test( req, -1 )
assert.Equal( t, http.StatusCreated, res.StatusCode ) assert.Equal( t, http.StatusCreated, res.StatusCode )
req = ht.NewRequest( "HEAD", statePath2, nil )
res, _ = router.Test( req, -1 )
contentLength, err := strconv.ParseInt( res.Header[ "Content-Length" ][0], 10, 64 )
assert.Nil( t, err )
assert.Equal( t, http.StatusOK, res.StatusCode )
assert.Equal( t, statePath2Mime, res.Header[ "Content-Type" ][0] )
assert.Equal( t, int64( statePath2BodySize ), contentLength )
assert.IsType( t, res.Body, http.NoBody )
req = ht.NewRequest( "GET", statePath2, nil ) req = ht.NewRequest( "GET", statePath2, nil )
res, _ = router.Test( req, -1 ) res, _ = router.Test( req, -1 )
bodyBytes, err := io.ReadAll( res.Body ) bodyBytes, err := io.ReadAll( res.Body )
......
...@@ -20,3 +20,14 @@ type indexHtmlData struct { ...@@ -20,3 +20,14 @@ type indexHtmlData struct {
Version string Version string
Color string Color string
} }
const metricsText = `
# HELP state_entries_quantity The current number of state entries being stored
# TYPE state_entries_quantity gauge
state_entries_quantity {{ .Count }}
`
type metricsTextData struct {
Count int
}
...@@ -20,7 +20,7 @@ func NewEphemeralStore() *Ephemeral { ...@@ -20,7 +20,7 @@ func NewEphemeralStore() *Ephemeral {
} }
func ( e *Ephemeral ) Add( i *Item ) error { func ( e *Ephemeral ) Add( i Item ) error {
if e.store == nil { if e.store == nil {
return errors.New( "ephemeral storage not available" ) return errors.New( "ephemeral storage not available" )
} }
...@@ -60,19 +60,19 @@ func ( e *Ephemeral ) Fetch( name string ) ( *Item, error ) { ...@@ -60,19 +60,19 @@ func ( e *Ephemeral ) Fetch( name string ) ( *Item, error ) {
if !found { if !found {
return nil, nil return nil, nil
} }
return item, nil return &item, nil
} }
func ( e *Ephemeral ) Show() ( []string, error ) { func ( e *Ephemeral ) List() ( []string, error ) {
if e.store == nil { if e.store == nil {
return nil, errors.New( "ephemeral storage not available" ) return nil, errors.New( "ephemeral storage not available" )
} }
e.mux.Lock() e.mux.Lock()
names := make( []string, 0, len( e.store ) ) names := make( []string, 0, len( e.store ) )
for k := range e.store { for _, item := range e.store {
names = append( names, k ) names = append( names, item.Name() )
} }
e.mux.Unlock() e.mux.Unlock()
......
package state
import (
"testing"
"mime"
"sync"
"github.com/stretchr/testify/assert"
)
var testItems = [] Item {
NewItem( "foo", "bar", []byte( "fasel" ) ),
NewItem( "qwertyASDFGH", mime.TypeByExtension( ".html" ), []byte{} ),
Item{
name: "Som!_🎵nam3",
mimeType: "any kind of string",
data: []byte{ 1, 2, 3, 4, 5, 6, 7, 8 },
},
}
func TestEphemeralAdd( t *testing.T ){
es := NewEphemeralStore()
wg := &sync.WaitGroup{}
for _, item := range testItems {
wg.Add( 1 )
go func( i Item ){
defer wg.Done()
es.Add( i )
}( item )
}
wg.Wait()
assert.Len( t, es.store, len( testItems ) )
}
...@@ -8,8 +8,8 @@ type Item struct { ...@@ -8,8 +8,8 @@ type Item struct {
} }
func NewItem( name string, mimeType string, data []byte ) *Item { func NewItem( name string, mimeType string, data []byte ) Item {
return &Item{ return Item{
name: name, name: name,
mimeType: mimeType, mimeType: mimeType,
data: data, data: data,
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"runtime" "runtime"
"context" "context"
"time" "time"
"os"
log "log/slog"
"webservice/configuration" "webservice/configuration"
...@@ -21,11 +23,18 @@ type Persistent struct { ...@@ -21,11 +23,18 @@ type Persistent struct {
func NewPersistentStore( c *configuration.Config ) *Persistent { func NewPersistentStore( c *configuration.Config ) *Persistent {
content, err := os.ReadFile( c.DatabasePassword )
if err != nil {
log.Error( fmt.Sprintf( "Database password not able to be read: %v", err ) )
os.Exit( 1 )
}
dbPassword := string( content )
return &Persistent{ return &Persistent{
client: db.NewClient( &db.Options{ client: db.NewClient( &db.Options{
Addr: fmt.Sprintf( "%s:%d", c.DatabaseHost, c.DatabasePort ), Addr: fmt.Sprintf( "%s:%d", c.DatabaseHost, c.DatabasePort ),
Username: c.DatabaseUsername, Username: c.DatabaseUsername,
Password: c.DatabasePassword, Password: dbPassword,
DB: c.DatabaseName, DB: c.DatabaseName,
DialTimeout: time.Second * 3, DialTimeout: time.Second * 3,
...@@ -44,15 +53,15 @@ func NewPersistentStore( c *configuration.Config ) *Persistent { ...@@ -44,15 +53,15 @@ func NewPersistentStore( c *configuration.Config ) *Persistent {
} }
func ( e *Persistent ) Add( i *Item ) error { func ( e *Persistent ) Add( i Item ) error {
ctx, cancel := context.WithTimeout( context.TODO(), e.timeout ) ctx, cancel := context.WithTimeout( context.TODO(), e.timeout )
defer cancel() defer cancel()
name := i.Name() name := i.Name()
if err := e.client.HSet( if err := e.client.HSet(
ctx, name, ctx, name,
"data", i.Data(),
"mime", i.MimeType(), "mime", i.MimeType(),
"data", i.Data(),
).Err(); err != nil { ).Err(); err != nil {
return err return err
} }
...@@ -82,13 +91,14 @@ func ( e *Persistent ) Fetch( name string ) ( *Item, error ) { ...@@ -82,13 +91,14 @@ func ( e *Persistent ) Fetch( name string ) ( *Item, error ) {
var item *Item = nil var item *Item = nil
if len( value ) >= 1 { if len( value ) >= 1 {
item = NewItem( name, value[ "mime" ], []byte( value[ "data" ] ) ) i := NewItem( name, value[ "mime" ], []byte( value[ "data" ] ) )
item = &i
} }
return item, nil return item, nil
} }
func ( e *Persistent ) Show() ( []string, error ) { func ( e *Persistent ) List() ( []string, error ) {
ctx, cancel := context.WithTimeout( context.TODO(), e.timeout ) ctx, cancel := context.WithTimeout( context.TODO(), e.timeout )
defer cancel() defer cancel()
......
...@@ -3,10 +3,10 @@ package state ...@@ -3,10 +3,10 @@ package state
type Store interface { type Store interface {
Add( i *Item ) error Add( i Item ) error
Remove( name string ) error Remove( name string ) error
Fetch( name string ) ( *Item, error ) Fetch( name string ) ( *Item, error )
Show() ( []string, error ) List() ( []string, error )
Disconnect() error Disconnect() error
} }