move feature from meta-sort to meta-core
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
package mounts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handlers provides HTTP handlers for mount operations
|
||||
type Handlers struct {
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
// NewHandlers creates new mount handlers
|
||||
func NewHandlers(manager *Manager) *Handlers {
|
||||
return &Handlers{manager: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all mount-related routes
|
||||
func (h *Handlers) RegisterRoutes(r *mux.Router) {
|
||||
// Mount management
|
||||
r.HandleFunc("/api/mounts", h.handleListMounts).Methods("GET")
|
||||
r.HandleFunc("/api/mounts", h.handleCreateMount).Methods("POST")
|
||||
r.HandleFunc("/api/mounts/rclone/remotes", h.handleListRcloneRemotes).Methods("GET")
|
||||
r.HandleFunc("/api/mounts/{id}", h.handleGetMount).Methods("GET")
|
||||
r.HandleFunc("/api/mounts/{id}", h.handleDeleteMount).Methods("DELETE")
|
||||
r.HandleFunc("/api/mounts/{id}/mount", h.handleRequestMount).Methods("POST")
|
||||
r.HandleFunc("/api/mounts/{id}/unmount", h.handleRequestUnmount).Methods("POST")
|
||||
r.HandleFunc("/api/mounts/{id}/safe-unmount", h.handleSafeUnmount).Methods("POST")
|
||||
}
|
||||
|
||||
// handleListMounts handles GET /api/mounts
|
||||
func (h *Handlers) handleListMounts(w http.ResponseWriter, r *http.Request) {
|
||||
mounts, err := h.manager.ListMounts()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, MountsListResponse{Mounts: mounts})
|
||||
}
|
||||
|
||||
// handleGetMount handles GET /api/mounts/{id}
|
||||
func (h *Handlers) handleGetMount(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
mount, err := h.manager.GetMount(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if mount == nil {
|
||||
writeError(w, http.StatusNotFound, "mount not found")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, MountResponse{Mount: mount})
|
||||
}
|
||||
|
||||
// handleCreateMount handles POST /api/mounts
|
||||
func (h *Handlers) handleCreateMount(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateMountRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
mount, err := h.manager.CreateMount(&req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, MountResponse{Mount: mount})
|
||||
}
|
||||
|
||||
// handleDeleteMount handles DELETE /api/mounts/{id}
|
||||
func (h *Handlers) handleDeleteMount(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
if err := h.manager.DeleteMount(id); err != nil {
|
||||
if err.Error() == "mount not found" {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, StatusResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
// handleRequestMount handles POST /api/mounts/{id}/mount
|
||||
func (h *Handlers) handleRequestMount(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
if err := h.manager.RequestMount(id); err != nil {
|
||||
if err.Error() == "mount not found" {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, StatusResponse{
|
||||
Status: "ok",
|
||||
Message: "Mount requested",
|
||||
})
|
||||
}
|
||||
|
||||
// handleRequestUnmount handles POST /api/mounts/{id}/unmount
|
||||
func (h *Handlers) handleRequestUnmount(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
if err := h.manager.RequestUnmount(id); err != nil {
|
||||
if err.Error() == "mount not found" {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, StatusResponse{
|
||||
Status: "ok",
|
||||
Message: "Unmount requested",
|
||||
})
|
||||
}
|
||||
|
||||
// handleSafeUnmount handles POST /api/mounts/{id}/safe-unmount
|
||||
func (h *Handlers) handleSafeUnmount(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
// Get timeout from query param (default 60000ms)
|
||||
timeoutStr := r.URL.Query().Get("timeout")
|
||||
timeoutMS := 60000
|
||||
if timeoutStr != "" {
|
||||
if parsed, err := parseIntOrDefault(timeoutStr, 60000); err == nil {
|
||||
timeoutMS = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Get mount info
|
||||
mount, err := h.manager.GetMount(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if mount == nil {
|
||||
writeError(w, http.StatusNotFound, "mount not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Request unmount
|
||||
if err := h.manager.RequestUnmount(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for unmount
|
||||
success := h.manager.WaitForUnmount(mount.MountPath, timeoutMS)
|
||||
|
||||
if !success {
|
||||
writeJSON(w, http.StatusOK, StatusResponse{
|
||||
Status: "warning",
|
||||
Message: "Unmount requested but mount may still be attached",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, StatusResponse{
|
||||
Status: "ok",
|
||||
Message: "Safe unmount completed",
|
||||
})
|
||||
}
|
||||
|
||||
// handleListRcloneRemotes handles GET /api/mounts/rclone/remotes
|
||||
func (h *Handlers) handleListRcloneRemotes(w http.ResponseWriter, r *http.Request) {
|
||||
remotes, err := h.manager.ListRcloneRemotes()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, RcloneRemotesResponse{Remotes: remotes})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]string{
|
||||
"error": http.StatusText(status),
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func parseIntOrDefault(s string, defaultVal int) (int, error) {
|
||||
var result int
|
||||
if s == "" {
|
||||
return defaultVal, nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(s), &result)
|
||||
if err != nil {
|
||||
return defaultVal, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
package mounts
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/metazla/meta-core/internal/config"
|
||||
)
|
||||
|
||||
// Manager handles mount configuration and status
|
||||
type Manager struct {
|
||||
config *config.Config
|
||||
mu sync.RWMutex
|
||||
filesPath string
|
||||
mountsDir string
|
||||
}
|
||||
|
||||
// NewManager creates a new mount manager
|
||||
func NewManager(cfg *config.Config) (*Manager, error) {
|
||||
m := &Manager{
|
||||
config: cfg,
|
||||
filesPath: cfg.FilesPath,
|
||||
mountsDir: cfg.MountsDir,
|
||||
}
|
||||
|
||||
// Ensure directories exist
|
||||
if err := m.ensureDirs(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create mount directories: %w", err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ensureDirs creates required directories
|
||||
func (m *Manager) ensureDirs() error {
|
||||
dirs := []string{
|
||||
m.mountsDir,
|
||||
m.config.MountsErrorDir(),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readConfig reads the mounts configuration file
|
||||
func (m *Manager) readConfig() (*MountsFile, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
data, err := os.ReadFile(m.config.MountsFilePath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &MountsFile{Version: 1, Mounts: []MountConfig{}}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mountsFile MountsFile
|
||||
if err := json.Unmarshal(data, &mountsFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mountsFile, nil
|
||||
}
|
||||
|
||||
// writeConfig writes the mounts configuration file
|
||||
func (m *Manager) writeConfig(mountsFile *MountsFile) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if err := m.ensureDirs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(mountsFile, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(m.config.MountsFilePath(), data, 0644)
|
||||
}
|
||||
|
||||
// IsMounted checks if a path is currently mounted
|
||||
func (m *Manager) IsMounted(mountPath string) bool {
|
||||
cmd := exec.Command("findmnt", "-n", mountPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(output)) != ""
|
||||
}
|
||||
|
||||
// ReadError reads the error file for a mount
|
||||
func (m *Manager) ReadError(id string) (string, error) {
|
||||
errorFile := filepath.Join(m.config.MountsErrorDir(), id+".error")
|
||||
data, err := os.ReadFile(errorFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
if len(lines) > 1 {
|
||||
// Skip timestamp line, return rest
|
||||
return strings.Join(lines[1:], "\n"), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// SanitizeName sanitizes a mount name for use as a directory name
|
||||
func SanitizeName(name string) string {
|
||||
// Convert to lowercase
|
||||
result := strings.ToLower(name)
|
||||
|
||||
// Replace non-alphanumeric chars with hyphens
|
||||
re := regexp.MustCompile(`[^a-z0-9-_]`)
|
||||
result = re.ReplaceAllString(result, "-")
|
||||
|
||||
// Collapse multiple hyphens
|
||||
re = regexp.MustCompile(`-+`)
|
||||
result = re.ReplaceAllString(result, "-")
|
||||
|
||||
// Remove leading/trailing hyphens
|
||||
result = strings.Trim(result, "-")
|
||||
|
||||
// Limit length
|
||||
if len(result) > 64 {
|
||||
result = result[:64]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ObscurePassword obscures a password using rclone
|
||||
func ObscurePassword(password string) (string, error) {
|
||||
// Escape special characters for shell
|
||||
escapedPass := strings.ReplaceAll(password, `"`, `\"`)
|
||||
|
||||
cmd := exec.Command("rclone", "obscure", escapedPass)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to obscure password: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
// ListMounts returns all mounts with their current status
|
||||
func (m *Manager) ListMounts() ([]MountStatus, error) {
|
||||
mountsFile, err := m.readConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statuses := make([]MountStatus, len(mountsFile.Mounts))
|
||||
for i, mount := range mountsFile.Mounts {
|
||||
mounted := m.IsMounted(mount.MountPath)
|
||||
errMsg, _ := m.ReadError(mount.ID)
|
||||
|
||||
statuses[i] = MountStatus{
|
||||
MountConfig: mount,
|
||||
Mounted: mounted,
|
||||
Error: errMsg,
|
||||
LastChecked: NowMS(),
|
||||
}
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetMount returns a single mount by ID
|
||||
func (m *Manager) GetMount(id string) (*MountStatus, error) {
|
||||
mountsFile, err := m.readConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, mount := range mountsFile.Mounts {
|
||||
if mount.ID == id {
|
||||
mounted := m.IsMounted(mount.MountPath)
|
||||
errMsg, _ := m.ReadError(mount.ID)
|
||||
|
||||
return &MountStatus{
|
||||
MountConfig: mount,
|
||||
Mounted: mounted,
|
||||
Error: errMsg,
|
||||
LastChecked: NowMS(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CreateMount creates a new mount configuration
|
||||
func (m *Manager) CreateMount(req *CreateMountRequest) (*MountStatus, error) {
|
||||
if req.Name == "" {
|
||||
return nil, fmt.Errorf("mount name is required")
|
||||
}
|
||||
|
||||
if req.Type != MountTypeNFS && req.Type != MountTypeSMB && req.Type != MountTypeRclone {
|
||||
return nil, fmt.Errorf("valid mount type (nfs, smb, rclone) is required")
|
||||
}
|
||||
|
||||
// Validate type-specific fields
|
||||
switch req.Type {
|
||||
case MountTypeNFS:
|
||||
if req.NFSServer == "" || req.NFSPath == "" {
|
||||
return nil, fmt.Errorf("NFS server and path are required")
|
||||
}
|
||||
case MountTypeSMB:
|
||||
if req.SMBServer == "" || req.SMBShare == "" {
|
||||
return nil, fmt.Errorf("SMB server and share are required")
|
||||
}
|
||||
case MountTypeRclone:
|
||||
if req.RcloneRemote == "" {
|
||||
return nil, fmt.Errorf("rclone remote is required")
|
||||
}
|
||||
}
|
||||
|
||||
// Generate ID and mount path
|
||||
id := uuid.New().String()
|
||||
safeName := SanitizeName(req.Name)
|
||||
mountPath := filepath.Join(m.filesPath, safeName)
|
||||
|
||||
// Check if path already exists
|
||||
mountsFile, err := m.readConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, mount := range mountsFile.Mounts {
|
||||
if mount.MountPath == mountPath {
|
||||
return nil, fmt.Errorf("mount path %s already configured", mountPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Default enabled to true if not specified
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
|
||||
mount := MountConfig{
|
||||
ID: id,
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Enabled: enabled,
|
||||
DesiredMounted: enabled, // Auto-mount if enabled
|
||||
MountPath: mountPath,
|
||||
Options: req.Options,
|
||||
}
|
||||
|
||||
// Type-specific fields
|
||||
switch req.Type {
|
||||
case MountTypeNFS:
|
||||
mount.NFSServer = req.NFSServer
|
||||
mount.NFSPath = req.NFSPath
|
||||
case MountTypeSMB:
|
||||
mount.SMBServer = req.SMBServer
|
||||
mount.SMBShare = req.SMBShare
|
||||
mount.SMBUsername = req.SMBUsername
|
||||
mount.SMBDomain = req.SMBDomain
|
||||
if req.SMBPassword != "" {
|
||||
obscured, err := ObscurePassword(req.SMBPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to secure password: %w", err)
|
||||
}
|
||||
mount.SMBPasswordObscured = obscured
|
||||
}
|
||||
case MountTypeRclone:
|
||||
mount.RcloneRemote = req.RcloneRemote
|
||||
mount.RclonePath = req.RclonePath
|
||||
}
|
||||
|
||||
mountsFile.Mounts = append(mountsFile.Mounts, mount)
|
||||
if err := m.writeConfig(mountsFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("[Mounts] Created mount config: %s (%s) -> %s", mount.Name, mount.Type, mount.MountPath)
|
||||
|
||||
return &MountStatus{
|
||||
MountConfig: mount,
|
||||
Mounted: false,
|
||||
LastChecked: NowMS(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RequestMount sets desiredMounted to true
|
||||
func (m *Manager) RequestMount(id string) error {
|
||||
mountsFile, err := m.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, mount := range mountsFile.Mounts {
|
||||
if mount.ID == id {
|
||||
mountsFile.Mounts[i].DesiredMounted = true
|
||||
if err := m.writeConfig(mountsFile); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[Mounts] Mount requested: %s", mount.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("mount not found")
|
||||
}
|
||||
|
||||
// RequestUnmount sets desiredMounted to false
|
||||
func (m *Manager) RequestUnmount(id string) error {
|
||||
mountsFile, err := m.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, mount := range mountsFile.Mounts {
|
||||
if mount.ID == id {
|
||||
mountsFile.Mounts[i].DesiredMounted = false
|
||||
if err := m.writeConfig(mountsFile); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[Mounts] Unmount requested: %s", mount.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("mount not found")
|
||||
}
|
||||
|
||||
// WaitForUnmount waits for a mount to be unmounted
|
||||
func (m *Manager) WaitForUnmount(mountPath string, timeoutMS int) bool {
|
||||
deadline := time.Now().Add(time.Duration(timeoutMS) * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if !m.IsMounted(mountPath) {
|
||||
return true
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DeleteMount removes a mount configuration
|
||||
func (m *Manager) DeleteMount(id string) error {
|
||||
mountsFile, err := m.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var mount *MountConfig
|
||||
index := -1
|
||||
for i, mnt := range mountsFile.Mounts {
|
||||
if mnt.ID == id {
|
||||
mount = &mnt
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return fmt.Errorf("mount not found")
|
||||
}
|
||||
|
||||
// Request unmount first
|
||||
mountsFile.Mounts[index].DesiredMounted = false
|
||||
if err := m.writeConfig(mountsFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for unmount (15 seconds max)
|
||||
m.WaitForUnmount(mount.MountPath, 15000)
|
||||
|
||||
// Remove from config
|
||||
mountsFile.Mounts = append(mountsFile.Mounts[:index], mountsFile.Mounts[index+1:]...)
|
||||
if err := m.writeConfig(mountsFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean up error file
|
||||
errorFile := filepath.Join(m.config.MountsErrorDir(), id+".error")
|
||||
os.Remove(errorFile)
|
||||
|
||||
// Try to remove mount directory (will fail if not empty, which is fine)
|
||||
os.Remove(mount.MountPath)
|
||||
|
||||
log.Printf("[Mounts] Deleted mount: %s", mount.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRcloneRemotes lists available rclone remotes
|
||||
func (m *Manager) ListRcloneRemotes() ([]RcloneRemote, error) {
|
||||
// Call rclone RC API
|
||||
cmd := exec.Command("curl", "-s", "-X", "POST",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-u", "admin:admin",
|
||||
"http://127.0.0.1:5572/config/listremotes")
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Printf("[Mounts] Failed to list rclone remotes: %v", err)
|
||||
return []RcloneRemote{}, nil
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Remotes []string `json:"remotes"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &response); err != nil {
|
||||
return []RcloneRemote{}, nil
|
||||
}
|
||||
|
||||
remotes := make([]RcloneRemote, 0, len(response.Remotes))
|
||||
for _, name := range response.Remotes {
|
||||
// Strip trailing colon if present
|
||||
cleanName := strings.TrimSuffix(name, ":")
|
||||
|
||||
// Get type for this remote
|
||||
remoteType := m.getRcloneRemoteType(cleanName)
|
||||
|
||||
remotes = append(remotes, RcloneRemote{
|
||||
Name: cleanName,
|
||||
Type: remoteType,
|
||||
})
|
||||
}
|
||||
|
||||
return remotes, nil
|
||||
}
|
||||
|
||||
// getRcloneRemoteType gets the type of an rclone remote
|
||||
func (m *Manager) getRcloneRemoteType(name string) string {
|
||||
// Build request body
|
||||
body := fmt.Sprintf(`{"name":"%s"}`, name)
|
||||
|
||||
cmd := exec.Command("curl", "-s", "-X", "POST",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-u", "admin:admin",
|
||||
"-d", body,
|
||||
"http://127.0.0.1:5572/config/get")
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(output, &response); err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
if typeStr, ok := response["type"].(string); ok {
|
||||
return typeStr
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// Basic auth helper for rclone API
|
||||
func basicAuth(username, password string) string {
|
||||
auth := username + ":" + password
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package mounts
|
||||
|
||||
import "time"
|
||||
|
||||
// MountType represents the type of mount
|
||||
type MountType string
|
||||
|
||||
const (
|
||||
MountTypeNFS MountType = "nfs"
|
||||
MountTypeSMB MountType = "smb"
|
||||
MountTypeRclone MountType = "rclone"
|
||||
)
|
||||
|
||||
// MountConfig represents a mount configuration
|
||||
type MountConfig struct {
|
||||
// Common fields
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type MountType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DesiredMounted bool `json:"desiredMounted"`
|
||||
MountPath string `json:"mountPath"`
|
||||
Options string `json:"options,omitempty"`
|
||||
|
||||
// NFS-specific fields
|
||||
NFSServer string `json:"nfsServer,omitempty"`
|
||||
NFSPath string `json:"nfsPath,omitempty"`
|
||||
|
||||
// SMB-specific fields
|
||||
SMBServer string `json:"smbServer,omitempty"`
|
||||
SMBShare string `json:"smbShare,omitempty"`
|
||||
SMBUsername string `json:"smbUsername,omitempty"`
|
||||
SMBPasswordObscured string `json:"smbPasswordObscured,omitempty"`
|
||||
SMBDomain string `json:"smbDomain,omitempty"`
|
||||
|
||||
// rclone-specific fields
|
||||
RcloneRemote string `json:"rcloneRemote,omitempty"`
|
||||
RclonePath string `json:"rclonePath,omitempty"`
|
||||
}
|
||||
|
||||
// MountStatus represents the runtime status of a mount
|
||||
type MountStatus struct {
|
||||
MountConfig
|
||||
Mounted bool `json:"mounted"`
|
||||
Error string `json:"error,omitempty"`
|
||||
LastChecked int64 `json:"lastChecked"`
|
||||
}
|
||||
|
||||
// MountsFile represents the mounts.json file structure
|
||||
type MountsFile struct {
|
||||
Version int `json:"version"`
|
||||
Mounts []MountConfig `json:"mounts"`
|
||||
}
|
||||
|
||||
// RcloneRemote represents an rclone remote
|
||||
type RcloneRemote struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// CreateMountRequest is the request body for creating a mount
|
||||
type CreateMountRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type MountType `json:"type"`
|
||||
Enabled *bool `json:"enabled,omitempty"` // Pointer to detect if set
|
||||
Options string `json:"options,omitempty"`
|
||||
|
||||
// NFS
|
||||
NFSServer string `json:"nfsServer,omitempty"`
|
||||
NFSPath string `json:"nfsPath,omitempty"`
|
||||
|
||||
// SMB
|
||||
SMBServer string `json:"smbServer,omitempty"`
|
||||
SMBShare string `json:"smbShare,omitempty"`
|
||||
SMBUsername string `json:"smbUsername,omitempty"`
|
||||
SMBPassword string `json:"smbPassword,omitempty"` // Plain text, will be obscured
|
||||
SMBDomain string `json:"smbDomain,omitempty"`
|
||||
|
||||
// rclone
|
||||
RcloneRemote string `json:"rcloneRemote,omitempty"`
|
||||
RclonePath string `json:"rclonePath,omitempty"`
|
||||
}
|
||||
|
||||
// MountResponse is the response for mount operations
|
||||
type MountResponse struct {
|
||||
Mount *MountStatus `json:"mount,omitempty"`
|
||||
}
|
||||
|
||||
// MountsListResponse is the response for listing mounts
|
||||
type MountsListResponse struct {
|
||||
Mounts []MountStatus `json:"mounts"`
|
||||
}
|
||||
|
||||
// RcloneRemotesResponse is the response for listing rclone remotes
|
||||
type RcloneRemotesResponse struct {
|
||||
Remotes []RcloneRemote `json:"remotes"`
|
||||
}
|
||||
|
||||
// StatusResponse is a generic status response
|
||||
type StatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
GateStatus interface{} `json:"gateStatus,omitempty"`
|
||||
}
|
||||
|
||||
// Timestamp helpers
|
||||
func NowMS() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
Reference in New Issue
Block a user