feat(gateway): 图片上传端点(B1) — POST /v1/upload + GET /v1/files/{name}
- upload.go: multipart上传(owner/admin+审计)→ {url,size,mime,name}; MaxBytesReader 5MB + DetectContentType嗅探 + 白名单raster(拒SVG/HTML) + crypto/rand 128-bit随机名 + 0700目录
- 静态服务: 同端口 GET /v1/files/{name}, 能力URL, 路径穿越全形态拒绝(.. / %2f / %2e%2e), nosniff, Range
- errors: ErrPayloadTooLarge(413)/ErrUnsupportedMediaType(415)
- server.go 路由+Deps.DataDir; wire.go resolveDataDir 注入
- 8测试+真实二进制E2E, build/vet/test绿+交叉编译
- Docs/api-client v0.24 + api-reference §0.3 同步
This commit is contained in:
1 parent
3fc10fb86f
commit
7ecb43bd5d
7 files changed
+723
-3
No files matched your search
@@ -276,6 +276,11 @@ type Deps struct {
|
||||
// It backs the admin backup/restore endpoints. Optional; when empty those
|
||||
// endpoints answer honestly with a clear not-configured error.
|
||||
DatabasePath string
|
||||
// DataDir is the resolved data directory (the value resolveDataDir returns,
|
||||
// which may differ from cfg.DataDir when the OS default applies). It backs
|
||||
// the image-upload store (<DataDir>/uploads) and its static route. Optional;
|
||||
// when empty the upload handlers fall back to cfg.DataDir.
|
||||
DataDir string
|
||||
// Streaming marks the injected Loop as streaming-capable (its OnStream is
|
||||
// wired to publish ws.stream_chunk). When set the hub skips its own single
|
||||
// final stream_chunk emission, since deltas already stream through OnStream.
|
||||
@@ -316,6 +321,10 @@ type Server struct {
|
||||
streaming bool
|
||||
frontend fs.FS
|
||||
dbPath string
|
||||
// dataDir is the resolved data directory backing the upload store
|
||||
// (<dataDir>/uploads). Injected by the composition root; cfg.DataDir is the
|
||||
// fallback. See Deps.DataDir.
|
||||
dataDir string
|
||||
// llm is the hot-swappable provider stack (models.json). When nil the
|
||||
// admin /llm endpoints fall back to the env config. It is optional so tests
|
||||
// that do not wire it keep working.
|
||||
@@ -364,6 +373,7 @@ func New(cfg *config.Config, deps Deps) (*Server, error) {
|
||||
ownerRegistrar: deps.OwnerRegistrar,
|
||||
webhooks: deps.Webhooks,
|
||||
dbPath: deps.DatabasePath,
|
||||
dataDir: deps.DataDir,
|
||||
log: sourcedLog(deps.Log),
|
||||
}
|
||||
s.hub = newHub(s)
|
||||
@@ -395,6 +405,14 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("POST /api/v1/audio/speech", s.auth.Authorize(auth.RoleOwner)(http.HandlerFunc(s.handleSpeech)))
|
||||
mux.Handle("POST /api/v1/audio/voice-clone", s.auth.Authorize(auth.RoleOwner)(http.HandlerFunc(s.handleVoiceClone)))
|
||||
|
||||
// Image upload (B1): a multipart POST that stores a raster image under the
|
||||
// resolved data dir and returns a same-origin URL usable directly as
|
||||
// chat.image. Upload is owner/admin + audited; the static route below is a
|
||||
// capability URL (unguessable name) because chat.image / <img> cannot carry a
|
||||
// bearer token. Both ride the one gateway port (single-binary red line).
|
||||
mux.Handle("POST /api/v1/upload", s.auth.Authorize(auth.RoleOwner, auth.RoleAdmin)(http.HandlerFunc(s.handleUpload)))
|
||||
mux.HandleFunc("GET /api/v1/files/{name}", s.handleUploadFile)
|
||||
|
||||
// Session/model reads and writes are the owner's private history; owner/admin only
|
||||
// (chat uses the same roles). Session metadata in GET /sessions stays readAuth for
|
||||
// any authenticated role, but the message content (and thinking) is owner/admin-only
|
||||
@@ -480,6 +498,7 @@ func (s *Server) Handler() http.Handler {
|
||||
}
|
||||
|
||||
var h http.Handler = mux
|
||||
h = guardUploadPaths(h)
|
||||
if s.frontend != nil {
|
||||
h = s.spa(h)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user