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/lorg"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark" "github.com/kovetskiy/mark/pkg/mark"
"github.com/reconquest/colorgful" "github.com/reconquest/cog"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
"github.com/zazab/zhash" "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 where 'smith' it's your username, 'matrixishere' it's your password and
'http://confluence.local' is base URL for your Confluence instance. '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, Mark understands extended file format, which, still being valid markdown,
contains several metadata headers, which can be used to locate page inside contains several metadata headers, which can be used to locate page inside
Confluence instance and update it accordingly. Confluence instance and update it accordingly.
@ -88,24 +82,21 @@ Options:
) )
var ( 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 { 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() { func main() {
@ -121,21 +112,21 @@ func main() {
trace = args["--trace"].(bool) trace = args["--trace"].(bool)
) )
initLogger(trace) initlog(trace)
config, err := getConfig(filepath.Join(os.Getenv("HOME"), ".config/mark")) config, err := getConfig(filepath.Join(os.Getenv("HOME"), ".config/mark"))
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
logger.Fatal(err) log.Fatal(err)
} }
markdownData, err := ioutil.ReadFile(targetFile) markdownData, err := ioutil.ReadFile(targetFile)
if err != nil { if err != nil {
logger.Fatal(err) log.Fatal(err)
} }
meta, err := mark.ExtractMeta(markdownData) meta, err := mark.ExtractMeta(markdownData)
if err != nil { if err != nil {
logger.Fatal(err) log.Fatal(err)
} }
htmlData := mark.CompileMarkdown(markdownData) htmlData := mark.CompileMarkdown(markdownData)
@ -147,14 +138,15 @@ func main() {
creds, err := GetCredentials(args, config) creds, err := GetCredentials(args, config)
if err != nil { if err != nil {
logger.Fatal(err) log.Fatal(err)
} }
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password) api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
if creds.PageID != "" && meta != nil { if creds.PageID != "" && meta != nil {
logger.Warningf( log.Warningf(
`specified file contains metadata, ` + nil,
`specified file contains metadata, `+
`but it will be ignored due specified command line URL`, `but it will be ignored due specified command line URL`,
) )
@ -162,9 +154,10 @@ func main() {
} }
if creds.PageID == "" && meta == nil { if creds.PageID == "" && meta == nil {
logger.Fatalf( log.Fatalf(
`specified file doesn't contain metadata ` + nil,
`and URL is not specified via command line ` + `specified file doesn't contain metadata `+
`and URL is not specified via command line `+
`or doesn't contain pageId GET-parameter`, `or doesn't contain pageId GET-parameter`,
) )
} }
@ -174,34 +167,40 @@ func main() {
if meta != nil { if meta != nil {
page, err := resolvePage(api, meta) page, err := resolvePage(api, meta)
if err != nil { if err != nil {
logger.Fatal(err) log.Fatalf(
karma.Describe("title", meta.Title).Reason(err),
"unable to resolve page",
)
} }
target = page target = page
} else { } else {
if creds.PageID == "" { 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) page, err := api.GetPageByID(creds.PageID)
if err != nil { if err != nil {
logger.Fatal(err) log.Fatalf(err, "unable to retrieve page by id")
} }
target = page target = page
} }
mark.ResolveAttachments(api, target, ".", meta.Attachments)
err = api.UpdatePage( err = api.UpdatePage(
target, target,
MacroLayout{meta.Layout, [][]byte{htmlData}}.Render(), MacroLayout{meta.Layout, [][]byte{htmlData}}.Render(),
) )
if err != nil { if err != nil {
logger.Fatal(err) log.Fatal(err)
} }
if editLock { if editLock {
logger.Infof( log.Infof(
`edit locked on page '%s' by user '%s' to prevent manual edits`, nil,
`edit locked on page %q by user %q to prevent manual edits`,
target.Title, target.Title,
creds.Username, creds.Username,
) )
@ -214,7 +213,7 @@ func main() {
}, },
) )
if err != nil { if err != nil {
logger.Fatal(err) log.Fatal(err)
} }
} }
@ -233,7 +232,7 @@ func resolvePage(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"error during finding page '%s': %s", "error during finding page %q",
meta.Title, meta.Title,
) )
} }
@ -254,8 +253,9 @@ func resolvePage(
} }
if page == nil { if page == nil {
logger.Warningf( log.Warningf(
"page '%s' is not found ", nil,
"page %q is not found ",
meta.Parents[len(ancestry)-1], meta.Parents[len(ancestry)-1],
) )
} }
@ -263,7 +263,8 @@ func resolvePage(
path := meta.Parents path := meta.Parents
path = append(path, meta.Title) path = append(path, meta.Title)
logger.Debugf( log.Debugf(
nil,
"resolving page path: ??? > %s", "resolving page path: ??? > %s",
strings.Join(path, ` > `), strings.Join(path, ` > `),
) )
@ -277,7 +278,7 @@ func resolvePage(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"can't create ancestry tree: %s; error: %s", "can't create ancestry tree: %s",
strings.Join(meta.Parents, ` > `), strings.Join(meta.Parents, ` > `),
) )
} }
@ -289,7 +290,8 @@ func resolvePage(
titles = append(titles, parent.Title) titles = append(titles, parent.Title)
logger.Infof( log.Infof(
nil,
"page will be stored under path: %s > %s", "page will be stored under path: %s > %s",
strings.Join(titles, ` > `), strings.Join(titles, ` > `),
meta.Title, meta.Title,
@ -300,7 +302,7 @@ func resolvePage(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"can't create page '%s': %s", "can't create page %q",
meta.Title, meta.Title,
) )
} }
@ -322,6 +324,7 @@ func getConfig(path string) (zhash.Hash, error) {
return zhash.NewHash(), karma.Format( return zhash.NewHash(), karma.Format(
err, err,
"can't decode toml file: %s", "can't decode toml file: %s",
path,
) )
} }

View File

@ -1,12 +1,31 @@
package confluence package confluence
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"github.com/bndr/gopencils" "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 type RestrictionOperation string
const ( const (
@ -61,16 +80,20 @@ func NewAPI(baseURL string, username string, password string) *API {
func (api *API) FindRootPage(space string) (*PageInfo, error) { func (api *API) FindRootPage(space string) (*PageInfo, error) {
page, err := api.FindPage(space, ``) page, err := api.FindPage(space, ``)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, karma.Format(
`can't obtain first page from space '%s': %s`,
space,
err, 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 { if len(page.Ancestors) == 0 {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"page '%s' from space '%s' has no parents", "page %q from space %q has no parents",
page.Title, page.Title,
space, space,
) )
@ -99,20 +122,14 @@ func (api *API) FindPage(space string, title string) (*PageInfo, error) {
request, err := api.rest.Res( request, err := api.rest.Res(
"content/", &result, "content/", &result,
).Get(payload) ).Get(payload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if request.Raw.StatusCode == 401 { // allow 404 because it's fine if page is not found,
return nil, fmt.Errorf("authentification failed") // the function will return nil, nil
} if request.Raw.StatusCode != 404 && request.Raw.StatusCode != 200 {
return nil, newErrorStatusNotOK(request)
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 { if len(result.Results) == 0 {
@ -122,31 +139,42 @@ func (api *API) FindPage(space string, title string) (*PageInfo, error) {
return &result.Results[0], nil 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) { func (api *API) GetPageByID(pageID string) (*PageInfo, error) {
request, err := api.rest.Res( request, err := api.rest.Res(
"content/"+pageID, &PageInfo{}, "content/"+pageID, &PageInfo{},
).Get(map[string]string{"expand": "ancestors,version"}) ).Get(map[string]string{"expand": "ancestors,version"})
if err != nil { if err != nil {
return nil, err 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 { if request.Raw.StatusCode != 200 {
return nil, fmt.Errorf( return nil, newErrorStatusNotOK(request)
"Confluence REST API returns unexpected HTTP status: %s",
request.Raw.Status,
)
} }
return request.Response.(*PageInfo), nil return request.Response.(*PageInfo), nil
@ -186,14 +214,7 @@ func (api *API) CreatePage(
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != 200 {
output, _ := ioutil.ReadAll(request.Raw.Body) return nil, newErrorStatusNotOK(request)
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 return request.Response.(*PageInfo), nil
@ -206,7 +227,7 @@ func (api *API) UpdatePage(
if len(page.Ancestors) == 0 { if len(page.Ancestors) == 0 {
return fmt.Errorf( 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, page.ID,
) )
} }
@ -241,14 +262,7 @@ func (api *API) UpdatePage(
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != 200 {
output, _ := ioutil.ReadAll(request.Raw.Body) return newErrorStatusNotOK(request)
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 return nil
@ -273,14 +287,7 @@ func (api *API) SetPagePermissions(
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != 200 {
output, _ := ioutil.ReadAll(request.Raw.Body) return newErrorStatusNotOK(request)
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 { if success, ok := result.(bool); !ok || !success {
@ -292,3 +299,26 @@ func (api *API) SetPagePermissions(
return nil 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" "strings"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/faces/logger"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )
@ -22,7 +23,7 @@ func EnsureAncestry(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
`error during finding parent page with title '%s': %s`, `error during finding parent page with title %q`,
title, title,
) )
} }
@ -31,7 +32,7 @@ func EnsureAncestry(
break 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:] rest = ancestry[i:]
parent = page parent = page
@ -44,19 +45,19 @@ func EnsureAncestry(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"can't find root page for space '%s': %s", space, "can't find root page for space %q",
space,
) )
} }
parent = page parent = page
} }
if len(rest) == 0 { if len(rest) == 0 {
return parent, nil return parent, nil
} }
logger.Debugf( logger.Debugf(
"empty pages under '%s' to be created: %s", "empty pages under %q to be created: %s",
parent.Title, parent.Title,
strings.Join(rest, ` > `), strings.Join(rest, ` > `),
) )
@ -66,7 +67,7 @@ func EnsureAncestry(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
`error during creating parent page with title '%s': %s`, `error during creating parent page with title %q`,
title, title,
) )
} }
@ -92,12 +93,12 @@ func ValidateAncestry(
} }
if len(page.Ancestors) < 1 { 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) { if len(page.Ancestors) < len(ancestry) {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"page '%s' has fewer parents than specified: %s", "page %q has fewer parents than specified: %s",
page.Title, page.Title,
strings.Join(ancestry, ` > `), strings.Join(ancestry, ` > `),
) )
@ -108,7 +109,7 @@ func ValidateAncestry(
if ancestor.Title != ancestry[i] { if ancestor.Title != ancestry[i] {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"broken ancestry tree; expected tree: %s; "+ "broken ancestry tree; expected tree: %s; "+
"encountered '%s' at position of '%s'", "encountered %q at position of %q",
strings.Join(ancestry, ` > `), strings.Join(ancestry, ` > `),
ancestor.Title, ancestor.Title,
ancestry[i], 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 ( import (
"strings" "strings"
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/faces/logger"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )
var (
logger lorg.Logger = lorg.NewDiscarder()
)
func SetLogger(log lorg.Logger) {
logger = log
}
func ResolvePage( func ResolvePage(
api *confluence.API, api *confluence.API,
meta *Meta, meta *Meta,
@ -24,7 +16,7 @@ func ResolvePage(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"error during finding page '%s': %s", "error while finding page %q",
meta.Title, meta.Title,
) )
} }
@ -45,8 +37,9 @@ func ResolvePage(
} }
if page == nil { if page == nil {
logger.Warningf( log.Warningf(
"page '%s' is not found ", nil,
"page %q is not found ",
meta.Parents[len(ancestry)-1], meta.Parents[len(ancestry)-1],
) )
} }
@ -68,7 +61,7 @@ func ResolvePage(
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"can't create ancestry tree: %s; error: %s", "can't create ancestry tree: %s",
strings.Join(meta.Parents, ` > `), strings.Join(meta.Parents, ` > `),
) )
} }
@ -80,7 +73,8 @@ func ResolvePage(
titles = append(titles, parent.Title) titles = append(titles, parent.Title)
logger.Infof( log.Infof(
nil,
"page will be stored under path: %s > %s", "page will be stored under path: %s > %s",
strings.Join(titles, ` > `), strings.Join(titles, ` > `),
meta.Title, meta.Title,

View File

@ -4,22 +4,42 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"regexp" "regexp"
"strings" "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 ( const (
HeaderParent string = `Parent` HeaderParent = `Parent`
HeaderSpace = `Space` HeaderSpace = `Space`
HeaderTitle = `Title` HeaderTitle = `Title`
HeaderLayout = `Layout` HeaderLayout = `Layout`
HeaderAttachment = `Attachment`
) )
type Meta struct { type Meta struct {
Parents []string Parents []string
Space string Space string
Title string Title string
Layout string Layout string
Attachments []string
} }
func ExtractMeta(data []byte) (*Meta, error) { func ExtractMeta(data []byte) (*Meta, error) {
@ -46,22 +66,31 @@ func ExtractMeta(data []byte) (*Meta, error) {
header := strings.Title(matches[1]) header := strings.Title(matches[1])
var value string
if len(matches) > 1 {
value = strings.TrimSpace(matches[2])
}
switch header { switch header {
case HeaderParent: case HeaderParent:
meta.Parents = append(meta.Parents, matches[2]) meta.Parents = append(meta.Parents, value)
case HeaderSpace: case HeaderSpace:
meta.Space = strings.ToUpper(matches[2]) meta.Space = strings.ToUpper(value)
case HeaderTitle: case HeaderTitle:
meta.Title = strings.TrimSpace(matches[2]) meta.Title = strings.TrimSpace(value)
case HeaderLayout: case HeaderLayout:
meta.Layout = strings.TrimSpace(matches[2]) meta.Layout = strings.TrimSpace(value)
case HeaderAttachment:
meta.Attachments = append(meta.Attachments, value)
default: default:
logger.Errorf( log.Errorf(
`encountered unknown header '%s' line: %#v`, nil,
`encountered unknown header %q line: %#v`,
header, header,
line, line,
) )

6
test.md Normal file
View File

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