From d34cf79e5ae1e5535a41ffa74b28933e3d3feb5d Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Oct 2023 01:43:23 +0900 Subject: [PATCH 01/10] adjust customPath from local file path --- main.go | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 4a7263f..936313c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "strings" "time" @@ -187,6 +188,13 @@ var commandFetch = &cli.Command{ }, } +var ( + defaultBlogPathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/[0-9]{6}$`) + hatenaDiaryPathReg = regexp.MustCompile(`^2[01][0-9]{2}[01][0-9][0-3][0-9]/[0-9]{9,12}$`) + titlePathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/.+$`) + draftReg = regexp.MustCompile(`^_draft/`) +) + var commandPush = &cli.Command{ Name: "push", Usage: "Push local entries to remote", @@ -207,6 +215,14 @@ var commandPush = &cli.Command{ } for _, path := range c.Args().Slice() { + if !filepath.IsAbs(path) { + var err error + path, err = filepath.Abs(path) + if err != nil { + return err + } + } + f, err := os.Open(path) if err != nil { return err @@ -226,13 +242,6 @@ var commandPush = &cli.Command{ if entry.EditURL == "" { // post new entry - if !filepath.IsAbs(path) { - var err error - path, err = filepath.Abs(path) - if err != nil { - return err - } - } bc := conf.detectBlogConfig(path) if bc == nil { return fmt.Errorf("cannot find blog for %q", path) @@ -264,6 +273,18 @@ var commandPush = &cli.Command{ return fmt.Errorf("cannot find blog for %s", path) } + blogPath, _ := filepath.Rel(bc.localRoot(), path) + blogPath = "/" + filepath.ToSlash(blogPath) + if stuffs := strings.SplitN(blogPath, "/entry/", 2); len(stuffs) == 2 { + cPath := strings.TrimSuffix(stuffs[1], entryExt) + if !defaultBlogPathReg.MatchString(cPath) && + !hatenaDiaryPathReg.MatchString(cPath) && + !titlePathReg.MatchString(cPath) && + !draftReg.MatchString(cPath) { + + entry.CustomPath = cPath + } + } _, err = newBroker(bc, c.App.Writer).UploadFresh(entry) if err != nil { return err From 1e81f8ec2215ae156817e54fc182afe561cb8936 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Oct 2023 01:54:22 +0900 Subject: [PATCH 02/10] adjust LocalPath --- broker.go | 19 ++++++++++++++++++- main.go | 14 ++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/broker.go b/broker.go index 3a6e5c3..caa7e6c 100644 --- a/broker.go +++ b/broker.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "io" "net/http" "os" @@ -89,7 +90,23 @@ func (b *broker) FetchRemoteEntries(published, drafts bool) ([]*entry, error) { const entryExt = ".md" // TODO regard re.ContentType func (b *broker) LocalPath(e *entry) string { - return filepath.Join(b.localRoot(), e.URL.Path+entryExt) + localPath := e.URL.Path + + if e.IsDraft && strings.Contains(e.EditURL, "/atom/entry/") { + stuffs := strings.SplitN(e.URL.Path, "/entry/", 2) + if len(stuffs) != 2 { + return "" + } + + cPath := stuffs[1] + if isGivenPath(cPath) { + paths := strings.Split(e.EditURL, "/") + if len(paths) == 8 { + localPath = stuffs[0] + "/entry/" + draftDir + paths[7] + } + } + } + return filepath.Join(b.localRoot(), localPath+entryExt) } func (b *broker) StoreFresh(e *entry, path string) (bool, error) { diff --git a/main.go b/main.go index 936313c..fb0210e 100644 --- a/main.go +++ b/main.go @@ -192,9 +192,15 @@ var ( defaultBlogPathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/[0-9]{6}$`) hatenaDiaryPathReg = regexp.MustCompile(`^2[01][0-9]{2}[01][0-9][0-3][0-9]/[0-9]{9,12}$`) titlePathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/.+$`) - draftReg = regexp.MustCompile(`^_draft/`) + draftDir = "_draft/" ) +func isGivenPath(path string) bool { + return defaultBlogPathReg.MatchString(path) || + hatenaDiaryPathReg.MatchString(path) || + titlePathReg.MatchString(path) +} + var commandPush = &cli.Command{ Name: "push", Usage: "Push local entries to remote", @@ -277,11 +283,7 @@ var commandPush = &cli.Command{ blogPath = "/" + filepath.ToSlash(blogPath) if stuffs := strings.SplitN(blogPath, "/entry/", 2); len(stuffs) == 2 { cPath := strings.TrimSuffix(stuffs[1], entryExt) - if !defaultBlogPathReg.MatchString(cPath) && - !hatenaDiaryPathReg.MatchString(cPath) && - !titlePathReg.MatchString(cPath) && - !draftReg.MatchString(cPath) { - + if !isGivenPath(cPath) && !strings.HasPrefix(cPath, draftDir) { entry.CustomPath = cPath } } From be3da10dcee5a3caf4c66321f4044b9e86a88d00 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Oct 2023 02:22:47 +0900 Subject: [PATCH 03/10] don't write CustomPath to frontmatter --- broker.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/broker.go b/broker.go index caa7e6c..64131c8 100644 --- a/broker.go +++ b/broker.go @@ -2,11 +2,11 @@ package main import ( "fmt" - "strings" "io" "net/http" "os" "path/filepath" + "strings" "github.com/motemen/go-wsse" "github.com/x-motemen/blogsync/atom" @@ -165,9 +165,6 @@ func (b *broker) PutEntry(e *entry) error { if err != nil { return err } - if e.CustomPath != "" { - newEntry.CustomPath = e.CustomPath - } return b.Store(newEntry, b.LocalPath(newEntry), b.originalPath(e)) } @@ -182,10 +179,6 @@ func (b *broker) PostEntry(e *entry, isPage bool) error { if err != nil { return err } - if e.CustomPath != "" { - newEntry.CustomPath = e.CustomPath - } - return b.Store(newEntry, b.LocalPath(newEntry), "") } From 2ed1c630a8b1bedb48dc804b760649f3af417f00 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Oct 2023 02:26:02 +0900 Subject: [PATCH 04/10] don't write URL to frontmatter when draft and non custom-path --- broker.go | 11 +++++++++++ entry.go | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/broker.go b/broker.go index 64131c8..1041594 100644 --- a/broker.go +++ b/broker.go @@ -126,6 +126,17 @@ func (b *broker) StoreFresh(e *entry, path string) (bool, error) { func (b *broker) Store(e *entry, path, origPath string) error { logf("store", "%s", path) + if e.IsDraft && strings.Contains(e.EditURL, "/atom/entry/") { + stuffs := strings.SplitN(e.URL.Path, "/entry/", 2) + if len(stuffs) != 2 { + return fmt.Errorf("invalid path: %s", e.URL.Path) + } + cPath := stuffs[1] + if isGivenPath(cPath) { + e.URL = nil + } + } + dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return err diff --git a/entry.go b/entry.go index 0f1fcea..7fe9969 100644 --- a/entry.go +++ b/entry.go @@ -27,7 +27,7 @@ type entryHeader struct { Title string `yaml:"Title"` Category []string `yaml:"Category,omitempty"` Date *time.Time `yaml:"Date,omitempty"` - URL *entryURL `yaml:"URL"` + URL *entryURL `yaml:"URL,omitempty"` EditURL string `yaml:"EditURL"` PreviewURL string `yaml:"PreviewURL,omitempty"` IsDraft bool `yaml:"Draft,omitempty"` From bb7bcc8b77cd6bc33c5ebb74a28e36bfc8c3c41d Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Oct 2023 14:12:19 +0900 Subject: [PATCH 05/10] define extractEntryPath --- broker.go | 17 +++++++---------- entry.go | 9 +++++++++ main.go | 16 ++++++++-------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/broker.go b/broker.go index 1041594..267c622 100644 --- a/broker.go +++ b/broker.go @@ -93,16 +93,14 @@ func (b *broker) LocalPath(e *entry) string { localPath := e.URL.Path if e.IsDraft && strings.Contains(e.EditURL, "/atom/entry/") { - stuffs := strings.SplitN(e.URL.Path, "/entry/", 2) - if len(stuffs) != 2 { + subdir, entryPath := extractEntryPath(e.URL.Path) + if entryPath == "" { return "" } - - cPath := stuffs[1] - if isGivenPath(cPath) { + if isGivenPath(entryPath) { paths := strings.Split(e.EditURL, "/") if len(paths) == 8 { - localPath = stuffs[0] + "/entry/" + draftDir + paths[7] + localPath = subdir + "/entry/" + draftDir + entryPath } } } @@ -127,12 +125,11 @@ func (b *broker) Store(e *entry, path, origPath string) error { logf("store", "%s", path) if e.IsDraft && strings.Contains(e.EditURL, "/atom/entry/") { - stuffs := strings.SplitN(e.URL.Path, "/entry/", 2) - if len(stuffs) != 2 { + _, entryPath := extractEntryPath(e.URL.Path) + if entryPath == "" { return fmt.Errorf("invalid path: %s", e.URL.Path) } - cPath := stuffs[1] - if isGivenPath(cPath) { + if isGivenPath(entryPath) { e.URL = nil } } diff --git a/entry.go b/entry.go index 7fe9969..33b108d 100644 --- a/entry.go +++ b/entry.go @@ -278,3 +278,12 @@ func modTime(fpath string) (time.Time, error) { } return ti, nil } + +func extractEntryPath(p string) (subdir string, entryPath string) { + stuffs := strings.SplitN(p, "/entry/", 2) + if len(stuffs) != 2 { + return "", "" + } + entryPath = strings.TrimSuffix(stuffs[1], entryExt) + return stuffs[0], entryPath +} diff --git a/main.go b/main.go index fb0210e..89dc421 100644 --- a/main.go +++ b/main.go @@ -192,7 +192,7 @@ var ( defaultBlogPathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/[0-9]{6}$`) hatenaDiaryPathReg = regexp.MustCompile(`^2[01][0-9]{2}[01][0-9][0-3][0-9]/[0-9]{9,12}$`) titlePathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/.+$`) - draftDir = "_draft/" + draftDir = "_draft/" ) func isGivenPath(path string) bool { @@ -257,11 +257,11 @@ var commandPush = &cli.Command{ // relative position from the entry directory is obtained as a custom path as below. blogPath, _ := filepath.Rel(bc.localRoot(), path) blogPath = "/" + filepath.ToSlash(blogPath) - stuffs := strings.SplitN(blogPath, "/entry/", 2) - if len(stuffs) != 2 { + _, entryPath := extractEntryPath(path) + if entryPath == "" { return fmt.Errorf("%q is not a blog entry", path) } - entry.CustomPath = strings.TrimSuffix(stuffs[1], entryExt) + entry.CustomPath = entryPath b := newBroker(bc, c.App.Writer) err = b.PostEntry(entry, false) if err != nil { @@ -281,10 +281,10 @@ var commandPush = &cli.Command{ blogPath, _ := filepath.Rel(bc.localRoot(), path) blogPath = "/" + filepath.ToSlash(blogPath) - if stuffs := strings.SplitN(blogPath, "/entry/", 2); len(stuffs) == 2 { - cPath := strings.TrimSuffix(stuffs[1], entryExt) - if !isGivenPath(cPath) && !strings.HasPrefix(cPath, draftDir) { - entry.CustomPath = cPath + + if _, entryPath := extractEntryPath(path); entryPath != "" { + if !isGivenPath(entryPath) && !strings.HasPrefix(entryPath, draftDir) { + entry.CustomPath = entryPath } } _, err = newBroker(bc, c.App.Writer).UploadFresh(entry) From c34c669110f42350bedd8ff001f9ab9d4f235362 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Oct 2023 23:59:32 +0900 Subject: [PATCH 06/10] adjust originalPath --- broker.go | 12 ++++++++++-- entry.go | 1 + main.go | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/broker.go b/broker.go index 267c622..2dec43f 100644 --- a/broker.go +++ b/broker.go @@ -90,6 +90,12 @@ func (b *broker) FetchRemoteEntries(published, drafts bool) ([]*entry, error) { const entryExt = ".md" // TODO regard re.ContentType func (b *broker) LocalPath(e *entry) string { + if e.localPath != "" { + return e.localPath + } + if e.URL == nil { + return "" + } localPath := e.URL.Path if e.IsDraft && strings.Contains(e.EditURL, "/atom/entry/") { @@ -98,9 +104,11 @@ func (b *broker) LocalPath(e *entry) string { return "" } if isGivenPath(entryPath) { + // EditURL is like bellow + // https://blog.hatena.ne.jp/Songmu/songmu.hatenadiary.org/atom/entry/6801883189050452361 paths := strings.Split(e.EditURL, "/") if len(paths) == 8 { - localPath = subdir + "/entry/" + draftDir + entryPath + localPath = subdir + "/entry/" + draftDir + paths[7] // path[7] is entryID } } } @@ -173,7 +181,7 @@ func (b *broker) PutEntry(e *entry) error { if err != nil { return err } - return b.Store(newEntry, b.LocalPath(newEntry), b.originalPath(e)) + return b.Store(newEntry, b.LocalPath(newEntry), b.LocalPath(e)) } func (b *broker) PostEntry(e *entry, isPage bool) error { diff --git a/entry.go b/entry.go index 33b108d..1e65504 100644 --- a/entry.go +++ b/entry.go @@ -68,6 +68,7 @@ type entry struct { LastModified *time.Time Content string ContentType string + localPath string } func (e *entry) HeaderString() string { diff --git a/main.go b/main.go index 89dc421..5fa3b78 100644 --- a/main.go +++ b/main.go @@ -245,6 +245,7 @@ var commandPush = &cli.Command{ ti := time.Now() entry.LastModified = &ti } + entry.localPath = path if entry.EditURL == "" { // post new entry From 5ba32ffee3ccb8d3247763701c264dde03815447 Mon Sep 17 00:00:00 2001 From: Songmu Date: Tue, 31 Oct 2023 02:31:48 +0900 Subject: [PATCH 07/10] enhance comment --- broker.go | 4 ++-- main.go | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/broker.go b/broker.go index 2dec43f..980f6f4 100644 --- a/broker.go +++ b/broker.go @@ -103,7 +103,7 @@ func (b *broker) LocalPath(e *entry) string { if entryPath == "" { return "" } - if isGivenPath(entryPath) { + if isLikelyGivenPath(entryPath) { // EditURL is like bellow // https://blog.hatena.ne.jp/Songmu/songmu.hatenadiary.org/atom/entry/6801883189050452361 paths := strings.Split(e.EditURL, "/") @@ -137,7 +137,7 @@ func (b *broker) Store(e *entry, path, origPath string) error { if entryPath == "" { return fmt.Errorf("invalid path: %s", e.URL.Path) } - if isGivenPath(entryPath) { + if isLikelyGivenPath(entryPath) { e.URL = nil } } diff --git a/main.go b/main.go index 5fa3b78..1c27117 100644 --- a/main.go +++ b/main.go @@ -189,16 +189,19 @@ var commandFetch = &cli.Command{ } var ( + // 標準フォーマット: 2011/11/07/161845 defaultBlogPathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/[0-9]{6}$`) + // はてなダイアリー風フォーマット: 20111107/1320650325 hatenaDiaryPathReg = regexp.MustCompile(`^2[01][0-9]{2}[01][0-9][0-3][0-9]/[0-9]{9,12}$`) - titlePathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/.+$`) - draftDir = "_draft/" + // タイトルフォーマット: 2011/11/07/週末は川に行きました + titlePathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/.+$`) + draftDir = "_draft/" ) -func isGivenPath(path string) bool { - return defaultBlogPathReg.MatchString(path) || - hatenaDiaryPathReg.MatchString(path) || - titlePathReg.MatchString(path) +func isLikelyGivenPath(p string) bool { + return defaultBlogPathReg.MatchString(p) || + hatenaDiaryPathReg.MatchString(p) || + titlePathReg.MatchString(p) } var commandPush = &cli.Command{ @@ -284,7 +287,7 @@ var commandPush = &cli.Command{ blogPath = "/" + filepath.ToSlash(blogPath) if _, entryPath := extractEntryPath(path); entryPath != "" { - if !isGivenPath(entryPath) && !strings.HasPrefix(entryPath, draftDir) { + if !isLikelyGivenPath(entryPath) && !strings.HasPrefix(entryPath, draftDir) { entry.CustomPath = entryPath } } From ddc8a565c585702fc3ee3dc968e9e406b9b2764b Mon Sep 17 00:00:00 2001 From: Songmu Date: Sat, 4 Nov 2023 00:45:17 +0900 Subject: [PATCH 08/10] remove func originalPath --- broker.go | 7 ------ broker_test.go | 65 -------------------------------------------------- 2 files changed, 72 deletions(-) diff --git a/broker.go b/broker.go index 980f6f4..f1fc0ef 100644 --- a/broker.go +++ b/broker.go @@ -206,13 +206,6 @@ func (b *broker) RemoveEntry(e *entry, p string) error { return os.Remove(p) } -func (b *broker) originalPath(e *entry) string { - if e.URL == nil { - return "" - } - return b.LocalPath(e) -} - func atomEndpointURLRoot(bc *blogConfig) string { owner := bc.Owner if owner == "" { diff --git a/broker_test.go b/broker_test.go index 2b346bf..321fe3a 100644 --- a/broker_test.go +++ b/broker_test.go @@ -1,10 +1,7 @@ package main import ( - "net/url" - "runtime" "testing" - "time" ) func TestEntryEndPointUrl(t *testing.T) { @@ -41,65 +38,3 @@ func TestEntryEndPointUrl(t *testing.T) { }) } } - -func TestOriginalPath(t *testing.T) { - u, _ := url.Parse("http://hatenablog.example.com/2") - jst, _ := time.LoadLocation("Asia/Tokyo") - d := time.Date(2023, 10, 10, 0, 0, 0, 0, jst) - - testCases := []struct { - name string - entry entry - expect string - expectWindows string - }{ - { - name: "entry has URL", - entry: entry{ - entryHeader: &entryHeader{ - URL: &entryURL{u}, - EditURL: u.String() + "/edit", - Title: "test", - Date: &d, - IsDraft: true, - }, - LastModified: &d, - Content: "テスト", - }, - expect: "example1.hatenablog.com/2.md", - expectWindows: "example1.hatenablog.com\\2.md", - }, - { - name: "Not URL", - entry: entry{ - entryHeader: &entryHeader{ - EditURL: u.String() + "/edit", - Title: "hoge", - IsDraft: true, - }, - LastModified: &d, - Content: "テスト", - }, - expect: "", - expectWindows: "", - }, - } - - config := blogConfig{ - BlogID: "example1.hatenablog.com", - Username: "sample1", - } - broker := newBroker(&config, nil) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := broker.originalPath(&tc.entry) - if runtime.GOOS == "windows" { - tc.expect = tc.expectWindows - } - if tc.expect != got { - t.Errorf("expect: %s, got: %s", tc.expect, got) - } - }) - } -} From 8a7c7c18b207a8b9e2f3c223708e24df81c9ad1c Mon Sep 17 00:00:00 2001 From: Songmu Date: Sat, 4 Nov 2023 04:43:02 +0900 Subject: [PATCH 09/10] add main_test.go --- .github/workflows/test.yaml | 5 + .gitignore | 1 + config_test.go | 2 +- main.go | 2 +- main_test.go | 202 ++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 main_test.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 58b3cd4..b54b700 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -30,6 +30,11 @@ jobs: go-version-file: go.mod - name: test run: go test -coverprofile coverage.out -covermode atomic ./... + env: + BLOGSYNC_TEST_BLOG: ${{ secrets.BLOGSYNC_BLOG }} + BLOGSYNC_USERNAME: ${{ secrets.BLOGSYNC_USERNAME }} + BLOGSYNC_PASSWORD: ${{ secrets.BLOGSYNC_PASSWORD }} + BLOGSYNC_OWNER: ${{ secrets.BLOGSYNC_OWNER }} - name: Send coverage uses: shogo82148/actions-goveralls@v1 with: diff --git a/.gitignore b/.gitignore index 1b2b627..50c5434 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env* /blogsync /dist coverage.out diff --git a/config_test.go b/config_test.go index 943b5d0..f403c84 100644 --- a/config_test.go +++ b/config_test.go @@ -32,7 +32,7 @@ func TestLoadConfigration(t *testing.T) { if tmpVal != "" { return os.Setenv(envKey, tmpVal) } - return nil + return os.Unsetenv(envKey) }, func() error { if ok { return os.Setenv(envKey, env) diff --git a/main.go b/main.go index 1c27117..16d58f2 100644 --- a/main.go +++ b/main.go @@ -325,7 +325,7 @@ var commandPost = &cli.Command{ return fmt.Errorf("blog not found: %s", blog) } - entry, err := entryFromReader(os.Stdin) + entry, err := entryFromReader(c.App.Reader) if err != nil { return err } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..c45624e --- /dev/null +++ b/main_test.go @@ -0,0 +1,202 @@ +//go:build darwin || integration + +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/urfave/cli/v2" +) + +func blogsyncApp(app *cli.App) func(...string) (string, error) { + buf := &bytes.Buffer{} + app.Writer = buf + return func(args ...string) (string, error) { + buf.Reset() + err := app.Run(append([]string{""}, args...)) + return strings.TrimSpace(buf.String()), err + } +} + +func exists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +var draftFileReg = regexp.MustCompile(`entry/_draft/\d+\.md$`) + +func TestBlogsync(t *testing.T) { + blogID := os.Getenv("BLOGSYNC_TEST_BLOG") + if blogID == "" { + t.Skip("BLOGSYNC_TEST_BLOG not set") + } + + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(pwd); err != nil { + t.Fatal(err) + } + }() + + dir, err = filepath.EvalSymlinks(dir) + if err != nil { + t.Fatal(err) + } + + confYAML := fmt.Sprintf(`%s: + local_root: . + omit_domain: true +`, blogID) + if owner := os.Getenv("BLOGSYNC_OWNER"); owner != "" { + confYAML += fmt.Sprintf(" owner: %s\n", owner) + } + conf := filepath.Join(dir, "blogsync.yaml") + if err := os.WriteFile(conf, []byte(confYAML), 0644); err != nil { + t.Fatal(err) + } + + app := newApp() + blogsync := blogsyncApp(app) + + t.Run("pull", func(t *testing.T) { + if _, err := blogsync("pull"); err != nil { + t.Fatal(err) + } + }) + + t.Run("post draft and publish", func(t *testing.T) { + t.Log("Post a draft without a custom path and check if the file is saved in the proper location") + app.Reader = strings.NewReader("draft\n") + entryFile, err := blogsync("post", "--draft", blogID) + app.Reader = os.Stdin + if err != nil { + t.Fatal(err) + } + defer func() { + t.Log("remove the published entry") + if _, err := blogsync("remove", entryFile); err != nil { + t.Fatal(err) + } + }() + + if !draftFileReg.MatchString(entryFile) { + t.Fatalf("unexpected draft file: %s", entryFile) + } + + t.Log("Draft files under `_draft/` will revert to the original file name if the file is renamed and pushed again") + d, f := filepath.Split(entryFile) + movedPath := filepath.Join(d, "_"+f) + if err := os.Rename(entryFile, movedPath); err != nil { + t.Fatal(err) + } + originalEntryFile := entryFile + entryFile = movedPath + if err := appendFile(movedPath, "updated\n"); err != nil { + t.Fatal(err) + } + draftFile, err := blogsync("push", entryFile) + if err != nil { + t.Fatal(err) + } + if draftFile != originalEntryFile { + entryFile = draftFile + t.Fatalf("unexpected draft file: %s", draftFile) + } + if exists(entryFile) { + t.Errorf("renamed draft file is not deleted: %s", movedPath) + } + entryFile = draftFile + + t.Log("When a draft is published, a URL is issued and the file is saved in the corresponding location") + publishedFile, err := blogsync("push", "--publish", entryFile) + if err != nil { + t.Fatal(err) + } + if exists(entryFile) { + t.Errorf("draft file not deleted: %s", entryFile) + } + entryFile = publishedFile + + _, entryPath := extractEntryPath(entryFile) + if !isLikelyGivenPath(entryPath) { + t.Errorf("unexpected published file: %s", entryFile) + } + }) + + t.Run("post draft and publish with custom path", func(t *testing.T) { + t.Log("Creating a draft with a custom path saves the file in the specified location, not under `_draft/`") + localFile := filepath.Join(dir, "entry", time.Now().Format("custom-20060102150405")+".md") + if err := os.WriteFile(localFile, []byte(`--- +Draft: true +--- +test`), 0644); err != nil { + t.Fatal(err) + } + entryFile, err := blogsync("push", localFile) + if err != nil { + t.Fatal(err) + } + defer func() { + t.Log("remove the published entry") + if _, err := blogsync("remove", entryFile); err != nil { + t.Fatal(err) + } + }() + if entryFile != localFile { + t.Errorf("unexpected published file: %s", entryFile) + } + + t.Log("When publishing a draft with a custom path, the file location is unchanged") + publishedFile, err := blogsync("push", "--publish", entryFile) + if err != nil { + t.Fatal(err) + } + if publishedFile != entryFile { + t.Errorf("unexpected published file: %s", publishedFile) + } + + t.Log("If the file name of an entry is changed, the custom path will follow suit") + d, f := filepath.Split(entryFile) + movedPath := filepath.Join(d, "custom-"+f) + if err := os.Rename(entryFile, movedPath); err != nil { + t.Fatal(err) + } + entryFile = movedPath + if err := appendFile(entryFile, "updated\n"); err != nil { + t.Fatal(err) + } + publishedFile, err = blogsync("push", entryFile) + if err != nil { + t.Fatal(err) + } + if publishedFile != entryFile { + entryFile = publishedFile + t.Errorf("unexpected published file: %s", publishedFile) + } + }) +} + +func appendFile(path string, content string) error { + fh, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + if _, err := fh.WriteString(content); err != nil { + return err + } + return fh.Close() +} From 3e0a42b8584518b365474299c61a0b4c0b1f95a2 Mon Sep 17 00:00:00 2001 From: Songmu Date: Sat, 4 Nov 2023 22:24:57 +0900 Subject: [PATCH 10/10] add doc/file-structure.md --- doc/file-structure.md | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 doc/file-structure.md diff --git a/doc/file-structure.md b/doc/file-structure.md new file mode 100644 index 0000000..d7c6dc0 --- /dev/null +++ b/doc/file-structure.md @@ -0,0 +1,59 @@ +# blogsyncのエントリーファイル構成 + +## ブログIDと配信ドメイン +前提として、blogsyncの設定ファイルのキーに使うドメイン状の文字列は「ブログID」です。これはブログ開設時に決めたドメインで不変です。ですので、多くの場合、ブログIDはブログの配信ドメインと一致します。しかし、独自ドメイン利用している場合、ブログIDと配信ドメインは一致しないことに注意してください。この場合、設定した独自ドメインがブログIDにはならず、当初のものがそのまま使われます。 + +例えば、筆者(Songmu)は、 https://blog.song.mu という独自ドメインでブログを運営していますが、開設時に指定した songmu.hateblo.jp がブログIDです。 + +## blogsyncがファイル配置するローカルのルートディレクトリ + +blogsyncは設定ファイルの `local_root` と ブログID を連結したパス(`$local_root/$blogID`)をルートディレクトリとしてコンテンツファイルを配置します。 + +```yaml +songmu.hateblo.jp: + local_root: /Users/Songmu/Blog +``` + +例えば上記のような設定に対して `blogsync pull songmu.hateblo.jp` すると `/Users/Songmu/Blog/songmu.hateblo.jp` 以下にコンテンツファイルが配置されます。 + +ブログIDの `songmu.hateblo.jp` ディレクトリが掘られることが冗長に感じるかもしれません。その場合は `omit_domain` 設定で階層を浅くできます。以下のような具合です。 + + +```yaml +songmu.hateblo.jp: + local_root: /Users/Songmu/Blog + omit_domain: true +``` + +こうすると `/Users/Songmu/Blog` がルートになります。 + +余談ですが、`local_root` 設定が少し分かりづらい理由としては、当初はオリジナル作者(motemen)に複数ブログを特定のディレクトリ配下で管理したいという動機があり `default.local_root` 一つだけ設定しておけば、そこ配下にブログID毎にディレクトリを掘ることを想定してたのではないかと予想しています。なので、ブログ個別設定に`local_root`を設定することをあまり想定していなかった可能性があります。例えば以下のような設定ファイルを想定していたのではないでしょうか。 + +```yaml +songmu.hateblo.jp: +songmu.hatenablog.com: +default: + local_root: /Users/Songmu/Blogs + username: Songmu + ... +``` + +この辺りは、複雑で分かりづらく、`omit_domain` という設定も後付け感があり名付けもイマイチなので、その辺りを含めて将来的に非互換変更を入れる可能性があります。 + +## コンテンツファイルの配置 +blogsyncは公開URLのパスと対応した位置にコンテンツファイルを保存します。サブディレクトリ運用の場合、サブディレクトリ含めて保存されます。また、URLのパスは以下の2種類に分かれます。 + +- 執筆者が明示的に指定するもの。いわゆるslug + - ブログ管理画面上では「カスタムURL」、blogsync上は"CustomPath"と呼ばれる +- はてなブログ側が自動付与するもの + +執筆者が明示しない場合の自動付与は **記事公開時**におこなわれる。標準では `2001/02/03/150405` のような時刻ベースのフォーマットになる。他にも、はてなダイヤリーフォーマットや、タイトルを付与したフォーマットがあり、ブログ管理画面から切り替え設定できる。 + +自動付与の場合「記事公開時に」と書いた通り、下書き時にはURLの付与は行われない。APIのレスポンスには仮のURLが返される。この仮のURLはAPIリクエスト時刻が基準になったものが返されるので、下書き更新時に毎回異なるものが返される。 + +なので、カスタムパス未指定の下書きは、URLのパスが実質的に未確定なので、blogsyncは `entry/_draft/$entryID.md` という位置にファイルを保存します。この `entryID` はエントリーのEditURLに含まれる数字列のIDです。ちなみに、固定ページはカスタムパス指定必須なので、この位置に下書きが保存されることはありません。 + +エントリーのURLを変更したい場合、ローカルのコンテンツファイル名を変更してpushすれば、URLも変更されます。 + +## エントリーと固定ページ +TBA