826 lines
16 KiB
Go
826 lines
16 KiB
Go
package fs
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"9fans.net/go/plan9"
|
|
"9fans.net/go/plan9/client"
|
|
)
|
|
|
|
// Metadata is the metadata encoded into Denote-style
|
|
// file names.
|
|
type Metadata struct {
|
|
Path string
|
|
Identifier string
|
|
Title string
|
|
Tags []string
|
|
Extension string
|
|
}
|
|
type Results []*Metadata
|
|
|
|
func (es Results) Bytes() []byte {
|
|
var buf strings.Builder
|
|
for _, e := range es {
|
|
title := e.Title
|
|
if title == "" {
|
|
title = "(untitled)"
|
|
}
|
|
|
|
tags := strings.Join(e.Tags, ", ")
|
|
fmt.Fprintf(&buf, "denote:%s | %s | %s\n", e.Identifier, title, tags)
|
|
}
|
|
return []byte(buf.String())
|
|
}
|
|
|
|
// Filter matches a given field in a Result to a regular expression
|
|
type Filter struct {
|
|
field FilterField
|
|
re *regexp.Regexp
|
|
negate bool
|
|
}
|
|
|
|
type Filters []*Filter
|
|
|
|
type FilterField string
|
|
|
|
const (
|
|
FilterDate FilterField = "date"
|
|
FilterTitle FilterField = "title"
|
|
FilterTag FilterField = "tag"
|
|
FilterAny FilterField = ""
|
|
)
|
|
|
|
// Parse converts a slice of strings of the form "tag:<tagname>",
|
|
// "date:<date>", "title:'<title>'" into a Filters list
|
|
func (fs Filters) Parse(S []string) (Filters, error) {
|
|
for _, fa := range S {
|
|
f, err := NewFilter(fa)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list notes: %w", err)
|
|
}
|
|
fs = append(fs, f)
|
|
}
|
|
return fs, nil
|
|
}
|
|
|
|
// NewFilter constructs a Filter from a filter string. arg takes the form
|
|
// field:criteria, e.g., tag:/dev|meeting/, date:20251101.
|
|
func NewFilter(arg string) (*Filter, error) {
|
|
negate := strings.HasPrefix(arg, "!")
|
|
if negate {
|
|
arg = strings.TrimPrefix(arg, "!")
|
|
}
|
|
|
|
m := regexp.MustCompile(`^(?:(date|title|tag):)?(.+)$`).FindStringSubmatch(arg)
|
|
if m == nil {
|
|
return nil, fmt.Errorf("invalid filter syntax: %s", arg)
|
|
}
|
|
|
|
pattern := m[2]
|
|
if strings.HasPrefix(pattern, "/") && strings.HasSuffix(pattern, "/") {
|
|
pattern = strings.TrimPrefix(strings.TrimSuffix(pattern, "/"), "/")
|
|
} else {
|
|
pattern = regexp.QuoteMeta(pattern)
|
|
}
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid regex: %v", err)
|
|
}
|
|
|
|
return &Filter{field: FilterField(m[1]), re: re, negate: negate}, nil
|
|
}
|
|
|
|
// IsMatch checks if a note matches this filter
|
|
func (f *Filter) IsMatch(n *Metadata) bool {
|
|
result := false
|
|
switch f.field {
|
|
case FilterDate:
|
|
result = f.re.MatchString(n.Identifier)
|
|
case FilterTitle:
|
|
result = f.re.MatchString(n.Title)
|
|
case FilterTag:
|
|
result = slices.ContainsFunc(n.Tags, func(kw string) bool {
|
|
return f.re.MatchString(kw)
|
|
})
|
|
case FilterAny: // any field
|
|
if f.re.MatchString(n.Identifier) {
|
|
result = true
|
|
} else if f.re.MatchString(n.Title) {
|
|
result = true
|
|
} else {
|
|
result = slices.ContainsFunc(n.Tags, func(kw string) bool {
|
|
return f.re.MatchString(kw)
|
|
})
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
if f.negate {
|
|
return !result
|
|
}
|
|
return result
|
|
}
|
|
|
|
type SortBy string
|
|
|
|
const (
|
|
SortById SortBy = "id"
|
|
SortByDate SortBy = "date"
|
|
SortByTitle SortBy = "title"
|
|
)
|
|
|
|
type SortOrder int
|
|
|
|
const (
|
|
SortOrderAsc SortOrder = iota
|
|
SortOrderDesc
|
|
)
|
|
|
|
// Sort organizes a list of notes by sortType and order using metadata.
|
|
func Sort(md Results, sortType SortBy, order SortOrder) {
|
|
switch sortType {
|
|
case SortById, SortByDate:
|
|
sort.Slice(md, func(i, j int) bool {
|
|
if order == SortOrderAsc {
|
|
return md[i].Identifier < md[j].Identifier // Reverse chronological by default
|
|
} else {
|
|
return md[i].Identifier > md[j].Identifier // Reverse chronological by default
|
|
}
|
|
})
|
|
case SortByTitle:
|
|
sort.Slice(md, func(i, j int) bool {
|
|
if order == SortOrderAsc {
|
|
return strings.ToLower(md[i].Title) < strings.ToLower(md[j].Title)
|
|
} else {
|
|
return strings.ToLower(md[i].Title) > strings.ToLower(md[j].Title)
|
|
}
|
|
})
|
|
default:
|
|
sort.Slice(md, func(i, j int) bool {
|
|
return md[i].Identifier > md[j].Identifier // Reverse chronological by default
|
|
})
|
|
}
|
|
}
|
|
|
|
// File types in our filesystem
|
|
const (
|
|
QTDir = plan9.QTDIR
|
|
QTFile = plan9.QTFILE
|
|
)
|
|
|
|
// Qid paths - we'll use a simple scheme:
|
|
// 0: root
|
|
// 1-1000000: note directories (qid = 1 + note_index)
|
|
// 1000001+: files (qid = 1000001 + note_index*100 + file_type)
|
|
const (
|
|
qidRoot = 0
|
|
qidNoteBase = 1
|
|
qidFileBase = 1000001
|
|
qidIndex = 999999
|
|
)
|
|
|
|
// File types within a note directory
|
|
const (
|
|
fileTypePath = iota
|
|
fileTypeTitle
|
|
fileTypeKeywords
|
|
fileTypeExtension
|
|
)
|
|
|
|
var fileNames = []string{"path", "title", "keywords", "extension"}
|
|
|
|
type server struct {
|
|
dir string
|
|
notes Results
|
|
mu sync.RWMutex
|
|
fids map[uint32]*fid
|
|
}
|
|
|
|
type fid struct {
|
|
qid plan9.Qid
|
|
path string
|
|
offset int64
|
|
}
|
|
|
|
var srv *server
|
|
|
|
// extractMetadata extracts Denote metadata from a filename
|
|
func extractMetadata(path string) *Metadata {
|
|
fname := filepath.Base(path)
|
|
note := &Metadata{Path: path}
|
|
|
|
if m := regexp.MustCompile(`^(\d{8}T\d{6})`).FindStringSubmatch(fname); m != nil {
|
|
note.Identifier = m[1]
|
|
}
|
|
|
|
// Extract title from filename
|
|
filenameTitle := ""
|
|
if m := regexp.MustCompile(`--([^_\.]+)`).FindStringSubmatch(fname); m != nil {
|
|
filenameTitle = strings.ReplaceAll(m[1], "-", " ")
|
|
}
|
|
|
|
// Try to get title from file content, fall back to filename
|
|
fileContentTitle := extractTitle(path)
|
|
if fileContentTitle != "" {
|
|
note.Title = fileContentTitle
|
|
} else {
|
|
note.Title = filenameTitle
|
|
}
|
|
|
|
if m := regexp.MustCompile(`__(.+?)(?:\.|$)`).FindStringSubmatch(fname); m != nil {
|
|
note.Tags = strings.Split(m[1], "_")
|
|
}
|
|
note.Extension = filepath.Ext(fname)
|
|
|
|
return note
|
|
}
|
|
|
|
func extractTitle(path string) string {
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
if ext != ".org" && ext != ".md" && ext != ".txt" {
|
|
return ""
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
text := string(data)
|
|
|
|
// Try org-mode #+title: first, then fall back to first heading
|
|
if ext == ".org" {
|
|
if m := regexp.MustCompile(`(?m)^#\+title:\s*(.+)$`).FindStringSubmatch(text); m != nil {
|
|
return strings.TrimSpace(m[1])
|
|
}
|
|
// Fallback to first heading (lines starting with *)
|
|
if m := regexp.MustCompile(`(?m)^\*+\s+(.+)$`).FindStringSubmatch(text); m != nil {
|
|
return strings.TrimSpace(m[1])
|
|
}
|
|
}
|
|
|
|
// Try markdown YAML front matter title: first, then fall back to # header
|
|
if ext == ".md" {
|
|
if m := regexp.MustCompile(`(?ms)^---\n.*?^title:\s*(.+?)$.*?^---`).FindStringSubmatch(text); m != nil {
|
|
return strings.TrimSpace(strings.Trim(m[1], `"`))
|
|
}
|
|
if m := regexp.MustCompile(`(?m)^#\s+(.+)$`).FindStringSubmatch(text); m != nil {
|
|
return strings.TrimSpace(m[1])
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// Getdir returns the denote directory
|
|
func getdir() string {
|
|
return fmt.Sprintf("%s/doc", os.Getenv("HOME"))
|
|
}
|
|
|
|
// loadData returns metadata for notes in a directory matching given filters
|
|
func loadData(filters []*Filter) (Results, error) {
|
|
var notes Results
|
|
err := filepath.Walk(getdir(), func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
if note := extractMetadata(path); note.Identifier != "" {
|
|
match := true
|
|
for _, filt := range filters {
|
|
if !filt.IsMatch(note) {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
if match {
|
|
notes = append(notes, note)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return notes, nil
|
|
}
|
|
|
|
func With9P(fn func(*client.Fsys) error) error {
|
|
ns := client.Namespace()
|
|
if ns == "" {
|
|
return fmt.Errorf("failed to get namespace")
|
|
}
|
|
|
|
conn, err := client.DialService("denote")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to denote service: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
root, err := conn.Attach(nil, "denote", "")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to attach: %w", err)
|
|
}
|
|
defer root.Close()
|
|
|
|
return fn(root)
|
|
}
|
|
|
|
// StartServer starts the 9P fileserver in the background
|
|
func StartServer() error {
|
|
if srv != nil {
|
|
return fmt.Errorf("server already running")
|
|
}
|
|
|
|
// Load all notes
|
|
notes, err := loadData([]*Filter{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load notes: %w", err)
|
|
}
|
|
|
|
srv = &server{
|
|
dir: getdir(),
|
|
notes: notes,
|
|
fids: make(map[uint32]*fid),
|
|
}
|
|
|
|
// Get namespace and create Unix socket path
|
|
ns := client.Namespace()
|
|
if ns == "" {
|
|
return fmt.Errorf("failed to get namespace")
|
|
}
|
|
|
|
sockPath := ns + "/denote"
|
|
|
|
// Remove old socket if it exists
|
|
os.Remove(sockPath)
|
|
|
|
// Create Unix domain socket listener
|
|
listener, err := net.Listen("unix", sockPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to listen on socket: %w", err)
|
|
}
|
|
|
|
// Start accepting connections in background
|
|
go srv.acceptLoop(listener)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *server) acceptLoop(listener net.Listener) {
|
|
defer listener.Close()
|
|
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "denote fs: accept error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
go s.serve(conn)
|
|
}
|
|
}
|
|
|
|
func (s *server) serve(conn net.Conn) {
|
|
defer conn.Close()
|
|
|
|
for {
|
|
fc, err := plan9.ReadFcall(conn)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
fmt.Fprintf(os.Stderr, "denote fs: read error: %v\n", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
rfc := s.handle(fc)
|
|
if err := plan9.WriteFcall(conn, rfc); err != nil {
|
|
fmt.Fprintf(os.Stderr, "denote fs: write error: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *server) handle(fc *plan9.Fcall) *plan9.Fcall {
|
|
switch fc.Type {
|
|
case plan9.Tversion:
|
|
return s.version(fc)
|
|
case plan9.Tauth:
|
|
return errorFcall(fc, "denote: authentication not required")
|
|
case plan9.Tattach:
|
|
return s.attach(fc)
|
|
case plan9.Twalk:
|
|
return s.walk(fc)
|
|
case plan9.Topen:
|
|
return s.open(fc)
|
|
case plan9.Tread:
|
|
return s.read(fc)
|
|
case plan9.Tstat:
|
|
return s.stat(fc)
|
|
case plan9.Tclunk:
|
|
return s.clunk(fc)
|
|
default:
|
|
return errorFcall(fc, "operation not supported")
|
|
}
|
|
}
|
|
|
|
func (s *server) version(fc *plan9.Fcall) *plan9.Fcall {
|
|
msize := min(fc.Msize, 8192)
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rversion,
|
|
Tag: fc.Tag,
|
|
Msize: msize,
|
|
Version: "9P2000",
|
|
}
|
|
}
|
|
|
|
func (s *server) attach(fc *plan9.Fcall) *plan9.Fcall {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
qid := plan9.Qid{
|
|
Type: QTDir,
|
|
Path: qidRoot,
|
|
}
|
|
|
|
s.fids[fc.Fid] = &fid{
|
|
qid: qid,
|
|
path: "/",
|
|
}
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rattach,
|
|
Tag: fc.Tag,
|
|
Qid: qid,
|
|
}
|
|
}
|
|
|
|
func (s *server) walk(fc *plan9.Fcall) *plan9.Fcall {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
f, ok := s.fids[fc.Fid]
|
|
if !ok {
|
|
return errorFcall(fc, "fid not found")
|
|
}
|
|
|
|
// If no wnames, this is a clone operation
|
|
if len(fc.Wname) == 0 {
|
|
newFid := &fid{
|
|
qid: f.qid,
|
|
path: f.path,
|
|
}
|
|
s.fids[fc.Newfid] = newFid
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rwalk,
|
|
Tag: fc.Tag,
|
|
Wqid: []plan9.Qid{},
|
|
}
|
|
}
|
|
|
|
// Walk the path
|
|
path := f.path
|
|
qids := []plan9.Qid{}
|
|
|
|
for _, name := range fc.Wname {
|
|
if path == "/" {
|
|
// Walking from root - should be a note ID
|
|
found := false
|
|
if "index" == name {
|
|
qid := plan9.Qid{
|
|
Type: QTFile,
|
|
Path: uint64(qidIndex),
|
|
}
|
|
qids = append(qids, qid)
|
|
path = "/index"
|
|
found = true
|
|
} else {
|
|
for i, note := range s.notes {
|
|
if note.Identifier == name {
|
|
qid := plan9.Qid{
|
|
Type: QTDir,
|
|
Path: uint64(qidNoteBase + i),
|
|
}
|
|
qids = append(qids, qid)
|
|
path = "/" + name
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
return errorFcall(fc, "file not found")
|
|
}
|
|
} else {
|
|
// Walking from note dir - should be a file
|
|
found := false
|
|
for i, fname := range fileNames {
|
|
if fname == name {
|
|
noteIdx := s.pathToNoteIndex(path)
|
|
qid := plan9.Qid{
|
|
Type: QTFile,
|
|
Path: uint64(qidFileBase + noteIdx*100 + i),
|
|
}
|
|
qids = append(qids, qid)
|
|
path = path + "/" + name
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return errorFcall(fc, "file not found")
|
|
}
|
|
}
|
|
}
|
|
|
|
newFid := &fid{
|
|
qid: qids[len(qids)-1],
|
|
path: path,
|
|
}
|
|
s.fids[fc.Newfid] = newFid
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rwalk,
|
|
Tag: fc.Tag,
|
|
Wqid: qids,
|
|
}
|
|
}
|
|
|
|
func (s *server) open(fc *plan9.Fcall) *plan9.Fcall {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
f, ok := s.fids[fc.Fid]
|
|
if !ok {
|
|
return errorFcall(fc, "fid not found")
|
|
}
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Ropen,
|
|
Tag: fc.Tag,
|
|
Qid: f.qid,
|
|
}
|
|
}
|
|
|
|
func (s *server) read(fc *plan9.Fcall) *plan9.Fcall {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
f, ok := s.fids[fc.Fid]
|
|
if !ok {
|
|
return errorFcall(fc, "fid not found")
|
|
}
|
|
|
|
var data []byte
|
|
|
|
if f.qid.Type&QTDir != 0 {
|
|
// Reading a directory
|
|
if fc.Offset == 0 {
|
|
f.offset = 0
|
|
}
|
|
data = s.readDir(f.path, int64(fc.Offset), fc.Count)
|
|
} else {
|
|
// Reading a file
|
|
content := s.getFileContent(f.path)
|
|
offset := int64(fc.Offset)
|
|
if offset >= int64(len(content)) {
|
|
data = []byte{}
|
|
} else {
|
|
end := min(offset+int64(fc.Count), int64(len(content)))
|
|
data = []byte(content[offset:end])
|
|
}
|
|
}
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rread,
|
|
Tag: fc.Tag,
|
|
Count: uint32(len(data)),
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *server) stat(fc *plan9.Fcall) *plan9.Fcall {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
f, ok := s.fids[fc.Fid]
|
|
if !ok {
|
|
return errorFcall(fc, "fid not found")
|
|
}
|
|
|
|
dir := s.pathToDir(f.path, f.qid)
|
|
stat, err := dir.Bytes()
|
|
if err != nil {
|
|
return errorFcall(fc, err.Error())
|
|
}
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rstat,
|
|
Tag: fc.Tag,
|
|
Stat: stat,
|
|
}
|
|
}
|
|
|
|
func (s *server) clunk(fc *plan9.Fcall) *plan9.Fcall {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
delete(s.fids, fc.Fid)
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rclunk,
|
|
Tag: fc.Tag,
|
|
}
|
|
}
|
|
|
|
func (s *server) readDir(path string, offset int64, count uint32) []byte {
|
|
var dirs []plan9.Dir
|
|
|
|
if path == "/" {
|
|
// add index node
|
|
dirs = append(dirs, plan9.Dir{
|
|
Qid: plan9.Qid{
|
|
Type: QTFile,
|
|
Path: uint64(qidIndex),
|
|
},
|
|
Mode: 0444,
|
|
Name: "index",
|
|
Uid: "denote",
|
|
Gid: "denote",
|
|
Muid: "denote",
|
|
Length: 0,
|
|
})
|
|
// List all note IDs
|
|
for i, note := range s.notes {
|
|
dirs = append(dirs, plan9.Dir{
|
|
Qid: plan9.Qid{
|
|
Type: QTDir,
|
|
Path: uint64(qidNoteBase + 1 + i),
|
|
},
|
|
Mode: plan9.DMDIR | 0555,
|
|
Name: note.Identifier,
|
|
Uid: "denote",
|
|
Gid: "denote",
|
|
Muid: "denote",
|
|
Length: 0,
|
|
})
|
|
}
|
|
} else if path == "/index" {
|
|
|
|
} else {
|
|
// List files in a note directory
|
|
noteIdx := s.pathToNoteIndex(path)
|
|
for i, fname := range fileNames {
|
|
content := s.getFileContent(path + "/" + fname)
|
|
dirs = append(dirs, plan9.Dir{
|
|
Qid: plan9.Qid{
|
|
Type: QTFile,
|
|
Path: uint64(qidFileBase + noteIdx*100 + i),
|
|
},
|
|
Mode: 0444,
|
|
Name: fname,
|
|
Uid: "denote",
|
|
Gid: "denote",
|
|
Muid: "denote",
|
|
Length: uint64(len(content)),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Serialize all directory entries to bytes first
|
|
var allData []byte
|
|
for _, dir := range dirs {
|
|
stat, _ := dir.Bytes()
|
|
allData = append(allData, stat...)
|
|
}
|
|
|
|
// Return slice starting from offset
|
|
if offset >= int64(len(allData)) {
|
|
return []byte{}
|
|
}
|
|
|
|
end := offset + int64(count)
|
|
if end > int64(len(allData)) {
|
|
end = int64(len(allData))
|
|
}
|
|
|
|
return allData[offset:end]
|
|
}
|
|
|
|
func (s *server) pathToDir(path string, qid plan9.Qid) plan9.Dir {
|
|
name := path
|
|
if path == "/" {
|
|
name = "."
|
|
} else if strings.Contains(path, "/") {
|
|
parts := strings.Split(path, "/")
|
|
name = parts[len(parts)-1]
|
|
}
|
|
|
|
mode := uint32(0444)
|
|
length := uint64(0)
|
|
content := ""
|
|
|
|
if qid.Type&QTDir != 0 {
|
|
mode = plan9.DMDIR | 0555
|
|
} else {
|
|
content = s.getFileContent(path)
|
|
length = uint64(len(content))
|
|
}
|
|
|
|
return plan9.Dir{
|
|
Qid: qid,
|
|
Mode: plan9.Perm(mode),
|
|
Name: name,
|
|
Uid: "denote",
|
|
Gid: "denote",
|
|
Muid: "denote",
|
|
Length: length,
|
|
}
|
|
}
|
|
|
|
func (s *server) pathToNoteIndex(path string) int {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) == 0 {
|
|
return -1
|
|
}
|
|
|
|
noteID := parts[0]
|
|
for i, note := range s.notes {
|
|
if note.Identifier == noteID {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (s *server) getIndexContent() string {
|
|
content := ""
|
|
for _, n := range s.notes {
|
|
content += fmt.Sprintf("%s|%s|", n.Identifier, n.Title)
|
|
for _, t := range n.Tags {
|
|
content += fmt.Sprintf("%s,", t)
|
|
}
|
|
content = strings.TrimSuffix(content, ",")
|
|
content += "\n"
|
|
}
|
|
return content
|
|
}
|
|
|
|
func (s *server) getFileContent(path string) string {
|
|
if path == "/index" {
|
|
return s.getIndexContent()
|
|
}
|
|
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
|
|
noteID := parts[0]
|
|
fileName := parts[1]
|
|
|
|
var note *Metadata
|
|
for _, n := range s.notes {
|
|
if n.Identifier == noteID {
|
|
note = n
|
|
break
|
|
}
|
|
}
|
|
|
|
if note == nil {
|
|
return ""
|
|
}
|
|
|
|
switch fileName {
|
|
case "path":
|
|
return note.Path
|
|
case "title":
|
|
return note.Title
|
|
case "keywords":
|
|
return strings.Join(note.Tags, ",")
|
|
case "extension":
|
|
return note.Extension
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func errorFcall(fc *plan9.Fcall, msg string) *plan9.Fcall {
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rerror,
|
|
Tag: fc.Tag,
|
|
Ename: msg,
|
|
}
|
|
}
|