summaryrefslogtreecommitdiff
path: root/src/cmd/export
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2024-07-24 21:48:38 +0200
committerBrian Picciano <mediocregopher@gmail.com>2024-07-24 21:48:38 +0200
commit45c20d03663878f3508eaa9b961cb0cb12cc5574 (patch)
treea965671ce18393c638b3d52b6a82b3b5cf90beb9 /src/cmd/export
parent60c6165d07b7fa633d7a8fafbe70e23c83d43c47 (diff)
Got post exporting working
Diffstat (limited to 'src/cmd/export')
-rw-r--r--src/cmd/export/main.go75
-rw-r--r--src/cmd/export/posts.go179
2 files changed, 254 insertions, 0 deletions
diff --git a/src/cmd/export/main.go b/src/cmd/export/main.go
new file mode 100644
index 0000000..1d39bc7
--- /dev/null
+++ b/src/cmd/export/main.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "context"
+
+ cfgpkg "dev.mediocregopher.com/mediocre-blog.git/src/cfg"
+ "dev.mediocregopher.com/mediocre-blog.git/src/gmi"
+ "dev.mediocregopher.com/mediocre-blog.git/src/http"
+ "dev.mediocregopher.com/mediocre-blog.git/src/post"
+ "dev.mediocregopher.com/mediocre-blog.git/src/render"
+ "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
+)
+
+func main() {
+ var (
+ ctx = context.Background()
+ cfg = cfgpkg.NewBlogCfg(cfgpkg.Params{})
+ dataDir cfgpkg.DataDir
+ httpParams http.Params
+ gmiParams gmi.Params
+ )
+
+ dataDir.SetupCfg(cfg)
+ defer dataDir.Close()
+
+ httpParams.SetupCfg(cfg)
+ gmiParams.SetupCfg(cfg)
+
+ exportDirPath := cfg.String("export-dir-path", "", "Directory to export into")
+
+ // initialization
+ err := cfg.Init(ctx)
+
+ logger := mlog.NewLogger(nil)
+ defer logger.Close()
+
+ logger.Info(ctx, "process started")
+ defer logger.Info(ctx, "process exiting")
+
+ if err != nil {
+ logger.Fatal(ctx, "initializing", err)
+ }
+
+ if *exportDirPath == "" {
+ logger.FatalString(ctx, "--export-dir-path is required")
+ }
+
+ postSQLDB, err := post.NewSQLDB(dataDir)
+ if err != nil {
+ logger.Fatal(ctx, "initializing sql db for post data", err)
+ }
+ defer postSQLDB.Close()
+
+ var (
+ urlBuilder = render.NewURLBuilder(
+ gmiParams.PublicURL,
+ httpParams.PublicURL,
+ gmiParams.PublicURL,
+ )
+ postStore = post.NewStore(postSQLDB)
+ //postAssetStore = asset.NewStore(postSQLDB)
+ //postDraftStore = post.NewDraftStore(postSQLDB)
+ )
+
+ err = exportPosts(
+ ctx,
+ logger.WithNamespace("posts"),
+ postStore,
+ urlBuilder,
+ *exportDirPath,
+ )
+ if err != nil {
+ logger.Fatal(ctx, "Failed to export post data", err)
+ }
+}
diff --git a/src/cmd/export/posts.go b/src/cmd/export/posts.go
new file mode 100644
index 0000000..f54fc2c
--- /dev/null
+++ b/src/cmd/export/posts.go
@@ -0,0 +1,179 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ txttpl "text/template"
+
+ gmnhg "github.com/tdemin/gmnhg"
+
+ "dev.mediocregopher.com/mediocre-blog.git/src/gmi"
+ "dev.mediocregopher.com/mediocre-blog.git/src/http"
+ "dev.mediocregopher.com/mediocre-blog.git/src/post"
+ "dev.mediocregopher.com/mediocre-blog.git/src/render"
+ "dev.mediocregopher.com/mediocre-go-lib.git/mctx"
+ "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
+)
+
+var postFileExtensions = map[post.Format]string{
+ post.FormatGemtext: "gmi",
+ post.FormatMarkdown: "md",
+}
+
+func postTargetFormat(p post.StoredPost) post.Format {
+ if p.Format == post.FormatMarkdown && strings.Contains(p.Body, `</`) {
+ return post.FormatMarkdown
+ }
+ return post.FormatGemtext
+}
+
+func writePostBody(post post.StoredPost, path, body string) error {
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("opening file: %w", err)
+ }
+ defer func() { _ = f.Close() }()
+
+ writeString := func(str string) {
+ if err != nil {
+ return
+ }
+ _, err = f.WriteString(str)
+ }
+
+ writeString(fmt.Sprintf("# %s\n\n", post.Title))
+ if post.Description != "" {
+ writeString(fmt.Sprintf("> %s\n\n", post.Description))
+ }
+ writeString(body)
+
+ return err
+}
+
+func writePosts(
+ urlBuilder render.URLBuilder, posts []post.StoredPost, path string,
+) error {
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("opening file: %w", err)
+ }
+ defer func() { _ = f.Close() }()
+
+ writeString := func(str string) {
+ if err != nil {
+ return
+ }
+ _, err = f.WriteString(str)
+ }
+
+ writeString("# Posts\n\n")
+ for _, p := range posts {
+ var (
+ targetFormat = postTargetFormat(p)
+ u = urlBuilder.Post(p.ID)
+ )
+
+ if targetFormat == post.FormatMarkdown {
+ u = u.HTTP()
+ }
+
+ writeString(fmt.Sprintf(
+ "=> %s %s - %s\n\n",
+ u.String(),
+ p.PublishedAt.Format("2006-01-02"),
+ p.Title,
+ ))
+ }
+
+ return err
+}
+
+func exportPosts(
+ ctx context.Context,
+ logger *mlog.Logger,
+ postStore post.Store,
+ urlBuilder render.URLBuilder,
+ exportDirPath string,
+) error {
+ var (
+ postsDir = filepath.Join(exportDirPath, "posts")
+ preprocessFuncs = map[post.Format]post.PreprocessFunctions{
+ post.FormatGemtext: gmi.PostPreprocessFuncs{URLBuilder: urlBuilder},
+ post.FormatMarkdown: http.NewPostPreprocessFuncs(urlBuilder),
+ }
+ )
+
+ if err := os.MkdirAll(postsDir, 0755); err != nil {
+ return fmt.Errorf("creating posts dir %q: %w", postsDir, err)
+ }
+
+ logger.Info(ctx, "Getting posts")
+ posts, _, err := postStore.Get(0, 10000)
+ if err != nil {
+ return fmt.Errorf("getting posts: %w", err)
+ }
+
+ {
+ postsIndexPath := filepath.Join(postsDir, "index.gmi")
+ ctx := mctx.Annotate(ctx, "path", postsIndexPath)
+ logger.Info(ctx, "Writing posts index page")
+ err = writePosts(urlBuilder, posts, postsIndexPath)
+ if err != nil {
+ return fmt.Errorf("writing posts index page: %w", err)
+ }
+ }
+
+ for _, p := range posts {
+ var (
+ targetFormat = postTargetFormat(p)
+ ext = postFileExtensions[targetFormat]
+ postFilePath = filepath.Join(postsDir, p.ID+"."+ext)
+
+ ctx = mctx.Annotate(
+ ctx,
+ "post_id", p.ID,
+ "post_title", p.Title,
+ "post_format", p.Format,
+ "target_format", targetFormat,
+ )
+
+ body = new(bytes.Buffer)
+ )
+
+ logger.Info(ctx, "Rendering post body")
+ tpl := txttpl.New("")
+
+ tpl, err := tpl.Parse(p.Body)
+ if err != nil {
+ return fmt.Errorf("parsing post %q body as template: %w", p.ID, err)
+ }
+
+ err = tpl.Execute(body, struct {
+ RootURL render.URLBuilder
+ post.PreprocessFunctions
+ }{
+ urlBuilder, preprocessFuncs[targetFormat],
+ })
+ if err != nil {
+ return fmt.Errorf("executing post %q body as template: %w", p.ID, err)
+ }
+
+ if p.Format == post.FormatMarkdown && targetFormat == post.FormatGemtext {
+ gemtextBodyBytes, err := gmnhg.RenderMarkdown(body.Bytes(), 0)
+ if err != nil {
+ return fmt.Errorf("converting post %q from markdown: %w", p.ID, err)
+ }
+ body = bytes.NewBuffer(gemtextBodyBytes)
+ }
+
+ if err := writePostBody(p, postFilePath, body.String()); err != nil {
+ return fmt.Errorf("writing post %q body to file %q: %w", p.ID, postFilePath, err)
+ }
+ }
+
+ return nil
+}