edge/pkg/http/blob.go

282 lines
6.4 KiB
Go

package http
import (
"encoding/json"
"io"
"io/fs"
"mime/multipart"
"net/http"
"os"
"time"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const (
errorCodeForbidden = "forbidden"
errorCodeInternalError = "internal-error"
errorCodeBadRequest = "bad-request"
errorCodeNotFound = "not-found"
)
type uploadResponse struct {
Bucket string `json:"bucket"`
BlobID storage.BlobID `json:"blobId"`
}
func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
h.mutex.RLock()
defer h.mutex.RUnlock()
ctx := r.Context()
r.Body = http.MaxBytesReader(w, r.Body, h.uploadMaxFileSize)
if err := r.ParseMultipartForm(h.uploadMaxFileSize); err != nil {
logger.Error(ctx, "could not parse multipart form", logger.E(errors.WithStack(err)))
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
return
}
_, fileHeader, err := r.FormFile("file")
if err != nil {
logger.Error(ctx, "could not read form file", logger.E(errors.WithStack(err)))
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
return
}
var metadata map[string]any
rawMetadata := r.Form.Get("metadata")
if rawMetadata != "" {
if err := json.Unmarshal([]byte(rawMetadata), &metadata); err != nil {
logger.Error(ctx, "could not parse metadata", logger.E(errors.WithStack(err)))
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
return
}
}
ctx = module.WithContext(ctx, map[module.ContextKey]any{
ContextKeyOriginRequest: r,
})
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata)
reply, err := h.bus.Request(ctx, requestMsg)
if err != nil {
logger.Error(ctx, "could not retrieve file", logger.E(errors.WithStack(err)))
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
logger.Debug(ctx, "upload reply", logger.F("reply", reply))
responseMsg, ok := reply.(*module.MessageUploadResponse)
if !ok {
logger.Error(
ctx, "unexpected upload response message",
logger.F("message", reply),
)
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
if !responseMsg.Allow {
jsonError(w, http.StatusForbidden, errorCodeForbidden)
return
}
encoder := json.NewEncoder(w)
res := &uploadResponse{
Bucket: responseMsg.Bucket,
BlobID: responseMsg.BlobID,
}
if err := encoder.Encode(res); err != nil {
panic(errors.Wrap(err, "could not encode upload response"))
}
}
func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
h.mutex.RLock()
defer h.mutex.RUnlock()
bucket := chi.URLParam(r, "bucket")
blobID := chi.URLParam(r, "blobID")
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
ctx = module.WithContext(ctx, map[module.ContextKey]any{
ContextKeyOriginRequest: r,
})
requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
reply, err := h.bus.Request(ctx, requestMsg)
if err != nil {
logger.Error(ctx, "could not retrieve file", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
replyMsg, ok := reply.(*module.MessageDownloadResponse)
if !ok {
logger.Error(
ctx, "unexpected download response message",
logger.E(errors.WithStack(bus.ErrUnexpectedMessage)),
logger.F("message", reply),
)
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
if !replyMsg.Allow {
jsonError(w, http.StatusForbidden, errorCodeForbidden)
return
}
if replyMsg.Blob == nil {
jsonError(w, http.StatusNotFound, errorCodeNotFound)
return
}
defer func() {
if err := replyMsg.Blob.Close(); err != nil {
logger.Error(ctx, "could not close blob", logger.E(errors.WithStack(err)))
}
}()
http.ServeContent(w, r, string(replyMsg.BlobInfo.ID()), replyMsg.BlobInfo.ModTime(), replyMsg.Blob)
}
func serveFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) {
ctx := logger.With(r.Context(), logger.F("path", path))
file, err := fs.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
logger.Error(ctx, "error while opening fs file", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() {
if err := file.Close(); err != nil {
logger.Error(ctx, "error while closing fs file", logger.E(errors.WithStack(err)))
}
}()
info, err := file.Stat()
if err != nil {
logger.Error(ctx, "error while retrieving fs file stat", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
reader, ok := file.(io.ReadSeeker)
if !ok {
return
}
http.ServeContent(w, r, path, info.ModTime(), reader)
}
type jsonErrorResponse struct {
Error jsonErr `json:"error"`
}
type jsonErr struct {
Code string `json:"code"`
}
func jsonError(w http.ResponseWriter, status int, code string) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(status)
encoder := json.NewEncoder(w)
response := jsonErrorResponse{
Error: jsonErr{
Code: code,
},
}
if err := encoder.Encode(response); err != nil {
panic(errors.WithStack(err))
}
}
type uploadedFile struct {
multipart.File
header *multipart.FileHeader
modTime time.Time
}
// Stat implements fs.File
func (f *uploadedFile) Stat() (fs.FileInfo, error) {
return &uploadedFileInfo{
header: f.header,
modTime: f.modTime,
}, nil
}
type uploadedFileInfo struct {
header *multipart.FileHeader
modTime time.Time
}
// IsDir implements fs.FileInfo
func (i *uploadedFileInfo) IsDir() bool {
return false
}
// ModTime implements fs.FileInfo
func (i *uploadedFileInfo) ModTime() time.Time {
return i.modTime
}
// Mode implements fs.FileInfo
func (i *uploadedFileInfo) Mode() fs.FileMode {
return os.ModePerm
}
// Name implements fs.FileInfo
func (i *uploadedFileInfo) Name() string {
return i.header.Filename
}
// Size implements fs.FileInfo
func (i *uploadedFileInfo) Size() int64 {
return i.header.Size
}
// Sys implements fs.FileInfo
func (i *uploadedFileInfo) Sys() any {
return nil
}
var (
_ fs.File = &uploadedFile{}
_ fs.FileInfo = &uploadedFileInfo{}
)