Skip to content

Commit 4d8b094

Browse files
committed
Improvement for legacy path
1 parent 7c1c024 commit 4d8b094

6 files changed

Lines changed: 36 additions & 145 deletions

File tree

Muxarr.Data/ContainerAppData.cs

Lines changed: 17 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,18 @@
33

44
namespace Muxarr.Data;
55

6-
/// <summary>Where the database ended up at startup, so the UI can nag. Only meaningful inside a container.</summary>
76
public enum AppDataState
87
{
9-
/// <summary>Not running in a container, or the database is on a mounted /config.</summary>
108
Ok,
119

12-
/// <summary>The database lives in /data: an older install, or nothing mounted at /config.</summary>
13-
LegacyLocation,
14-
15-
/// <summary>Nothing is mounted at /config; the database sits in the container's writable layer.</summary>
16-
Unpersisted
10+
/// <summary>An older install: the database still lives in /data and is used from there.</summary>
11+
LegacyLocation
1712
}
1813

1914
/// <summary>
2015
/// The database moved from /data to /config. An existing /data database keeps being used
21-
/// until the user remounts; nothing is copied or moved. /data is only ever written to when
22-
/// it already holds our database or is the empty volume the image declares.
16+
/// until the user remounts; nothing is copied, moved or deleted. Only acts on the default
17+
/// connection string, so custom or relative paths are left alone.
2318
/// </summary>
2419
public static class ContainerAppData
2520
{
@@ -33,19 +28,13 @@ public static class ContainerAppData
3328

3429
public static string ResolveConnectionString(string connectionString, ILogger? logger = null)
3530
{
36-
if (!RunningInContainer())
37-
{
38-
return connectionString;
39-
}
40-
4131
lock (ResolveLock)
4232
{
43-
return _resolved ??= Resolve(connectionString, ConfigDir, DataDir, IsMountPoint, logger);
33+
return _resolved ??= Resolve(connectionString, ConfigDir, DataDir, logger);
4434
}
4535
}
4636

47-
internal static string Resolve(string connectionString, string configDir, string dataDir,
48-
Func<string, bool> isMountPoint, ILogger? logger)
37+
internal static string Resolve(string connectionString, string configDir, string dataDir, ILogger? logger)
4938
{
5039
var builder = new SqliteConnectionStringBuilder(connectionString);
5140
if (string.IsNullOrWhiteSpace(builder.DataSource) || builder.DataSource == ":memory:")
@@ -62,69 +51,19 @@ internal static string Resolve(string connectionString, string configDir, string
6251
}
6352

6453
var legacyDbPath = Path.Combine(Path.GetFullPath(dataDir), Path.GetFileName(dbPath));
65-
var configMounted = isMountPoint(configDir);
66-
67-
if (!File.Exists(dbPath) && (File.Exists(legacyDbPath) || !configMounted && IsEmptyMount(dataDir, isMountPoint)))
68-
{
69-
logger?.LogWarning(
70-
"Muxarr now stores its database in {ConfigDir} but this install runs from {DataDir}. " +
71-
"Mount your appdata folder at {ConfigDir} instead; nothing is moved automatically. " +
72-
"See https://muxarr.app/docs/faq.html#appdata",
73-
configDir, dataDir, configDir);
74-
State = AppDataState.LegacyLocation;
75-
builder.DataSource = legacyDbPath;
76-
return builder.ToString();
77-
}
78-
79-
State = configMounted ? AppDataState.Ok : AppDataState.Unpersisted;
80-
if (!configMounted)
81-
{
82-
logger?.LogWarning(
83-
"No volume is mounted at {ConfigDir}. The database will not survive a container recreation.",
84-
configDir);
85-
}
86-
87-
return connectionString;
88-
}
89-
90-
// With no mounts at all Docker gives /data an anonymous volume that survives a compose
91-
// recreate; the writable layer behind /config does not. A media mount at /data is never empty.
92-
private static bool IsEmptyMount(string dir, Func<string, bool> isMountPoint)
93-
{
94-
return isMountPoint(dir) && Directory.Exists(dir) && !Directory.EnumerateFileSystemEntries(dir).Any();
95-
}
96-
97-
private static bool RunningInContainer()
98-
{
99-
return string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true",
100-
StringComparison.OrdinalIgnoreCase)
101-
|| File.Exists("/.dockerenv");
102-
}
103-
104-
/// <summary>
105-
/// Distinguishes a real mount (bind mount or named volume) from the container's
106-
/// writable layer, where data is silently lost on recreation.
107-
/// </summary>
108-
private static bool IsMountPoint(string path)
109-
{
110-
try
111-
{
112-
foreach (var line in File.ReadLines("/proc/self/mounts"))
113-
{
114-
var fields = line.Split(' ');
115-
if (fields.Length > 1 && fields[1] == path)
116-
{
117-
return true;
118-
}
119-
}
120-
}
121-
catch (IOException)
122-
{
123-
}
124-
catch (UnauthorizedAccessException)
54+
if (File.Exists(dbPath) || !File.Exists(legacyDbPath))
12555
{
56+
State = AppDataState.Ok;
57+
return connectionString;
12658
}
12759

128-
return false;
60+
logger?.LogWarning(
61+
"Muxarr now stores its database in {ConfigDir} but this install runs from {DataDir}. " +
62+
"Mount your appdata folder at {ConfigDir} instead; nothing is moved automatically. " +
63+
"See https://muxarr.app/docs/faq.html#appdata",
64+
configDir, dataDir, configDir);
65+
State = AppDataState.LegacyLocation;
66+
builder.DataSource = legacyDbPath;
67+
return builder.ToString();
12968
}
13069
}

Muxarr.Tests/ContainerAppDataTests.cs

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,74 +20,37 @@ protected override Task OnSetup()
2020

2121
private string ConfigDb => Path.Combine(_configDir, "muxarr.db");
2222
private string LegacyDb => Path.Combine(_dataDir, "muxarr.db");
23-
private string DefaultConnectionString => $"Data Source={ConfigDb}";
2423

25-
private string Resolve(bool configMounted, bool dataMounted = true, string? connectionString = null)
24+
private string Resolve(string? connectionString = null)
2625
{
27-
var result = ContainerAppData.Resolve(connectionString ?? DefaultConnectionString,
28-
_configDir, _dataDir, path => path == _configDir ? configMounted : dataMounted, null);
26+
var result = ContainerAppData.Resolve(connectionString ?? $"Data Source={ConfigDb}", _configDir, _dataDir, null);
2927
return new SqliteConnectionStringBuilder(result).DataSource;
3028
}
3129

3230
[TestMethod]
33-
public void FreshInstall_ConfigMounted()
31+
public void FreshInstall_UsesConfig()
3432
{
35-
Assert.AreEqual(ConfigDb, Resolve(configMounted: true));
33+
Assert.AreEqual(ConfigDb, Resolve());
3634
Assert.AreEqual(AppDataState.Ok, ContainerAppData.State);
3735
}
3836

39-
// With nothing mounted the image's /data volume is the only place that survives a recreate.
40-
[TestMethod]
41-
public void FreshInstall_NothingMounted_UsesEmptyDataVolume()
42-
{
43-
Assert.AreEqual(LegacyDb, Resolve(configMounted: false, dataMounted: true));
44-
Assert.AreEqual(AppDataState.LegacyLocation, ContainerAppData.State);
45-
}
46-
47-
[TestMethod]
48-
public void FreshInstall_MediaAtData_NeverWritesThere()
49-
{
50-
File.WriteAllText(Path.Combine(_dataDir, "movie.mkv"), "");
51-
52-
Assert.AreEqual(ConfigDb, Resolve(configMounted: false, dataMounted: true));
53-
Assert.AreEqual(AppDataState.Unpersisted, ContainerAppData.State);
54-
}
55-
56-
[TestMethod]
57-
public void FreshInstall_NoMountsAtAll_Unpersisted()
58-
{
59-
Assert.AreEqual(ConfigDb, Resolve(configMounted: false, dataMounted: false));
60-
Assert.AreEqual(AppDataState.Unpersisted, ContainerAppData.State);
61-
}
62-
6337
[TestMethod]
6438
public void LegacyDatabase_KeepsRunningFromData()
6539
{
6640
File.WriteAllText(LegacyDb, "");
6741

68-
Assert.AreEqual(LegacyDb, Resolve(configMounted: false));
42+
Assert.AreEqual(LegacyDb, Resolve());
6943
Assert.AreEqual(AppDataState.LegacyLocation, ContainerAppData.State);
7044
Assert.IsFalse(File.Exists(ConfigDb), "Nothing may be copied or created in /config");
7145
}
7246

73-
// An empty /config mount next to an existing /data database must not start a blank app.
74-
[TestMethod]
75-
public void LegacyDatabase_EmptyConfigMounted_StillUsesData()
76-
{
77-
File.WriteAllText(LegacyDb, "");
78-
79-
Assert.AreEqual(LegacyDb, Resolve(configMounted: true));
80-
Assert.AreEqual(AppDataState.LegacyLocation, ContainerAppData.State);
81-
Assert.IsFalse(File.Exists(ConfigDb));
82-
}
83-
8447
[TestMethod]
8548
public void BothDatabasesExist_ConfigWins()
8649
{
8750
File.WriteAllText(ConfigDb, "");
8851
File.WriteAllText(LegacyDb, "");
8952

90-
Assert.AreEqual(ConfigDb, Resolve(configMounted: true));
53+
Assert.AreEqual(ConfigDb, Resolve());
9154
Assert.AreEqual(AppDataState.Ok, ContainerAppData.State);
9255
}
9356

@@ -97,7 +60,7 @@ public void CustomOrInMemoryConnectionString_IsLeftUntouched()
9760
File.WriteAllText(LegacyDb, "");
9861
var custom = TempPath("elsewhere.db");
9962

100-
Assert.AreEqual(custom, Resolve(configMounted: false, connectionString: $"Data Source={custom}"));
101-
Assert.AreEqual(":memory:", Resolve(configMounted: false, connectionString: "Data Source=:memory:"));
63+
Assert.AreEqual(custom, Resolve($"Data Source={custom}"));
64+
Assert.AreEqual(":memory:", Resolve("Data Source=:memory:"));
10265
}
10366
}

Muxarr.Web/Components/Shared/AppDataBanner.razor

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
@using Muxarr.Data
22

3-
@* Decided once at startup; the log is easy to miss, so anything but a mounted /config gets a permanent bar. *@
4-
@if (ContainerAppData.State != AppDataState.Ok)
3+
@* Decided once at startup; the log is easy to miss, so an install still on /data gets a permanent bar. *@
4+
@if (ContainerAppData.State == AppDataState.LegacyLocation)
55
{
6-
<div class="alert @(ContainerAppData.State == AppDataState.Unpersisted ? "alert-danger" : "alert-warning") d-flex align-items-start gap-2 mb-3" role="alert">
6+
<div class="alert alert-warning d-flex align-items-start gap-2 mb-3" role="alert">
77
<i class="bi bi-exclamation-triangle-fill mt-1"></i>
88
<div>
9-
@if (ContainerAppData.State == AppDataState.LegacyLocation)
10-
{
11-
<strong>Muxarr now keeps its database in <code>/config</code>, but this install still runs from <code>/data</code>.</strong>
12-
<span> Mount your appdata folder at <code>:/config</code> instead of <code>:/data</code> and restart. If it lives in a Docker volume, copy it out first. Nothing is moved automatically.</span>
13-
}
14-
else
15-
{
16-
<strong>No folder is mounted at <code>/config</code>. The database will be lost when this container is recreated.</strong>
17-
<span> Mount a host folder at <code>/config</code> before you set anything up.</span>
18-
}
9+
<strong>Muxarr now keeps its database in <code>/config</code>, but this install still runs from <code>/data</code>.</strong>
10+
<span> Mount your appdata folder at <code>:/config</code> instead of <code>:/data</code> and restart. If it lives in a Docker volume, copy it out first. Nothing is moved automatically.</span>
1911
<a class="ms-1" href="https://muxarr.app/docs/faq.html#appdata" target="_blank" rel="noopener">How to fix</a>
2012
</div>
2113
</div>

Muxarr.Web/Dockerfile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,9 @@ COPY publish/ ./
4040
COPY entrypoint.sh ./entrypoint.sh
4141
RUN chmod +x ./entrypoint.sh
4242

43-
# /data keeps its VOLUME: older images stored the database there and compose only
43+
# /data stays declared: older images stored the database there and compose only
4444
# carries that anonymous volume across a recreate while the image still declares it.
45-
# /config gets none on purpose: an unmounted /config must look unmounted at startup
46-
# so the app can warn instead of silently using an anonymous volume.
47-
VOLUME ["/data"]
45+
VOLUME ["/config", "/data"]
4846

4947
ENV ASPNETCORE_HTTP_PORTS=8183
5048
EXPOSE 8183

Muxarr.Web/entrypoint.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ chown appuser:appgroup /app || echo "Warning: Could not set ownership on /app. R
2020
chown appuser:appgroup /config || echo "Warning: Could not set ownership on /config. Remote or read-only mount?"
2121
chmod 755 /config || true
2222

23-
# Only touch /data when it holds our database (older layout) or is the empty
24-
# volume the image declares. Anything else there is the user's media.
25-
if [ -e /data/muxarr.db ] || [ -z "$(ls -A /data)" ]; then
23+
# Older images kept the database in /data; anything else there is the user's media.
24+
if [ -e /data/muxarr.db ]; then
2625
chown appuser:appgroup /data || echo "Warning: Could not set ownership on /data. Remote or read-only mount?"
2726
chmod 755 /data || true
2827
fi

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ Until you do, Muxarr keeps running from `/data` and shows a reminder in the app.
134134
| Variable | Description | Default |
135135
|---|---|---|
136136
| `TZ` | Timezone | `UTC` |
137-
| `PUID` | User ID for file permissions | `1000` |
138-
| `PGID` | Group ID for file permissions | `1000` |
137+
| `PUID` | User ID for file permissions | `888` |
138+
| `PGID` | Group ID for file permissions | `888` |
139139

140140
### Volumes
141141

0 commit comments

Comments
 (0)