Files
opencloud/services/groupware/pkg/groupware/api_blob.go
Pascal Bleser 7d4bce2307 groupware: add tracking of backend call durations
* add new configuration setting GROUPWARE_SEND_DURATIONS_RESPONSE
   (defaults to false)

 * keep track of lists of durations of backend calls

 * when enabled, report them as response headers Durations (human
   readable) and Durations-Nanos (as raw nanosecond values for machine
   consumption)
2026-06-16 16:51:37 +02:00

130 lines
3.6 KiB
Go

package groupware
import (
"io"
"net/http"
"strconv"
"time"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
const (
DefaultBlobDownloadType = "application/octet-stream"
)
func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
get(Blob, w, r, g, g.jmap.GetBlobMetadata)
}
func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
contentType := r.Header.Get("Content-Type")
body := r.Body
if body != nil {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
req.logger.Error().Err(err).Msg("failed to close response body")
}
}(body)
}
accountId, err := req.GetAccountIdForBlob()
if err != nil {
return req.errorV(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)))
ctx := req.ctx.WithLogger(logger)
before := time.Now()
resp, _, jerr := g.jmap.UploadBlobStream(accountId, contentType, body, ctx)
duration := time.Since(before)
if jerr != nil {
return req.jmapError(accountId, jerr, req.session)
}
return req.respondWithoutStatus(accountId, resp, single(duration))
})
}
// Download a BLOB by its identifier.
func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
g.stream(w, r, func(req Request, w http.ResponseWriter) (bool, Response) {
ok, accountId, resp := req.needBloblWithAccount()
if !ok {
return false, resp
}
blobId, err := req.PathParam(UriParamBlobId) // the unique identifier of the blob to download
if err != nil {
return false, req.errorV(accountId, err)
}
name, err := req.PathParam(UriParamBlobName) // the filename of the blob to download, which is then used in the response and may be arbitrary if unknown
if err != nil {
return false, req.errorV(accountId, err)
}
typ, _ := req.getStringParam(QueryParamBlobType, "") // optionally, the Content-Type of the blob, which is then used in the response
logger := log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)).Str(UriParamBlobId, blobId))
ctx := req.ctx.WithLogger(logger)
before := time.Now()
if err := req.serveBlob(blobId, name, typ, ctx, accountId, w); err != nil {
return false, req.error(accountId, err, single(time.Since(before)))
} else {
return true, req.noop(accountId, single(time.Since(before)))
}
})
}
func (r *Request) serveBlob(blobId string, name string, typ string, ctx jmap.Context, accountId jmap.AccountId, w http.ResponseWriter) *Error { //NOSONAR
if typ == "" {
typ = DefaultBlobDownloadType
}
blob, lang, jerr := r.g.jmap.DownloadBlobStream(accountId, blobId, name, typ, ctx)
if blob != nil && blob.Body != nil {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
ctx.Logger.Error().Err(err).Msg("failed to close response body")
}
}(blob.Body)
}
if jerr != nil {
switch e := jerr.(type) {
case jmap.Error:
return r.apiErrorFromJmap(e)
default:
return apiError(r.errorId(), ErrorGeneric, withDetail(e.Error()))
}
}
if blob == nil {
w.WriteHeader(http.StatusNotFound)
return nil
}
if blob.Type != "" {
w.Header().Add("Content-Type", blob.Type)
}
if blob.CacheControl != "" {
w.Header().Add("Cache-Control", blob.CacheControl)
}
if blob.ContentDisposition != "" {
w.Header().Add("Content-Disposition", blob.ContentDisposition)
}
if blob.Size >= 0 {
w.Header().Add("Content-Size", strconv.Itoa(blob.Size))
}
if lang != "" {
w.Header().Add("Content-Language", string(lang))
}
_, err := io.Copy(w, blob.Body)
if err != nil {
return r.observedParameterError(ErrorStreamingResponse)
}
return nil
}