218 lines
4.7 KiB
Go
218 lines
4.7 KiB
Go
package gemlog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-kivik/couchdb/v3" // CouchDB driver
|
|
"github.com/go-kivik/kivik/v3"
|
|
)
|
|
|
|
// Post represents a blog post document in CouchDB
|
|
type Post struct {
|
|
ID string `json:"_id,omitempty"`
|
|
Rev string `json:"_rev,omitempty"`
|
|
Title string `json:"title"`
|
|
Date time.Time `json:"date"`
|
|
Slug string `json:"slug"`
|
|
Tags []string `json:"tags"`
|
|
Content string `json:"content"`
|
|
Created time.Time `json:"created"`
|
|
Modified time.Time `json:"modified"`
|
|
}
|
|
|
|
// CouchDBClient wraps the CouchDB connection and operations
|
|
type CouchDBClient struct {
|
|
client *kivik.Client
|
|
db *kivik.DB
|
|
}
|
|
|
|
// NewCouchDBClient creates a new CouchDB client connection
|
|
func NewCouchDBClient(url, dbName string) (*CouchDBClient, error) {
|
|
client, err := kivik.New("couch", url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create CouchDB client: %w", err)
|
|
}
|
|
|
|
// Check if database exists, create if it doesn't
|
|
exists, err := client.DBExists(context.TODO(), dbName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check if database exists: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
err = client.CreateDB(context.TODO(), dbName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create database: %w", err)
|
|
}
|
|
}
|
|
|
|
db := client.DB(context.TODO(), dbName)
|
|
|
|
return &CouchDBClient{
|
|
client: client,
|
|
db: db,
|
|
}, nil
|
|
}
|
|
|
|
// SavePost saves a post to CouchDB
|
|
func (c *CouchDBClient) SavePost(post *Post) error {
|
|
ctx := context.TODO()
|
|
|
|
// Set timestamps
|
|
now := time.Now()
|
|
if post.Created.IsZero() {
|
|
post.Created = now
|
|
}
|
|
post.Modified = now
|
|
|
|
// Generate ID if not set (use slug-timestamp)
|
|
if post.ID == "" {
|
|
post.ID = fmt.Sprintf("%s-%d", post.Slug, now.Unix())
|
|
}
|
|
|
|
// Save the document
|
|
rev, err := c.db.Put(ctx, post.ID, post)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save post: %w", err)
|
|
}
|
|
|
|
// Update the revision
|
|
post.Rev = rev
|
|
return nil
|
|
}
|
|
|
|
// GetPost retrieves a post by ID
|
|
func (c *CouchDBClient) GetPost(id string) (*Post, error) {
|
|
ctx := context.TODO()
|
|
|
|
row := c.db.Get(ctx, id)
|
|
if row.Err != nil {
|
|
return nil, fmt.Errorf("failed to get post: %w", row.Err)
|
|
}
|
|
|
|
var post Post
|
|
if err := row.ScanDoc(&post); err != nil {
|
|
return nil, fmt.Errorf("failed to scan post document: %w", err)
|
|
}
|
|
|
|
return &post, nil
|
|
}
|
|
|
|
// GetAllPosts retrieves all posts from the database
|
|
func (c *CouchDBClient) GetAllPosts() ([]*Post, error) {
|
|
ctx := context.TODO()
|
|
|
|
rows, err := c.db.AllDocs(ctx, kivik.Options{
|
|
"include_docs": true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get all posts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var posts []*Post
|
|
for rows.Next() {
|
|
var post Post
|
|
if err := rows.ScanDoc(&post); err != nil {
|
|
continue // Skip invalid documents
|
|
}
|
|
posts = append(posts, &post)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating posts: %w", err)
|
|
}
|
|
|
|
return posts, nil
|
|
}
|
|
|
|
// UpdatePost updates an existing post
|
|
func (c *CouchDBClient) UpdatePost(post *Post) error {
|
|
if post.ID == "" || post.Rev == "" {
|
|
return fmt.Errorf("post ID and revision are required for updates")
|
|
}
|
|
|
|
post.Modified = time.Now()
|
|
return c.SavePost(post)
|
|
}
|
|
|
|
// DeletePost deletes a post by ID and revision
|
|
func (c *CouchDBClient) DeletePost(id, rev string) error {
|
|
ctx := context.TODO()
|
|
|
|
_, err := c.db.Delete(ctx, id, rev)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete post: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseGeminiPost parses a gemini post content and returns a Post struct
|
|
func ParseGeminiPost(content string) (*Post, error) {
|
|
lines := strings.Split(content, "\n")
|
|
|
|
var post Post
|
|
var contentStart int
|
|
inFrontmatter := false
|
|
|
|
for i, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Check for frontmatter separator
|
|
if line == "---" {
|
|
if !inFrontmatter {
|
|
inFrontmatter = true
|
|
continue
|
|
} else {
|
|
// End of frontmatter
|
|
contentStart = i + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
if !inFrontmatter {
|
|
continue
|
|
}
|
|
|
|
// Parse frontmatter
|
|
if strings.Contains(line, ":") {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
switch key {
|
|
case "title":
|
|
post.Title = value
|
|
case "date":
|
|
if date, err := time.Parse("2006-01-02", value); err == nil {
|
|
post.Date = date
|
|
}
|
|
case "slug":
|
|
post.Slug = value
|
|
case "tags":
|
|
// Parse comma-separated tags
|
|
if value != "" {
|
|
tagList := strings.Split(value, ",")
|
|
for _, tag := range tagList {
|
|
post.Tags = append(post.Tags, strings.TrimSpace(tag))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract content after frontmatter
|
|
if contentStart < len(lines) {
|
|
post.Content = strings.Join(lines[contentStart:], "\n")
|
|
}
|
|
|
|
return &post, nil
|
|
}
|