content.go

v0.1.0
Doc Versions Source
1
package api
2
3
import (
4
	"bytes"
5
	"encoding/json"
6
	"fmt"
7
	"io"
8
	"net/http"
9
	"net/url"
10
)
11
12
// ContentResponse represents a Confluence page from the REST API.
13
type ContentResponse struct {
14
	ID      string  `json:"id"`
15
	Type    string  `json:"type"`
16
	Title   string  `json:"title"`
17
	Version Version `json:"version"`
18
	Body    Body    `json:"body"`
19
}
20
21
// Version holds page version info.
22
type Version struct {
23
	Number  int    `json:"number"`
24
	Message string `json:"message,omitempty"`
25
}
26
27
// Body holds page body content.
28
type Body struct {
29
	Storage StorageBody `json:"storage"`
30
}
31
32
// StorageBody holds the storage format representation.
33
type StorageBody struct {
34
	Value          string `json:"value"`
35
	Representation string `json:"representation"`
36
}
37
38
// updateRequest is the JSON payload for updating a page.
39
type updateRequest struct {
40
	Version Version `json:"version"`
41
	Title   string  `json:"title"`
42
	Type    string  `json:"type"`
43
	Body    Body    `json:"body"`
44
}
45
46
// searchResults wraps the /rest/api/content search response.
47
type searchResults struct {
48
	Results []ContentResponse `json:"results"`
49
	Size    int               `json:"size"`
50
}
51
52
// GetContent fetches a page by ID with storage body and version info.
53
func (c *Client) GetContent(pageID string) (*ContentResponse, error) {
54
	path := fmt.Sprintf("/rest/api/content/%s?expand=body.storage,version", pageID)
55
	req, err := c.newRequest(http.MethodGet, path)
56
	if err != nil {
57
		return nil, err
58
	}
59
60
	resp, err := c.HTTPClient.Do(req)
61
	if err != nil {
62
		return nil, fmt.Errorf("fetching page %s: %w", pageID, err)
63
	}
64
	defer resp.Body.Close()
65
66
	if err := checkResponse(resp); err != nil {
67
		return nil, err
68
	}
69
70
	var content ContentResponse
71
	if err := json.NewDecoder(resp.Body).Decode(&content); err != nil {
72
		return nil, fmt.Errorf("decoding response: %w", err)
73
	}
74
	return &content, nil
75
}
76
77
// FindContent searches for a page by space key and title.
78
func (c *Client) FindContent(spaceKey, title string) (*ContentResponse, error) {
79
	params := url.Values{
80
		"spaceKey": {spaceKey},
81
		"title":    {title},
82
		"expand":   {"body.storage,version"},
83
	}
84
	path := "/rest/api/content?" + params.Encode()
85
	req, err := c.newRequest(http.MethodGet, path)
86
	if err != nil {
87
		return nil, err
88
	}
89
90
	resp, err := c.HTTPClient.Do(req)
91
	if err != nil {
92
		return nil, fmt.Errorf("searching for page %q in space %q: %w", title, spaceKey, err)
93
	}
94
	defer resp.Body.Close()
95
96
	if err := checkResponse(resp); err != nil {
97
		return nil, err
98
	}
99
100
	var results searchResults
101
	if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
102
		return nil, fmt.Errorf("decoding search results: %w", err)
103
	}
104
105
	if results.Size == 0 {
106
		return nil, fmt.Errorf("page %q not found in space %q", title, spaceKey)
107
	}
108
109
	return &results.Results[0], nil
110
}
111
112
// GetPage resolves a PageRef to a full ContentResponse.
113
func (c *Client) GetPage(ref *PageRef) (*ContentResponse, error) {
114
	if ref.PageID != "" {
115
		return c.GetContent(ref.PageID)
116
	}
117
	return c.FindContent(ref.SpaceKey, ref.Title)
118
}
119
120
// UpdateContent updates a page's storage body, incrementing the version.
121
func (c *Client) UpdateContent(pageID string, current *ContentResponse, newBody string, message string) error {
122
	payload := updateRequest{
123
		Version: Version{
124
			Number:  current.Version.Number + 1,
125
			Message: message,
126
		},
127
		Title: current.Title,
128
		Type:  current.Type,
129
		Body: Body{
130
			Storage: StorageBody{
131
				Value:          newBody,
132
				Representation: "storage",
133
			},
134
		},
135
	}
136
137
	data, err := json.Marshal(payload)
138
	if err != nil {
139
		return fmt.Errorf("marshaling update: %w", err)
140
	}
141
142
	path := fmt.Sprintf("/rest/api/content/%s", pageID)
143
	req, err := c.newRequest(http.MethodPut, path)
144
	if err != nil {
145
		return err
146
	}
147
	req.Body = io.NopCloser(bytes.NewReader(data))
148
	req.ContentLength = int64(len(data))
149
150
	resp, err := c.HTTPClient.Do(req)
151
	if err != nil {
152
		return fmt.Errorf("updating page %s: %w", pageID, err)
153
	}
154
	defer resp.Body.Close()
155
156
	return checkResponse(resp)
157
}
158
159
// checkResponse returns an error for non-2xx status codes.
160
func checkResponse(resp *http.Response) error {
161
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
162
		return nil
163
	}
164
165
	body, _ := io.ReadAll(resp.Body)
166
167
	switch resp.StatusCode {
168
	case http.StatusUnauthorized:
169
		return fmt.Errorf("authentication failed (401): invalid or expired token")
170
	case http.StatusForbidden:
171
		return fmt.Errorf("access denied (403): insufficient permissions")
172
	case http.StatusNotFound:
173
		return fmt.Errorf("page not found (404)")
174
	case http.StatusConflict:
175
		return fmt.Errorf("version conflict (409): page was modified since last fetch, re-pull and try again")
176
	default:
177
		return fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(body))
178
	}
179
}
180

Source Files