Skip to content

Commit

Permalink
Merge pull request #128 from x-motemen/draft
Browse files Browse the repository at this point in the history
  • Loading branch information
Songmu authored Nov 4, 2023
2 parents d17cd14 + 3e0a42b commit 21c6ee7
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 94 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env*
/blogsync
/dist
coverage.out
Expand Down
51 changes: 35 additions & 16 deletions broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"

"github.com/motemen/go-wsse"
"github.com/x-motemen/blogsync/atom"
Expand Down Expand Up @@ -89,7 +90,29 @@ 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)
if e.localPath != "" {
return e.localPath
}
if e.URL == nil {
return ""
}
localPath := e.URL.Path

if e.IsDraft && strings.Contains(e.EditURL, "/atom/entry/") {
subdir, entryPath := extractEntryPath(e.URL.Path)
if entryPath == "" {
return ""
}
if isLikelyGivenPath(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 + paths[7] // path[7] is entryID
}
}
}
return filepath.Join(b.localRoot(), localPath+entryExt)
}

func (b *broker) StoreFresh(e *entry, path string) (bool, error) {
Expand All @@ -109,6 +132,16 @@ 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/") {
_, entryPath := extractEntryPath(e.URL.Path)
if entryPath == "" {
return fmt.Errorf("invalid path: %s", e.URL.Path)
}
if isLikelyGivenPath(entryPath) {
e.URL = nil
}
}

dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
Expand Down Expand Up @@ -148,10 +181,7 @@ 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))
return b.Store(newEntry, b.LocalPath(newEntry), b.LocalPath(e))
}

func (b *broker) PostEntry(e *entry, isPage bool) error {
Expand All @@ -165,10 +195,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), "")
}

Expand All @@ -180,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 == "" {
Expand Down
65 changes: 0 additions & 65 deletions broker_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package main

import (
"net/url"
"runtime"
"testing"
"time"
)

func TestEntryEndPointUrl(t *testing.T) {
Expand Down Expand Up @@ -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)
}
})
}
}
2 changes: 1 addition & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions doc/file-structure.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -68,6 +68,7 @@ type entry struct {
LastModified *time.Time
Content string
ContentType string
localPath string
}

func (e *entry) HeaderString() string {
Expand Down Expand Up @@ -278,3 +279,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
}
49 changes: 38 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -187,6 +188,22 @@ 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}$`)
// タイトルフォーマット: 2011/11/07/週末は川に行きました
titlePathReg = regexp.MustCompile(`^2[01][0-9]{2}/[01][0-9]/[0-3][0-9]/.+$`)
draftDir = "_draft/"
)

func isLikelyGivenPath(p string) bool {
return defaultBlogPathReg.MatchString(p) ||
hatenaDiaryPathReg.MatchString(p) ||
titlePathReg.MatchString(p)
}

var commandPush = &cli.Command{
Name: "push",
Usage: "Push local entries to remote",
Expand All @@ -207,6 +224,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
Expand All @@ -223,16 +248,10 @@ var commandPush = &cli.Command{
ti := time.Now()
entry.LastModified = &ti
}
entry.localPath = path

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)
Expand All @@ -242,11 +261,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 {
Expand All @@ -264,6 +283,14 @@ var commandPush = &cli.Command{
return fmt.Errorf("cannot find blog for %s", path)
}

blogPath, _ := filepath.Rel(bc.localRoot(), path)
blogPath = "/" + filepath.ToSlash(blogPath)

if _, entryPath := extractEntryPath(path); entryPath != "" {
if !isLikelyGivenPath(entryPath) && !strings.HasPrefix(entryPath, draftDir) {
entry.CustomPath = entryPath
}
}
_, err = newBroker(bc, c.App.Writer).UploadFresh(entry)
if err != nil {
return err
Expand Down Expand Up @@ -298,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
}
Expand Down
Loading

0 comments on commit 21c6ee7

Please sign in to comment.