aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2024-12-23 02:42:06 +0700
committerShulhan <ms@kilabit.info>2025-01-07 00:13:55 +0700
commitb686ea0a41b7af68d4d86ff3cc2c3068ebb88b66 (patch)
treeb8725c3c2e2461e2b0542c76880d385ee5aa6741
parentff338f853eb7537230c84ccc06feae3b63859877 (diff)
downloadciigo-b686ea0a41b7af68d4d86ff3cc2c3068ebb88b66.tar.xz
all: refactoring to use [watchfs/v2]
The [watchfs/v2] bring new enhancements that watching only single file instead of all markup files for changes. This minimize number of goroutine calling os.Stat on each markup files being watched on directory.
-rw-r--r--cmd/ciigo/main.go2
-rw-r--r--convert_options.go15
-rw-r--r--file_markup.go16
-rw-r--r--go.mod11
-rw-r--r--go.sum18
-rw-r--r--watcher.go168
-rw-r--r--watcher_test.go31
7 files changed, 120 insertions, 141 deletions
diff --git a/cmd/ciigo/main.go b/cmd/ciigo/main.go
index a4b734c..ceb6389 100644
--- a/cmd/ciigo/main.go
+++ b/cmd/ciigo/main.go
@@ -52,7 +52,7 @@ func main() {
convertOpts = ciigo.ConvertOptions{
Root: flag.Arg(1),
HTMLTemplate: *htmlTemplate,
- Exclude: *exclude,
+ Exclude: []string{*exclude},
}
err error
)
diff --git a/convert_options.go b/convert_options.go
index 0423644..5879151 100644
--- a/convert_options.go
+++ b/convert_options.go
@@ -6,6 +6,7 @@ package ciigo
import (
"fmt"
"regexp"
+ "strings"
)
const (
@@ -22,7 +23,7 @@ type ConvertOptions struct {
// Exclude define regular expresion to exclude certain paths from
// being scanned.
- Exclude string
+ Exclude []string
// HTMLTemplate the HTML template to be used when converting markup
// file into HTML.
@@ -41,16 +42,20 @@ func (opts *ConvertOptions) init() (err error) {
if len(opts.Root) == 0 {
opts.Root = DefaultRoot
}
- if len(opts.Exclude) > 0 {
- var re *regexp.Regexp
+ for _, str := range opts.Exclude {
+ str = strings.TrimSpace(str)
+ if len(str) == 0 {
+ continue
+ }
- re, err = regexp.Compile(opts.Exclude)
+ var re *regexp.Regexp
+ re, err = regexp.Compile(str)
if err != nil {
return fmt.Errorf(`%s: %w`, logp, err)
}
opts.excRE = append(opts.excRE, re)
- defExcludes = append(defExcludes, opts.Exclude)
+ defExcludes = append(defExcludes, str)
}
return nil
}
diff --git a/file_markup.go b/file_markup.go
index b2d051f..95785bb 100644
--- a/file_markup.go
+++ b/file_markup.go
@@ -20,7 +20,6 @@ const (
type FileMarkup struct {
info os.FileInfo // info contains FileInfo of markup file.
- basePath string // Full path to file without markup extension.
path string // Full path to markup file.
pathHTML string // path to HTML file.
@@ -30,11 +29,7 @@ type FileMarkup struct {
// NewFileMarkup create new FileMarkup instance form file in "filePath".
// The "fi" option is optional, if its nil it will Stat-ed manually.
func NewFileMarkup(filePath string, fi os.FileInfo) (fmarkup *FileMarkup, err error) {
- var (
- logp = `NewFileMarkup`
-
- ext string
- )
+ var logp = `NewFileMarkup`
if len(filePath) == 0 {
return nil, fmt.Errorf(`%s: empty path`, logp)
@@ -46,17 +41,16 @@ func NewFileMarkup(filePath string, fi os.FileInfo) (fmarkup *FileMarkup, err er
}
}
- ext = strings.ToLower(filepath.Ext(filePath))
+ var ext = strings.ToLower(filepath.Ext(filePath))
+ var basePath = strings.TrimSuffix(filePath, ext)
fmarkup = &FileMarkup{
- path: filePath,
info: fi,
- basePath: strings.TrimSuffix(filePath, ext),
+ path: filePath,
+ pathHTML: basePath + `.html`,
kind: markupKind(ext),
}
- fmarkup.pathHTML = fmarkup.basePath + `.html`
-
return fmarkup, nil
}
diff --git a/go.mod b/go.mod
index 3ff5213..b87d456 100644
--- a/go.mod
+++ b/go.mod
@@ -3,18 +3,19 @@
module git.sr.ht/~shulhan/ciigo
-go 1.22.0
+go 1.23.4
require (
- git.sr.ht/~shulhan/asciidoctor-go v0.6.1
- git.sr.ht/~shulhan/pakakeh.go v0.58.1
+ git.sr.ht/~shulhan/asciidoctor-go v0.6.2-0.20250106025231-ed20fc1c23e8
+ git.sr.ht/~shulhan/pakakeh.go v0.59.0
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-meta v1.1.0
)
require (
- golang.org/x/net v0.32.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index 24b8c6a..f26d525 100644
--- a/go.sum
+++ b/go.sum
@@ -1,15 +1,17 @@
-git.sr.ht/~shulhan/asciidoctor-go v0.6.1 h1:uXRFNYV+iiI8XLo8Bp1ZZM/orULkAqD2lfYtra/PzhU=
-git.sr.ht/~shulhan/asciidoctor-go v0.6.1/go.mod h1:QeBhtZf4GjRgENlX2Xe/TXIupqZJUtH6U89YtOvZ08Q=
-git.sr.ht/~shulhan/pakakeh.go v0.58.1 h1:KBb/6rT/IjBOP1MqjI6uKVw/miTjTuwR27e8oWzz3es=
-git.sr.ht/~shulhan/pakakeh.go v0.58.1/go.mod h1:QOiVaVWOilYaB+OlQtQfZo9uSvSVSVP1r8s2zve6imY=
+git.sr.ht/~shulhan/asciidoctor-go v0.6.2-0.20250106025231-ed20fc1c23e8 h1:8qzcug5hnyz1QPVxCf9J3JhIzPSnc+zUK5VQachVQf0=
+git.sr.ht/~shulhan/asciidoctor-go v0.6.2-0.20250106025231-ed20fc1c23e8/go.mod h1:QOHiOctWAPcVlTMOKnnwmpnLrobkv7oNAPAf4gZjtWs=
+git.sr.ht/~shulhan/pakakeh.go v0.59.0 h1:haUyuusXQ0YVgHbbhi46Nkl/CEPhRe+cIJz22kQFGuM=
+git.sr.ht/~shulhan/pakakeh.go v0.59.0/go.mod h1:UsrvzhSUJZpm1Aub99RI/XwtNnbBlJmaNoWWd9++VyI=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
-golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
-golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
+golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/watcher.go b/watcher.go
index 6568629..4bb0a9c 100644
--- a/watcher.go
+++ b/watcher.go
@@ -4,34 +4,35 @@
package ciigo
import (
- "fmt"
"log"
+ "os"
"path/filepath"
- "strings"
"time"
"git.sr.ht/~shulhan/pakakeh.go/lib/clise"
- "git.sr.ht/~shulhan/pakakeh.go/lib/memfs"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/watchfs/v2"
)
// watcher watch for changes on all markup files and convert them
// automatically to HTML.
type watcher struct {
changes *clise.Clise
- watchDir *memfs.DirWatcher
- watchTemplate *memfs.Watcher
+ watchDir *watchfs.DirWatcher
+ watchTemplate *watchfs.FileWatcher
converter *Converter
- // fileMarkups contains all markup files found inside "dir".
+ // fileMarkups contains all markup files found inside the
+ // [options.Root] directory recursively.
// Its used to convert all markup files when the template file
// changes.
fileMarkups map[string]*FileMarkup
- dir string
+ opts watchfs.DirWatcherOptions
}
// newWatcher create a watcher that monitor every files changes in directory
-// "dir" for new, modified, and deleted markup files and HTML template file.
+// [options.Root] for new, modified, and deleted markup files and HTML
+// template file.
//
// The watcher depends on Converter to convert the markup to HTML using
// the HTML template in Converter.
@@ -43,58 +44,63 @@ type watcher struct {
// +-- watchHTMLTemplate +--> DELETE --> Converter.htmlTemplateUseInternal()
// |
// +--> UPDATE --> Converter.convertFileMarkups()
-func newWatcher(converter *Converter, convertOpts ConvertOptions) (w *watcher, err error) {
- var (
- logp = `newWatcher`
- )
-
+func newWatcher(
+ converter *Converter, convertOpts ConvertOptions,
+) (w *watcher, err error) {
w = &watcher{
- dir: convertOpts.Root,
converter: converter,
changes: clise.New(1),
}
- w.watchDir = &memfs.DirWatcher{
- Options: memfs.Options{
- Root: convertOpts.Root,
- Includes: []string{
- `.*\.adoc$`,
- `.*\.md$`,
- },
- Excludes: []string{
- `^\..*`,
- `node_modules/.*`,
- `vendor/.*`,
- },
+ w.opts = watchfs.DirWatcherOptions{
+ FileWatcherOptions: watchfs.FileWatcherOptions{
+ File: filepath.Join(convertOpts.Root, `.ciigo_rescan`),
+ Interval: time.Second,
+ },
+ Root: convertOpts.Root,
+ Includes: []string{
+ `.*\.(adoc|md)$`,
+ },
+ Excludes: []string{
+ `^\..*`,
+ `node_modules/.*`,
+ `vendor/.*`,
},
- Delay: time.Second,
}
- if len(convertOpts.Exclude) > 0 {
- w.watchDir.Options.Excludes = append(w.watchDir.Options.Excludes, convertOpts.Exclude)
- }
+ w.opts.Excludes = append(w.opts.Excludes, convertOpts.Exclude...)
- w.fileMarkups, err = listFileMarkups(convertOpts.Root, convertOpts.excRE)
+ w.watchDir, err = watchfs.WatchDir(w.opts)
if err != nil {
- return nil, fmt.Errorf(`%s: %w`, logp, err)
+ return nil, err
}
+ w.scanFileMarkup()
+
return w, nil
}
-// start watching for changes.
-func (w *watcher) start() (err error) {
- err = w.watchDir.Start()
- if err != nil {
- return fmt.Errorf(`start: %w`, err)
+func (w *watcher) scanFileMarkup() {
+ w.fileMarkups = make(map[string]*FileMarkup)
+ var files = w.watchDir.Files()
+ for path, fi := range files {
+ fmarkup, err := NewFileMarkup(path, fi)
+ if err != nil {
+ continue
+ }
+ w.fileMarkups[path] = fmarkup
}
+}
+// start watching for changes.
+func (w *watcher) start() (err error) {
go w.watchFileMarkup()
if len(w.converter.htmlTemplate) > 0 {
- w.watchTemplate, err = memfs.NewWatcher(w.converter.htmlTemplate, 0)
- if err != nil {
- return fmt.Errorf(`start: %w`, err)
+ var opts = watchfs.FileWatcherOptions{
+ File: w.converter.htmlTemplate,
+ Interval: 5 * time.Second,
}
+ w.watchTemplate = watchfs.WatchFile(opts)
go w.watchHTMLTemplate()
}
return nil
@@ -107,71 +113,56 @@ func (w *watcher) stop() {
}
}
-// watchFileMarkup watch the markup files inside the "content" directory,
-// and re-generate them into HTML file when changed.
+// watchFileMarkup watch the file ".ciigo_rescan" inside the "content"
+// directory and reconvert all the markup into HTML files when its changes.
func (w *watcher) watchFileMarkup() {
var (
logp = `watchFileMarkup`
- ns memfs.NodeState
+ listfi []os.FileInfo
fmarkup *FileMarkup
- ext string
err error
ok bool
)
- for ns = range w.watchDir.C {
- ext = strings.ToLower(filepath.Ext(ns.Node.SysPath))
- if !isExtensionMarkup(ext) {
+ for listfi = range w.watchDir.C {
+ if len(listfi) == 0 {
continue
}
- switch ns.State {
- case memfs.FileStateDeleted:
- log.Printf(`%s: %q deleted`, logp, ns.Node.SysPath)
- fmarkup, ok = w.fileMarkups[ns.Node.SysPath]
- if ok {
- delete(w.fileMarkups, ns.Node.SysPath)
- w.changes.Push(fmarkup)
- }
- continue
+ for _, fi := range listfi {
+ var name = fi.Name()
- case memfs.FileStateCreated:
- log.Printf(`%s: %s created`, logp, ns.Node.SysPath)
- fmarkup, err = NewFileMarkup(ns.Node.SysPath, nil)
- if err != nil {
- log.Printf("%s: %s\n", logp, err)
+ if fi.Size() == watchfs.FileFlagDeleted {
+ log.Printf(`%s: %q deleted`, logp, name)
+ fmarkup, ok = w.fileMarkups[name]
+ if ok {
+ delete(w.fileMarkups, name)
+ w.changes.Push(fmarkup)
+ }
continue
}
- w.fileMarkups[ns.Node.SysPath] = fmarkup
-
- case memfs.FileStateUpdateMode:
- log.Printf(`%s: %s mode updated`, logp, ns.Node.SysPath)
- continue
-
- case memfs.FileStateUpdateContent:
- log.Printf(`%s: %s content updated`, logp, ns.Node.SysPath)
- fmarkup = w.fileMarkups[ns.Node.SysPath]
+ fmarkup = w.fileMarkups[name]
if fmarkup == nil {
- log.Printf("%s: %s not found\n", logp, ns.Node.SysPath)
-
- fmarkup, err = NewFileMarkup(ns.Node.SysPath, nil)
+ log.Printf(`%s: %s created`, logp, name)
+ fmarkup, err = NewFileMarkup(name, nil)
if err != nil {
- log.Printf("%s: %s\n", logp, err)
+ log.Printf(`%s: %s`, logp, err)
continue
}
- w.fileMarkups[ns.Node.SysPath] = fmarkup
+ w.fileMarkups[name] = fmarkup
}
- }
- err = w.converter.ToHTMLFile(fmarkup)
- if err != nil {
- log.Printf(`%s: %s`, logp, err)
- }
+ err = w.converter.ToHTMLFile(fmarkup)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ }
- w.changes.Push(fmarkup)
+ log.Printf(`%s: %q converted`, logp, fmarkup.path)
+ w.changes.Push(fmarkup)
+ }
}
}
@@ -181,19 +172,18 @@ func (w *watcher) watchHTMLTemplate() {
var (
logp = `watchHTMLTemplate`
- ns memfs.NodeState
err error
)
- for ns = range w.watchTemplate.C {
- if ns.State == memfs.FileStateDeleted {
- log.Printf("%s: HTML template file %q has been deleted\n",
- logp, ns.Node.SysPath)
+ for fi := range w.watchTemplate.C {
+ if fi == nil {
+ log.Printf(`%s: HTML template file has been deleted`, logp)
err = w.converter.htmlTemplateUseInternal()
- } else {
- log.Printf(`%s: recompiling HTML template %q ...`, logp, ns.Node.SysPath)
- err = w.converter.SetHTMLTemplateFile(w.converter.htmlTemplate)
+ continue
}
+
+ log.Printf(`%s: recompiling HTML template %q ...`, logp, fi.Name())
+ err = w.converter.SetHTMLTemplateFile(w.converter.htmlTemplate)
if err != nil {
log.Printf(`%s: %s`, logp, err)
continue
diff --git a/watcher_test.go b/watcher_test.go
index 063eb8f..9067d57 100644
--- a/watcher_test.go
+++ b/watcher_test.go
@@ -40,8 +40,7 @@ func TestWatcher(t *testing.T) {
if err != nil {
t.Fatal(err)
}
-
- testWatcher.watchDir.Delay = 100 * time.Millisecond
+ testWatcher.opts.FileWatcherOptions.Interval = 50 * time.Millisecond
err = testWatcher.start()
if err != nil {
@@ -55,7 +54,7 @@ func TestWatcher(t *testing.T) {
t.Fatal(err)
}
- var pathFileMarkup = filepath.Join(testWatcher.dir, `test.adoc`)
+ var pathFileMarkup = filepath.Join(testWatcher.opts.Root, `test.adoc`)
t.Run(`testAdocCreate`, func(tt *testing.T) {
testAdocCreate(tt, testWatcher, tdata, pathFileMarkup)
@@ -67,7 +66,7 @@ func TestWatcher(t *testing.T) {
testAdocDelete(tt, testWatcher, pathFileMarkup)
})
- pathFileMarkup = filepath.Join(testWatcher.dir, `test.md`)
+ pathFileMarkup = filepath.Join(testWatcher.opts.Root, `test.md`)
t.Run(`testMarkdownCreate`, func(tt *testing.T) {
testMarkdownCreate(tt, testWatcher, tdata, pathFileMarkup)
@@ -91,14 +90,11 @@ func testAdocCreate(t *testing.T, testWatcher *watcher, tdata *test.Data, pathFi
gotBody []byte
)
- // Let the OS sync the file system before we create new file,
- // otherwise the modtime for fs.Root does not changes.
- time.Sleep(100 * time.Millisecond)
-
err = os.WriteFile(pathFile, expBody, 0600)
if err != nil {
t.Fatal(err)
}
+ testWatcher.watchDir.ForceRescan()
got = testWatcher.waitChanges()
@@ -124,14 +120,11 @@ func testAdocUpdate(t *testing.T, testWatcher *watcher, tdata *test.Data, pathFi
gotBody []byte
)
- // Let the OS sync the file system before we create new file,
- // otherwise the modtime for fs.Root does not changes.
- time.Sleep(100 * time.Millisecond)
-
err = os.WriteFile(pathFile, expBody, 0600)
if err != nil {
t.Fatal(err)
}
+ testWatcher.watchDir.ForceRescan()
got = testWatcher.waitChanges()
@@ -159,6 +152,7 @@ func testAdocDelete(t *testing.T, testWatcher *watcher, pathFile string) {
if err != nil {
t.Fatal(err)
}
+ testWatcher.watchDir.ForceRescan()
got = testWatcher.waitChanges()
@@ -178,14 +172,11 @@ func testMarkdownCreate(t *testing.T, testWatcher *watcher, tdata *test.Data, pa
gotBody []byte
)
- // Let the OS sync the file system before we create new file,
- // otherwise the modtime for fs.Root does not changes.
- time.Sleep(100 * time.Millisecond)
-
err = os.WriteFile(pathFileMarkdown, body, 0600)
if err != nil {
t.Fatal(err)
}
+ testWatcher.watchDir.ForceRescan()
got = testWatcher.waitChanges()
@@ -211,14 +202,11 @@ func testMarkdownUpdate(t *testing.T, testWatcher *watcher, tdata *test.Data, pa
gotBody []byte
)
- // Let the OS sync the file system before we create new file,
- // otherwise the modtime for fs.Root does not changes.
- time.Sleep(100 * time.Millisecond)
-
err = os.WriteFile(pathFileMarkdown, body, 0600)
if err != nil {
t.Fatal(err)
}
+ testWatcher.watchDir.ForceRescan()
got = testWatcher.waitChanges()
@@ -241,12 +229,11 @@ func testMarkdownDelete(t *testing.T, testWatcher *watcher, pathFileMarkdown str
gotIsExist bool
)
- time.Sleep(100 * time.Millisecond)
-
err = os.Remove(pathFileMarkdown)
if err != nil {
t.Fatal(err)
}
+ testWatcher.watchDir.ForceRescan()
got = testWatcher.waitChanges()
test.Assert(t, `md file updated`, pathFileMarkdown, got.path)