package storage import ( "errors" "os" "path/filepath" "strings" ) // Store manages AsciiDoc files on disk. type Store struct { root string } func New(root string) (*Store, error) { if err := os.MkdirAll(root, 0755); err != nil { return nil, err } return &Store{root: root}, nil } // Read returns the content of a document by its slug path (e.g. "guides/install"). func (s *Store) Read(slug string) (string, error) { data, err := os.ReadFile(s.filePath(slug)) if err != nil { if errors.Is(err, os.ErrNotExist) { return "", ErrNotFound } return "", err } return string(data), nil } // Write persists content to disk, creating parent directories as needed. func (s *Store) Write(slug, content string) error { path := s.filePath(slug) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return os.WriteFile(path, []byte(content), 0644) } // CreateFolder creates a new folder/directory. func (s *Store) CreateFolder(folderSlug string) error { path := s.folderPath(folderSlug) return os.MkdirAll(path, 0755) } // MoveDocument moves a document from one location to another. func (s *Store) MoveDocument(fromSlug, toSlug string) error { fromPath := s.filePath(fromSlug) toPath := s.filePath(toSlug) // Ensure target directory exists if err := os.MkdirAll(filepath.Dir(toPath), 0755); err != nil { return err } // Move the file return os.Rename(fromPath, toPath) } // Delete removes a document from disk. func (s *Store) Delete(slug string) error { return os.Remove(s.filePath(slug)) } // List returns all document slugs under an optional prefix. func (s *Store) List(prefix string) ([]string, error) { base := filepath.Join(s.root, filepath.FromSlash(prefix)) var slugs []string err := filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() && strings.HasSuffix(path, ".adoc") { rel, _ := filepath.Rel(s.root, path) slug := strings.TrimSuffix(filepath.ToSlash(rel), ".adoc") slugs = append(slugs, slug) } return nil }) return slugs, err } // ListImages returns all images in the directory of a given slug. func (s *Store) ListImages(slug string) ([]map[string]interface{}, error) { dir := filepath.Dir(s.filePath(slug)) var images []map[string]interface{} entries, err := os.ReadDir(dir) if err != nil { if errors.Is(err, os.ErrNotExist) { return images, nil } return nil, err } for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() ext := strings.ToLower(filepath.Ext(name)) if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".svg" || ext == ".webp" { info, err := entry.Info() if err == nil { images = append(images, map[string]interface{}{ "name": name, "size": info.Size(), // The URL is relative to the media endpoint mapped in the server. "url": "/media/" + strings.TrimPrefix(filepath.ToSlash(filepath.Join(filepath.Dir(slug), name)), "/"), }) } } } return images, nil } // SaveImage saves an uploaded image file into the directory of a slug. func (s *Store) SaveImage(slug, filename string, data []byte) error { dir := filepath.Dir(s.filePath(slug)) if err := os.MkdirAll(dir, 0755); err != nil { return err } return os.WriteFile(filepath.Join(dir, filename), data, 0644) } // DeleteImage removes an image file from the directory of a slug. func (s *Store) DeleteImage(slug, filename string) error { dir := filepath.Dir(s.filePath(slug)) return os.Remove(filepath.Join(dir, filename)) } // GetImagePath returns the absolute path on disk for a media file. // It prevents directory traversal out of the root storage path. func (s *Store) GetImagePath(relPath string) (string, error) { if strings.Contains(relPath, "..") { return "", errors.New("invalid path") } return filepath.Join(s.root, filepath.FromSlash(relPath)), nil } // ListFolders returns all folder paths relative to root, excluding .git. func (s *Store) ListFolders() ([]string, error) { var folders []string err := filepath.WalkDir(s.root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if d.Name() == ".git" { return filepath.SkipDir } if path != s.root { rel, _ := filepath.Rel(s.root, path) folders = append(folders, filepath.ToSlash(rel)) } } return nil }) return folders, err } func (s *Store) filePath(slug string) string { return filepath.Join(s.root, filepath.FromSlash(slug)+".adoc") } func (s *Store) folderPath(folderSlug string) string { return filepath.Join(s.root, filepath.FromSlash(folderSlug)) } var ErrNotFound = errors.New("document not found")