diff --git a/srv/src/cfg/cfg.go b/srv/src/cfg/cfg.go index 8513e16..32fc3e7 100644 --- a/srv/src/cfg/cfg.go +++ b/srv/src/cfg/cfg.go @@ -130,6 +130,40 @@ func (c *Cfg) StringVar(p *string, name, value, usage string) { } } +// Args returns a pointer which will be filled with the process's positional +// arguments after Init is called. The positional arguments are all CLI +// arguments starting with the first non-flag argument. +// +// The usage argument should describe what these arguments are, and its notation +// should indicate if they are optional or variadic. For example: +// +// // optional variadic +// "[names...]" +// +// // required single args +// " " +// +// // Mixed +// " [baz] [other...]" +// +func (c *Cfg) Args(usage string) *[]string { + + args := new([]string) + + c.flagSet.Usage = func() { + fmt.Fprintf(os.Stderr, "USAGE [flags...] %s\n", usage) + fmt.Fprintf(os.Stderr, "\nFLAGS\n\n") + c.flagSet.PrintDefaults() + } + + c.OnInit(func(ctx context.Context) error { + *args = c.flagSet.Args() + return nil + }) + + return args +} + // String is equivalent to flag.FlagSet's String method, but will additionally // set up an environment variable for the parameter. func (c *Cfg) String(name, value, usage string) *string { diff --git a/srv/src/cmd/import-posts/main.go b/srv/src/cmd/import-posts/main.go new file mode 100644 index 0000000..6a1e952 --- /dev/null +++ b/srv/src/cmd/import-posts/main.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/adrg/frontmatter" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" + "github.com/mediocregopher/mediocre-go-lib/v2/mlog" +) + +type postFrontmatter struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Tags string `yaml:"tags"` + Series string `yaml:"series"` + Updated string `yaml:"updated"` +} + +func parseDate(str string) (time.Time, error) { + const layout = "2006-01-02" + return time.Parse(layout, str) +} + +var postNameRegexp = regexp.MustCompile(`(20..-..-..)-([^.]+).md`) + +func importPost(postStore post.Store, path string) (post.StoredPost, error) { + + fileName := filepath.Base(path) + fileNameMatches := postNameRegexp.FindStringSubmatch(fileName) + + if len(fileNameMatches) != 3 { + return post.StoredPost{}, fmt.Errorf("file name %q didn't match regex", fileName) + } + + publishedAtStr := fileNameMatches[1] + publishedAt, err := parseDate(publishedAtStr) + if err != nil { + return post.StoredPost{}, fmt.Errorf("parsing publish date %q: %w", publishedAtStr, err) + } + + postID := fileNameMatches[2] + + f, err := os.Open(path) + if err != nil { + return post.StoredPost{}, fmt.Errorf("opening file: %w", err) + } + defer f.Close() + + var matter postFrontmatter + + body, err := frontmatter.Parse(f, &matter) + + if err != nil { + return post.StoredPost{}, fmt.Errorf("parsing frontmatter: %w", err) + } + + // if there is already a post for this ID, delete it, we're overwriting + if err := postStore.Delete(postID); err != nil { + return post.StoredPost{}, fmt.Errorf("deleting post id %q: %w", postID, err) + } + + p := post.Post{ + ID: postID, + Title: matter.Title, + Description: matter.Description, + Tags: strings.Fields(matter.Tags), + Series: matter.Series, + Body: string(body), + } + + if err := postStore.Set(p, publishedAt); err != nil { + return post.StoredPost{}, fmt.Errorf("storing post id %q: %w", p.ID, err) + } + + if matter.Updated != "" { + + lastUpdatedAt, err := parseDate(matter.Updated) + if err != nil { + return post.StoredPost{}, fmt.Errorf("parsing updated date %q: %w", matter.Updated, err) + } + + // as a hack, we store the post again with the updated date as now. This + // will update the LastUpdatedAt field in the Store. + if err := postStore.Set(p, lastUpdatedAt); err != nil { + return post.StoredPost{}, fmt.Errorf("updating post id %q: %w", p.ID, err) + } + } + + storedPost, err := postStore.GetByID(p.ID) + if err != nil { + return post.StoredPost{}, fmt.Errorf("retrieving stored post by id %q: %w", p.ID, err) + } + + return storedPost, nil +} + +func main() { + + ctx := context.Background() + + cfg := cfg.NewBlogCfg(cfg.Params{}) + + var dataDir cfgpkg.DataDir + dataDir.SetupCfg(cfg) + defer dataDir.Close() + ctx = mctx.WithAnnotator(ctx, &dataDir) + + paths := cfg.Args("") + + // 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 len(*paths) == 0 { + logger.FatalString(ctx, "no paths given") + } + + postStore, err := post.NewStore(post.StoreParams{ + DataDir: dataDir, + }) + if err != nil { + logger.Fatal(ctx, "initializing post store", err) + } + defer postStore.Close() + + for _, path := range *paths { + + ctx := mctx.Annotate(ctx, "postPath", path) + + storedPost, err := importPost(postStore, path) + if err != nil { + logger.Error(ctx, "importing post", err) + } + + ctx = mctx.Annotate(ctx, + "postID", storedPost.ID, + "postTitle", storedPost.Title, + "postDescription", storedPost.Description, + "postTags", storedPost.Tags, + "postSeries", storedPost.Series, + "postPublishedAt", storedPost.PublishedAt, + ) + + if !storedPost.LastUpdatedAt.IsZero() { + ctx = mctx.Annotate(ctx, + "postLastUpdatedAt", storedPost.LastUpdatedAt) + } + + logger.Info(ctx, "post stored") + } +} diff --git a/srv/src/go.mod b/srv/src/go.mod index 6a912e2..48ca311 100644 --- a/srv/src/go.mod +++ b/srv/src/go.mod @@ -3,6 +3,7 @@ module github.com/mediocregopher/blog.mediocregopher.com/srv go 1.16 require ( + github.com/adrg/frontmatter v0.2.0 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.15.0 github.com/google/uuid v1.3.0 diff --git a/srv/src/go.sum b/srv/src/go.sum index 77aac2e..79b2bf6 100644 --- a/srv/src/go.sum +++ b/srv/src/go.sum @@ -1,9 +1,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= +github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -241,6 +244,7 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=