Author: Kevin Schoon [me@kevinschoon.com]
Hash: 29746bdec67fdd9f098efb86fcb123c577dc8ec6
Timestamp: Sat, 04 Jun 2022 17:45:09 +0000 (2 years ago)

+185 -28 +/-7 browse
add fuse based fs support
1diff --git a/go.mod b/go.mod
2index b84acf3..78b204d 100644
3--- a/go.mod
4+++ b/go.mod
5 @@ -3,11 +3,16 @@ module kevinschoon.com/hierarchy
6 go 1.18
7
8 require (
9+ github.com/ghodss/yaml v1.0.0
10+ github.com/hanwen/go-fuse v1.0.0
11+ github.com/mattn/go-sqlite3 v1.14.13
12+ github.com/urfave/cli/v2 v2.8.1
13+ )
14+
15+ require (
16 github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
17- github.com/ghodss/yaml v1.0.0 // indirect
18- github.com/mattn/go-sqlite3 v1.14.13 // indirect
19 github.com/russross/blackfriday/v2 v2.1.0 // indirect
20- github.com/urfave/cli/v2 v2.8.1 // indirect
21 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
22+ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 // indirect
23 gopkg.in/yaml.v2 v2.4.0 // indirect
24 )
25 diff --git a/go.sum b/go.sum
26index d7fec5e..4198c93 100644
27--- a/go.sum
28+++ b/go.sum
29 @@ -2,6 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKY
30 github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
31 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
32 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
33+ github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
34+ github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
35 github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
36 github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
37 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
38 @@ -10,6 +12,9 @@ github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
39 github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
40 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
41 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
42+ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE=
43+ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
44+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
45 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
46 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
47 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
48 diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go
49index f1e255f..6345381 100644
50--- a/pkg/cmd/app.go
51+++ b/pkg/cmd/app.go
52 @@ -103,7 +103,7 @@ data structure. It uses SQLite to store note content and metadata.
53 Action: func(ctx *cli.Context) error {
54 path := ctx.Args().First()
55 if path == "" {
56- notes, err := hierarchy.LoadNotes(*cfg)
57+ notes, err := hierarchy.Load(*cfg)
58 if err != nil {
59 return err
60 }
61 @@ -174,6 +174,14 @@ data structure. It uses SQLite to store note content and metadata.
62 Name: "read-only",
63 Usage: "disallow writing",
64 },
65+ &cli.BoolFlag{
66+ Name: "debug",
67+ Usage: "FUSE level debugging",
68+ },
69+ },
70+ Action: func(ctx *cli.Context) error {
71+ return hierarchy.Mount(*cfg, ctx.String("path"), ctx.Bool("read-only"),
72+ ctx.Bool("debug"))
73 },
74 },
75 {
76 diff --git a/pkg/hierarchy/database.go b/pkg/hierarchy/database.go
77index 6e672eb..6bc9900 100644
78--- a/pkg/hierarchy/database.go
79+++ b/pkg/hierarchy/database.go
80 @@ -3,6 +3,7 @@ package hierarchy
81 import (
82 "database/sql"
83 _ "embed"
84+ "time"
85
86 _ "github.com/mattn/go-sqlite3"
87 "kevinschoon.com/hierarchy/pkg/config"
88 @@ -28,6 +29,7 @@ func With(path string, fn func(*sql.Tx) error) error {
89 if err != nil {
90 return err
91 }
92+ defer db.Close()
93 tx, err := db.Begin()
94 if err != nil {
95 return err
96 @@ -43,12 +45,14 @@ func With(path string, fn func(*sql.Tx) error) error {
97 func resolve(seed *Note, tx *sql.Tx) error {
98 rows, err := tx.Query(`
99 WITH RECURSIVE
100- with_decendants(id, parent, name, content, level) AS (
101- VALUES(?, ?, '', '', 0)
102+ with_decendants(id, parent, created_at, modified_at, name, content, level) AS (
103+ VALUES(?, ?, NULL, NULL, '', '', 0)
104 UNION ALL
105 SELECT
106 notes.id,
107 notes.parent,
108+ notes.created_at,
109+ notes.modified_at,
110 notes.name,
111 notes.content,
112 with_decendants.level+1
113 @@ -64,23 +68,27 @@ WITH RECURSIVE
114 results[seed.ID] = seed
115 for rows.Next() {
116 var (
117- id = new(int64)
118- parentId = new(int64)
119- name = new(string)
120- content = new(string)
121- depth = new(int64)
122+ id = new(int64)
123+ parentId = new(int64)
124+ createdAt = new(time.Time)
125+ modifiedAt = new(time.Time)
126+ name = new(string)
127+ content = new(string)
128+ depth = new(int64)
129 )
130- err = rows.Scan(id, &parentId, name, content, depth)
131+ err = rows.Scan(id, &parentId, &createdAt, &modifiedAt, name, content, depth)
132 if err != nil {
133 return err
134 }
135 if *depth > 0 {
136 parent := results[*parentId]
137 note := &Note{
138- ID: *id,
139- Parent: parent,
140- Name: *name,
141- Content: *content,
142+ ID: *id,
143+ Parent: parent,
144+ Name: *name,
145+ CreatedAt: *createdAt,
146+ ModifiedAt: *modifiedAt,
147+ Content: *content,
148 }
149 parent.Descendants = append(parent.Descendants, note)
150 results[note.ID] = note
151 @@ -95,12 +103,12 @@ func Find(cfg config.Config, notePath string) (*Note, error) {
152 return note, With(cfg.Database, func(tx *sql.Tx) error {
153 row := tx.QueryRow(`
154 SELECT
155- id, name, content
156+ id, created_at, modified_at, name, content
157 FROM notes
158 WHERE
159 name = ?
160 `, np.String())
161- err := row.Scan(&note.ID, &note.Name, &note.Content)
162+ err := row.Scan(&note.ID, &note.CreatedAt, &note.ModifiedAt, &note.Name, &note.Content)
163 if err != nil {
164 return err
165 }
166 @@ -108,11 +116,16 @@ WHERE
167 })
168 }
169
170- func LoadNotes(cfg config.Config) (notes []*Note, err error) {
171+ func Exists(cfg config.Config, notePath string) bool {
172+ _, err := Find(cfg, notePath)
173+ return err == nil
174+ }
175+
176+ func Load(cfg config.Config) (notes []*Note, err error) {
177 return notes, With(cfg.Database, func(tx *sql.Tx) error {
178 rows, err := tx.Query(`
179 SELECT
180- id, name, content
181+ id, created_at, modified_at, name, content
182 FROM notes
183 WHERE parent IS NULL
184 `)
185 @@ -121,7 +134,7 @@ WHERE parent IS NULL
186 }
187 for rows.Next() {
188 note := &Note{}
189- err = rows.Scan(&note.ID, &note.Name, &note.Content)
190+ err = rows.Scan(&note.ID, &note.CreatedAt, &note.ModifiedAt, &note.Name, &note.Content)
191 if err != nil {
192 return err
193 }
194 @@ -135,6 +148,14 @@ WHERE parent IS NULL
195 })
196 }
197
198+ func MustLoad(cfg config.Config) []*Note {
199+ notes, err := Load(cfg)
200+ if err != nil {
201+ panic(err)
202+ }
203+ return notes
204+ }
205+
206 func Create(cfg config.Config, path string) error {
207 np := ReadPath(path)
208 return With(cfg.Database, func(tx *sql.Tx) error {
209 @@ -160,7 +181,7 @@ func Save(cfg config.Config, path, content string) error {
210 _, err := tx.Exec(`
211 UPDATE notes
212 set content = ?,
213- updated_at = CURRENT_TIMESTAMP
214+ modified_at = CURRENT_TIMESTAMP
215 WHERE
216 name = ?
217 `, content, np.String())
218 diff --git a/pkg/hierarchy/fuse.go b/pkg/hierarchy/fuse.go
219new file mode 100644
220index 0000000..e5cdd54
221--- /dev/null
222+++ b/pkg/hierarchy/fuse.go
223 @@ -0,0 +1,114 @@
224+ package hierarchy
225+
226+ import (
227+ "github.com/hanwen/go-fuse/fuse"
228+ "github.com/hanwen/go-fuse/fuse/nodefs"
229+ "github.com/hanwen/go-fuse/fuse/pathfs"
230+ "kevinschoon.com/hierarchy/pkg/config"
231+ )
232+
233+ type noteFs struct {
234+ cfg config.Config
235+ pathfs.FileSystem
236+ }
237+
238+ func (me *noteFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
239+ if name == "" {
240+ return &fuse.Attr{
241+ Owner: *fuse.CurrentOwner(),
242+ Mode: fuse.S_IFDIR | 0755,
243+ }, fuse.OK
244+ }
245+ np := ReadPath(name)
246+ if np.Name() == "content.md" {
247+ if !Exists(me.cfg, np.Parent()) {
248+ return nil, fuse.ENOENT
249+ }
250+ note, err := Find(me.cfg, np.Parent())
251+ if err != nil {
252+ panic(err)
253+ }
254+ return &fuse.Attr{
255+ Owner: *fuse.CurrentOwner(),
256+ Ctime: uint64(note.CreatedAt.Unix()),
257+ Mtime: uint64(note.ModifiedAt.Unix()),
258+ Mode: fuse.S_IFREG,
259+ Size: uint64(len(note.Content)),
260+ }, fuse.OK
261+ }
262+ if !Exists(me.cfg, np.String()) {
263+ return nil, fuse.ENOENT
264+ }
265+ note, err := Find(me.cfg, np.String())
266+ if err != nil {
267+ panic(err)
268+ }
269+ return &fuse.Attr{
270+ Owner: *fuse.CurrentOwner(),
271+ Ctime: uint64(note.CreatedAt.Unix()),
272+ Mtime: uint64(note.ModifiedAt.Unix()),
273+ Mode: fuse.S_IFDIR | 0755,
274+ }, fuse.OK
275+ }
276+
277+ func (me *noteFs) OpenDir(name string, context *fuse.Context) (c []fuse.DirEntry, code fuse.Status) {
278+ if name == "" {
279+ notes := MustLoad(me.cfg)
280+ entries := []fuse.DirEntry{}
281+ for _, note := range notes {
282+ np := ReadPath(note.Name)
283+ entries = append(entries, fuse.DirEntry{
284+ Name: np.Name(),
285+ Mode: fuse.S_IFDIR,
286+ })
287+ }
288+ return entries, fuse.OK
289+ }
290+ if !Exists(me.cfg, name) {
291+ return nil, fuse.ENOENT
292+ }
293+ note, _ := Find(me.cfg, name)
294+ entries := []fuse.DirEntry{
295+ {
296+ Name: "content.md",
297+ Mode: fuse.S_IFREG,
298+ },
299+ }
300+ for _, decendant := range note.Descendants {
301+ np := ReadPath(decendant.Name)
302+ entries = append(entries, fuse.DirEntry{
303+ Name: np.Name(),
304+ Mode: fuse.S_IFDIR | 0755,
305+ })
306+ }
307+ return entries, fuse.OK
308+ }
309+
310+ func (me *noteFs) Open(name string, flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) {
311+ np := ReadPath(name)
312+ if np.Name() != "content.md" {
313+ return nil, fuse.ENOENT
314+ }
315+ if flags&fuse.O_ANYWRITE != 0 {
316+ return nil, fuse.EPERM
317+ }
318+ if !Exists(me.cfg, np.Parent()) {
319+ return nil, fuse.ENOENT
320+ }
321+ note, _ := Find(me.cfg, np.Parent())
322+ return nodefs.NewDataFile([]byte(note.Content)), fuse.OK
323+ }
324+
325+ var _ pathfs.FileSystem = (*noteFs)(nil)
326+
327+ func Mount(cfg config.Config, path string, readOnly, debug bool) error {
328+ fs := pathfs.NewPathNodeFs(&noteFs{FileSystem: pathfs.NewDefaultFileSystem(), cfg: cfg}, nil)
329+ server, _, err := nodefs.MountRoot(path, fs.Root(), &nodefs.Options{
330+ Debug: debug,
331+ })
332+ if err != nil {
333+ return err
334+ }
335+ server.Serve()
336+ return nil
337+ }
338 diff --git a/pkg/hierarchy/migrate/init.sql b/pkg/hierarchy/migrate/init.sql
339index 716bd23..c64369d 100644
340--- a/pkg/hierarchy/migrate/init.sql
341+++ b/pkg/hierarchy/migrate/init.sql
342 @@ -4,7 +4,7 @@ PRAGMA foreign_keys = on;
343 CREATE TABLE IF NOT EXISTS notes (
344 id INTEGER PRIMARY KEY,
345 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
346- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
347+ modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
348 parent INTEGER,
349 name VARCHAR NOT NULL UNIQUE,
350 content VARCHAR DEFAULT '',
351 diff --git a/pkg/hierarchy/note.go b/pkg/hierarchy/note.go
352index 6826368..6114188 100644
353--- a/pkg/hierarchy/note.go
354+++ b/pkg/hierarchy/note.go
355 @@ -1,9 +1,13 @@
356 package hierarchy
357
358+ import "time"
359+
360 type Note struct {
361- ID int64 `json:"id"`
362- Name string `json:"name"`
363- Content string `json:"content"`
364- Parent *Note `json:"-"`
365- Descendants []*Note `json:"descendants"`
366+ ID int64 `json:"id"`
367+ CreatedAt time.Time `json:"created_at"`
368+ ModifiedAt time.Time `json:"modified_at"`
369+ Name string `json:"name"`
370+ Content string `json:"content"`
371+ Parent *Note `json:"-"`
372+ Descendants []*Note `json:"descendants"`
373 }