Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8bf9578
WIP: start entity merge stuff (very broken)
tankerkiller125 May 8, 2025
aa4de3c
Add location relationship to prevent stupid complexity
tankerkiller125 May 8, 2025
fbe56c4
In theory fix repo_items.go
tankerkiller125 May 8, 2025
d8e8bf4
Application can run (although no DB changes yet)
tankerkiller125 May 8, 2025
666237d
Merge branch 'main' into mk/merge-entities
tankerkiller125 May 8, 2025
ed53026
Initial test SQL migration, and deprecation of items and locations AP…
tankerkiller125 May 11, 2025
c947369
Merge remote-tracking branch 'origin/mk/merge-entities' into mk/merge…
tankerkiller125 May 11, 2025
22cb2f8
For some reason swaggo doesn't mark the endpoints as deprecated
tankerkiller125 May 11, 2025
a085f95
Fix docs openapi json
tankerkiller125 May 11, 2025
2789c1c
Let's go crazy with user defined types
tankerkiller125 May 14, 2025
b0693cb
ci: fix golang dependencies cache path
tankerkiller125 May 26, 2025
635ad26
chore: better column name for location entity type check
tankerkiller125 May 26, 2025
51657d6
fix: bad edge design for location column
tankerkiller125 May 26, 2025
be7f276
fix: completely broken code
tankerkiller125 May 26, 2025
b61edf1
fix: completely broken test
tankerkiller125 May 26, 2025
b989cf0
WIP: Place holder while I switch devices
tankerkiller125 May 31, 2025
2a486ec
WIP: Postgres migrations
tankerkiller125 Jun 1, 2025
1fa0bdc
fix: conflicting migrations
tankerkiller125 Jun 1, 2025
fa50f3a
fix: missing column from entities migration for postgres
tankerkiller125 Jun 1, 2025
6518db2
WIP: still working things out
tankerkiller125 Jun 3, 2025
de14911
Merge branch 'main' into mk/merge-entities
tankerkiller125 Jul 27, 2025
64907bb
Cleanup merge
tankerkiller125 Jul 27, 2025
77b14f3
More merge fixing
tankerkiller125 Jul 27, 2025
a35e92b
Weird issue with go.mod file
tankerkiller125 Jul 27, 2025
b107ab0
Refactor entity attachment handling and update API routes for entities
tankerkiller125 Jul 27, 2025
e00cebb
Merge branch 'refs/heads/main' into mk/merge-entities
tankerkiller125 Sep 2, 2025
564bd8c
Get main into this PR.
tankerkiller125 Sep 2, 2025
234302e
Merge remote-tracking branch 'origin/main' into mk/merge-entities
tankerkiller125 Sep 6, 2025
8f4ed9a
Slow progress, but we're getting there.
tankerkiller125 Sep 6, 2025
e7f5866
Further progress on sqlite
tankerkiller125 Sep 6, 2025
fa9b7af
Working locations/items tree
tankerkiller125 Sep 6, 2025
5456390
What should be a working Postgres migration
tankerkiller125 Sep 11, 2025
3f2ae36
API stuff for entity types
tankerkiller125 Sep 11, 2025
c0cb579
Lint the things
tankerkiller125 Sep 11, 2025
ab1fd67
Add entitytypes delete method
tankerkiller125 Sep 12, 2025
9f16aad
Commit to change branch
tankerkiller125 Sep 21, 2025
9c121f2
Start putting things together for the new entities repository.
tankerkiller125 Oct 5, 2025
b8cca4b
Merge branch 'main' into mk/merge-entities
tankerkiller125 Oct 5, 2025
462c5d2
Complete merge of main into branch
tankerkiller125 Oct 5, 2025
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
1 change: 1 addition & 0 deletions backend/app/api/handlers/v1/v1_ctrl_entities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package v1
243 changes: 243 additions & 0 deletions backend/app/api/handlers/v1/v1_ctrl_entities_attachments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package v1

import (
"errors"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"

"github.com/hay-kot/httpkit/errchain"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/sys/validate"

"gocloud.dev/blob"
_ "gocloud.dev/blob/azureblob"
_ "gocloud.dev/blob/fileblob"
_ "gocloud.dev/blob/gcsblob"
_ "gocloud.dev/blob/memblob"
_ "gocloud.dev/blob/s3blob"
)

type (
EntityAttachmentToken struct {
Token string `json:"token"`
}
)

// HandleEntityAttachmentCreate godocs
//
// @Summary Create Item Attachment
// @Tags Items Attachments
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string false "Type of file"
// @Param primary formData bool false "Is this the primary attachment"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} repo.ItemOut
// @Failure 422 {object} validate.ErrorResponse
// @Router /v1/entities/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityAttachmentCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil {
log.Err(err).Msg("failed to parse multipart form")
return validate.NewRequestError(errors.New("failed to parse multipart form"), http.StatusBadRequest)
}

errs := validate.NewFieldErrors()

file, _, err := r.FormFile("file")
if err != nil {
switch {
case errors.Is(err, http.ErrMissingFile):
log.Debug().Msg("file for attachment is missing")
errs = errs.Append("file", "file is required")
default:
log.Err(err).Msg("failed to get file from form")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
}

attachmentName := r.FormValue("name")
if attachmentName == "" {
log.Debug().Msg("failed to get name from form")
errs = errs.Append("name", "name is required")
}

if !errs.Nil() {
return server.JSON(w, http.StatusUnprocessableEntity, errs)
}

attachmentType := r.FormValue("type")
if attachmentType == "" {
// Attempt to auto-detect the type of the file
ext := filepath.Ext(attachmentName)

switch strings.ToLower(ext) {
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".avif", ".ico", ".heic", ".jxl":
attachmentType = attachment.TypePhoto.String()
default:
attachmentType = attachment.TypeAttachment.String()
}
}

primary, err := strconv.ParseBool(r.FormValue("primary"))
if err != nil {
log.Debug().Msg("failed to parse primary from form")
primary = false
}

id, err := ctrl.routeID(r)
if err != nil {
return err
}

ctx := services.NewContext(r.Context())

item, err := ctrl.svc.Items.AttachmentAdd(
ctx,
id,
attachmentName,
attachment.Type(attachmentType),
primary,
file,
)
if err != nil {
log.Err(err).Msg("failed to add attachment")
return validate.NewRequestError(err, http.StatusInternalServerError)
}

return server.JSON(w, http.StatusCreated, item)
}
}

// HandleEntityAttachmentGet godocs
//
// @Summary Get Item Attachment
// @Tags Items Attachments
// @Produce application/octet-stream
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 200 {object} ItemAttachmentToken
// @Router /v1/entities/{id}/attachments/{attachment_id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityAttachmentGet() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}

// HandleEntityAttachmentDelete godocs
//
// @Summary Delete Item Attachment
// @Tags Items Attachments
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 204
// @Router /v1/entities/{id}/attachments/{attachment_id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityAttachmentDelete() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}

// HandleEntityAttachmentUpdate godocs
//
// @Summary Update Item Attachment
// @Tags Items Attachments
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Param payload body repo.EntityAttachmentUpdate true "Attachment Update"
// @Success 200 {object} repo.ItemOut
// @Router /v1/entities/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityAttachmentUpdate() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}

func (ctrl *V1Controller) handleEntityAttachmentsHandler(w http.ResponseWriter, r *http.Request) error {
ID, err := ctrl.routeID(r)
if err != nil {
return err
}

attachmentID, err := ctrl.routeUUID(r, "attachment_id")
if err != nil {
return err
}

ctx := services.NewContext(r.Context())
switch r.Method {
case http.MethodGet:
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), ctx.GID, attachmentID)
if err != nil {
log.Err(err).Msg("failed to get attachment path")
return validate.NewRequestError(err, http.StatusInternalServerError)
}

bucket, err := blob.OpenBucket(ctx, ctrl.repo.Attachments.GetConnString())
if err != nil {
log.Err(err).Msg("failed to open bucket")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
file, err := bucket.NewReader(ctx, doc.Path, nil)
if err != nil {
log.Err(err).Msg("failed to open file")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
defer func(file *blob.Reader) {
err := file.Close()
if err != nil {
log.Err(err).Msg("failed to close file")
}
}(file)
defer func(bucket *blob.Bucket) {
err := bucket.Close()
if err != nil {
log.Err(err).Msg("failed to close bucket")
}
}(bucket)

// Set the Content-Disposition header for RFC6266 compliance
disposition := "attachment; filename*=UTF-8''" + url.QueryEscape(doc.Title)
w.Header().Set("Content-Disposition", disposition)
http.ServeContent(w, r, doc.Title, doc.CreatedAt, file)
return nil

// Delete Attachment Handler
case http.MethodDelete:
err = ctrl.svc.Items.AttachmentDelete(r.Context(), ctx.GID, ID, attachmentID)
if err != nil {
log.Err(err).Msg("failed to delete attachment")
return validate.NewRequestError(err, http.StatusInternalServerError)
}

return server.JSON(w, http.StatusNoContent, nil)

// Update Attachment Handler
case http.MethodPut:
var attachment repo.EntityAttachmentUpdate
err = server.Decode(r, &attachment)
if err != nil {
log.Err(err).Msg("failed to decode attachment")
return validate.NewRequestError(err, http.StatusBadRequest)
}

attachment.ID = attachmentID
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ctx.GID, ID, &attachment)
if err != nil {
log.Err(err).Msg("failed to update attachment")
return validate.NewRequestError(err, http.StatusInternalServerError)
}

return server.JSON(w, http.StatusOK, val)
}

return nil
}
99 changes: 99 additions & 0 deletions backend/app/api/handlers/v1/v1_ctrl_entitytypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package v1

import (
"net/http"

"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
)

// HandleEntityTypesGetAll godoc
//
// @Summary Query All Entity Types
// @Tags EntityTypes
// @Produce json
// @Success 200 {array} repo.EntityType[]
// @Router /v1/entitytype [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypesGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.GetEntityTypesByGroupID(auth, auth.GID)
}
return adapters.Command(fn, http.StatusOK)
}

// HandleEntityTypeGetOne godoc
//
// @Summary Get One Entity Type
// @Tags EntityTypes
// @Produce json
// @Param id path string true "Entity Type ID"
// @Success 200 {object} repo.EntityType
// @Router /v1/entitytype/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeGetOne() errchain.HandlerFunc {
fn := func(r *http.Request, entityTypeID uuid.UUID) (repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.GetOneByGroup(auth, auth.GID, entityTypeID)
}
return adapters.CommandID("id", fn, http.StatusOK)
}

// HandleEntityTypeCreate godoc
//
// @Summary Create Entity Type
// @Tags EntityTypes
// @Accept json
// @Produce json
// @Param payload body repo.EntityTypeCreate true "Entity Type Data"
// @Success 201 {object} repo.EntityType
// @Router /v1/entitytype [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeCreate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.EntityTypeCreate) (repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.CreateEntityType(auth, auth.GID, body)
}
return adapters.Action(fn, http.StatusCreated)
}

// HandleEntityTypeUpdate godoc
//
// @Summary Update Entity Type
// @Tags EntityTypes
// @Accept json
// @Produce json
// @Param id path string true "Entity Type ID"
// @Param payload body repo.EntityTypeUpdate true "Entity Type Data"
// @Success 200 {object} repo.EntityType
// @Router /v1/entitytype/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, entityTypeID uuid.UUID, body repo.EntityTypeUpdate) (repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.UpdateEntityType(auth, auth.GID, entityTypeID, body)
}
return adapters.ActionID("id", fn, http.StatusOK)
}

// HandleEntityTypeDelete godoc
//
// @Summary Delete Entity Type
// @Tags EntityTypes
// @Param id path string true "Entity Type ID"
// @Param payload body repo.EntityTypeDelete true "Entity Type Delete Options"
// @Success 204
// @Router /v1/entitytype/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeDelete() errchain.HandlerFunc {
fn := func(r *http.Request, entityTypeID uuid.UUID, body repo.EntityTypeDelete) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.EntityType.DeleteEntityType(auth, auth.GID, entityTypeID, body)
return nil, err
}
return adapters.ActionID("id", fn, http.StatusNoContent)
}
Loading
Loading