diff --git a/main.go b/main.go index 324e5ed..b183e18 100644 --- a/main.go +++ b/main.go @@ -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 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, ) } diff --git a/pkg/confluence/api.go b/pkg/confluence/api.go index 61ffc16..d3c8e81 100644 --- a/pkg/confluence/api.go +++ b/pkg/confluence/api.go @@ -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, + ) +} diff --git a/pkg/mark/ancestry.go b/pkg/mark/ancestry.go index 2136e4f..710e551 100644 --- a/pkg/mark/ancestry.go +++ b/pkg/mark/ancestry.go @@ -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], diff --git a/pkg/mark/attachment.go b/pkg/mark/attachment.go new file mode 100644 index 0000000..514cd97 --- /dev/null +++ b/pkg/mark/attachment.go @@ -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) + } +} diff --git a/pkg/mark/mark.go b/pkg/mark/mark.go index ad4277c..becbcf2 100644 --- a/pkg/mark/mark.go +++ b/pkg/mark/mark.go @@ -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, diff --git a/pkg/mark/meta.go b/pkg/mark/meta.go index b4986fd..b49f9b2 100644 --- a/pkg/mark/meta.go +++ b/pkg/mark/meta.go @@ -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, ) diff --git a/test.md b/test.md new file mode 100644 index 0000000..1932fdb --- /dev/null +++ b/test.md @@ -0,0 +1,6 @@ +[]:# (Space: PROD) +[]:# (Parent: Parent1) +[]:# (Parent: Parent2) +[]:# (Title: Title) + +Content