Add R2 image uploads to admin
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

This commit is contained in:
yarnom 2026-06-04 14:31:45 +08:00
parent 9186801c7f
commit 49a0d078da
16 changed files with 809 additions and 14 deletions

View file

@ -22,6 +22,7 @@ type Server struct {
builder *Builder
deepSeek DeepSeekConfig
localLLM LocalLLMConfig
uploader *ImageUploader
slugProvider string
adminDir string
staticDir string
@ -46,12 +47,23 @@ func NewServerWithContext(ctx context.Context, db *pgxpool.Pool, cfg Config) *Se
builder.Start(ctx)
}
}
var uploader *ImageUploader
if cfg.R2.Endpoint != "" || cfg.R2.Bucket != "" || cfg.R2.AccessKeyID != "" || cfg.R2.SecretAccessKey != "" {
createdUploader, err := NewImageUploader(ctx, cfg.R2)
if err != nil {
log.Printf("R2 image uploader disabled: %v", err)
} else {
uploader = createdUploader
}
}
return &Server{
db: db,
store: store,
builder: builder,
deepSeek: cfg.DeepSeek,
localLLM: cfg.LocalLLM,
uploader: uploader,
slugProvider: cfg.SlugProvider,
adminDir: cfg.AdminDir,
staticDir: cfg.StaticDir,
@ -81,6 +93,7 @@ func (s *Server) Router() http.Handler {
protected.GET("/me", s.me)
protected.POST("/logout", s.logout)
protected.POST("/slug", s.generateSlug)
protected.POST("/images", s.uploadImage)
protected.GET("/audit-logs", s.listAuditLogs)
protected.GET("/posts", s.listPosts)
protected.POST("/posts", s.createPost)
@ -347,6 +360,39 @@ func (s *Server) generateSlug(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"slug": slug})
}
func (s *Server) uploadImage(c *gin.Context) {
if s.uploader == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "R2 image uploader is not configured"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
return
}
opened, err := file.Open()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
defer opened.Close()
uploaded, err := s.uploader.Upload(c.Request.Context(), file.Filename, opened, file.Size)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
s.auditCurrentUser(c, "image_upload", "image", uploaded.Key, gin.H{
"filename": uploaded.Filename,
"url": uploaded.URL,
"size": uploaded.Size,
"contentType": uploaded.ContentType,
})
c.JSON(http.StatusCreated, gin.H{"image": uploaded})
}
func (s *Server) listAuditLogs(c *gin.Context) {
if !s.requireStore(c) {
return