267 lines
5.0 KiB
Go
267 lines
5.0 KiB
Go
package adapters
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.max-richter.dev/max/marka/parser"
|
|
)
|
|
|
|
type LocalFsAdapter struct {
|
|
roots []string
|
|
cache map[string]*Entry
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (l *LocalFsAdapter) readDir(path string, root string) (*Entry, error) {
|
|
dirInfo, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make([]*Entry, 0, len(entries))
|
|
for _, e := range entries {
|
|
info, _ := e.Info()
|
|
|
|
entryType := "dir"
|
|
if !e.IsDir() {
|
|
entryType = "file"
|
|
}
|
|
|
|
var content any
|
|
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 {
|
|
parsedContent, err := parser.ParseFile(string(fileContent))
|
|
if err == nil {
|
|
content = parsedContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
ModTime: info.ModTime(),
|
|
MIME: mime,
|
|
Size: size,
|
|
Content: content,
|
|
})
|
|
}
|
|
|
|
relPath, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if relPath == "." {
|
|
relPath = ""
|
|
}
|
|
|
|
responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
|
|
|
|
return &Entry{
|
|
Type: "dir",
|
|
Name: filepath.Base(responsePath),
|
|
Path: responsePath,
|
|
ModTime: dirInfo.ModTime(),
|
|
Content: out,
|
|
}, nil
|
|
}
|
|
|
|
func (l *LocalFsAdapter) readFile(path string, root string) (*Entry, error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
relPath, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
|
|
mime := contentTypeFor(path)
|
|
var content any
|
|
|
|
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) (*Entry, error) {
|
|
if path == "/" {
|
|
var latestModTime time.Time
|
|
for _, r := range l.roots {
|
|
info, err := os.Stat(r)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if info.ModTime().After(latestModTime) {
|
|
latestModTime = info.ModTime()
|
|
}
|
|
}
|
|
|
|
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 nil, ErrNotFound
|
|
}
|
|
|
|
rootIdentifier := pathParts[0]
|
|
var targetRoot string
|
|
for _, r := range l.roots {
|
|
if filepath.Base(r) == rootIdentifier {
|
|
targetRoot = r
|
|
break
|
|
}
|
|
}
|
|
|
|
if targetRoot == "" {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
subPath := filepath.Join(pathParts[1:]...)
|
|
target := filepath.Join(targetRoot, subPath)
|
|
|
|
absTarget, err := filepath.Abs(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
absRoot, err := filepath.Abs(targetRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !strings.HasPrefix(absTarget, absRoot) {
|
|
return nil, errors.New("path escapes root")
|
|
}
|
|
|
|
fi, err := os.Stat(target)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
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() {
|
|
newEntry, err = l.readDir(target, targetRoot)
|
|
} else {
|
|
newEntry, err = l.readFile(target, targetRoot)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
l.mu.Lock()
|
|
l.cache[path] = newEntry
|
|
l.mu.Unlock()
|
|
|
|
return newEntry, nil
|
|
}
|
|
|
|
func (l *LocalFsAdapter) Write(path string, content []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func NewLocalFsAdapter(roots []string) (FileAdapter, error) {
|
|
return &LocalFsAdapter{
|
|
roots: roots,
|
|
cache: make(map[string]*Entry),
|
|
}, nil
|
|
}
|