feat: simplify data and add cache to LocalFsAdapter
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.max-richter.dev/max/marka/parser"
|
||||
@@ -12,52 +13,70 @@ import (
|
||||
|
||||
type LocalFsAdapter struct {
|
||||
roots []string
|
||||
cache map[string]*Entry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (l LocalFsAdapter) readDir(path string, root string) (FsResponse, error) {
|
||||
func (l *LocalFsAdapter) readDir(path string, root string) (*Entry, error) {
|
||||
dirInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]FsDirEntry, 0, len(entries))
|
||||
out := make([]*Entry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
info, _ := e.Info()
|
||||
|
||||
entryType := "dir"
|
||||
if !e.IsDir() {
|
||||
entryType = contentTypeFor(e.Name())
|
||||
entryType = "file"
|
||||
}
|
||||
|
||||
var content any
|
||||
if !e.IsDir() && entryType == "application/markdown" {
|
||||
entryPath := filepath.Join(path, e.Name())
|
||||
fileContent, err := os.ReadFile(entryPath)
|
||||
if err == nil {
|
||||
parsedContent, err := parser.ParseFile(string(fileContent))
|
||||
var mime string
|
||||
var size int64
|
||||
if !e.IsDir() {
|
||||
mime = contentTypeFor(e.Name())
|
||||
if info != nil {
|
||||
size = info.Size()
|
||||
}
|
||||
if mime == "application/markdown" {
|
||||
entryPath := filepath.Join(path, e.Name())
|
||||
fileContent, err := os.ReadFile(entryPath)
|
||||
if err == nil {
|
||||
content = parsedContent
|
||||
parsedContent, err := parser.ParseFile(string(fileContent))
|
||||
if err == nil {
|
||||
content = parsedContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, FsDirEntry{
|
||||
childPath, err := filepath.Rel(root, filepath.Join(path, e.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responseChildPath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), childPath))
|
||||
|
||||
out = append(out, &Entry{
|
||||
Name: e.Name(),
|
||||
Path: responseChildPath,
|
||||
Type: entryType,
|
||||
IsDir: e.IsDir(),
|
||||
ModTime: info.ModTime(),
|
||||
MIME: mime,
|
||||
Size: size,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
if relPath == "." {
|
||||
relPath = ""
|
||||
@@ -65,75 +84,111 @@ func (l LocalFsAdapter) readDir(path string, root string) (FsResponse, error) {
|
||||
|
||||
responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
|
||||
|
||||
return FsResponse{
|
||||
Dir: &FsDir{
|
||||
Files: out,
|
||||
Name: responsePath,
|
||||
ModTime: dirInfo.ModTime(),
|
||||
},
|
||||
return &Entry{
|
||||
Type: "dir",
|
||||
Name: filepath.Base(responsePath),
|
||||
Path: responsePath,
|
||||
ModTime: dirInfo.ModTime(),
|
||||
Content: out,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l LocalFsAdapter) readFile(path string, root string) (FsResponse, error) {
|
||||
func (l *LocalFsAdapter) readFile(path string, root string) (*Entry, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
|
||||
mime := contentTypeFor(path)
|
||||
var content any
|
||||
|
||||
return FsResponse{
|
||||
File: &FsFile{
|
||||
Name: responsePath,
|
||||
Type: contentTypeFor(path),
|
||||
ModTime: fi.ModTime(),
|
||||
Content: data,
|
||||
},
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mime == "application/markdown" {
|
||||
parsedContent, err := parser.ParseFile(string(fileContent))
|
||||
if err == nil {
|
||||
content = parsedContent
|
||||
} else {
|
||||
// Fallback to raw content on parsing error
|
||||
content = fileContent
|
||||
}
|
||||
} else {
|
||||
content = fileContent
|
||||
}
|
||||
|
||||
return &Entry{
|
||||
Type: "file",
|
||||
Name: fi.Name(),
|
||||
Path: responsePath,
|
||||
ModTime: fi.ModTime(),
|
||||
MIME: mime,
|
||||
Size: fi.Size(),
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l LocalFsAdapter) Read(path string) (FsResponse, error) {
|
||||
func (l *LocalFsAdapter) Read(path string) (*Entry, error) {
|
||||
if path == "/" {
|
||||
entries := make([]FsDirEntry, 0, len(l.roots))
|
||||
var latestModTime time.Time
|
||||
for _, r := range l.roots {
|
||||
info, err := os.Stat(r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, FsDirEntry{
|
||||
Name: filepath.Base(r),
|
||||
Type: "dir",
|
||||
IsDir: true,
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
if info.ModTime().After(latestModTime) {
|
||||
latestModTime = info.ModTime()
|
||||
}
|
||||
}
|
||||
|
||||
return FsResponse{
|
||||
Dir: &FsDir{
|
||||
Files: entries,
|
||||
Name: "/",
|
||||
ModTime: latestModTime,
|
||||
},
|
||||
}, nil
|
||||
l.mu.RLock()
|
||||
cached, found := l.cache[path]
|
||||
l.mu.RUnlock()
|
||||
|
||||
if found && !latestModTime.After(cached.ModTime) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
entries := make([]*Entry, 0, len(l.roots))
|
||||
for _, r := range l.roots {
|
||||
info, err := os.Stat(r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
name := filepath.Base(r)
|
||||
entries = append(entries, &Entry{
|
||||
Name: name,
|
||||
Path: "/" + name,
|
||||
Type: "dir",
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
entry := &Entry{
|
||||
Type: "dir",
|
||||
Name: "/",
|
||||
Path: "/",
|
||||
ModTime: latestModTime,
|
||||
Content: entries,
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
l.cache[path] = entry
|
||||
l.mu.Unlock()
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(pathParts) == 0 || pathParts[0] == "" {
|
||||
return FsResponse{}, ErrNotFound
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
rootIdentifier := pathParts[0]
|
||||
@@ -146,7 +201,7 @@ func (l LocalFsAdapter) Read(path string) (FsResponse, error) {
|
||||
}
|
||||
|
||||
if targetRoot == "" {
|
||||
return FsResponse{}, ErrNotFound
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
subPath := filepath.Join(pathParts[1:]...)
|
||||
@@ -154,38 +209,58 @@ func (l LocalFsAdapter) Read(path string) (FsResponse, error) {
|
||||
|
||||
absTarget, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
absRoot, err := filepath.Abs(targetRoot)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(absTarget, absRoot) {
|
||||
return FsResponse{}, errors.New("path escapes root")
|
||||
return nil, errors.New("path escapes root")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(target)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return FsResponse{}, ErrNotFound
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.mu.RLock()
|
||||
cached, found := l.cache[path]
|
||||
l.mu.RUnlock()
|
||||
|
||||
if found && !fi.ModTime().After(cached.ModTime) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var newEntry *Entry
|
||||
if fi.IsDir() {
|
||||
return l.readDir(target, targetRoot)
|
||||
newEntry, err = l.readDir(target, targetRoot)
|
||||
} else {
|
||||
newEntry, err = l.readFile(target, targetRoot)
|
||||
}
|
||||
|
||||
return l.readFile(target, targetRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
l.cache[path] = newEntry
|
||||
l.mu.Unlock()
|
||||
|
||||
return newEntry, nil
|
||||
}
|
||||
|
||||
func (LocalFsAdapter) Write(path string, content []byte) error {
|
||||
func (l *LocalFsAdapter) Write(path string, content []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocalFsAdapter(roots []string) (FileAdapter, error) {
|
||||
return LocalFsAdapter{
|
||||
return &LocalFsAdapter{
|
||||
roots: roots,
|
||||
cache: make(map[string]*Entry),
|
||||
}, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user