mirror of
https://github.com/notherealmarco/WASAPhoto.git
synced 2025-05-05 12:22:35 +02:00
Integrated Fantastic coffie (decaffeinated) base version
This commit is contained in:
parent
2fc5535f0f
commit
94036c4831
482 changed files with 476112 additions and 0 deletions
48
cmd/healthcheck/healthcheck.go
Normal file
48
cmd/healthcheck/healthcheck.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Healthcheck is a simple program that sends an HTTP request to the local host (self) to a configured port number.
|
||||
It's used in environment where you need a simple probe for health checks (e.g., an empty container in docker).
|
||||
The probe URL is http://localhost:3000/liveness . Only the port can be changed.
|
||||
|
||||
Usage:
|
||||
|
||||
healthcheck [flags]
|
||||
|
||||
The flags are:
|
||||
|
||||
-port <1-65535>
|
||||
Change the port where the request is sent.
|
||||
|
||||
Return values (exit codes):
|
||||
|
||||
0
|
||||
The request was successful (HTTP 200 or HTTP 204)
|
||||
|
||||
> 0
|
||||
The request was not successful (connection error or unexpected HTTP status code)
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var port = flag.Int("port", 3000, "HTTP port for healthcheck")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("http://localhost:%d/liveness", *port))
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
} else if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
|
||||
_ = res.Body.Close()
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Healthcheck request not OK: ", res.Status)
|
||||
os.Exit(1)
|
||||
}
|
||||
_ = res.Body.Close()
|
||||
os.Exit(0)
|
||||
}
|
19
cmd/webapi/cors.go
Normal file
19
cmd/webapi/cors.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/handlers"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// applyCORSHandler applies a CORS policy to the router. CORS stands for Cross-Origin Resource Sharing: it's a security
|
||||
// feature present in web browsers that blocks JavaScript requests going across different domains if not specified in a
|
||||
// policy. This function sends the policy of this API server.
|
||||
func applyCORSHandler(h http.Handler) http.Handler {
|
||||
return handlers.CORS(
|
||||
handlers.AllowedHeaders([]string{
|
||||
"x-example-header",
|
||||
}),
|
||||
handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS", "DELETE", "PUT"}),
|
||||
handlers.AllowedOrigins([]string{"*"}),
|
||||
)(h)
|
||||
}
|
70
cmd/webapi/load-configuration.go
Normal file
70
cmd/webapi/load-configuration.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ardanlabs/conf"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WebAPIConfiguration describes the web API configuration. This structure is automatically parsed by
|
||||
// loadConfiguration and values from flags, environment variable or configuration file will be loaded.
|
||||
type WebAPIConfiguration struct {
|
||||
Config struct {
|
||||
Path string `conf:"default:/conf/config.yml"`
|
||||
}
|
||||
Web struct {
|
||||
APIHost string `conf:"default:0.0.0.0:3000"`
|
||||
DebugHost string `conf:"default:0.0.0.0:4000"`
|
||||
ReadTimeout time.Duration `conf:"default:5s"`
|
||||
WriteTimeout time.Duration `conf:"default:5s"`
|
||||
ShutdownTimeout time.Duration `conf:"default:5s"`
|
||||
}
|
||||
Debug bool
|
||||
DB struct {
|
||||
Filename string `conf:"default:/tmp/decaf.db"`
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfiguration creates a WebAPIConfiguration starting from flags, environment variables and configuration file.
|
||||
// It works by loading environment variables first, then update the config using command line flags, finally loading the
|
||||
// configuration file (specified in WebAPIConfiguration.Config.Path).
|
||||
// So, CLI parameters will override the environment, and configuration file will override everything.
|
||||
// Note that the configuration file can be specified only via CLI or environment variable.
|
||||
func loadConfiguration() (WebAPIConfiguration, error) {
|
||||
var cfg WebAPIConfiguration
|
||||
|
||||
// Try to load configuration from environment variables and command line switches
|
||||
if err := conf.Parse(os.Args[1:], "CFG", &cfg); err != nil {
|
||||
if errors.Is(err, conf.ErrHelpWanted) {
|
||||
usage, err := conf.Usage("CFG", &cfg)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("generating config usage: %w", err)
|
||||
}
|
||||
fmt.Println(usage) //nolint:forbidigo
|
||||
return cfg, conf.ErrHelpWanted
|
||||
}
|
||||
return cfg, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
|
||||
// Override values from YAML if specified and if it exists (useful in k8s/compose)
|
||||
fp, err := os.Open(cfg.Config.Path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return cfg, fmt.Errorf("can't read the config file, while it exists: %w", err)
|
||||
} else if err == nil {
|
||||
yamlFile, err := io.ReadAll(fp)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("can't read config file: %w", err)
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &cfg)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("can't unmarshal config file: %w", err)
|
||||
}
|
||||
_ = fp.Close()
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
184
cmd/webapi/main.go
Normal file
184
cmd/webapi/main.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
Webapi is the executable for the main web server.
|
||||
It builds a web server around APIs from `service/api`.
|
||||
Webapi connects to external resources needed (database) and starts two web servers: the API web server, and the debug.
|
||||
Everything is served via the API web server, except debug variables (/debug/vars) and profiler infos (pprof).
|
||||
|
||||
Usage:
|
||||
|
||||
webapi [flags]
|
||||
|
||||
Flags and configurations are handled automatically by the code in `load-configuration.go`.
|
||||
|
||||
Return values (exit codes):
|
||||
|
||||
0
|
||||
The program ended successfully (no errors, stopped by signal)
|
||||
|
||||
> 0
|
||||
The program ended due to an error
|
||||
|
||||
Note that this program will update the schema of the database to the latest version available (embedded in the
|
||||
executable during the build).
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/notherealmarco/WASAPhoto/service/api"
|
||||
"github.com/notherealmarco/WASAPhoto/service/database"
|
||||
"github.com/notherealmarco/WASAPhoto/service/globaltime"
|
||||
"github.com/ardanlabs/conf"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/sirupsen/logrus"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// main is the program entry point. The only purpose of this function is to call run() and set the exit code if there is
|
||||
// any error
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "error: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// run executes the program. The body of this function should perform the following steps:
|
||||
// * reads the configuration
|
||||
// * creates and configure the logger
|
||||
// * connects to any external resources (like databases, authenticators, etc.)
|
||||
// * creates an instance of the service/api package
|
||||
// * starts the principal web server (using the service/api.Router.Handler() for HTTP handlers)
|
||||
// * waits for any termination event: SIGTERM signal (UNIX), non-recoverable server error, etc.
|
||||
// * closes the principal web server
|
||||
func run() error {
|
||||
rand.Seed(globaltime.Now().UnixNano())
|
||||
// Load Configuration and defaults
|
||||
cfg, err := loadConfiguration()
|
||||
if err != nil {
|
||||
if errors.Is(err, conf.ErrHelpWanted) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Init logging
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(os.Stdout)
|
||||
if cfg.Debug {
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
logger.Infof("application initializing")
|
||||
|
||||
// Start Database
|
||||
logger.Println("initializing database support")
|
||||
dbconn, err := sql.Open("sqlite3", cfg.DB.Filename)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("error opening SQLite DB")
|
||||
return fmt.Errorf("opening SQLite: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
logger.Debug("database stopping")
|
||||
_ = dbconn.Close()
|
||||
}()
|
||||
db, err := database.New(dbconn)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("error creating AppDatabase")
|
||||
return fmt.Errorf("creating AppDatabase: %w", err)
|
||||
}
|
||||
|
||||
// Start (main) API server
|
||||
logger.Info("initializing API server")
|
||||
|
||||
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
||||
// Use a buffered channel because the signal package requires it.
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Make a channel to listen for errors coming from the listener. Use a
|
||||
// buffered channel so the goroutine can exit if we don't collect this error.
|
||||
serverErrors := make(chan error, 1)
|
||||
|
||||
// Create the API router
|
||||
apirouter, err := api.New(api.Config{
|
||||
Logger: logger,
|
||||
Database: db,
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("error creating the API server instance")
|
||||
return fmt.Errorf("creating the API server instance: %w", err)
|
||||
}
|
||||
router := apirouter.Handler()
|
||||
|
||||
//router, err = registerWebUI(router)
|
||||
//if err != nil {
|
||||
// logger.WithError(err).Error("error registering web UI handler")
|
||||
// return fmt.Errorf("registering web UI handler: %w", err)
|
||||
//}
|
||||
|
||||
// Apply CORS policy
|
||||
router = applyCORSHandler(router)
|
||||
|
||||
// Create the API server
|
||||
apiserver := http.Server{
|
||||
Addr: cfg.Web.APIHost,
|
||||
Handler: router,
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
ReadHeaderTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
}
|
||||
|
||||
// Start the service listening for requests in a separate goroutine
|
||||
go func() {
|
||||
logger.Infof("API listening on %s", apiserver.Addr)
|
||||
serverErrors <- apiserver.ListenAndServe()
|
||||
logger.Infof("stopping API server")
|
||||
}()
|
||||
|
||||
// Waiting for shutdown signal or POSIX signals
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
// Non-recoverable server error
|
||||
return fmt.Errorf("server error: %w", err)
|
||||
|
||||
case sig := <-shutdown:
|
||||
logger.Infof("signal %v received, start shutdown", sig)
|
||||
|
||||
// Asking API server to shut down and load shed.
|
||||
err := apirouter.Close()
|
||||
if err != nil {
|
||||
logger.WithError(err).Warning("graceful shutdown of apirouter error")
|
||||
}
|
||||
|
||||
// Give outstanding requests a deadline for completion.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Asking listener to shut down and load shed.
|
||||
err = apiserver.Shutdown(ctx)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warning("error during graceful shutdown of HTTP server")
|
||||
err = apiserver.Close()
|
||||
}
|
||||
|
||||
// Log the status of this shutdown.
|
||||
switch {
|
||||
case sig == syscall.SIGSTOP:
|
||||
return errors.New("integrity issue caused shutdown")
|
||||
case err != nil:
|
||||
return fmt.Errorf("could not stop server gracefully: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
12
cmd/webapi/register-web-ui-stub.go
Normal file
12
cmd/webapi/register-web-ui-stub.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !webui
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// registerWebUI is an empty stub because `webui` tag has not been specified.
|
||||
func registerWebUI(hdl http.Handler) (http.Handler, error) {
|
||||
return hdl, nil
|
||||
}
|
25
cmd/webapi/register-web-ui.go
Normal file
25
cmd/webapi/register-web-ui.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
//go:build webui
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
//"github.com/notherealmarco/WASAPhoto/webui"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func registerWebUI(hdl http.Handler) (http.Handler, error) {
|
||||
distDirectory, err := fs.Sub(webui.Dist, "dist")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error embedding WebUI dist/ directory: %w", err)
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.RequestURI, "/dashboard/") {
|
||||
http.StripPrefix("/dashboard/", http.FileServer(http.FS(distDirectory))).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
hdl.ServeHTTP(w, r)
|
||||
}), nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue