Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
962fe76
Started stubbing a backup feature
chadweimer Jun 8, 2025
586d2e1
Add Tag Explorer page (#447)
chadweimer Jun 15, 2025
e6a4d13
Fix uploading artifacts to release
chadweimer Jun 15, 2025
2317d17
Show total number of recipes in the main menu (#448)
chadweimer Jun 15, 2025
2b90c29
Address minor maintainability issue from Sonar
chadweimer Jun 15, 2025
b7885d5
Fix menu bugs
chadweimer Jun 15, 2025
0c3585e
Update Dockerfile to latest alpine 3
chadweimer Jun 17, 2025
20c0772
Increase number of search results that can be seen per page (#450)
chadweimer Jun 21, 2025
ccc1aa4
Allow selecting number of results per page (#451)
chadweimer Jun 21, 2025
2727530
Fix unit test
chadweimer Jun 21, 2025
734d1bd
Interactivity animations and other layout tweaks (#453)
chadweimer Jun 22, 2025
e83e471
Recipe viewer layout refresh (#454)
chadweimer Jun 26, 2025
3d50f76
Usage ion-img for lazy loading; switch to thumbnail instead of avatar…
chadweimer Jun 27, 2025
1f21196
Implement more of backup database query
chadweimer Aug 9, 2025
9892c1a
Add placeholder to creating backup
chadweimer Aug 9, 2025
5407e94
Partial implementatiion of recipe backup
chadweimer Aug 9, 2025
a4a4cdf
Refactor upload package to general file access
chadweimer Aug 9, 2025
3a91515
Copy images into backup
chadweimer Aug 9, 2025
ee544cb
Backup users also
chadweimer Aug 9, 2025
563ded7
Merge branch 'master' into feature/338-backup-and-restore
chadweimer Aug 10, 2025
e826c62
Write backups toa zip file
chadweimer Aug 10, 2025
f980f82
Use constant for base path
chadweimer Aug 10, 2025
d87668d
Small cleanup
chadweimer Aug 10, 2025
5430a99
Removed unused method
chadweimer Aug 10, 2025
d86bad2
Update for new env
chadweimer Aug 10, 2025
fa76c7b
Fix unit test mocks
chadweimer Aug 10, 2025
70569f9
Added some helper utilities
chadweimer Aug 10, 2025
f7f33e1
Update backup to be all tables. Implement db import
chadweimer Aug 14, 2025
20d8415
Add unit test for db Export
chadweimer Aug 15, 2025
8b0e4cc
Ensure consistent order of row values
chadweimer Aug 15, 2025
3f438e6
Add unit test for Import
chadweimer Aug 16, 2025
564ba8b
Create the backup on disk instead of in memory
chadweimer Aug 16, 2025
a372d3f
Merge branch 'master' into feature/338-backup-and-restore
chadweimer Apr 22, 2026
7dc0572
Remove references to old upload driver
chadweimer Apr 22, 2026
d294132
Another obsolete reference
chadweimer Apr 22, 2026
8d17712
Still another one
chadweimer Apr 22, 2026
4c27caa
Pass context
chadweimer Apr 22, 2026
f42bb36
Fix bug in path and URL construction
chadweimer Apr 22, 2026
25a81d6
Support both SQLite and PostgreSQL for backup
chadweimer Apr 22, 2026
843a2ce
Use local temp dir
chadweimer Apr 22, 2026
40a1f5d
Add some debug code
chadweimer Apr 22, 2026
bcebb4d
Write to backup directly
chadweimer Apr 22, 2026
14136d4
Use alpine again, to support execing into the container
chadweimer Apr 22, 2026
094ea39
Implemented more of the backup functionality
chadweimer Apr 22, 2026
400df38
Remove unused sendDeactivatingCallback
chadweimer Apr 22, 2026
4f6fc15
Fix tests
chadweimer Apr 22, 2026
07fcecf
Merge branch 'master' into feature/338-backup-and-restore
chadweimer Apr 23, 2026
7d993f6
Simplify CopyDirectoryToZip
chadweimer Apr 23, 2026
4bb7d1b
Implement initial version of deleting a backup
chadweimer Apr 23, 2026
5e41e2f
Remove obsolete nosec
chadweimer Apr 23, 2026
04d179d
Convert image-upload-browser to generic file upload browser
chadweimer Apr 23, 2026
2f471a9
Update UI approach
chadweimer Apr 23, 2026
97ee074
Fix folder structure of uploads in backup
chadweimer Apr 23, 2026
ca63e6b
Implement backup metadata
chadweimer Apr 23, 2026
f0fcf43
Built out most of the rest of backup and restore
chadweimer Apr 23, 2026
a04daf8
Implement the database import part of restore
chadweimer Apr 23, 2026
8850201
Simplification
chadweimer Apr 23, 2026
6355490
Fix mock
chadweimer Apr 23, 2026
d8f00f5
Fix bare file uploads
chadweimer Apr 23, 2026
c3b502c
Code cleanup
chadweimer Apr 24, 2026
6a86cee
Configure revive
chadweimer Apr 24, 2026
85bb3ab
Small clarity cleanup
chadweimer Apr 24, 2026
e719a3b
Small cleanup
chadweimer Apr 24, 2026
4faeacd
Small consistency change
chadweimer Apr 24, 2026
cb7ed8d
Oops, missed a file
chadweimer Apr 24, 2026
e5305d3
Show backup size
chadweimer Apr 24, 2026
85deba5
Fix buttons on smaller screens
chadweimer Apr 24, 2026
49319f4
Handle DB special cases
chadweimer Apr 24, 2026
4605746
Handle triggers and constraints on import
chadweimer Apr 24, 2026
a58306b
750 permissions
chadweimer Apr 24, 2026
3d29c6a
Eliminate need for Trap method
chadweimer Apr 24, 2026
15e91fa
Fix mocking
chadweimer Apr 24, 2026
6586eac
Add tests for zip methods
chadweimer Apr 24, 2026
e660b48
Added tests for API layer
chadweimer Apr 24, 2026
2d3bb85
Fix code smell
chadweimer Apr 24, 2026
bdad192
Test and fix unbuferedReaderAt
chadweimer Apr 24, 2026
1fd1a45
Code smells
chadweimer Apr 24, 2026
adffee9
More code smells
chadweimer Apr 24, 2026
01c3cad
Add version to UI
chadweimer Apr 24, 2026
cfb33c0
Reorder restore for better error handling
chadweimer Apr 24, 2026
312067f
Allow alert to close before executing selected option
chadweimer Apr 24, 2026
33327e6
Fix log message
chadweimer Apr 24, 2026
bb9fc23
Consolidate logging infra for contexts
chadweimer Apr 24, 2026
fdc7dbd
Fix several tests that were susceptible to ordering issues
chadweimer Apr 25, 2026
867532d
Tests for onlyFilesFileSystem
chadweimer Apr 25, 2026
691fe69
Fix some contexts in tests
chadweimer Apr 25, 2026
d44ffaf
Convert prompts to skills
chadweimer Apr 25, 2026
e089f41
Version bump
chadweimer Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
name: new-api-endpoint
description: Guide for adding new API endpoints in GOMP. Use this when asked to create or update API endpoints.
---

# New API Endpoint Checklist

Use this checklist when adding a new API endpoint to GOMP.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
name: new-db-migration
description: Guide for creating new database migrations in GOMP. Use this when asked to add or update database schema.
---

# New DB Migration Checklist

Use this checklist when making schema changes.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
---
name: writing-tests
description: Guide for writing tests in GOMP. Use this when asked to create or run tests.
---

# Writing Tests in GOMP

**ALWAYS use this skill when asked to write or add tests.**

Use this guide for consistent tests across API, DB, and middleware layers.

## Core Patterns
- Prefer table-driven tests for input/output coverage.
- Keep tests in package-local `*_test.go` files near the implementation.
- For Go, **always** use table-driven tests when testing multiple input/output scenarios.
- For Go, Keep tests in package-local `*_test.go` files near the implementation.
- Focus on behavior and contract verification over implementation detail.

## API Layer Tests (`api/`)
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ CLIENT_CODEGEN_DIR:=static/src/generated
MODELS_CODEGEN_FILE:=models/models.gen.go
API_CODEGEN_FILE:=api/routes.gen.go
MOCKS_CODEGEN_DIR:=mocks
CODEGEN_FILES=$(API_CODEGEN_FILE) $(MODELS_CODEGEN_FILE) $(MOCKS_CODEGEN_DIR)/db/mocks.gen.go $(MOCKS_CODEGEN_DIR)/upload/mocks.gen.go
CODEGEN_FILES=$(API_CODEGEN_FILE) $(MODELS_CODEGEN_FILE) $(MOCKS_CODEGEN_DIR)/db/mocks.gen.go $(MOCKS_CODEGEN_DIR)/fileaccess/mocks.gen.go

# Source files
GO_FILES:=$(shell find . -type f -name "*.go" ! -name "*.gen.go")
Expand Down Expand Up @@ -87,7 +87,7 @@ lint-client: $(CLIENT_INSTALL_DIR) $(CLIENT_CODEGEN_DIR)
lint-server: $(CODEGEN_FILES)
mkdir -p $(ROOT_BUILD_DIR)
go vet ./...
go tool revive -config=revive.toml ./... > $(ROOT_BUILD_DIR)/revive.golint
go tool revive -exclude static/... -config=revive.toml ./... > $(ROOT_BUILD_DIR)/revive.golint
go tool gosec -no-fail -fmt=sonarqube -out=$(ROOT_BUILD_DIR)/gosec.json -stdout ./...


Expand Down
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,27 @@ spec:
The following table summarizes the available configuration settings, which are settable through environment variables.
Unless otherwise specified, for settings that are arrays, multiple values should be separated by commas.

ENV |Value(s) |Default |Description
------------------------|----------------|-----------------------------------------|------------
BASE_ASSETS_PATH |string |static |The base path to the client assets.
DATABASE_DRIVER |postgres, sqlite|<empty> |Which database/sql driver to use. If blank, the app will attempt to infer it based on the value of DATABASE_URL.
DATABASE_URL |string |file:data/data.db?_pragma=foreign_keys(1)|The url (path, connection string, etc) to use with the associated database driver when opening the database connection.
IS_DEVELOPMENT |0, 1 |0 |Defines whether to run the application in "development mode". Development mode turns on additional features, such as logging, that may not be desirable in a production environment.
MIGRATIONS_FORCE_VERSION|int |-1 |A version to force the migrations to on startup (will not run any of the migrations themselves). Set to a negative number to skip forcing a version.
MIGRATIONS_TABLE_NAME |string |&lt;empty&gt; |The name of the database migrations table to use. Leave blank to use the default from <https://github.com/golang-migrate/migrate.>
PORT |uint |5000 |The port number under which the site is being hosted.
SECURE_KEY |[]string |ChangeMe |Used for session authentication. Recommended to be 32 or 64 ASCII characters.
TRUSTED_PROXIES |[]string |&lt;empty&gt; |List of IP addresses or CIDR ranges that are considered trusted proxies. When determining the client IP address, if the request comes from a trusted proxy, the `X-Forwarded-For` header will be used to determine the original client IP.
UPLOAD_PATH |string |data/uploads |The path (full or relative) under which to store uploads.
IMAGE_QUALITY |original, high, medium, low|original |The quality level for recipe images. Original quality falls back to High if the uploaded image is not a JPEG. JPEG Qualities: High == 92, Medium == 80, Low == 70. Resizing Algorithm: High = CatmullRom, Medium = BiLinear, Low = NearestNeighbor.
IMAGE_SIZE |uint |2000 |The size of the bounding box to fit recipe images to. Ignored if IMAGE_QUALITY == original.
THUMBNAIL_QUALITY |high, medium, low|medium |The quality level for the thumbnails of recipe images. JPEG Qualities: High == 92, Medium == 80, Low == 70. Low also uses the Nearest Neighbor instead of the Box resizing algorithm.
THUMBNAIL_SIZE |uint |500 |The size of the bounding box to fit the thumbnails of recipe images to.
ENV |Value(s) |Default |Description
------------------------|---------------------------|-----------------------------------------|------------
BASE_ASSETS_PATH |string |static |The base path to the client assets.
DATABASE_DRIVER |postgres, sqlite |&lt;empty&gt; |Which database/sql driver to use. If blank, the app will attempt to infer it based on the value of DATABASE_URL.
DATABASE_URL |string |file:data/data.db?_pragma=foreign_keys(1)|The url (path, connection string, etc) to use with the associated database driver when opening the database connection.
IS_DEVELOPMENT |0, 1 |0 |Defines whether to run the application in "development mode". Development mode turns on additional features, such as logging, that may not be desirable in a production environment.
MIGRATIONS_FORCE_VERSION|int |-1 |A version to force the migrations to on startup (will not run any of the migrations themselves). Set to a negative number to skip forcing a version.
MIGRATIONS_TABLE_NAME |string |&lt;empty&gt; |The name of the database migrations table to use. Leave blank to use the default from <https://github.com/golang-migrate/migrate.>
PORT |uint |5000 |The port number under which the site is being hosted.
SECURE_KEY |[]string |ChangeMe |Used for session authentication. Recommended to be 32 or 64 ASCII characters.
TRUSTED_PROXIES |[]string |&lt;empty&gt; |List of IP addresses or CIDR ranges that are considered trusted proxies. When determining the client IP address, if the request comes from a trusted proxy, the `X-Forwarded-For` header will be used to determine the original client IP.
FILES_PATH |string |data |The path (full or relative) under which to store file data.
IMAGE_QUALITY |original, high, medium, low|original |The quality level for recipe images. Original quality falls back to High if the uploaded image is not a JPEG. JPEG Qualities: High == 92, Medium == 80, Low == 70. Resizing Algorithm: High = CatmullRom, Medium = BiLinear, Low = NearestNeighbor.
IMAGE_SIZE |uint |2000 |The size of the bounding box to fit recipe images to. Ignored if IMAGE_QUALITY == original.
THUMBNAIL_QUALITY |high, medium, low |medium |The quality level for the thumbnails of recipe images. JPEG Qualities: High == 92, Medium == 80, Low == 70. Low also uses the Nearest Neighbor instead of the Box resizing algorithm.
THUMBNAIL_SIZE |uint |500 |The size of the bounding box to fit the thumbnails of recipe images to.

All environment variables can also be prefixed with "GOMP_" (e.g., GOMP_IS_DEVELOPMENT=1) in cases where there is a need to avoid collisions with other applications.
The name with "GOMP_" is prefered if both are present.

For values that allow releative paths (e.g., BASE_ASSETS_PATH, DATABASE_URL for SQLite, and UPLOAD_PATH for the fs driver), they are always relative to the application working directory.
For values that allow releative paths (e.g., BASE_ASSETS_PATH, DATABASE_URL for SQLite, and FILES_PATH), they are always relative to the application working directory.
When using docker, this is "/var/app/gomp", so anything at or below the "data/" relative path is in the exposed "/var/app/gomp/data" volume.

## Database Support
Expand Down
29 changes: 10 additions & 19 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"

"github.com/chadweimer/gomp/db"
"github.com/chadweimer/gomp/middleware"
"github.com/chadweimer/gomp/upload"
"github.com/chadweimer/gomp/fileaccess"
"github.com/chadweimer/gomp/infra"
)

// ---- Begin Standard Errors ----
Expand All @@ -24,26 +23,22 @@ var errMismatchedID = errors.New("id in the path does not match the one specifie

// ---- Begin Context Keys ----

type contextKey string

func (k contextKey) String() string {
return "gomp context key: " + string(k)
}

const currentUserIDCtxKey = contextKey("current-user-id")
const currentUserIDCtxKey = infra.ContextKey("current-user-id")

// ---- End Context Keys ----

type apiHandler struct {
secureKeys []string
upl *upload.ImageUploader
fs fileaccess.Driver
upl *fileaccess.ImageUploader
db db.Driver
}

// NewHandler returns a new instance of http.Handler
func NewHandler(secureKeys []string, upl *upload.ImageUploader, drDriver db.Driver) http.Handler {
func NewHandler(secureKeys []string, upl *fileaccess.ImageUploader, drDriver db.Driver, fs fileaccess.Driver) http.Handler {
h := apiHandler{
secureKeys: secureKeys,
fs: fs,
upl: upl,
db: drDriver,
}
Expand All @@ -68,14 +63,10 @@ func NewHandler(secureKeys []string, upl *upload.ImageUploader, drDriver db.Driv
})
}

func logger(ctx context.Context) *slog.Logger {
return middleware.GetLoggerFromContext(ctx)
}

func writeJSONResponse(w http.ResponseWriter, r *http.Request, status int, v any) {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(v); err != nil {
logger(r.Context()).
infra.GetLoggerFromContext(r.Context()).
Error("Failed to encode response",
"error", err,
"original-status", status)
Expand All @@ -92,12 +83,12 @@ func writeJSONResponse(w http.ResponseWriter, r *http.Request, status int, v any
}

func writeErrorResponse(w http.ResponseWriter, r *http.Request, status int, err error) {
logger(r.Context()).Error("failure on request", "error", err)
infra.GetLoggerFromContext(r.Context()).Error("failure on request", "error", err)
status = getStatusFromError(err, status)
writeJSONResponse(w, r, status, http.StatusText(status))
}

func getResourceIDFromCtx(ctx context.Context, idKey contextKey) (int64, error) {
func getResourceIDFromCtx(ctx context.Context, idKey infra.ContextKey) (int64, error) {
idVal := ctx.Value(idKey)

id, ok := idVal.(int64)
Expand Down
9 changes: 5 additions & 4 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"github.com/chadweimer/gomp/db"
"github.com/chadweimer/gomp/infra"
)

func Test_getStatusFromError(t *testing.T) {
Expand Down Expand Up @@ -56,16 +57,16 @@ func Test_getStatusFromError(t *testing.T) {

func Test_getResourceIDFromCtx(t *testing.T) {
type getResourceIDFromCtxTest struct {
key contextKey
key infra.ContextKey
val int64
usePtr bool
}

// Arrange
tests := []getResourceIDFromCtxTest{
{contextKey("the-item"), 10, false},
{contextKey("the-item"), 10, true},
{contextKey("the-item"), -1, false},
{infra.ContextKey("the-item"), 10, false},
{infra.ContextKey("the-item"), 10, true},
{infra.ContextKey("the-item"), -1, false},
}

for i, test := range tests {
Expand Down
4 changes: 2 additions & 2 deletions api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (

func (apiHandler) GetInfo(_ context.Context, _ GetInfoRequestObject) (GetInfoResponseObject, error) {
return GetInfo200JSONResponse{
Copyright: &metadata.Copyright,
Version: &metadata.BuildVersion,
Copyright: metadata.Copyright,
Version: metadata.BuildVersion,
}, nil
}

Expand Down
18 changes: 9 additions & 9 deletions api/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"errors"
"testing"

"github.com/chadweimer/gomp/fileaccess"
"github.com/chadweimer/gomp/metadata"
"github.com/chadweimer/gomp/mocks/db"
uploadmock "github.com/chadweimer/gomp/mocks/upload"
fileaccessmock "github.com/chadweimer/gomp/mocks/fileaccess"
"github.com/chadweimer/gomp/models"
"github.com/chadweimer/gomp/upload"
"go.uber.org/mock/gomock"
)

Expand All @@ -30,8 +30,8 @@ func Test_GetInfo(t *testing.T) {
if !ok {
t.Fatal("invalid response")
}
if typedResp.Version != &metadata.BuildVersion {
t.Errorf("unexpected version: %s", *typedResp.Version)
if typedResp.Version != metadata.BuildVersion {
t.Errorf("unexpected version: %s", typedResp.Version)
}
}
}
Expand Down Expand Up @@ -106,14 +106,14 @@ func getMockAppConfigurationAPI(ctrl *gomock.Controller) (apiHandler, *db.MockAp
dbDriver := db.NewMockDriver(ctrl)
appDriver := db.NewMockAppConfigurationDriver(ctrl)
dbDriver.EXPECT().AppConfiguration().AnyTimes().Return(appDriver)
uplDriver := uploadmock.NewMockDriver(ctrl)
imgCfg := upload.ImageConfig{
ImageQuality: upload.ImageQualityOriginal,
uplDriver := fileaccessmock.NewMockDriver(ctrl)
imgCfg := fileaccess.ImageConfig{
ImageQuality: fileaccess.ImageQualityOriginal,
ImageSize: 2000,
ThumbnailQuality: upload.ImageQualityMedium,
ThumbnailQuality: fileaccess.ImageQualityMedium,
ThumbnailSize: 500,
}
upl, _ := upload.CreateImageUploader(uplDriver, imgCfg)
upl, _ := fileaccess.CreateImageUploader(uplDriver, imgCfg)

api := apiHandler{
secureKeys: []string{},
Expand Down
9 changes: 5 additions & 4 deletions api/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/chadweimer/gomp/db"
"github.com/chadweimer/gomp/infra"
"github.com/chadweimer/gomp/models"
"github.com/golang-jwt/jwt/v4"
"github.com/samber/lo"
Expand All @@ -27,7 +28,7 @@ func (h apiHandler) Authenticate(ctx context.Context, request AuthenticateReques
credentials := request.Body
user, err := h.db.Users().Authenticate(ctx, credentials.Username, credentials.Password)
if err != nil {
logger(ctx).Error("failure authenticating", "error", err)
infra.GetLoggerFromContext(ctx).Error("failure authenticating", "error", err)
return Authenticate401Response{}, nil
}

Expand All @@ -43,7 +44,7 @@ func (h apiHandler) RefreshToken(ctx context.Context, _ RefreshTokenRequestObjec
return withCurrentUser[RefreshTokenResponseObject](ctx, RefreshToken401Response{}, func(userID int64) (RefreshTokenResponseObject, error) {
user, err := h.db.Users().Read(ctx, userID)
if err != nil {
logger(ctx).Error("failure refreshing token", "error", err)
infra.GetLoggerFromContext(ctx).Error("failure refreshing token", "error", err)
return RefreshToken401Response{}, nil
}

Expand Down Expand Up @@ -101,7 +102,7 @@ func (h apiHandler) checkScopes(next http.Handler) http.Handler {
}

func (h apiHandler) isAuthenticated(ctx context.Context, header http.Header) (*models.User, *gompClaims, error) {
logger := logger(ctx)
logger := infra.GetLoggerFromContext(ctx)

token, err := h.getAuthTokenFromRequest(header, logger)
if err != nil {
Expand Down Expand Up @@ -250,7 +251,7 @@ func getUserIDFromClaims(claims jwt.RegisteredClaims, logger *slog.Logger) (int6
func withCurrentUser[TResponse any](ctx context.Context, invalidUserResponse TResponse, do func(userID int64) (TResponse, error)) (TResponse, error) {
userID, err := getResourceIDFromCtx(ctx, currentUserIDCtxKey)
if err != nil {
logger(ctx).Error("failed to get current user from request context", "error", err)
infra.GetLoggerFromContext(ctx).Error("failed to get current user from request context", "error", err)
return invalidUserResponse, nil
}

Expand Down
Loading
Loading