Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,97 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/archive.go
package file
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ArchiveFileHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.ArchiveFileService
middleware middleware.Middleware
}
func NewArchiveFileHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.ArchiveFileService,
middleware middleware.Middleware,
) *ArchiveFileHTTPHandler {
logger = logger.Named("ArchiveFileHTTPHandler")
return &ArchiveFileHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*ArchiveFileHTTPHandler) Pattern() string {
return "PUT /api/v1/file/{id}/archive"
}
func (h *ArchiveFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ArchiveFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from the URL
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
// Create request DTO
dtoReq := &svc_file.ArchiveFileRequestDTO{
FileID: fileID,
}
resp, err := h.service.Execute(ctx, dtoReq)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.String("file_id", fileIDStr),
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,129 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/complete_file_upload.go
package file
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CompleteFileUploadHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.CompleteFileUploadService
middleware middleware.Middleware
}
func NewCompleteFileUploadHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.CompleteFileUploadService,
middleware middleware.Middleware,
) *CompleteFileUploadHTTPHandler {
logger = logger.Named("CompleteFileUploadHTTPHandler")
return &CompleteFileUploadHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*CompleteFileUploadHTTPHandler) Pattern() string {
return "POST /api/v1/file/{id}/complete"
}
func (h *CompleteFileUploadHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *CompleteFileUploadHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
fileID gocql.UUID,
) (*svc_file.CompleteFileUploadRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_file.CompleteFileUploadRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Set the file ID from the URL parameter
requestData.FileID = fileID
return &requestData, nil
}
func (h *CompleteFileUploadHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from URL parameters
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, fileID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,108 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/create_pending_file.go
package file
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CreatePendingFileHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.CreatePendingFileService
middleware middleware.Middleware
}
func NewCreatePendingFileHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.CreatePendingFileService,
middleware middleware.Middleware,
) *CreatePendingFileHTTPHandler {
logger = logger.Named("CreatePendingFileHTTPHandler")
return &CreatePendingFileHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*CreatePendingFileHTTPHandler) Pattern() string {
return "POST /api/v1/files/pending"
}
func (h *CreatePendingFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *CreatePendingFileHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
) (*svc_file.CreatePendingFileRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_file.CreatePendingFileRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err))
// Log raw JSON at debug level only to avoid PII exposure in production logs
h.logger.Debug("raw request body for debugging",
zap.String("json", rawJSON.String()))
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
return &requestData, nil
}
func (h *CreatePendingFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
req, err := h.unmarshalRequest(ctx, r)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,91 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/get.go
package file
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFileHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.GetFileService
middleware middleware.Middleware
}
func NewGetFileHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.GetFileService,
middleware middleware.Middleware,
) *GetFileHTTPHandler {
logger = logger.Named("GetFileHTTPHandler")
return &GetFileHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*GetFileHTTPHandler) Pattern() string {
return "GET /api/v1/file/{id}"
}
func (h *GetFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *GetFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from URL parameters
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
resp, err := h.service.Execute(ctx, fileID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,134 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/get_presigned_download_url.go
package file
import (
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetPresignedDownloadURLHTTPRequestDTO struct {
URLDurationStr string `json:"url_duration,omitempty"` // Optional, duration as string of nanoseconds, defaults to 1 hour
}
type GetPresignedDownloadURLHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.GetPresignedDownloadURLService
middleware middleware.Middleware
}
func NewGetPresignedDownloadURLHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.GetPresignedDownloadURLService,
middleware middleware.Middleware,
) *GetPresignedDownloadURLHTTPHandler {
logger = logger.Named("GetPresignedDownloadURLHTTPHandler")
return &GetPresignedDownloadURLHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*GetPresignedDownloadURLHTTPHandler) Pattern() string {
return "GET /api/v1/file/{id}/download-url"
}
func (h *GetPresignedDownloadURLHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *GetPresignedDownloadURLHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
fileID gocql.UUID,
) (*svc_file.GetPresignedDownloadURLRequestDTO, error) {
// For GET requests, read from query parameters instead of body
urlDurationStr := r.URL.Query().Get("url_duration")
// Set default URL duration if not provided (1 hour in nanoseconds)
var urlDuration time.Duration
if urlDurationStr == "" {
urlDuration = 1 * time.Hour
} else {
// Parse the string to int64 (nanoseconds)
durationNanos, err := strconv.ParseInt(urlDurationStr, 10, 64)
if err != nil {
return nil, httperror.NewForSingleField(http.StatusBadRequest, "url_duration", "Invalid duration format")
}
urlDuration = time.Duration(durationNanos)
}
// Convert to service DTO
serviceRequest := &svc_file.GetPresignedDownloadURLRequestDTO{
FileID: fileID,
URLDuration: urlDuration,
}
return serviceRequest, nil
}
func (h *GetPresignedDownloadURLHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from URL parameters
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, fileID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,152 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/get_presigned_upload_url.go
package file
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetPresignedUploadURLHTTPRequestDTO struct {
URLDurationStr string `json:"url_duration,omitempty"` // Optional, duration as string of nanoseconds, defaults to 1 hour
}
type GetPresignedUploadURLHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.GetPresignedUploadURLService
middleware middleware.Middleware
}
func NewGetPresignedUploadURLHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.GetPresignedUploadURLService,
middleware middleware.Middleware,
) *GetPresignedUploadURLHTTPHandler {
logger = logger.Named("GetPresignedUploadURLHTTPHandler")
return &GetPresignedUploadURLHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*GetPresignedUploadURLHTTPHandler) Pattern() string {
return "GET /api/v1/file/{id}/upload-url"
}
func (h *GetPresignedUploadURLHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *GetPresignedUploadURLHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
fileID gocql.UUID,
) (*svc_file.GetPresignedUploadURLRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var httpRequestData GetPresignedUploadURLHTTPRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&httpRequestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Set default URL duration if not provided (1 hour in nanoseconds)
var urlDuration time.Duration
if httpRequestData.URLDurationStr == "" {
urlDuration = 1 * time.Hour
} else {
// Parse the string to int64 (nanoseconds)
durationNanos, err := strconv.ParseInt(httpRequestData.URLDurationStr, 10, 64)
if err != nil {
return nil, httperror.NewForSingleField(http.StatusBadRequest, "url_duration", "Invalid duration format")
}
urlDuration = time.Duration(durationNanos)
}
// Convert to service DTO
serviceRequest := &svc_file.GetPresignedUploadURLRequestDTO{
FileID: fileID,
URLDuration: urlDuration,
}
return serviceRequest, nil
}
func (h *GetPresignedUploadURLHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from URL parameters
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, fileID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Call service
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,96 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/list_by_collection.go
package file
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListFilesByCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.ListFilesByCollectionService
middleware middleware.Middleware
}
func NewListFilesByCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.ListFilesByCollectionService,
middleware middleware.Middleware,
) *ListFilesByCollectionHTTPHandler {
logger = logger.Named("ListFilesByCollectionHTTPHandler")
return &ListFilesByCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*ListFilesByCollectionHTTPHandler) Pattern() string {
return "GET /api/v1/collection/{collection_id}/files"
}
func (h *ListFilesByCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ListFilesByCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from URL parameters
collectionIDStr := r.PathValue("collection_id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
// Create request DTO
req := &svc_file.ListFilesByCollectionRequestDTO{
CollectionID: collectionID,
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,106 @@
// cloud/maplefile-backend/internal/maplefile/interface/http/file/list_recent_files.go
package file
import (
"encoding/json"
"net/http"
"strconv"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
file_service "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListRecentFilesHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
listRecentFilesService file_service.ListRecentFilesService
middleware middleware.Middleware
}
func NewListRecentFilesHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
listRecentFilesService file_service.ListRecentFilesService,
middleware middleware.Middleware,
) *ListRecentFilesHTTPHandler {
logger = logger.Named("ListRecentFilesHTTPHandler")
return &ListRecentFilesHTTPHandler{
config: config,
logger: logger,
listRecentFilesService: listRecentFilesService,
middleware: middleware,
}
}
func (*ListRecentFilesHTTPHandler) Pattern() string {
return "GET /api/v1/files/recent"
}
func (h *ListRecentFilesHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ListRecentFilesHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Parse query parameters
queryParams := r.URL.Query()
// Parse limit parameter (default: 30, max: 100)
limit := int64(30)
if limitStr := queryParams.Get("limit"); limitStr != "" {
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil {
if parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
} else {
h.logger.Warn("Invalid limit parameter, using default",
zap.String("limit", limitStr),
zap.Int64("default", limit))
}
} else {
h.logger.Warn("Failed to parse limit parameter, using default",
zap.String("limit", limitStr),
zap.Error(err))
}
}
// Parse cursor parameter
var cursor *string
if cursorStr := queryParams.Get("cursor"); cursorStr != "" {
cursor = &cursorStr
}
h.logger.Debug("Processing recent files request",
zap.Int64("limit", limit),
zap.Any("cursor", cursor))
// Call service to get recent files
response, err := h.listRecentFilesService.Execute(ctx, cursor, limit)
if err != nil {
h.logger.Error("Failed to get recent files",
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
// Encode and return response
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Error("Failed to encode recent files response",
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
h.logger.Info("Successfully served recent files",
zap.Int("files_count", len(response.Files)),
zap.Bool("has_more", response.HasMore),
zap.Any("next_cursor", response.NextCursor))
}

View file

@ -0,0 +1,146 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/list_sync.go
package file
import (
"encoding/json"
"net/http"
"strconv"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
file_service "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type FileSyncHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
fileSyncService file_service.ListFileSyncDataService
middleware middleware.Middleware
}
func NewFileSyncHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
fileSyncService file_service.ListFileSyncDataService,
middleware middleware.Middleware,
) *FileSyncHTTPHandler {
logger = logger.Named("FileSyncHTTPHandler")
return &FileSyncHTTPHandler{
config: config,
logger: logger,
fileSyncService: fileSyncService,
middleware: middleware,
}
}
func (*FileSyncHTTPHandler) Pattern() string {
return "POST /api/v1/files/sync"
}
func (h *FileSyncHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *FileSyncHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Get user ID from context
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
if !ok {
h.logger.Error("Failed getting user ID from context")
httperror.RespondWithError(w, r, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error"))
return
}
// Parse query parameters
queryParams := r.URL.Query()
// Parse limit parameter (default: 5000, max: 10000)
limit := int64(5000)
if limitStr := queryParams.Get("limit"); limitStr != "" {
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil {
if parsedLimit > 0 && parsedLimit <= 10000 {
limit = parsedLimit
} else {
h.logger.Warn("Invalid limit parameter, using default",
zap.String("limit", limitStr),
zap.Int64("default", limit))
}
} else {
h.logger.Warn("Failed to parse limit parameter, using default",
zap.String("limit", limitStr),
zap.Error(err))
}
}
// Parse cursor parameter
var cursor *dom_file.FileSyncCursor
if cursorStr := queryParams.Get("cursor"); cursorStr != "" {
var parsedCursor dom_file.FileSyncCursor
if err := json.Unmarshal([]byte(cursorStr), &parsedCursor); err != nil {
h.logger.Error("Failed to parse cursor parameter",
zap.String("cursor", cursorStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("cursor", "Invalid cursor format"))
return
}
cursor = &parsedCursor
}
h.logger.Debug("Processing file sync request",
zap.Any("user_id", userID),
zap.Int64("limit", limit),
zap.Any("cursor", cursor))
// Call service to get sync data
response, err := h.fileSyncService.Execute(ctx, cursor, limit)
if err != nil {
h.logger.Error("Failed to get file sync data",
zap.Any("user_id", userID),
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
// Verify the response contains all fields including EncryptedFileSizeInBytes before encoding
h.logger.Debug("File sync response validation",
zap.Any("user_id", userID),
zap.Int("files_count", len(response.Files)))
for i, item := range response.Files {
h.logger.Debug("File sync response item",
zap.Int("index", i),
zap.String("file_id", item.ID.String()),
zap.String("collection_id", item.CollectionID.String()),
zap.Uint64("version", item.Version),
zap.Time("modified_at", item.ModifiedAt),
zap.String("state", item.State),
zap.Uint64("tombstone_version", item.TombstoneVersion),
zap.Time("tombstone_expiry", item.TombstoneExpiry),
zap.Int64("encrypted_file_size_in_bytes", item.EncryptedFileSizeInBytes))
}
// Encode and return response
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Error("Failed to encode file sync response",
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
h.logger.Info("Successfully served file sync data",
zap.Any("user_id", userID),
zap.Int("files_count", len(response.Files)),
zap.Bool("has_more", response.HasMore),
zap.Any("next_cursor", response.NextCursor))
}

View file

@ -0,0 +1,136 @@
package file
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
)
// Wire providers for file HTTP handlers
func ProvideCreatePendingFileHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.CreatePendingFileService,
mw middleware.Middleware,
) *CreatePendingFileHTTPHandler {
return NewCreatePendingFileHTTPHandler(cfg, logger, service, mw)
}
func ProvideGetPresignedUploadURLHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.GetPresignedUploadURLService,
mw middleware.Middleware,
) *GetPresignedUploadURLHTTPHandler {
return NewGetPresignedUploadURLHTTPHandler(cfg, logger, service, mw)
}
func ProvideCompleteFileUploadHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.CompleteFileUploadService,
mw middleware.Middleware,
) *CompleteFileUploadHTTPHandler {
return NewCompleteFileUploadHTTPHandler(cfg, logger, service, mw)
}
func ProvideGetFileHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.GetFileService,
mw middleware.Middleware,
) *GetFileHTTPHandler {
return NewGetFileHTTPHandler(cfg, logger, service, mw)
}
func ProvideGetPresignedDownloadURLHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.GetPresignedDownloadURLService,
mw middleware.Middleware,
) *GetPresignedDownloadURLHTTPHandler {
return NewGetPresignedDownloadURLHTTPHandler(cfg, logger, service, mw)
}
func ProvideListFilesByCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.ListFilesByCollectionService,
mw middleware.Middleware,
) *ListFilesByCollectionHTTPHandler {
return NewListFilesByCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideListRecentFilesHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.ListRecentFilesService,
mw middleware.Middleware,
) *ListRecentFilesHTTPHandler {
return NewListRecentFilesHTTPHandler(cfg, logger, service, mw)
}
func ProvideUpdateFileHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.UpdateFileService,
mw middleware.Middleware,
) *UpdateFileHTTPHandler {
return NewUpdateFileHTTPHandler(cfg, logger, service, mw)
}
func ProvideSoftDeleteFileHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.SoftDeleteFileService,
mw middleware.Middleware,
) *SoftDeleteFileHTTPHandler {
return NewSoftDeleteFileHTTPHandler(cfg, logger, service, mw)
}
func ProvideArchiveFileHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.ArchiveFileService,
mw middleware.Middleware,
) *ArchiveFileHTTPHandler {
return NewArchiveFileHTTPHandler(cfg, logger, service, mw)
}
func ProvideRestoreFileHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.RestoreFileService,
mw middleware.Middleware,
) *RestoreFileHTTPHandler {
return NewRestoreFileHTTPHandler(cfg, logger, service, mw)
}
func ProvideDeleteMultipleFilesHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.DeleteMultipleFilesService,
mw middleware.Middleware,
) *DeleteMultipleFilesHTTPHandler {
return NewDeleteMultipleFilesHTTPHandler(cfg, logger, service, mw)
}
func ProvideFileSyncHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_file.ListFileSyncDataService,
mw middleware.Middleware,
) *FileSyncHTTPHandler {
return NewFileSyncHTTPHandler(cfg, logger, service, mw)
}
func ProvideReportDownloadCompletedHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
mw middleware.Middleware,
) *ReportDownloadCompletedHTTPHandler {
return NewReportDownloadCompletedHTTPHandler(cfg, logger, mw)
}

View file

@ -0,0 +1,82 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/report_download_completed.go
package file
import (
"encoding/json"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ReportDownloadCompletedHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
middleware middleware.Middleware
}
func NewReportDownloadCompletedHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
middleware middleware.Middleware,
) *ReportDownloadCompletedHTTPHandler {
logger = logger.Named("ReportDownloadCompletedHTTPHandler")
return &ReportDownloadCompletedHTTPHandler{
config: config,
logger: logger,
middleware: middleware,
}
}
func (*ReportDownloadCompletedHTTPHandler) Pattern() string {
return "POST /api/v1/file/{id}/download-completed"
}
func (h *ReportDownloadCompletedHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ReportDownloadCompletedHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
// Extract file ID from the URL
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Validate UUID format
_, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
// Log the download completion (analytics/telemetry)
h.logger.Debug("download completed reported",
zap.String("file_id", fileIDStr))
// Return success response
response := map[string]interface{}{
"success": true,
"message": "Download completion recorded",
}
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Error("failed to encode response",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,97 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/restore.go
package file
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RestoreFileHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.RestoreFileService
middleware middleware.Middleware
}
func NewRestoreFileHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.RestoreFileService,
middleware middleware.Middleware,
) *RestoreFileHTTPHandler {
logger = logger.Named("RestoreFileHTTPHandler")
return &RestoreFileHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*RestoreFileHTTPHandler) Pattern() string {
return "PUT /api/v1/file/{id}/restore"
}
func (h *RestoreFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *RestoreFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from the URL
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
// Create request DTO
dtoReq := &svc_file.RestoreFileRequestDTO{
FileID: fileID,
}
resp, err := h.service.Execute(ctx, dtoReq)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.String("file_id", fileIDStr),
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,97 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/softdelete.go
package file
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type SoftDeleteFileHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.SoftDeleteFileService
middleware middleware.Middleware
}
func NewSoftDeleteFileHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.SoftDeleteFileService,
middleware middleware.Middleware,
) *SoftDeleteFileHTTPHandler {
logger = logger.Named("SoftDeleteFileHTTPHandler")
return &SoftDeleteFileHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*SoftDeleteFileHTTPHandler) Pattern() string {
return "DELETE /api/v1/file/{id}"
}
func (h *SoftDeleteFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *SoftDeleteFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from the URL
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
// Create request DTO
dtoReq := &svc_file.SoftDeleteFileRequestDTO{
FileID: fileID,
}
resp, err := h.service.Execute(ctx, dtoReq)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.String("file_id", fileIDStr),
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,107 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/delete_multiple.go
package file
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type DeleteMultipleFilesHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.DeleteMultipleFilesService
middleware middleware.Middleware
}
func NewDeleteMultipleFilesHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.DeleteMultipleFilesService,
middleware middleware.Middleware,
) *DeleteMultipleFilesHTTPHandler {
logger = logger.Named("DeleteMultipleFilesHTTPHandler")
return &DeleteMultipleFilesHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*DeleteMultipleFilesHTTPHandler) Pattern() string {
return "POST /api/v1/files/delete-multiple"
}
func (h *DeleteMultipleFilesHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *DeleteMultipleFilesHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
) (*svc_file.DeleteMultipleFilesRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_file.DeleteMultipleFilesRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
return &requestData, nil
}
func (h *DeleteMultipleFilesHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
req, err := h.unmarshalRequest(ctx, r)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,135 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/file/update.go
package file
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UpdateFileHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_file.UpdateFileService
middleware middleware.Middleware
}
func NewUpdateFileHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_file.UpdateFileService,
middleware middleware.Middleware,
) *UpdateFileHTTPHandler {
logger = logger.Named("UpdateFileHTTPHandler")
return &UpdateFileHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*UpdateFileHTTPHandler) Pattern() string {
return "PUT /api/v1/file/{id}"
}
func (h *UpdateFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *UpdateFileHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
fileID gocql.UUID,
) (*svc_file.UpdateFileRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_file.UpdateFileRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err))
// Log raw JSON at debug level only to avoid PII exposure in production logs
h.logger.Debug("raw request body for debugging",
zap.String("json", rawJSON.String()))
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Set the file ID from the URL parameter
requestData.ID = fileID
return &requestData, nil
}
func (h *UpdateFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract file ID from the URL path parameter
fileIDStr := r.PathValue("id")
if fileIDStr == "" {
h.logger.Warn("file_id not found in path parameters or is empty",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
)
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
return
}
// Convert string ID to ObjectID
fileID, err := gocql.ParseUUID(fileIDStr)
if err != nil {
h.logger.Error("invalid file ID format",
zap.String("file_id", fileIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, fileID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("transaction completed with no result")
h.logger.Error("transaction completed with no result", zap.Any("request_payload", req))
httperror.RespondWithError(w, r, err)
return
}
}