~samwhited/blogsync

ref: v0.0.1 blogsync/convert.go -rw-r--r-- 4.2 KiB
c7d923e2Sam Whited blogsync: fix name of convert command flagset 1 year, 10 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright 2019 The Blog Sync Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package main

import (
	"bufio"
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"

	"github.com/BurntSushi/toml"
	"mellium.im/blogsync/internal/blog"
	"mellium.im/cli"
)

func convertCmd(logger, debug *log.Logger) *cli.Command {
	var (
		dryRun  = false
		content = "content/"
	)
	flags := flag.NewFlagSet("convert", flag.ContinueOnError)
	flags.BoolVar(&dryRun, "dry-run", dryRun, "Perform a trial run with no changes made")
	flags.StringVar(&content, "content", content, "A directory containing pages and posts")

	return &cli.Command{
		Usage: "convert [options]",
		Flags: flags,
		Description: `Converts post metadata to the required format.

Convert searches the content directory for posts and performs the following
transformations on any posts it finds:

	- Convert YAML frontmatter beginning with --- to TOML
	- Convert "date" and "lastmod" fields to TOML date types
	- Ensure the body has a single leading and trailing \n and trim other leading
	  and trailing whitespace

This command does not attempt to preserve files if it fails. Be sure to make a
backup or commit all files to source control before converting them.`,
		Run: func(cmd *cli.Command, args ...string) error {
			return blog.WalkPages(content, func(path string, info os.FileInfo, err error) error {
				fd, err := os.OpenFile(path, os.O_RDWR, 0666)
				if err != nil {
					logger.Printf("error opening %s, skipping: %v", path, err)
					return nil
				}
				defer func() {
					if err := fd.Close(); err != nil {
						debug.Printf("error closing %s: %v", path, err)
					}
				}()

				var madeChanges bool
				f := bufio.NewReader(fd)
				meta := make(blog.Metadata)
				header, err := meta.Decode(f)
				if err != nil {
					logger.Printf("error decoding metadata for %s, skipping: %v", path, err)
					return nil
				}

				if header != "+++\n" {
					madeChanges = true
					debug.Printf("converting non-TOML frontmatter in %s…", path)
				}
				const (
					dateKey   = "date"
					updateKey = "lastmod"
				)
				if date, ok := meta[dateKey]; ok {
					if _, ok := date.(string); ok {
						debug.Printf("converting string date in %s…", path)
						meta[dateKey] = meta.GetTime(dateKey)
						madeChanges = true
					}
				}
				if date, ok := meta[updateKey]; ok {
					if _, ok := date.(string); ok {
						debug.Printf("converting string lastmod in %s…", path)
						meta[updateKey] = meta.GetTime(updateKey)
						madeChanges = true
					}
				}

				body, err := ioutil.ReadAll(f)
				if err != nil {
					logger.Printf("error reading body from %s, skipping: %v", path, err)
					return nil
				}
				prevBody := string(body)
				body = bytes.TrimSpace(body)
				if len(body) > 0 {
					body = append([]byte{'\n'}, body...)
					body = append(body, '\n')
				}
				if !bytes.Equal([]byte(prevBody), body) {
					logger.Printf("trimming body on %s…", path)
					madeChanges = true
				}

				// If there are no changes to make, don't bother rewriting the file
				// which could cause a diff because we can't guarantee the order of
				// metadata in the frontmatter.
				if !madeChanges || dryRun {
					return nil
				}

				_, err = fd.Seek(0, io.SeekStart)
				if err != nil {
					return fmt.Errorf("error seeking in %s: %v", path, err)
				}
				err = fd.Truncate(0)
				if err != nil {
					return fmt.Errorf("error truncating %s: %v", path, err)
				}

				// Write the new metadata to the file
				_, err = fmt.Fprint(fd, "+++\n")
				if err != nil {
					logger.Printf("could not write header start to %s: %v", path, err)
				}
				e := toml.NewEncoder(fd)
				err = e.Encode(meta)
				if err != nil {
					logger.Printf("error encoding TOML in %s: %v", path, err)
				}
				_, err = fmt.Fprint(fd, "+++\n")
				if err != nil {
					logger.Printf("could not write header close to %s: %v", path, err)
				}

				// If there is no body, we're done. Don't bother adding an extra
				// trailing newline.
				if len(body) == 0 {
					return nil
				}

				_, err = fd.Write(body)
				if err != nil {
					logger.Printf("failed to write body to %s: %v", path, err)
				}
				return nil
			})
		},
	}
}