Files
Archivum/backend/internal/storage/storage.go
Björn Blomberg a46d56e2fc feat: integrate @tailwindcss/typography and enhance folder picker functionality
- 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.
2026-04-13 10:55:02 +02:00

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")