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) } // MoveFile moves a file or folder to a new path. func MoveFile(dataDir, userID, fromRel, toRel string) error { from := safePath(dataDir, userID, fromRel) to := safePath(dataDir, userID, toRel) if from == "" || to == "" { return os.ErrPermission } if err := os.MkdirAll(filepath.Dir(to), 0755); err != nil { return err } return os.Rename(from, to) } // 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") }