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/module/blob" "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.CapturedE(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.CapturedE(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.CapturedE(errors.WithStack(err))) jsonError(w, http.StatusBadRequest, errorCodeBadRequest) return } } ctx = module.WithContext(ctx, map[module.ContextKey]any{ ContextKeyOriginRequest: r, }) requestMsg := blob.NewMessageUploadRequest(ctx, fileHeader, metadata) reply, err := h.bus.Request(ctx, requestMsg) if err != nil { logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err))) jsonError(w, http.StatusInternalServerError, errorCodeInternalError) return } logger.Debug(ctx, "upload reply", logger.F("reply", reply)) responseMsg, ok := reply.(*blob.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 := blob.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID)) reply, err := h.bus.Request(ctx, requestMsg) if err != nil { logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } replyMsg, ok := reply.(*blob.MessageDownloadResponse) if !ok { logger.Error( ctx, "unexpected download response message", logger.CapturedE(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.CapturedE(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.CapturedE(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.CapturedE(errors.WithStack(err))) } }() info, err := file.Stat() if err != nil { logger.Error(ctx, "error while retrieving fs file stat", logger.CapturedE(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{} )