mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 16:26:36 +08:00
fix: map docker host library paths
This commit is contained in:
@@ -366,6 +366,14 @@ services:
|
|||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
|
# 宿主机到容器的路径映射提示。用于用户误填宿主机路径时自动转换为容器路径:
|
||||||
|
# /vol1/1000/Docker/moviepilot-v2/media/电视剧 -> /media/电视剧
|
||||||
|
# /vol1/1000/qBittorrent/downloads/国产剧 -> /downloads/国产剧
|
||||||
|
MEDIASTATION_MEDIA_DIR: ${MEDIASTATION_MEDIA_DIR:-./media}
|
||||||
|
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
||||||
|
MEDIASTATION_DOWNLOAD_DIR: ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}
|
||||||
|
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
# 日志级别:debug / info / warn / error。
|
# 日志级别:debug / info / warn / error。
|
||||||
MEDIASTATION_LOGGING_LEVEL: ${MEDIASTATION_LOGGING_LEVEL:-info}
|
MEDIASTATION_LOGGING_LEVEL: ${MEDIASTATION_LOGGING_LEVEL:-info}
|
||||||
|
|
||||||
@@ -488,6 +496,8 @@ Docker Compose 左侧是宿主机真实目录,右侧是容器内目录。宿
|
|||||||
|
|
||||||
容器内只需要使用 `/media` 和 `/downloads`。如果你看到 `/vol1/1000/Docker/MediaStationGo/vol1/...`,说明 `.env` 或 compose 里把路径写成了 `./vol1/...`,需要去掉前面的点,改为 `/vol1/...`。
|
容器内只需要使用 `/media` 和 `/downloads`。如果你看到 `/vol1/1000/Docker/MediaStationGo/vol1/...`,说明 `.env` 或 compose 里把路径写成了 `./vol1/...`,需要去掉前面的点,改为 `/vol1/...`。
|
||||||
|
|
||||||
|
> 如果你在添加媒体库时填写了 `/vol1/1000/Docker/moviepilot-v2/media/电视剧/国产剧` 并提示不可访问,原因是应用在容器内运行,默认只能看到 `/media/电视剧/国产剧`。新版 compose 会把宿主机路径作为映射提示传入容器,尽量自动纠正;但最推荐、最稳定的填写方式仍然是容器路径 `/media/...`。
|
||||||
|
|
||||||
如果 compose 中这样挂载:
|
如果 compose 中这样挂载:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ The left side of a Docker Compose volume is the real host directory, and the rig
|
|||||||
|
|
||||||
Inside the app, use only `/media` and `/downloads`. If you see `/vol1/1000/Docker/MediaStationGo/vol1/...`, your `.env` or compose file probably uses `./vol1/...`; remove the dot and use `/vol1/...`.
|
Inside the app, use only `/media` and `/downloads`. If you see `/vol1/1000/Docker/MediaStationGo/vol1/...`, your `.env` or compose file probably uses `./vol1/...`; remove the dot and use `/vol1/...`.
|
||||||
|
|
||||||
|
> If adding `/vol1/1000/Docker/moviepilot-v2/media/电视剧/国产剧` as a library reports inaccessible, the app is running inside the container and normally sees `/media/电视剧/国产剧`. The updated compose passes host-path hints so the backend can auto-convert common mistakes, but `/media/...` remains the recommended and most stable input.
|
||||||
|
|
||||||
If your compose mounts are:
|
If your compose mounts are:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ services:
|
|||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
|
# 宿主机到容器的路径映射提示。用于用户误填宿主机路径时自动转换为容器路径:
|
||||||
|
# /vol1/1000/Docker/moviepilot-v2/media/电视剧 -> /media/电视剧
|
||||||
|
# /vol1/1000/qBittorrent/downloads/国产剧 -> /downloads/国产剧
|
||||||
|
MEDIASTATION_MEDIA_DIR: ${MEDIASTATION_MEDIA_DIR:-./media}
|
||||||
|
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
||||||
|
MEDIASTATION_DOWNLOAD_DIR: ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}
|
||||||
|
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
# 日志级别:debug / info / warn / error。
|
# 日志级别:debug / info / warn / error。
|
||||||
MEDIASTATION_LOGGING_LEVEL: ${MEDIASTATION_LOGGING_LEVEL:-info}
|
MEDIASTATION_LOGGING_LEVEL: ${MEDIASTATION_LOGGING_LEVEL:-info}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@@ -32,12 +33,9 @@ func (s *MediaService) CreateLibrary(ctx context.Context, name, path, kind strin
|
|||||||
if name == "" || path == "" {
|
if name == "" || path == "" {
|
||||||
return nil, errors.New("name and path required")
|
return nil, errors.New("name and path required")
|
||||||
}
|
}
|
||||||
abs, err := filepath.Abs(path)
|
abs, err := resolveAccessibleLibraryPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid path: %w", err)
|
return nil, err
|
||||||
}
|
|
||||||
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
|
|
||||||
return nil, fmt.Errorf("path is not an accessible directory: %s", abs)
|
|
||||||
}
|
}
|
||||||
if kind == "" {
|
if kind == "" {
|
||||||
kind = "movie"
|
kind = "movie"
|
||||||
@@ -49,6 +47,86 @@ func (s *MediaService) CreateLibrary(ctx context.Context, name, path, kind strin
|
|||||||
return lib, nil
|
return lib, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveAccessibleLibraryPath(path string) (string, error) {
|
||||||
|
abs, err := filepath.Abs(strings.TrimSpace(path))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid path: %w", err)
|
||||||
|
}
|
||||||
|
if isAccessibleDir(abs) {
|
||||||
|
return abs, nil
|
||||||
|
}
|
||||||
|
for _, candidate := range dockerVolumePathCandidates(abs) {
|
||||||
|
if isAccessibleDir(candidate) {
|
||||||
|
return filepath.Clean(candidate), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("path is not an accessible directory: %s", abs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAccessibleDir(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerVolumePathCandidates(path string) []string {
|
||||||
|
normalized := filepath.ToSlash(filepath.Clean(path))
|
||||||
|
var candidates []string
|
||||||
|
addCandidate := func(candidate string) {
|
||||||
|
candidate = filepath.Clean(filepath.FromSlash(candidate))
|
||||||
|
for _, existing := range candidates {
|
||||||
|
if sameLibraryPath(existing, candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates = append(candidates, candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mapping := range []struct {
|
||||||
|
env string
|
||||||
|
container string
|
||||||
|
}{
|
||||||
|
{env: "MEDIASTATION_MEDIA_DIR", container: envOrDefault("MEDIASTATION_MEDIA_CONTAINER_DIR", "/media")},
|
||||||
|
{env: "MEDIASTATION_DOWNLOAD_DIR", container: envOrDefault("MEDIASTATION_DOWNLOAD_CONTAINER_DIR", "/downloads")},
|
||||||
|
} {
|
||||||
|
host := filepath.ToSlash(filepath.Clean(os.Getenv(mapping.env)))
|
||||||
|
if host == "." || host == "" || strings.HasPrefix(host, ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalized == host {
|
||||||
|
addCandidate(mapping.container)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalized, host+"/") {
|
||||||
|
addCandidate(mapping.container + strings.TrimPrefix(normalized, host))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, marker := range []struct {
|
||||||
|
part string
|
||||||
|
container string
|
||||||
|
}{
|
||||||
|
{part: "/media/", container: "/media/"},
|
||||||
|
{part: "/downloads/", container: "/downloads/"},
|
||||||
|
} {
|
||||||
|
if idx := strings.Index(normalized, marker.part); idx >= 0 {
|
||||||
|
addCandidate(marker.container + strings.TrimPrefix(normalized[idx+len(marker.part):], "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameLibraryPath(a, b string) bool {
|
||||||
|
return filepath.Clean(a) == filepath.Clean(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOrDefault(key, fallback string) string {
|
||||||
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
// ListLibraries returns every library configured on the server.
|
// ListLibraries returns every library configured on the server.
|
||||||
func (s *MediaService) ListLibraries(ctx context.Context) ([]model.Library, error) {
|
func (s *MediaService) ListLibraries(ctx context.Context) ([]model.Library, error) {
|
||||||
return s.repo.Library.List(ctx)
|
return s.repo.Library.List(ctx)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveAccessibleLibraryPathMapsConfiguredHostMediaDir(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
hostRoot := filepath.Join(root, "nas", "moviepilot-v2", "media")
|
||||||
|
containerRoot := filepath.Join(root, "container", "media")
|
||||||
|
containerLibrary := filepath.Join(containerRoot, "电视剧", "国产剧")
|
||||||
|
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("MEDIASTATION_MEDIA_DIR", hostRoot)
|
||||||
|
t.Setenv("MEDIASTATION_MEDIA_CONTAINER_DIR", containerRoot)
|
||||||
|
|
||||||
|
got, err := resolveAccessibleLibraryPath(filepath.Join(hostRoot, "电视剧", "国产剧"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveAccessibleLibraryPath() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != filepath.Clean(containerLibrary) {
|
||||||
|
t.Fatalf("resolveAccessibleLibraryPath() = %q, want %q", got, filepath.Clean(containerLibrary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveAccessibleLibraryPathKeepsAccessibleContainerPath(t *testing.T) {
|
||||||
|
containerLibrary := filepath.Join(t.TempDir(), "media", "电影")
|
||||||
|
if err := os.MkdirAll(containerLibrary, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := resolveAccessibleLibraryPath(containerLibrary)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveAccessibleLibraryPath() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != filepath.Clean(containerLibrary) {
|
||||||
|
t.Fatalf("resolveAccessibleLibraryPath() = %q, want %q", got, filepath.Clean(containerLibrary))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,10 +92,14 @@ function LibraryPanel() {
|
|||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
className="input-base md:col-span-2"
|
className="input-base md:col-span-2"
|
||||||
placeholder="服务器路径,如 /media/movies"
|
placeholder="容器路径,如 /media/电视剧/国产剧"
|
||||||
value={path}
|
value={path}
|
||||||
onChange={(e) => setPath(e.target.value)}
|
onChange={(e) => setPath(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<p className="md:col-span-4 -mt-2 text-xs text-sand-500">
|
||||||
|
Docker 部署时请优先填写容器内路径,例如 /media/电影、/media/电视剧/国产剧;如果误填 NAS
|
||||||
|
宿主机路径,系统会尝试按 compose 挂载自动转换。
|
||||||
|
</p>
|
||||||
<select className="input-base" value={type} onChange={(e) => setType(e.target.value)}>
|
<select className="input-base" value={type} onChange={(e) => setType(e.target.value)}>
|
||||||
<option value="movie">电影</option>
|
<option value="movie">电影</option>
|
||||||
<option value="tv">电视剧</option>
|
<option value="tv">电视剧</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user