wip: attachments

This commit is contained in:
Egor Kovetskiy 2019-04-19 10:31:41 +03:00
parent 5f582e51e6
commit 76ddde31ab
7 changed files with 219 additions and 137 deletions

93
main.go
View File

@ -12,7 +12,7 @@ import (
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark"
"github.com/reconquest/colorgful"
"github.com/reconquest/cog"
"github.com/reconquest/karma-go"
"github.com/zazab/zhash"
)
@ -32,12 +32,6 @@ located in ~/.config/mark with following format:
where 'smith' it's your username, 'matrixishere' it's your password and
'http://confluence.local' is base URL for your Confluence instance.
Mark can read Confluence page URL and markdown file path from another specified
configuration file, which you can specify using -c <file> flag. It is very
usable for git hooks. That file should have following format:
url = "http://confluence.local/pages/viewpage.action?pageId=123456"
file = "docs/README.md"
Mark understands extended file format, which, still being valid markdown,
contains several metadata headers, which can be used to locate page inside
Confluence instance and update it accordingly.
@ -88,24 +82,21 @@ Options:
)
var (
logger = lorg.NewLog()
log *cog.Logger
)
func initLogger(trace bool) {
func initlog(trace bool) {
stderr := lorg.NewLog()
stderr.SetIndentLines(true)
stderr.SetFormat(
lorg.NewFormat("${time} ${level:[%s]:right:short} ${prefix}%s"),
)
log = cog.NewLogger(stderr)
if trace {
logger.SetLevel(lorg.LevelTrace)
log.SetLevel(lorg.LevelTrace)
}
logFormat := `${time} ${level:[%s]:right:true} %s`
if format := os.Getenv("LOG_FORMAT"); format != "" {
logFormat = format
}
logger.SetFormat(colorgful.MustApplyDefaultTheme(
logFormat,
colorgful.Default,
))
}
func main() {
@ -121,21 +112,21 @@ func main() {
trace = args["--trace"].(bool)
)
initLogger(trace)
initlog(trace)
config, err := getConfig(filepath.Join(os.Getenv("HOME"), ".config/mark"))
if err != nil && !os.IsNotExist(err) {
logger.Fatal(err)
log.Fatal(err)
}
markdownData, err := ioutil.ReadFile(targetFile)
if err != nil {
logger.Fatal(err)
log.Fatal(err)
}
meta, err := mark.ExtractMeta(markdownData)
if err != nil {
logger.Fatal(err)
log.Fatal(err)
}
htmlData := mark.CompileMarkdown(markdownData)
@ -147,14 +138,15 @@ func main() {
creds, err := GetCredentials(args, config)
if err != nil {
logger.Fatal(err)
log.Fatal(err)
}
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
if creds.PageID != "" && meta != nil {
logger.Warningf(
`specified file contains metadata, ` +
log.Warningf(
nil,
`specified file contains metadata, `+
`but it will be ignored due specified command line URL`,
)
@ -162,9 +154,10 @@ func main() {
}
if creds.PageID == "" && meta == nil {
logger.Fatalf(
`specified file doesn't contain metadata ` +
`and URL is not specified via command line ` +
log.Fatalf(
nil,
`specified file doesn't contain metadata `+
`and URL is not specified via command line `+
`or doesn't contain pageId GET-parameter`,
)
}
@ -174,34 +167,40 @@ func main() {
if meta != nil {
page, err := resolvePage(api, meta)
if err != nil {
logger.Fatal(err)
log.Fatalf(
karma.Describe("title", meta.Title).Reason(err),
"unable to resolve page",
)
}
target = page
} else {
if creds.PageID == "" {
logger.Fatalf("URL should provide 'pageId' GET-parameter")
log.Fatalf(nil, "URL should provide 'pageId' GET-parameter")
}
page, err := api.GetPageByID(creds.PageID)
if err != nil {
logger.Fatal(err)
log.Fatalf(err, "unable to retrieve page by id")
}
target = page
}
mark.ResolveAttachments(api, target, ".", meta.Attachments)
err = api.UpdatePage(
target,
MacroLayout{meta.Layout, [][]byte{htmlData}}.Render(),
)
if err != nil {
logger.Fatal(err)
log.Fatal(err)
}
if editLock {
logger.Infof(
`edit locked on page '%s' by user '%s' to prevent manual edits`,
log.Infof(
nil,
`edit locked on page %q by user %q to prevent manual edits`,
target.Title,
creds.Username,
)
@ -214,7 +213,7 @@ func main() {
},
)
if err != nil {
logger.Fatal(err)
log.Fatal(err)
}
}
@ -233,7 +232,7 @@ func resolvePage(
if err != nil {
return nil, karma.Format(
err,
"error during finding page '%s': %s",
"error during finding page %q",
meta.Title,
)
}
@ -254,8 +253,9 @@ func resolvePage(
}
if page == nil {
logger.Warningf(
"page '%s' is not found ",
log.Warningf(
nil,
"page %q is not found ",
meta.Parents[len(ancestry)-1],
)
}
@ -263,7 +263,8 @@ func resolvePage(
path := meta.Parents
path = append(path, meta.Title)
logger.Debugf(
log.Debugf(
nil,
"resolving page path: ??? > %s",
strings.Join(path, ` > `),
)
@ -277,7 +278,7 @@ func resolvePage(
if err != nil {
return nil, karma.Format(
err,
"can't create ancestry tree: %s; error: %s",
"can't create ancestry tree: %s",
strings.Join(meta.Parents, ` > `),
)
}
@ -289,7 +290,8 @@ func resolvePage(
titles = append(titles, parent.Title)
logger.Infof(
log.Infof(
nil,
"page will be stored under path: %s > %s",
strings.Join(titles, ` > `),
meta.Title,
@ -300,7 +302,7 @@ func resolvePage(
if err != nil {
return nil, karma.Format(
err,
"can't create page '%s': %s",
"can't create page %q",
meta.Title,
)
}
@ -322,6 +324,7 @@ func getConfig(path string) (zhash.Hash, error) {
return zhash.NewHash(), karma.Format(
err,
"can't decode toml file: %s",
path,
)
}

View File

@ -1,12 +1,31 @@
package confluence
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"github.com/bndr/gopencils"
"github.com/kovetskiy/lorg"
"github.com/reconquest/cog"
"github.com/reconquest/karma-go"
)
func discarder() *lorg.Log {
stderr := lorg.NewLog()
stderr.SetOutput(ioutil.Discard)
return stderr
}
var (
log = cog.NewLogger(discarder())
)
func SetLogger(logger *cog.Logger) {
log = logger
}
type RestrictionOperation string
const (
@ -61,16 +80,20 @@ func NewAPI(baseURL string, username string, password string) *API {
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,
return nil, karma.Format(
err,
"can't obtain first page from space %q",
space,
)
}
if page == nil {
return nil, errors.New("no such space")
}
if len(page.Ancestors) == 0 {
return nil, fmt.Errorf(
"page '%s' from space '%s' has no parents",
"page %q from space %q has no parents",
page.Title,
space,
)
@ -99,20 +122,14 @@ func (api *API) FindPage(space string, title string) (*PageInfo, error) {
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,
)
// allow 404 because it's fine if page is not found,
// the function will return nil, nil
if request.Raw.StatusCode != 404 && request.Raw.StatusCode != 200 {
return nil, newErrorStatusNotOK(request)
}
if len(result.Results) == 0 {
@ -122,31 +139,42 @@ func (api *API) FindPage(space string, title string) (*PageInfo, error) {
return &result.Results[0], nil
}
func (api *API) GetAttachments(pageID string) error {
result := map[string]interface{}{}
payload := map[string]string{
"expand": "version,container",
}
request, err := api.rest.Res(
"content/"+pageID+"/child/attachment", &result,
).Get(payload)
if err != nil {
return err
}
if request.Raw.StatusCode != 200 {
return newErrorStatusNotOK(request)
}
{
marshaledXXX, _ := json.MarshalIndent(result, "", " ")
fmt.Printf("result: %s\n", string(marshaledXXX))
}
return 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 nil, newErrorStatusNotOK(request)
}
return request.Response.(*PageInfo), nil
@ -186,14 +214,7 @@ func (api *API) CreatePage(
}
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 nil, newErrorStatusNotOK(request)
}
return request.Response.(*PageInfo), nil
@ -206,7 +227,7 @@ func (api *API) UpdatePage(
if len(page.Ancestors) == 0 {
return fmt.Errorf(
"page '%s' info does not contain any information about parents",
"page %q info does not contain any information about parents",
page.ID,
)
}
@ -241,14 +262,7 @@ func (api *API) UpdatePage(
}
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 newErrorStatusNotOK(request)
}
return nil
@ -273,14 +287,7 @@ func (api *API) SetPagePermissions(
}
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,
)
return newErrorStatusNotOK(request)
}
if success, ok := result.(bool); !ok || !success {
@ -292,3 +299,26 @@ func (api *API) SetPagePermissions(
return nil
}
func newErrorStatusNotOK(request *gopencils.Resource) error {
if request.Raw.StatusCode == 401 {
return errors.New(
"Confluence API returned unexpected status: 401 (Unauthorized)",
)
}
if request.Raw.StatusCode == 404 {
return errors.New(
"Confluence API returned unexpected status: 404 (Not Found)",
)
}
output, _ := ioutil.ReadAll(request.Raw.Body)
defer request.Raw.Body.Close()
return fmt.Errorf(
"Confluence API returned unexpected status: %v, "+
"output: %s",
request.Raw.Status, output,
)
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/faces/logger"
"github.com/reconquest/karma-go"
)
@ -22,7 +23,7 @@ func EnsureAncestry(
if err != nil {
return nil, karma.Format(
err,
`error during finding parent page with title '%s': %s`,
`error during finding parent page with title %q`,
title,
)
}
@ -31,7 +32,7 @@ func EnsureAncestry(
break
}
logger.Tracef("parent page '%s' exists: %s", title, page.Links.Full)
log.Tracef(nil, "parent page %q exists: %s", title, page.Links.Full)
rest = ancestry[i:]
parent = page
@ -44,19 +45,19 @@ func EnsureAncestry(
if err != nil {
return nil, karma.Format(
err,
"can't find root page for space '%s': %s", space,
"can't find root page for space %q",
space,
)
}
parent = page
}
if len(rest) == 0 {
return parent, nil
}
logger.Debugf(
"empty pages under '%s' to be created: %s",
"empty pages under %q to be created: %s",
parent.Title,
strings.Join(rest, ` > `),
)
@ -66,7 +67,7 @@ func EnsureAncestry(
if err != nil {
return nil, karma.Format(
err,
`error during creating parent page with title '%s': %s`,
`error during creating parent page with title %q`,
title,
)
}
@ -92,12 +93,12 @@ func ValidateAncestry(
}
if len(page.Ancestors) < 1 {
return nil, fmt.Errorf(`page '%s' has no parents`, page.Title)
return nil, fmt.Errorf(`page %q has no parents`, page.Title)
}
if len(page.Ancestors) < len(ancestry) {
return nil, fmt.Errorf(
"page '%s' has fewer parents than specified: %s",
"page %q has fewer parents than specified: %s",
page.Title,
strings.Join(ancestry, ` > `),
)
@ -108,7 +109,7 @@ func ValidateAncestry(
if ancestor.Title != ancestry[i] {
return nil, fmt.Errorf(
"broken ancestry tree; expected tree: %s; "+
"encountered '%s' at position of '%s'",
"encountered %q at position of %q",
strings.Join(ancestry, ` > `),
ancestor.Title,
ancestry[i],

19
pkg/mark/attachment.go Normal file
View File

@ -0,0 +1,19 @@
package mark
import "github.com/kovetskiy/mark/pkg/confluence"
type Attachment struct {
Name string
}
func ResolveAttachments(
api *confluence.API,
page *confluence.PageInfo,
base string,
names []string,
) {
err := api.GetAttachments(page.ID)
if err != nil {
panic(err)
}
}

View File

@ -3,19 +3,11 @@ package mark
import (
"strings"
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/faces/logger"
"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,
@ -24,7 +16,7 @@ func ResolvePage(
if err != nil {
return nil, karma.Format(
err,
"error during finding page '%s': %s",
"error while finding page %q",
meta.Title,
)
}
@ -45,8 +37,9 @@ func ResolvePage(
}
if page == nil {
logger.Warningf(
"page '%s' is not found ",
log.Warningf(
nil,
"page %q is not found ",
meta.Parents[len(ancestry)-1],
)
}
@ -68,7 +61,7 @@ func ResolvePage(
if err != nil {
return nil, karma.Format(
err,
"can't create ancestry tree: %s; error: %s",
"can't create ancestry tree: %s",
strings.Join(meta.Parents, ` > `),
)
}
@ -80,7 +73,8 @@ func ResolvePage(
titles = append(titles, parent.Title)
logger.Infof(
log.Infof(
nil,
"page will be stored under path: %s > %s",
strings.Join(titles, ` > `),
meta.Title,

View File

@ -4,22 +4,42 @@ import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/kovetskiy/lorg"
"github.com/reconquest/cog"
)
func discarder() *lorg.Log {
stderr := lorg.NewLog()
stderr.SetOutput(ioutil.Discard)
return stderr
}
var (
log = cog.NewLogger(discarder())
)
func SetLogger(logger *cog.Logger) {
log = logger
}
const (
HeaderParent string = `Parent`
HeaderSpace = `Space`
HeaderTitle = `Title`
HeaderLayout = `Layout`
HeaderParent = `Parent`
HeaderSpace = `Space`
HeaderTitle = `Title`
HeaderLayout = `Layout`
HeaderAttachment = `Attachment`
)
type Meta struct {
Parents []string
Space string
Title string
Layout string
Parents []string
Space string
Title string
Layout string
Attachments []string
}
func ExtractMeta(data []byte) (*Meta, error) {
@ -46,22 +66,31 @@ func ExtractMeta(data []byte) (*Meta, error) {
header := strings.Title(matches[1])
var value string
if len(matches) > 1 {
value = strings.TrimSpace(matches[2])
}
switch header {
case HeaderParent:
meta.Parents = append(meta.Parents, matches[2])
meta.Parents = append(meta.Parents, value)
case HeaderSpace:
meta.Space = strings.ToUpper(matches[2])
meta.Space = strings.ToUpper(value)
case HeaderTitle:
meta.Title = strings.TrimSpace(matches[2])
meta.Title = strings.TrimSpace(value)
case HeaderLayout:
meta.Layout = strings.TrimSpace(matches[2])
meta.Layout = strings.TrimSpace(value)
case HeaderAttachment:
meta.Attachments = append(meta.Attachments, value)
default:
logger.Errorf(
`encountered unknown header '%s' line: %#v`,
log.Errorf(
nil,
`encountered unknown header %q line: %#v`,
header,
line,
)

6
test.md Normal file
View File

@ -0,0 +1,6 @@
[]:# (Space: PROD)
[]:# (Parent: Parent1)
[]:# (Parent: Parent2)
[]:# (Title: Title)
Content