EXPERIMENTAL: this endpoint is experimental and is subject to change. @Summary Upload chat file @ID upload-chat-file @Security CoderSessionToken @Tags Chats @Accept image/png,image/jpeg,image/gif,image/webp,text/plain,text/markdown,text/csv,application/json,application/pdf @Produce json @Param orga
(rw http.ResponseWriter, r *http.Request)
| 6021 | // @Router /api/experimental/chats/files [post] |
| 6022 | // @Description Experimental: this endpoint is subject to change. |
| 6023 | func (api *API) postChatFile(rw http.ResponseWriter, r *http.Request) { |
| 6024 | ctx := r.Context() |
| 6025 | apiKey := httpmw.APIKey(r) |
| 6026 | |
| 6027 | orgIDStr := r.URL.Query().Get("organization") |
| 6028 | if orgIDStr == "" { |
| 6029 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ |
| 6030 | Message: "Missing organization query parameter.", |
| 6031 | }) |
| 6032 | return |
| 6033 | } |
| 6034 | orgID, err := uuid.Parse(orgIDStr) |
| 6035 | if err != nil { |
| 6036 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ |
| 6037 | Message: "Invalid organization ID.", |
| 6038 | }) |
| 6039 | return |
| 6040 | } |
| 6041 | // NOTE: This authorize check is intentionally placed after query |
| 6042 | // parameter parsing because we need orgID to scope the RBAC check |
| 6043 | // to the correct org. |
| 6044 | if !api.Authorize(r, policy.ActionCreate, rbac.ResourceChat.WithOwner(apiKey.UserID.String()).InOrg(orgID)) { |
| 6045 | httpapi.Forbidden(rw) |
| 6046 | return |
| 6047 | } |
| 6048 | |
| 6049 | contentType := r.Header.Get("Content-Type") |
| 6050 | if contentType == "" { |
| 6051 | contentType = "application/octet-stream" |
| 6052 | } |
| 6053 | // Strip parameters (e.g. "image/png; charset=utf-8" → "image/png") |
| 6054 | // so the allowlist check matches the base media type. |
| 6055 | if mediaType, _, err := mime.ParseMediaType(contentType); err == nil { |
| 6056 | contentType = mediaType |
| 6057 | } |
| 6058 | // application/octet-stream means the client could not classify the file |
| 6059 | // ahead of time, so we defer to byte classification below. |
| 6060 | if contentType != "application/octet-stream" && !chatfiles.IsAllowedStoredMediaType(contentType) { |
| 6061 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ |
| 6062 | Message: "Unsupported file type.", |
| 6063 | Detail: fmt.Sprintf("Allowed types: %s.", chatfiles.AllowedStoredMediaTypesString()), |
| 6064 | }) |
| 6065 | return |
| 6066 | } |
| 6067 | |
| 6068 | // Extract filename from Content-Disposition header if provided. |
| 6069 | var filename string |
| 6070 | if cd := r.Header.Get("Content-Disposition"); cd != "" { |
| 6071 | if _, params, err := mime.ParseMediaType(cd); err == nil { |
| 6072 | filename = params["filename"] |
| 6073 | } |
| 6074 | } |
| 6075 | |
| 6076 | r.Body = http.MaxBytesReader(rw, r.Body, codersdk.MaxChatFileSizeBytes) |
| 6077 | data, err := io.ReadAll(r.Body) |
| 6078 | if err != nil { |
| 6079 | var maxBytesErr *http.MaxBytesError |
| 6080 | if errors.As(err, &maxBytesErr) { |
nothing calls this directly
no test coverage detected