mirror of
https://github.com/kovetskiy/mark.git
synced 2026-05-02 21:32:34 +00:00
separate mark into packages
This commit is contained in:
294
pkg/confluence/api.go
Normal file
294
pkg/confluence/api.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package confluence
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/bndr/gopencils"
|
||||
)
|
||||
|
||||
type RestrictionOperation string
|
||||
|
||||
const (
|
||||
RestrictionEdit RestrictionOperation = `Edit`
|
||||
RestrictionView = `View`
|
||||
)
|
||||
|
||||
type Restriction struct {
|
||||
User string `json:"userName"`
|
||||
Group string `json:"groupName",omitempty`
|
||||
}
|
||||
|
||||
type API struct {
|
||||
rest *gopencils.Resource
|
||||
|
||||
// it's deprecated accordingly to Atlassian documentation,
|
||||
// but it's only way to set permissions
|
||||
json *gopencils.Resource
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
|
||||
Version struct {
|
||||
Number int64 `json:"number"`
|
||||
} `json:"version"`
|
||||
|
||||
Ancestors []struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
} `json:"ancestors"`
|
||||
|
||||
Links struct {
|
||||
Full string `json:"webui"`
|
||||
} `json:"_links"`
|
||||
}
|
||||
|
||||
func NewAPI(baseURL string, username string, password string) *API {
|
||||
auth := &gopencils.BasicAuth{username, password}
|
||||
|
||||
return &API{
|
||||
rest: gopencils.Api(baseURL+"/rest/api", auth),
|
||||
|
||||
json: gopencils.Api(
|
||||
baseURL+"/rpc/json-rpc/confluenceservice-v2",
|
||||
auth,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) FindRootPage(space string) (*PageInfo, error) {
|
||||
page, err := api.FindPage(space, ``)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
`can't obtain first page from space '%s': %s`,
|
||||
space,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(page.Ancestors) == 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"page '%s' from space '%s' has no parents",
|
||||
page.Title,
|
||||
space,
|
||||
)
|
||||
}
|
||||
|
||||
return &PageInfo{
|
||||
ID: page.Ancestors[0].Id,
|
||||
Title: page.Ancestors[0].Title,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *API) FindPage(space string, title string) (*PageInfo, error) {
|
||||
result := struct {
|
||||
Results []PageInfo `json:"results"`
|
||||
}{}
|
||||
|
||||
payload := map[string]string{
|
||||
"spaceKey": space,
|
||||
"expand": "ancestors,version",
|
||||
}
|
||||
|
||||
if title != "" {
|
||||
payload["title"] = title
|
||||
}
|
||||
|
||||
request, err := api.rest.Res(
|
||||
"content/", &result,
|
||||
).Get(payload)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode == 401 {
|
||||
return nil, fmt.Errorf("authentification failed")
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode != 200 {
|
||||
return nil, fmt.Errorf(
|
||||
"Confluence REST API returns unexpected non-200 HTTP status: %s",
|
||||
request.Raw.Status,
|
||||
)
|
||||
}
|
||||
|
||||
if len(result.Results) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &result.Results[0], nil
|
||||
}
|
||||
|
||||
func (api *API) GetPageByID(pageID string) (*PageInfo, error) {
|
||||
request, err := api.rest.Res(
|
||||
"content/"+pageID, &PageInfo{},
|
||||
).Get(map[string]string{"expand": "ancestors,version"})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode == 401 {
|
||||
return nil, fmt.Errorf("authentification failed")
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode == 404 {
|
||||
return nil, fmt.Errorf(
|
||||
"page with id '%s' not found, Confluence REST API returns 404",
|
||||
pageID,
|
||||
)
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode != 200 {
|
||||
return nil, fmt.Errorf(
|
||||
"Confluence REST API returns unexpected HTTP status: %s",
|
||||
request.Raw.Status,
|
||||
)
|
||||
}
|
||||
|
||||
return request.Response.(*PageInfo), nil
|
||||
}
|
||||
|
||||
func (api *API) CreatePage(
|
||||
space string,
|
||||
parent *PageInfo,
|
||||
title string,
|
||||
body string,
|
||||
) (*PageInfo, error) {
|
||||
payload := map[string]interface{}{
|
||||
"type": "page",
|
||||
"title": title,
|
||||
"space": map[string]interface{}{
|
||||
"key": space,
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"storage": map[string]interface{}{
|
||||
"representation": "storage",
|
||||
"value": body,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if parent != nil {
|
||||
payload["ancestors"] = []map[string]interface{}{
|
||||
{"id": parent.ID},
|
||||
}
|
||||
}
|
||||
|
||||
request, err := api.rest.Res(
|
||||
"content/", &PageInfo{},
|
||||
).Post(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode != 200 {
|
||||
output, _ := ioutil.ReadAll(request.Raw.Body)
|
||||
defer request.Raw.Body.Close()
|
||||
|
||||
return nil, fmt.Errorf(
|
||||
"Confluence REST API returns unexpected non-200 HTTP status: %s, "+
|
||||
"output: %s",
|
||||
request.Raw.Status, output,
|
||||
)
|
||||
}
|
||||
|
||||
return request.Response.(*PageInfo), nil
|
||||
}
|
||||
|
||||
func (api *API) UpdatePage(
|
||||
page *PageInfo, newContent string,
|
||||
) error {
|
||||
nextPageVersion := page.Version.Number + 1
|
||||
|
||||
if len(page.Ancestors) == 0 {
|
||||
return fmt.Errorf(
|
||||
"page '%s' info does not contain any information about parents",
|
||||
page.ID,
|
||||
)
|
||||
}
|
||||
|
||||
// picking only the last one, which is required by confluence
|
||||
oldAncestors := []map[string]interface{}{
|
||||
{"id": page.Ancestors[len(page.Ancestors)-1].Id},
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"id": page.ID,
|
||||
"type": "page",
|
||||
"title": page.Title,
|
||||
"version": map[string]interface{}{
|
||||
"number": nextPageVersion,
|
||||
"minorEdit": false,
|
||||
},
|
||||
"ancestors": oldAncestors,
|
||||
"body": map[string]interface{}{
|
||||
"storage": map[string]interface{}{
|
||||
"value": string(newContent),
|
||||
"representation": "storage",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
request, err := api.rest.Res(
|
||||
"content/"+page.ID, &map[string]interface{}{},
|
||||
).Put(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode != 200 {
|
||||
output, _ := ioutil.ReadAll(request.Raw.Body)
|
||||
defer request.Raw.Body.Close()
|
||||
|
||||
return fmt.Errorf(
|
||||
"Confluence REST API returns unexpected non-200 HTTP status: %s, "+
|
||||
"output: %s",
|
||||
request.Raw.Status, output,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) SetPagePermissions(
|
||||
page *PageInfo,
|
||||
operation RestrictionOperation,
|
||||
restrictions []Restriction,
|
||||
) error {
|
||||
var result interface{}
|
||||
|
||||
request, err := api.json.Res(
|
||||
"setContentPermissions", &result,
|
||||
).Post([]interface{}{
|
||||
page.ID,
|
||||
operation,
|
||||
restrictions,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Raw.StatusCode != 200 {
|
||||
output, _ := ioutil.ReadAll(request.Raw.Body)
|
||||
defer request.Raw.Body.Close()
|
||||
|
||||
return fmt.Errorf(
|
||||
"Confluence JSON RPC returns unexpected non-200 HTTP status: %s, "+
|
||||
"output: %s",
|
||||
request.Raw.Status, output,
|
||||
)
|
||||
}
|
||||
|
||||
if success, ok := result.(bool); !ok || !success {
|
||||
return fmt.Errorf(
|
||||
"'true' response expected, but '%v' encountered",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
120
pkg/mark/ancestry.go
Normal file
120
pkg/mark/ancestry.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package mark
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
func EnsureAncestry(
|
||||
api *confluence.API,
|
||||
space string,
|
||||
ancestry []string,
|
||||
) (*confluence.PageInfo, error) {
|
||||
var parent *confluence.PageInfo
|
||||
|
||||
rest := ancestry
|
||||
|
||||
for i, title := range ancestry {
|
||||
page, err := api.FindPage(space, title)
|
||||
if err != nil {
|
||||
return nil, karma.Format(
|
||||
err,
|
||||
`error during finding parent page with title '%s': %s`,
|
||||
title,
|
||||
)
|
||||
}
|
||||
|
||||
if page == nil {
|
||||
break
|
||||
}
|
||||
|
||||
logger.Tracef("parent page '%s' exists: %s", title, page.Links.Full)
|
||||
|
||||
rest = ancestry[i:]
|
||||
parent = page
|
||||
}
|
||||
|
||||
if parent != nil {
|
||||
rest = rest[1:]
|
||||
} else {
|
||||
page, err := api.FindRootPage(space)
|
||||
if err != nil {
|
||||
return nil, karma.Format(
|
||||
err,
|
||||
"can't find root page for space '%s': %s", space,
|
||||
)
|
||||
}
|
||||
|
||||
parent = page
|
||||
}
|
||||
|
||||
if len(rest) == 0 {
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
logger.Debugf(
|
||||
"empty pages under '%s' to be created: %s",
|
||||
parent.Title,
|
||||
strings.Join(rest, ` > `),
|
||||
)
|
||||
|
||||
for _, title := range rest {
|
||||
page, err := api.CreatePage(space, parent, title, ``)
|
||||
if err != nil {
|
||||
return nil, karma.Format(
|
||||
err,
|
||||
`error during creating parent page with title '%s': %s`,
|
||||
title,
|
||||
)
|
||||
}
|
||||
|
||||
parent = page
|
||||
}
|
||||
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
func ValidateAncestry(
|
||||
api *confluence.API,
|
||||
space string,
|
||||
ancestry []string,
|
||||
) (*confluence.PageInfo, error) {
|
||||
page, err := api.FindPage(space, ancestry[len(ancestry)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if page == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(page.Ancestors) < 1 {
|
||||
return nil, fmt.Errorf(`page '%s' has no parents`, page.Title)
|
||||
}
|
||||
|
||||
if len(page.Ancestors) < len(ancestry) {
|
||||
return nil, fmt.Errorf(
|
||||
"page '%s' has fewer parents than specified: %s",
|
||||
page.Title,
|
||||
strings.Join(ancestry, ` > `),
|
||||
)
|
||||
}
|
||||
|
||||
// skipping root article title
|
||||
for i, ancestor := range page.Ancestors[1:len(ancestry)] {
|
||||
if ancestor.Title != ancestry[i] {
|
||||
return nil, fmt.Errorf(
|
||||
"broken ancestry tree; expected tree: %s; "+
|
||||
"encountered '%s' at position of '%s'",
|
||||
strings.Join(ancestry, ` > `),
|
||||
ancestor.Title,
|
||||
ancestry[i],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
90
pkg/mark/mark.go
Normal file
90
pkg/mark/mark.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package mark
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/lorg"
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
var (
|
||||
logger lorg.Logger = lorg.NewDiscarder()
|
||||
)
|
||||
|
||||
func SetLogger(log lorg.Logger) {
|
||||
logger = log
|
||||
}
|
||||
|
||||
func ResolvePage(
|
||||
api *confluence.API,
|
||||
meta *Meta,
|
||||
) (*confluence.PageInfo, error) {
|
||||
page, err := api.FindPage(meta.Space, meta.Title)
|
||||
if err != nil {
|
||||
return nil, karma.Format(
|
||||
err,
|
||||
"error during finding page '%s': %s",
|
||||
meta.Title,
|
||||
)
|
||||
}
|
||||
|
||||
ancestry := meta.Parents
|
||||
if page != nil {
|
||||
ancestry = append(ancestry, page.Title)
|
||||
}
|
||||
|
||||
if len(ancestry) > 0 {
|
||||
page, err := ValidateAncestry(
|
||||
api,
|
||||
meta.Space,
|
||||
ancestry,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if page == nil {
|
||||
logger.Warningf(
|
||||
"page '%s' is not found ",
|
||||
meta.Parents[len(ancestry)-1],
|
||||
)
|
||||
}
|
||||
|
||||
path := meta.Parents
|
||||
path = append(path, meta.Title)
|
||||
|
||||
logger.Debugf(
|
||||
"resolving page path: ??? > %s",
|
||||
strings.Join(path, ` > `),
|
||||
)
|
||||
}
|
||||
|
||||
parent, err := EnsureAncestry(
|
||||
api,
|
||||
meta.Space,
|
||||
meta.Parents,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, karma.Format(
|
||||
err,
|
||||
"can't create ancestry tree: %s; error: %s",
|
||||
strings.Join(meta.Parents, ` > `),
|
||||
)
|
||||
}
|
||||
|
||||
titles := []string{}
|
||||
for _, page := range parent.Ancestors {
|
||||
titles = append(titles, page.Title)
|
||||
}
|
||||
|
||||
titles = append(titles, parent.Title)
|
||||
|
||||
logger.Infof(
|
||||
"page will be stored under path: %s > %s",
|
||||
strings.Join(titles, ` > `),
|
||||
meta.Title,
|
||||
)
|
||||
|
||||
return page, nil
|
||||
}
|
||||
92
pkg/mark/meta.go
Normal file
92
pkg/mark/meta.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package mark
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderParent string = `Parent`
|
||||
HeaderSpace = `Space`
|
||||
HeaderTitle = `Title`
|
||||
HeaderLayout = `Layout`
|
||||
)
|
||||
|
||||
type Meta struct {
|
||||
Parents []string
|
||||
Space string
|
||||
Title string
|
||||
Layout string
|
||||
}
|
||||
|
||||
func ExtractMeta(data []byte) (*Meta, error) {
|
||||
headerPattern := regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`)
|
||||
|
||||
var meta *Meta
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewBuffer(data))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matches := headerPattern.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if meta == nil {
|
||||
meta = &Meta{}
|
||||
}
|
||||
|
||||
header := strings.Title(matches[1])
|
||||
|
||||
switch header {
|
||||
case HeaderParent:
|
||||
meta.Parents = append(meta.Parents, matches[2])
|
||||
|
||||
case HeaderSpace:
|
||||
meta.Space = strings.ToUpper(matches[2])
|
||||
|
||||
case HeaderTitle:
|
||||
meta.Title = strings.TrimSpace(matches[2])
|
||||
|
||||
case HeaderLayout:
|
||||
meta.Layout = strings.TrimSpace(matches[2])
|
||||
|
||||
default:
|
||||
logger.Errorf(
|
||||
`encountered unknown header '%s' line: %#v`,
|
||||
header,
|
||||
line,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if meta == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if meta.Space == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"space key is not set (%s header is not set)",
|
||||
HeaderSpace,
|
||||
)
|
||||
}
|
||||
|
||||
if meta.Title == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"page title is not set (%s header is not set)",
|
||||
HeaderTitle,
|
||||
)
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
Reference in New Issue
Block a user