Files
gobsidian/internal/utils/utils.go
2025-08-25 18:15:51 +01:00

301 lines
6.6 KiB
Go

package utils
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gobsidian/internal/config"
"gobsidian/internal/models"
)
func BuildTreeStructure(rootDir string, hiddenDirs []string, cfg *config.Config) (*models.TreeNode, error) {
root := &models.TreeNode{
Name: filepath.Base(rootDir),
Path: "",
Type: models.FileTypeDirectory,
}
err := buildTreeRecursive(rootDir, root, hiddenDirs, cfg)
return root, err
}
func buildTreeRecursive(currentPath string, node *models.TreeNode, hiddenDirs []string, cfg *config.Config) error {
entries, err := os.ReadDir(currentPath)
if err != nil {
return err
}
// Sort entries: directories first, then files
sort.Slice(entries, func(i, j int) bool {
if entries[i].IsDir() != entries[j].IsDir() {
return entries[i].IsDir()
}
return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name())
})
for _, entry := range entries {
// Skip hidden files
if strings.HasPrefix(entry.Name(), ".") {
continue
}
// Skip hidden directories
if entry.IsDir() && contains(hiddenDirs, entry.Name()) {
continue
}
childPath := filepath.Join(currentPath, entry.Name())
relativePath := getRelativePath(childPath, cfg.NotesDir)
child := &models.TreeNode{
Name: entry.Name(),
Path: relativePath,
}
if entry.IsDir() {
child.Type = models.FileTypeDirectory
// Recursively build children
if err := buildTreeRecursive(childPath, child, hiddenDirs, cfg); err != nil {
continue // Skip directories that can't be read
}
} else {
child.Type = models.GetFileType(filepath.Ext(entry.Name()), cfg.AllowedImageExtensions, cfg.AllowedFileExtensions)
// Apply visibility for tree (left navigation)
switch child.Type {
case models.FileTypeMarkdown:
// always show
case models.FileTypeImage:
if !cfg.ShowImagesInTree {
continue
}
case models.FileTypeText:
if !cfg.ShowFilesInTree {
continue
}
default:
continue
}
}
node.Children = append(node.Children, child)
}
return nil
}
func GetFolderContents(folderPath string, cfg *config.Config) ([]models.FileInfo, error) {
fullPath := filepath.Join(cfg.NotesDir, folderPath)
entries, err := os.ReadDir(fullPath)
if err != nil {
return nil, err
}
var contents []models.FileInfo
// Sort entries: directories first, then files
sort.Slice(entries, func(i, j int) bool {
if entries[i].IsDir() != entries[j].IsDir() {
return entries[i].IsDir()
}
return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name())
})
for _, entry := range entries {
// Skip hidden files
if strings.HasPrefix(entry.Name(), ".") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
relativePath := filepath.Join(folderPath, entry.Name())
if folderPath == "" {
relativePath = entry.Name()
}
fileInfo := models.FileInfo{
Name: entry.Name(),
Path: relativePath,
Size: info.Size(),
ModTime: info.ModTime(),
}
if entry.IsDir() {
fileInfo.Type = models.FileTypeDirectory
fileInfo.DisplayName = entry.Name()
} else {
fileInfo.Type = models.GetFileType(filepath.Ext(entry.Name()), cfg.AllowedImageExtensions, cfg.AllowedFileExtensions)
// Set display name based on file type
if fileInfo.Type == models.FileTypeMarkdown {
fileInfo.DisplayName = strings.TrimSuffix(entry.Name(), ".md")
} else {
fileInfo.DisplayName = entry.Name()
}
// Visibility for folder view
if fileInfo.Type == models.FileTypeImage {
// prefer new flag; fallback to legacy ImagesHide (handled by default in config load)
if !cfg.ShowImagesInFolder {
continue
}
}
if fileInfo.Type == models.FileTypeText {
if !cfg.ShowFilesInFolder {
continue
}
}
// Skip files that are not allowed
if fileInfo.Type == models.FileTypeOther {
continue
}
}
contents = append(contents, fileInfo)
}
return contents, nil
}
func GenerateBreadcrumbs(path string) []models.Breadcrumb {
var breadcrumbs []models.Breadcrumb
// Add root
breadcrumbs = append(breadcrumbs, models.Breadcrumb{
Name: "/",
URL: "/",
})
if path == "" {
return breadcrumbs
}
parts := strings.Split(path, "/")
currentPath := ""
for _, part := range parts {
if part == "" {
continue
}
if currentPath == "" {
currentPath = part
} else {
currentPath = filepath.Join(currentPath, part)
}
breadcrumbs = append(breadcrumbs, models.Breadcrumb{
Name: part,
URL: "/folder/" + currentPath,
})
}
return breadcrumbs
}
func GetImageStorageInfo(notePath string, cfg *config.Config) models.ImageStorageInfo {
var storageDir, markdownPath string
switch cfg.ImageStorageMode {
case 1: // Store directly in NOTES_DIR
storageDir = cfg.NotesDir
markdownPath = ""
case 2: // Store in specific folder
storageDir = cfg.ImageStoragePath
markdownPath = ""
case 3: // Store in same directory as note
if notePath != "" {
storageDir = filepath.Join(cfg.NotesDir, filepath.Dir(notePath))
} else {
storageDir = cfg.NotesDir
}
markdownPath = ""
case 4: // Store in subfolder of note's directory
if notePath != "" {
storageDir = filepath.Join(cfg.NotesDir, filepath.Dir(notePath), cfg.ImageSubfolderName)
} else {
storageDir = filepath.Join(cfg.NotesDir, cfg.ImageSubfolderName)
}
markdownPath = cfg.ImageSubfolderName
default:
storageDir = cfg.NotesDir
markdownPath = ""
}
return models.ImageStorageInfo{
StorageDir: storageDir,
MarkdownPath: markdownPath,
}
}
func EnsureDir(dirPath string) error {
return os.MkdirAll(dirPath, 0755)
}
func IsPathInSkippedDirs(path string, skippedDirs []string) bool {
parts := strings.Split(filepath.Clean(path), string(filepath.Separator))
for _, part := range parts {
if contains(skippedDirs, part) {
return true
}
}
return false
}
func GetActivePath(currentPath string) []string {
if currentPath == "" {
return []string{}
}
return strings.Split(strings.Trim(currentPath, "/"), "/")
}
// Helper functions
func contains(slice []string, item string) bool {
for _, s := range slice {
if strings.TrimSpace(s) == item {
return true
}
}
return false
}
func getRelativePath(fullPath, basePath string) string {
rel, err := filepath.Rel(basePath, fullPath)
if err != nil {
return fullPath
}
return filepath.ToSlash(rel)
}
func FormatFileSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
func FormatTime(t time.Time) string {
return t.Format("2006-01-02 15:04")
}