Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ COPY --from=build-node /app/build ./wwwroot

# Set non-privileged user
ARG APP_UID=1000

# Ensure the app user owns the files they need to modify
RUN chown -R $APP_UID:$APP_UID /app/wwwroot

# Create a startup script to handle BaseUrl replacement
RUN echo '#!/bin/sh\n\
if [ -n "$BaseUrl" ] && [ "$BaseUrl" != "/" ]; then\n\
BASE_PATH=$(echo "$BaseUrl" | sed "s|/*$||")\n\
else\n\
BASE_PATH=""\n\
fi\n\
echo "Applying BaseUrl: $BASE_PATH"\n\
find /app/wwwroot -type f \( -name "*.html" -o -name "*.js" -o -name "*.json" -o -name "*.webmanifest" -o -name "*.css" \) -exec sed -i "s|/__IMMICH_FRAME_BASE__|$BASE_PATH|g" {} +\n\
exec dotnet ImmichFrame.WebApi.dll' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

USER $APP_UID

ENTRYPOINT ["dotnet", "ImmichFrame.WebApi.dll"]
ENTRYPOINT ["/app/entrypoint.sh"]
1 change: 1 addition & 0 deletions ImmichFrame.Core/Interfaces/IServerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public interface IGeneralSettings
public bool ImageFill { get; }
public string Layout { get; }
public string Language { get; }
public string BaseUrl { get; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

public void Validate();
}
Expand Down
27 changes: 26 additions & 1 deletion ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using ImmichFrame.WebApi.Models;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using AwesomeAssertions;
using FluentAssertions;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

namespace ImmichFrame.WebApi.Tests.Helpers.Config;

Expand Down Expand Up @@ -68,6 +68,31 @@ public void TestLoadConfigV2Yaml()
VerifyConfig(config, true, false);
}

[Test]
public void TestApplyEnvironmentVariables_V1()
{
var v1 = new ServerSettingsV1 { BaseUrl = "/" };
var adapter = new ServerSettingsV1Adapter(v1);

var env = new Dictionary<string, string> { { "BaseUrl", "'/new-path'" } };

_configLoader.MapDictionaryToConfig(v1, env);

Assert.That(v1.BaseUrl, Is.EqualTo("/new-path"));
}

[Test]
public void TestApplyEnvironmentVariables_V2()
{
var settings = new ServerSettings { GeneralSettingsImpl = new GeneralSettings { BaseUrl = "/" } };

var env = new Dictionary<string, string> { { "BaseUrl", "\"/new-path\"" } };

_configLoader.MapDictionaryToConfig(settings.GeneralSettingsImpl, env);

Assert.That(settings.GeneralSettings.BaseUrl, Is.EqualTo("/new-path"));
}

private void VerifyConfig(IServerSettings serverSettings, bool usePrefix, bool expectNullApiKeyFile)
{
VerifyProperties(serverSettings.GeneralSettings);
Expand Down
44 changes: 38 additions & 6 deletions ImmichFrame.WebApi/Helpers/Config/ConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private string FindConfigFile(string dir, params string[] fileNames)
public IServerSettings LoadConfig(string configPath)
{
var config = LoadConfigRaw(configPath);
ApplyEnvironmentVariables(config);
config.Validate();
return config;
}
Expand Down Expand Up @@ -86,12 +87,23 @@ private IServerSettings LoadConfigRaw(string configPath)

throw new ImmichFrameException("Failed to load configuration");
}

internal T LoadConfigFromDictionary<T>(IDictionary env) where T : IConfigSettable, new()
private void ApplyEnvironmentVariables(IServerSettings config)
{
var config = new T();
var propertiesSet = 0;
var env = Environment.GetEnvironmentVariables();
if (config is ServerSettings serverSettings)
{
if (serverSettings.GeneralSettingsImpl == null)
serverSettings.GeneralSettingsImpl = new GeneralSettings();

MapDictionaryToConfig(serverSettings.GeneralSettingsImpl, env);
}
else if (config is ServerSettingsV1Adapter v1Adapter)
{
MapDictionaryToConfig(v1Adapter.Settings, env);
}
}
internal void MapDictionaryToConfig<T>(T config, IDictionary env) where T : IConfigSettable
{
foreach (var key in env.Keys)
{
if (key == null) continue;
Expand All @@ -100,10 +112,30 @@ private IServerSettings LoadConfigRaw(string configPath)

if (propertyInfo != null)
{
config.SetValue(propertyInfo, env[key]?.ToString() ?? string.Empty);
propertiesSet++;
var value = env[key]?.ToString() ?? string.Empty;
// Clean up quotes if present
if (value.StartsWith("'") && value.EndsWith("'"))
value = value.Substring(1, value.Length - 2);
if (value.StartsWith("\"") && value.EndsWith("\""))
value = value.Substring(1, value.Length - 2);

config.SetValue(propertyInfo, value);
}
}
}
internal T LoadConfigFromDictionary<T>(IDictionary env) where T : IConfigSettable, new()
{
var config = new T();
MapDictionaryToConfig(config, env);

// Count set properties to see if we have anything
var propertiesSet = 0;
foreach (var key in env.Keys)
{
if (key == null) continue;
if (typeof(T).GetProperty(key.ToString() ?? string.Empty) != null)
propertiesSet++;
}

if (propertiesSet < 2)
{
Expand Down
105 changes: 54 additions & 51 deletions ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@ public class ServerSettingsV1 : IConfigSettable
public bool ImagePan { get; set; } = false;
public bool ImageFill { get; set; } = false;
public string Layout { get; set; } = "splitview";
public string BaseUrl { get; set; } = "/";
}

/// <summary>
/// Adapter to present a SettingsV1 object as an IServerSettings
/// </summary>
/// <param name="_delegate">the V1 settings object to wrap</param>
public class ServerSettingsV1Adapter(ServerSettingsV1 _delegate) : IServerSettings
/// <param name="Settings">the V1 settings object to wrap</param>
public class ServerSettingsV1Adapter(ServerSettingsV1 Settings) : IServerSettings
{
public IEnumerable<IAccountSettings> Accounts => new List<AccountSettingsV1Adapter> { new(_delegate) };
public IGeneralSettings GeneralSettings => new GeneralSettingsV1Adapter(_delegate);
public ServerSettingsV1 Settings { get; } = Settings;
public IEnumerable<IAccountSettings> Accounts => new List<AccountSettingsV1Adapter> { new(Settings) };
public IGeneralSettings GeneralSettings => new GeneralSettingsV1Adapter(Settings);

public void Validate()
{
Expand All @@ -72,60 +74,61 @@ public void Validate()
}
}

class AccountSettingsV1Adapter(ServerSettingsV1 _delegate) : IAccountSettings
class AccountSettingsV1Adapter(ServerSettingsV1 Settings) : IAccountSettings
{
public string ImmichServerUrl => _delegate.ImmichServerUrl;
public string ApiKey => _delegate.ApiKey;
public string ImmichServerUrl => Settings.ImmichServerUrl;
public string ApiKey => Settings.ApiKey;
public string? ApiKeyFile => null; // V1 settings didn't support paths to api keys.
public bool ShowMemories => _delegate.ShowMemories;
public bool ShowFavorites => _delegate.ShowFavorites;
public bool ShowArchived => _delegate.ShowArchived;
public int? ImagesFromDays => _delegate.ImagesFromDays;
public DateTime? ImagesFromDate => _delegate.ImagesFromDate;
public DateTime? ImagesUntilDate => _delegate.ImagesUntilDate;
public List<Guid> Albums => _delegate.Albums;
public List<Guid> ExcludedAlbums => _delegate.ExcludedAlbums;
public List<Guid> People => _delegate.People;
public int? Rating => _delegate.Rating;
public bool ShowMemories => Settings.ShowMemories;
public bool ShowFavorites => Settings.ShowFavorites;
public bool ShowArchived => Settings.ShowArchived;
public int? ImagesFromDays => Settings.ImagesFromDays;
public DateTime? ImagesFromDate => Settings.ImagesFromDate;
public DateTime? ImagesUntilDate => Settings.ImagesUntilDate;
public List<Guid> Albums => Settings.Albums;
public List<Guid> ExcludedAlbums => Settings.ExcludedAlbums;
public List<Guid> People => Settings.People;
public int? Rating => Settings.Rating;

public void ValidateAndInitialize() { }
}

class GeneralSettingsV1Adapter(ServerSettingsV1 _delegate) : IGeneralSettings
class GeneralSettingsV1Adapter(ServerSettingsV1 Settings) : IGeneralSettings
{
public List<string> Webcalendars => _delegate.Webcalendars;
public int RefreshAlbumPeopleInterval => _delegate.RefreshAlbumPeopleInterval;
public string? WeatherApiKey => _delegate.WeatherApiKey;
public string? WeatherLatLong => _delegate.WeatherLatLong;
public string? UnitSystem => _delegate.UnitSystem;
public string? Webhook => _delegate.Webhook;
public string? AuthenticationSecret => _delegate.AuthenticationSecret;
public int Interval => _delegate.Interval;
public double TransitionDuration => _delegate.TransitionDuration;
public bool DownloadImages => _delegate.DownloadImages;
public int RenewImagesDuration => _delegate.RenewImagesDuration;
public bool ShowClock => _delegate.ShowClock;
public string? ClockFormat => _delegate.ClockFormat;
public string? ClockDateFormat => _delegate.ClockDateFormat;
public bool ShowProgressBar => _delegate.ShowProgressBar;
public bool ShowPhotoDate => _delegate.ShowPhotoDate;
public string? PhotoDateFormat => _delegate.PhotoDateFormat;
public bool ShowImageDesc => _delegate.ShowImageDesc;
public bool ShowPeopleDesc => _delegate.ShowPeopleDesc;
public bool ShowAlbumName => _delegate.ShowAlbumName;
public bool ShowImageLocation => _delegate.ShowImageLocation;
public string? ImageLocationFormat => _delegate.ImageLocationFormat;
public string? PrimaryColor => _delegate.PrimaryColor;
public string? SecondaryColor => _delegate.SecondaryColor;
public string Style => _delegate.Style;
public string? BaseFontSize => _delegate.BaseFontSize;
public bool ShowWeatherDescription => _delegate.ShowWeatherDescription;
public string? WeatherIconUrl => _delegate.WeatherIconUrl;
public bool ImageZoom => _delegate.ImageZoom;
public bool ImagePan => _delegate.ImagePan;
public bool ImageFill => _delegate.ImageFill;
public string Layout => _delegate.Layout;
public string Language => _delegate.Language;
public List<string> Webcalendars => Settings.Webcalendars;
public int RefreshAlbumPeopleInterval => Settings.RefreshAlbumPeopleInterval;
public string? WeatherApiKey => Settings.WeatherApiKey;
public string? WeatherLatLong => Settings.WeatherLatLong;
public string? UnitSystem => Settings.UnitSystem;
public string? Webhook => Settings.Webhook;
public string? AuthenticationSecret => Settings.AuthenticationSecret;
public int Interval => Settings.Interval;
public double TransitionDuration => Settings.TransitionDuration;
public bool DownloadImages => Settings.DownloadImages;
public int RenewImagesDuration => Settings.RenewImagesDuration;
public bool ShowClock => Settings.ShowClock;
public string? ClockFormat => Settings.ClockFormat;
public string? ClockDateFormat => Settings.ClockDateFormat;
public bool ShowProgressBar => Settings.ShowProgressBar;
public bool ShowPhotoDate => Settings.ShowPhotoDate;
public string? PhotoDateFormat => Settings.PhotoDateFormat;
public bool ShowImageDesc => Settings.ShowImageDesc;
public bool ShowPeopleDesc => Settings.ShowPeopleDesc;
public bool ShowAlbumName => Settings.ShowAlbumName;
public bool ShowImageLocation => Settings.ShowImageLocation;
public string? ImageLocationFormat => Settings.ImageLocationFormat;
public string? PrimaryColor => Settings.PrimaryColor;
public string? SecondaryColor => Settings.SecondaryColor;
public string Style => Settings.Style;
public string? BaseFontSize => Settings.BaseFontSize;
public bool ShowWeatherDescription => Settings.ShowWeatherDescription;
public string? WeatherIconUrl => Settings.WeatherIconUrl;
public bool ImageZoom => Settings.ImageZoom;
public bool ImagePan => Settings.ImagePan;
public bool ImageFill => Settings.ImageFill;
public string Layout => Settings.Layout;
public string Language => Settings.Language;
public string BaseUrl => Settings.BaseUrl;

public void Validate() { }
}
Expand Down
2 changes: 2 additions & 0 deletions ImmichFrame.WebApi/Models/ClientSettingsDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class ClientSettingsDto
public bool ImageFill { get; set; }
public string Layout { get; set; }
public string Language { get; set; }
public string BaseUrl { get; set; }

public static ClientSettingsDto FromGeneralSettings(IGeneralSettings generalSettings)
{
Expand Down Expand Up @@ -60,6 +61,7 @@ public static ClientSettingsDto FromGeneralSettings(IGeneralSettings generalSett
dto.ImageFill = generalSettings.ImageFill;
dto.Layout = generalSettings.Layout;
dto.Language = generalSettings.Language;
dto.BaseUrl = generalSettings.BaseUrl;
return dto;
}
}
1 change: 1 addition & 0 deletions ImmichFrame.WebApi/Models/ServerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class GeneralSettings : IGeneralSettings, IConfigSettable
public bool ImagePan { get; set; } = false;
public bool ImageFill { get; set; } = false;
public string Layout { get; set; } = "splitview";
public string BaseUrl { get; set; } = "/";
public int RenewImagesDuration { get; set; } = 30;
public List<string> Webcalendars { get; set; } = new();
public int RefreshAlbumPeopleInterval { get; set; } = 12;
Expand Down
41 changes: 32 additions & 9 deletions ImmichFrame.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
using ImmichFrame.WebApi.Helpers.Config;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
var root = Directory.GetCurrentDirectory();
var dotenv = Path.Combine(root, "..", "docker", ".env");

dotenv = Path.GetFullPath(dotenv);
DotEnv.Load(dotenv);
}

//log the version number
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
Console.WriteLine($@"
Expand Down Expand Up @@ -80,6 +90,28 @@ _ _ __ ___ _ __ ___ _ ___| |__ | |_ _ __ __ _ _ __ ___ ___

var app = builder.Build();

var settings = app.Services.GetRequiredService<IGeneralSettings>();
var baseUrl = settings.BaseUrl?.TrimEnd('/');

if (!string.IsNullOrEmpty(baseUrl) && baseUrl != "/")
{
app.UsePathBase(baseUrl);

// Ensure that requests not starting with BaseUrl do not fall through to the app
app.Use(async (context, next) =>
{
if (!context.Request.PathBase.HasValue || !context.Request.PathBase.Value.Equals(baseUrl, StringComparison.OrdinalIgnoreCase))
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
await context.Response.WriteAsync("Not Found");
return;
}
await next();
});
}

app.UseRouting();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
Expand All @@ -93,15 +125,6 @@ _ _ __ ___ _ __ ___ _ ___| |__ | |_ _ __ __ _ _ __ ___ ___
app.UseDefaultFiles();
}

if (app.Environment.IsDevelopment())
{
var root = Directory.GetCurrentDirectory();
var dotenv = Path.Combine(root, "..", "docker", ".env");

dotenv = Path.GetFullPath(dotenv);
DotEnv.Load(dotenv);
}

// app.UseHttpsRedirection();
app.UseMiddleware<CustomAuthenticationMiddleware>();

Expand Down
3 changes: 2 additions & 1 deletion docker/Settings.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"ImageZoom": true,
"ImagePan": false,
"ImageFill": false,
"Layout": "splitview"
"Layout": "splitview",
"BaseUrl": "/"
},
"Accounts": [
{
Expand Down
1 change: 1 addition & 0 deletions docker/Settings.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ General:
ImagePan: false
ImageFill: false
Layout: splitview
BaseUrl: '/'
Accounts:
- ImmichServerUrl: REQUIRED
# Exactly one of ApiKey or ApiKeyFile must be set.
Expand Down
1 change: 1 addition & 0 deletions docker/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ApiKey=KEY
# ImageZoom=true
# ImagePan=false
# Layout=splitview
# BaseUrl=/
# DownloadImages=false
# ShowMemories=false
# ShowFavorites=false
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ General:
ImageFill: false # boolean
# Allow two portrait images to be displayed next to each other
Layout: 'splitview' # single | splitview
# The base URL the app is hosted on. Useful when using a reverse proxy.
BaseUrl: '/' # string

# multiple accounts permitted
Accounts:
Expand Down
Loading