Initial commit: Phase 1+2 prototype
- Go backend with SQLite, JWT auth, file CRUD - Vue 3 frontend with split/raw/WYSIWYG editor modes - Markdown preview (marked, GFM) - Formatting toolbar + keyboard shortcuts - File tree with search, create, delete - Light/dark theme toggle - Admin panel (user management) - Preferences (timezone, theme, default mode) - Shared documents section (placeholder) - Export: PDF, HTML, MD - Build daemon (Python, stdlib only) - Build job queue API - Docker deployment
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Children []FileInfo `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// UserDir returns the base directory for a user's files.
|
||||
func UserDir(dataDir, userID string) string {
|
||||
return filepath.Join(dataDir, "files", userID)
|
||||
}
|
||||
|
||||
// EnsureUserDir creates the user's file directory if it doesn't exist.
|
||||
func EnsureUserDir(dataDir, userID string) error {
|
||||
return os.MkdirAll(UserDir(dataDir, userID), 0755)
|
||||
}
|
||||
|
||||
// ReadFile reads a markdown file for a user.
|
||||
func ReadFile(dataDir, userID, relPath string) (string, error) {
|
||||
p := safePath(dataDir, userID, relPath)
|
||||
if p == "" {
|
||||
return "", os.ErrPermission
|
||||
}
|
||||
b, err := os.ReadFile(p)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// WriteFile writes content to a markdown file for a user.
|
||||
func WriteFile(dataDir, userID, relPath, content string) error {
|
||||
p := safePath(dataDir, userID, relPath)
|
||||
if p == "" {
|
||||
return os.ErrPermission
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// CreateFolder creates a directory for a user.
|
||||
func CreateFolder(dataDir, userID, relPath string) error {
|
||||
p := safePath(dataDir, userID, relPath)
|
||||
if p == "" {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return os.MkdirAll(p, 0755)
|
||||
}
|
||||
|
||||
// DeleteFile removes a file or folder for a user.
|
||||
func DeleteFile(dataDir, userID, relPath string) error {
|
||||
p := safePath(dataDir, userID, relPath)
|
||||
if p == "" {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return os.RemoveAll(p)
|
||||
}
|
||||
|
||||
// ListTree returns the file tree for a user.
|
||||
func ListTree(dataDir, userID string) ([]FileInfo, error) {
|
||||
root := UserDir(dataDir, userID)
|
||||
if err := os.MkdirAll(root, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listDir(root, "")
|
||||
}
|
||||
|
||||
func listDir(base, rel string) ([]FileInfo, error) {
|
||||
dir := filepath.Join(base, rel)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []FileInfo
|
||||
for _, e := range entries {
|
||||
entryRel := filepath.Join(rel, e.Name())
|
||||
info := FileInfo{
|
||||
Name: e.Name(),
|
||||
Path: entryRel,
|
||||
IsDir: e.IsDir(),
|
||||
}
|
||||
if e.IsDir() {
|
||||
children, err := listDir(base, entryRel)
|
||||
if err == nil {
|
||||
info.Children = children
|
||||
}
|
||||
}
|
||||
result = append(result, info)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// safePath validates and returns the absolute path, preventing traversal.
|
||||
func safePath(dataDir, userID, relPath string) string {
|
||||
if strings.Contains(relPath, "..") {
|
||||
return ""
|
||||
}
|
||||
root := UserDir(dataDir, userID)
|
||||
p := filepath.Join(root, filepath.Clean(relPath))
|
||||
if !strings.HasPrefix(p, root) {
|
||||
return ""
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// GetCreatedTime returns the file's modification time as ISO string.
|
||||
func GetCreatedTime(dataDir, userID, relPath string) string {
|
||||
p := safePath(dataDir, userID, relPath)
|
||||
if p == "" {
|
||||
return ""
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return info.ModTime().UTC().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
Path string `json:"path"`
|
||||
Snippet string `json:"snippet"`
|
||||
}
|
||||
|
||||
// Search finds files containing the query string (case-insensitive).
|
||||
func Search(dataDir, userID, query string) ([]SearchResult, error) {
|
||||
root := UserDir(dataDir, userID)
|
||||
query = strings.ToLower(query)
|
||||
var results []SearchResult
|
||||
|
||||
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
|
||||
// Match filename
|
||||
if strings.Contains(strings.ToLower(info.Name()), query) {
|
||||
results = append(results, SearchResult{Path: rel, Snippet: "(filename match)"})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match content (only for .md and .txt)
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext == ".md" || ext == ".txt" {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
lower := strings.ToLower(string(content))
|
||||
idx := strings.Index(lower, query)
|
||||
if idx >= 0 {
|
||||
// Extract snippet
|
||||
start := idx - 40
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := idx + len(query) + 40
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
snippet := string(content[start:end])
|
||||
results = append(results, SearchResult{Path: rel, Snippet: snippet})
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) >= 50 {
|
||||
return filepath.SkipAll
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if results == nil {
|
||||
results = []SearchResult{}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
Reference in New Issue
Block a user