282 lines
6.4 KiB
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{}
|
|
)
|