feat: auto-configure FileBrowser sidebar with per-drive sources
Generate config.yaml with a separate source per registered storage path. Each source uses the drive's label as its display name, making it appear automatically in FileBrowser's sidebar. The config.yaml is bind-mounted into the container (read-only) alongside the data volume. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1260,10 +1260,11 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
http.Redirect(w, r, "/settings?storage_msg=success&storage_detail="+url.QueryEscape("Megnevezés módosítva: "+label), http.StatusFound)
|
http.Redirect(w, r, "/settings?storage_msg=success&storage_detail="+url.QueryEscape("Megnevezés módosítva: "+label), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// syncFileBrowserMounts regenerates FileBrowser's docker-compose.yml
|
// syncFileBrowserMounts regenerates FileBrowser's docker-compose.yml and config.yaml
|
||||||
// with volume mounts for all registered storage paths, then recreates the container.
|
// with volume mounts and sources for all registered storage paths, then recreates the container.
|
||||||
func (s *Server) syncFileBrowserMounts() {
|
func (s *Server) syncFileBrowserMounts() {
|
||||||
composePath := "/opt/docker/stacks/filebrowser/docker-compose.yml"
|
stackDir := "/opt/docker/stacks/filebrowser"
|
||||||
|
composePath := stackDir + "/docker-compose.yml"
|
||||||
|
|
||||||
// Check if FileBrowser stack exists
|
// Check if FileBrowser stack exists
|
||||||
if _, err := os.Stat(composePath); os.IsNotExist(err) {
|
if _, err := os.Stat(composePath); os.IsNotExist(err) {
|
||||||
@@ -1275,7 +1276,7 @@ func (s *Server) syncFileBrowserMounts() {
|
|||||||
paths := s.settings.GetStoragePaths()
|
paths := s.settings.GetStoragePaths()
|
||||||
|
|
||||||
// Read domain from .env
|
// Read domain from .env
|
||||||
envPath := "/opt/docker/stacks/filebrowser/.env"
|
envPath := stackDir + "/.env"
|
||||||
domain := ""
|
domain := ""
|
||||||
if data, err := os.ReadFile(envPath); err == nil {
|
if data, err := os.ReadFile(envPath); err == nil {
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
@@ -1298,10 +1299,16 @@ func (s *Server) syncFileBrowserMounts() {
|
|||||||
storageMounts = append(storageMounts, line)
|
storageMounts = append(storageMounts, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate compose from template
|
// Generate and write config.yaml (sources + sidebar entries per drive)
|
||||||
compose := generateFileBrowserCompose(domain, storageMounts)
|
configPath := stackDir + "/config.yaml"
|
||||||
|
fbConfig := generateFileBrowserConfig(paths)
|
||||||
|
if err := os.WriteFile(configPath, []byte(fbConfig), 0644); err != nil {
|
||||||
|
s.logger.Printf("[ERROR] Failed to write FileBrowser config: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Write compose
|
// Generate and write compose (includes config.yaml mount)
|
||||||
|
compose := generateFileBrowserCompose(domain, storageMounts)
|
||||||
if err := os.WriteFile(composePath, []byte(compose), 0644); err != nil {
|
if err := os.WriteFile(composePath, []byte(compose), 0644); err != nil {
|
||||||
s.logger.Printf("[ERROR] Failed to write FileBrowser compose: %v", err)
|
s.logger.Printf("[ERROR] Failed to write FileBrowser compose: %v", err)
|
||||||
return
|
return
|
||||||
@@ -1311,11 +1318,11 @@ func (s *Server) syncFileBrowserMounts() {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
cmd := exec.CommandContext(ctx, "docker", "compose", "up", "-d", "--remove-orphans")
|
cmd := exec.CommandContext(ctx, "docker", "compose", "up", "-d", "--remove-orphans")
|
||||||
cmd.Dir = filepath.Dir(composePath)
|
cmd.Dir = stackDir
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
s.logger.Printf("[ERROR] Failed to recreate FileBrowser: %s — %v", string(out), err)
|
s.logger.Printf("[ERROR] Failed to recreate FileBrowser: %s — %v", string(out), err)
|
||||||
} else {
|
} else {
|
||||||
s.logger.Printf("[INFO] FileBrowser mounts synced — %d storage path(s)", len(paths))
|
s.logger.Printf("[INFO] FileBrowser mounts synced — %d storage path(s), config updated", len(paths))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1341,7 +1348,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Budapest
|
- TZ=Europe/Budapest
|
||||||
volumes:
|
volumes:
|
||||||
- filebrowser_data:/home/filebrowser/data%s
|
- filebrowser_data:/home/filebrowser/data
|
||||||
|
- ./config.yaml:/home/filebrowser/data/config.yaml:ro%s
|
||||||
networks:
|
networks:
|
||||||
- traefik-public
|
- traefik-public
|
||||||
deploy:
|
deploy:
|
||||||
@@ -1370,3 +1378,61 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
`, domain, storageSection, domain)
|
`, domain, storageSection, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateFileBrowserConfig returns a FileBrowser Quantum config.yaml with
|
||||||
|
// a separate source per registered storage path. Each source appears as a
|
||||||
|
// named sidebar entry in the FileBrowser UI.
|
||||||
|
func generateFileBrowserConfig(paths []settings.StoragePath) string {
|
||||||
|
var sources string
|
||||||
|
if len(paths) == 0 {
|
||||||
|
sources = ` - path: "/srv"
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
for _, sp := range paths {
|
||||||
|
mountName := filepath.Base(sp.Path)
|
||||||
|
label := sp.Label
|
||||||
|
if label == "" {
|
||||||
|
label = mountName
|
||||||
|
}
|
||||||
|
sources += fmt.Sprintf(" - path: \"/srv/%s\"\n name: %q\n config:\n defaultEnabled: true\n", mountName, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`# FileBrowser Quantum — managed by felhom-controller
|
||||||
|
# WARNING: This file is auto-generated. Manual edits will be overwritten.
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 80
|
||||||
|
baseURL: "/"
|
||||||
|
logging:
|
||||||
|
- levels: "info|warning|error"
|
||||||
|
sources:
|
||||||
|
%suserDefaults:
|
||||||
|
stickySidebar: true
|
||||||
|
darkMode: true
|
||||||
|
viewMode: "normal"
|
||||||
|
showHidden: false
|
||||||
|
dateFormat: false
|
||||||
|
gallerySize: 3
|
||||||
|
themeColor: "var(--blue)"
|
||||||
|
preview:
|
||||||
|
disableHideSidebar: false
|
||||||
|
highQuality: true
|
||||||
|
image: true
|
||||||
|
video: true
|
||||||
|
motionVideoPreview: true
|
||||||
|
office: true
|
||||||
|
popup: true
|
||||||
|
autoplayMedia: true
|
||||||
|
folder: true
|
||||||
|
permissions:
|
||||||
|
api: false
|
||||||
|
admin: false
|
||||||
|
modify: false
|
||||||
|
share: false
|
||||||
|
realtime: false
|
||||||
|
delete: false
|
||||||
|
create: false
|
||||||
|
download: true
|
||||||
|
`, sources)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user