~bigbes/confluence-md-utilities

confluence-md-utilities/api/content.go -rw-r--r-- 4.6 KiB
e0e81bc6 — Eugene Blikh chore: rename module to go.bigb.es/confluence-md-utilities a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package api

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

// ContentResponse represents a Confluence page from the REST API.
type ContentResponse struct {
	ID      string  `json:"id"`
	Type    string  `json:"type"`
	Title   string  `json:"title"`
	Version Version `json:"version"`
	Body    Body    `json:"body"`
}

// Version holds page version info.
type Version struct {
	Number  int    `json:"number"`
	Message string `json:"message,omitempty"`
}

// Body holds page body content.
type Body struct {
	Storage StorageBody `json:"storage"`
}

// StorageBody holds the storage format representation.
type StorageBody struct {
	Value          string `json:"value"`
	Representation string `json:"representation"`
}

// updateRequest is the JSON payload for updating a page.
type updateRequest struct {
	Version Version `json:"version"`
	Title   string  `json:"title"`
	Type    string  `json:"type"`
	Body    Body    `json:"body"`
}

// searchResults wraps the /rest/api/content search response.
type searchResults struct {
	Results []ContentResponse `json:"results"`
	Size    int               `json:"size"`
}

// GetContent fetches a page by ID with storage body and version info.
func (c *Client) GetContent(pageID string) (*ContentResponse, error) {
	path := fmt.Sprintf("/rest/api/content/%s?expand=body.storage,version", pageID)
	req, err := c.newRequest(http.MethodGet, path)
	if err != nil {
		return nil, err
	}

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("fetching page %s: %w", pageID, err)
	}
	defer resp.Body.Close()

	if err := checkResponse(resp); err != nil {
		return nil, err
	}

	var content ContentResponse
	if err := json.NewDecoder(resp.Body).Decode(&content); err != nil {
		return nil, fmt.Errorf("decoding response: %w", err)
	}
	return &content, nil
}

// FindContent searches for a page by space key and title.
func (c *Client) FindContent(spaceKey, title string) (*ContentResponse, error) {
	params := url.Values{
		"spaceKey": {spaceKey},
		"title":    {title},
		"expand":   {"body.storage,version"},
	}
	path := "/rest/api/content?" + params.Encode()
	req, err := c.newRequest(http.MethodGet, path)
	if err != nil {
		return nil, err
	}

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("searching for page %q in space %q: %w", title, spaceKey, err)
	}
	defer resp.Body.Close()

	if err := checkResponse(resp); err != nil {
		return nil, err
	}

	var results searchResults
	if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
		return nil, fmt.Errorf("decoding search results: %w", err)
	}

	if results.Size == 0 {
		return nil, fmt.Errorf("page %q not found in space %q", title, spaceKey)
	}

	return &results.Results[0], nil
}

// GetPage resolves a PageRef to a full ContentResponse.
func (c *Client) GetPage(ref *PageRef) (*ContentResponse, error) {
	if ref.PageID != "" {
		return c.GetContent(ref.PageID)
	}
	return c.FindContent(ref.SpaceKey, ref.Title)
}

// UpdateContent updates a page's storage body, incrementing the version.
func (c *Client) UpdateContent(pageID string, current *ContentResponse, newBody string, message string) error {
	payload := updateRequest{
		Version: Version{
			Number:  current.Version.Number + 1,
			Message: message,
		},
		Title: current.Title,
		Type:  current.Type,
		Body: Body{
			Storage: StorageBody{
				Value:          newBody,
				Representation: "storage",
			},
		},
	}

	data, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshaling update: %w", err)
	}

	path := fmt.Sprintf("/rest/api/content/%s", pageID)
	req, err := c.newRequest(http.MethodPut, path)
	if err != nil {
		return err
	}
	req.Body = io.NopCloser(bytes.NewReader(data))
	req.ContentLength = int64(len(data))

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return fmt.Errorf("updating page %s: %w", pageID, err)
	}
	defer resp.Body.Close()

	return checkResponse(resp)
}

// checkResponse returns an error for non-2xx status codes.
func checkResponse(resp *http.Response) error {
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return nil
	}

	body, _ := io.ReadAll(resp.Body)

	switch resp.StatusCode {
	case http.StatusUnauthorized:
		return fmt.Errorf("authentication failed (401): invalid or expired token")
	case http.StatusForbidden:
		return fmt.Errorf("access denied (403): insufficient permissions")
	case http.StatusNotFound:
		return fmt.Errorf("page not found (404)")
	case http.StatusConflict:
		return fmt.Errorf("version conflict (409): page was modified since last fetch, re-pull and try again")
	default:
		return fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(body))
	}
}