- Added @tailwindcss/typography dependency to package.json and tailwind.config.js. - Updated FolderPicker.vue to include additional properties (isGitRepo, hasDocuments) for directories. - Enhanced Sidebar.vue with folder creation and document handling features, including drag-and-drop functionality. - Implemented createFolder and moveDocument mutations in gql.ts for managing folder and document operations. - Added logic to handle folder creation and document creation prompts in Sidebar.vue. - Updated the layout and interactions in Sidebar.vue to improve user experience.
174 lines
4.6 KiB
Go
174 lines
4.6 KiB
Go
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")
|