Files
codegirl.games-hugo/cms/internal/media/media.go
T
Cursor Agentandcodegirl007 ce15f54a52 Add lightweight self-hosted Hugo CMS with GitHub API backend
Implements a Go-based CMS for editing Hugo posts and uploading images
via the GitHub REST API, with no database and Git as the source of truth.

Features:
- Single admin auth with bcrypt password and signed session cookies
- Dashboard, post listing with search, create/edit with EasyMDE editor
- Media library for static/uploads/ with drag-drop and clipboard paste
- Client-side autosave, unsaved changes warning, dark mode, responsive UI
- Modular packages for auth, github, posts, media, session, and handlers

Co-authored-by: codegirl007 <s.raide@gmail.com>
2026-07-06 18:56:17 +00:00

114 lines
2.5 KiB
Go

package media
import (
"context"
"fmt"
"path"
"sort"
"strings"
"github.com/codegirl-007/hugo-cms/internal/github"
)
const uploadsDir = "static/uploads"
// Item represents an uploaded media file.
type Item struct {
Name string `json:"name"`
Path string `json:"path"`
URL string `json:"url"`
Size int `json:"size"`
}
// Service manages media uploads via GitHub.
type Service struct {
github github.Client
}
// NewService creates a media service.
func NewService(client github.Client) *Service {
return &Service{github: client}
}
// List returns all files in the uploads directory.
func (s *Service) List(ctx context.Context) ([]Item, error) {
items, err := s.github.ListDirectory(ctx, uploadsDir)
if err != nil {
// Directory may not exist yet.
if strings.Contains(err.Error(), "404") {
return []Item{}, nil
}
return nil, fmt.Errorf("list media: %w", err)
}
var media []Item
for _, item := range items {
if item.Type != "file" {
continue
}
media = append(media, Item{
Name: item.Name,
Path: item.Path,
URL: "/uploads/" + item.Name,
Size: item.Size,
})
}
sort.Slice(media, func(i, j int) bool {
return media[i].Name < media[j].Name
})
return media, nil
}
// Upload saves a binary file to static/uploads/ via the GitHub API.
func (s *Service) Upload(ctx context.Context, filename string, data []byte) (*Item, error) {
filename = sanitizeFilename(filename)
if filename == "" {
return nil, fmt.Errorf("invalid filename")
}
filePath := path.Join(uploadsDir, filename)
existingSHA := ""
if existing, err := s.github.GetFile(ctx, filePath); err == nil {
existingSHA = existing.SHA
}
message := fmt.Sprintf("Upload image: %s", filename)
if err := s.github.CreateOrUpdateFile(ctx, filePath, message, data, existingSHA); err != nil {
return nil, fmt.Errorf("upload media: %w", err)
}
return &Item{
Name: filename,
Path: filePath,
URL: "/uploads/" + filename,
Size: len(data),
}, nil
}
func sanitizeFilename(name string) string {
name = path.Base(strings.TrimSpace(name))
name = strings.ToLower(name)
var b strings.Builder
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
b.WriteRune(r)
}
}
return b.String()
}
// IsImage returns true if the filename has a common image extension.
func IsImage(filename string) bool {
lower := strings.ToLower(filename)
for _, ext := range []string{".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"} {
if strings.HasSuffix(lower, ext) {
return true
}
}
return false
}