Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 8 additions & 1 deletion ImmichFrame.Core/Interfaces/IServerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ public interface IServerSettings
{
public IEnumerable<IAccountSettings> Accounts { get; }
public IGeneralSettings GeneralSettings { get; }

public void Validate();
}

public interface IAccountSettings
{
public string ImmichServerUrl { get; }
public string ApiKey { get; }
public string? ApiKeyFile { get; }
public bool ShowMemories { get; }
public bool ShowFavorites { get; }
public bool ShowArchived { get; }
Expand All @@ -20,6 +23,8 @@ public interface IAccountSettings
public List<Guid> ExcludedAlbums { get; }
public List<Guid> People { get; }
public int? Rating { get; }

public void ValidateAndInitialize();
}

public interface IGeneralSettings
Expand Down Expand Up @@ -57,5 +62,7 @@ public interface IGeneralSettings
public bool ImageFill { get; }
public string Layout { get; }
public string Language { get; }

public void Validate();
}
}
}
29 changes: 18 additions & 11 deletions ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
{
var config = _configLoader.LoadConfigJson<ServerSettingsV1>(Path.Combine(
TestContext.CurrentContext.TestDirectory, "Resources/TestV1.json"));
VerifyConfig(new ServerSettingsV1Adapter(config), false);
VerifyConfig(new ServerSettingsV1Adapter(config), false, true);
}

[Test]
Expand All @@ -39,15 +39,15 @@
TestContext.CurrentContext.TestDirectory, "Resources/TestV1.json"));

var config = _configLoader.LoadConfigFromDictionary<ServerSettingsV1>(ToDictionary(jsonConfig));
VerifyConfig(new ServerSettingsV1Adapter(config), false);
VerifyConfig(new ServerSettingsV1Adapter(config), false, true);
}

[Test]
public void TestLoadConfigV2Json()
{
var config = _configLoader.LoadConfigJson<ServerSettings>(Path.Combine(
TestContext.CurrentContext.TestDirectory, "Resources/TestV2.json"));
VerifyConfig(config, true);
VerifyConfig(config, true, false);
}

[Test]
Expand All @@ -65,26 +65,26 @@
{
var config = _configLoader.LoadConfigYaml<ServerSettings>(Path.Combine(
TestContext.CurrentContext.TestDirectory, "Resources/TestV2.yml"));
VerifyConfig(config, true);
VerifyConfig(config, true, false);
}

private void VerifyConfig(IServerSettings serverSettings, bool usePrefix)
private void VerifyConfig(IServerSettings serverSettings, bool usePrefix, bool expectNullApiKeyFile)
{
VerifyProperties(serverSettings.GeneralSettings);
VerifyAccounts(serverSettings.Accounts, usePrefix);
VerifyAccounts(serverSettings.Accounts, usePrefix, expectNullApiKeyFile);
}

private void VerifyAccounts(IEnumerable<IAccountSettings> accounts, bool usePrefix)
private void VerifyAccounts(IEnumerable<IAccountSettings> accounts, bool usePrefix, bool expectNullApiKeyFile)
{
var idx = 1;
foreach (var account in accounts)
{
VerifyProperties(account, usePrefix ? "Account" + idx + "." : "");
VerifyProperties(account, usePrefix ? "Account" + idx + "." : "", expectNullApiKeyFile);
idx++;
}
}

private void VerifyProperties(object o, string? prefix = "")
private void VerifyProperties(object o, string? prefix = "", bool expectNullApiKeyFile = false)
{
foreach (var prop in o.GetType().GetProperties())
{
Expand All @@ -95,7 +95,7 @@
if (type.IsGenericType && typeof(IEnumerable).IsAssignableFrom(type))
{
type = type.GetGenericArguments()[0];
value = (value as IEnumerable).Cast<object>().FirstOrDefault();

Check warning on line 98 in ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'source' in 'IEnumerable<object> Enumerable.Cast<object>(IEnumerable source)'.
}

//if it's nullable, unwrap
Expand All @@ -107,7 +107,14 @@
switch (type)
{
case var t when t == typeof(string):
Assert.That(value, Is.EqualTo(prefix + prop.Name + "_TEST"), prop.Name);
if (prop.Name.Equals("ApiKeyFile") && expectNullApiKeyFile)
{
Assert.That(value, Is.EqualTo(null), prop.Name);
}
else
{
Assert.That(value, Is.EqualTo(prefix + prop.Name + "_TEST"), prop.Name);
}
break;
case var t when t == typeof(Boolean):
Assert.That(value, Is.EqualTo(true), prop.Name);
Expand Down Expand Up @@ -148,7 +155,7 @@
// Ensure the property has a public getter
if (prop.CanRead && prop.GetMethod?.IsPublic == true)
{
object value = prop.GetValue(obj);

Check warning on line 158 in ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs

View workflow job for this annotation

GitHub Actions / test

Converting null literal or possible null value to non-nullable type.

if (ignoreNullValues && value == null)
{
Expand All @@ -157,18 +164,18 @@

if (!(value is string) && value is IEnumerable)
{
value = string.Join(",", (value as IEnumerable).Cast<object>().Select(x => x.ToString()));

Check warning on line 167 in ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'source' in 'IEnumerable<object> Enumerable.Cast<object>(IEnumerable source)'.
}
else
{
value = value.ToString();

Check warning on line 171 in ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs

View workflow job for this annotation

GitHub Actions / test

Converting null literal or possible null value to non-nullable type.

Check warning on line 171 in ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.
}

dictionary.Add(prop.Name, value);

Check warning on line 174 in ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'value' in 'void Dictionary<string, object>.Add(string key, object value)'.
}
}

return dictionary;
}

}
}
2 changes: 1 addition & 1 deletion ImmichFrame.WebApi.Tests/Resources/TestV1.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@
"Account2.ImmichServerUrl": "Account2.ImmichServerUrl_TEST",
"Account2.ApiKey": "Account2.ApiKey_TEST",
"Account2.ImagesFromDate": "Account2.ImagesFromDate_TEST"
}
}
4 changes: 3 additions & 1 deletion ImmichFrame.WebApi.Tests/Resources/TestV2.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
{
"ImmichServerUrl": "Account1.ImmichServerUrl_TEST",
"ApiKey": "Account1.ApiKey_TEST",
"ApiKeyFile": "Account1.ApiKeyFile_TEST",
"ImagesFromDate": "2020-01-02",
"ShowMemories": true,
"ShowFavorites": true,
Expand All @@ -60,6 +61,7 @@
{
"ImmichServerUrl": "Account2.ImmichServerUrl_TEST",
"ApiKey": "Account2.ApiKey_TEST",
"ApiKeyFile": "Account2.ApiKeyFile_TEST",
"ImagesFromDate": "2020-01-02",
"ShowMemories": true,
"ShowFavorites": true,
Expand All @@ -78,4 +80,4 @@
]
}
]
}
}
4 changes: 3 additions & 1 deletion ImmichFrame.WebApi.Tests/Resources/TestV2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ General:
Accounts:
- ImmichServerUrl: Account1.ImmichServerUrl_TEST
ApiKey: Account1.ApiKey_TEST
ApiKeyFile: Account1.ApiKeyFile_TEST
ImagesFromDate: '2020-01-02'
ShowMemories: true
ShowFavorites: true
Expand All @@ -52,6 +53,7 @@ Accounts:
- 00000000-0000-0000-0000-000000000001
- ImmichServerUrl: Account2.ImmichServerUrl_TEST
ApiKey: Account2.ApiKey_TEST
ApiKeyFile: Account2.ApiKeyFile_TEST
ImagesFromDate: '2020-01-02'
ShowMemories: true
ShowFavorites: true
Expand All @@ -64,4 +66,4 @@ Accounts:
ExcludedAlbums:
- 00000000-0000-0000-0000-000000000001
People:
- 00000000-0000-0000-0000-000000000001
- 00000000-0000-0000-0000-000000000001
8 changes: 7 additions & 1 deletion ImmichFrame.WebApi/Helpers/Config/ConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ private string FindConfigFile(string dir, params string[] fileNames)
?? Path.Combine(dir, fileNames.First());
}
public IServerSettings LoadConfig(string configPath)
{
var config = LoadConfigRaw(configPath);
config.Validate();
return config;
}
private IServerSettings LoadConfigRaw(string configPath)
{
var jsonConfigPath = FindConfigFile(configPath, "Settings.json");
if (File.Exists(jsonConfigPath))
Expand Down Expand Up @@ -145,4 +151,4 @@ public IServerSettings LoadConfig(string configPath)
throw new SettingsNotValidException($"Problem with parsing the settings: {ex.Message}", ex);
}
}
}
}
15 changes: 14 additions & 1 deletion ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,20 @@ public class ServerSettingsV1Adapter(ServerSettingsV1 _delegate) : IServerSettin
public IEnumerable<IAccountSettings> Accounts => new List<AccountSettingsV1Adapter> { new(_delegate) };
public IGeneralSettings GeneralSettings => new GeneralSettingsV1Adapter(_delegate);

public void Validate()
{
GeneralSettings.Validate();
foreach (var account in Accounts)
{
account.ValidateAndInitialize();
}
}

class AccountSettingsV1Adapter(ServerSettingsV1 _delegate) : IAccountSettings
{
public string ImmichServerUrl => _delegate.ImmichServerUrl;
public string ApiKey => _delegate.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;
Expand All @@ -78,6 +87,8 @@ class AccountSettingsV1Adapter(ServerSettingsV1 _delegate) : IAccountSettings
public List<Guid> ExcludedAlbums => _delegate.ExcludedAlbums;
public List<Guid> People => _delegate.People;
public int? Rating => _delegate.Rating;

public void ValidateAndInitialize() { }
}

class GeneralSettingsV1Adapter(ServerSettingsV1 _delegate) : IGeneralSettings
Expand Down Expand Up @@ -115,5 +126,7 @@ class GeneralSettingsV1Adapter(ServerSettingsV1 _delegate) : IGeneralSettings
public bool ImageFill => _delegate.ImageFill;
public string Layout => _delegate.Layout;
public string Language => _delegate.Language;

public void Validate() { }
}
}
}
32 changes: 31 additions & 1 deletion ImmichFrame.WebApi/Models/ServerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

[YamlMember(Alias = "Accounts")]
[JsonPropertyName("Accounts")]
public IEnumerable<ServerAccountSettings> AccountsImpl { get; set; }

Check warning on line 16 in ImmichFrame.WebApi/Models/ServerSettings.cs

View workflow job for this annotation

GitHub Actions / test

Non-nullable property 'AccountsImpl' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

//Covariance not allowed on interface impls
[JsonIgnore]
Expand All @@ -23,6 +23,16 @@
[JsonIgnore]
[YamlIgnore]
public IEnumerable<IAccountSettings> Accounts => AccountsImpl;

public void Validate()
{
GeneralSettings.Validate();

foreach (var account in Accounts)
{
account.ValidateAndInitialize();
}
}
}

public class GeneralSettings : IGeneralSettings, IConfigSettable
Expand Down Expand Up @@ -60,12 +70,15 @@
public string? WeatherLatLong { get; set; } = "40.7128,74.0060";
public string? Webhook { get; set; }
public string? AuthenticationSecret { get; set; }

public void Validate() { }
}

public class ServerAccountSettings : IAccountSettings, IConfigSettable
{
public string ImmichServerUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public string? ApiKeyFile { get; set; } = null;
public bool ShowMemories { get; set; } = false;
public bool ShowFavorites { get; set; } = false;
public bool ShowArchived { get; set; } = false;
Expand All @@ -77,4 +90,21 @@
public List<Guid> ExcludedAlbums { get; set; } = new();
public List<Guid> People { get; set; } = new();
public int? Rating { get; set; }
}

public void ValidateAndInitialize()
{
if (!string.IsNullOrWhiteSpace(ApiKeyFile))
{
if (!string.IsNullOrWhiteSpace(ApiKey))
{
throw new Exception("Cannot specify both ApiKey and ApiKeyFile. Please provide only one.");
}
ApiKey = File.ReadAllText(ApiKeyFile).Trim();
}

if (string.IsNullOrWhiteSpace(ApiKey))
{
throw new InvalidOperationException("Either ApiKey or ApiKeyFile must be provided.");
}
}
}
5 changes: 3 additions & 2 deletions docker/Settings.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"Accounts": [
{
"ImmichServerUrl": "REQUIRED",
"ApiKey": "REQUIRED",
"ApiKey": "super-secret-api-key",
"ApiKeyFile": "/path/to/api.key",
Comment thread
jfly marked this conversation as resolved.
"ImagesFromDate": null,
"ShowMemories": false,
"ShowFavorites": false,
Expand All @@ -58,4 +59,4 @@
]
}
]
}
}
5 changes: 4 additions & 1 deletion docker/Settings.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ General:
Layout: splitview
Accounts:
- ImmichServerUrl: REQUIRED
ApiKey: REQUIRED
# Exactly one of ApiKey or ApiKeyFile must be set.
ApiKey: "super-secret-api-key"
# ApiKeyFile: "/path/to/api.key"
Comment thread
jfly marked this conversation as resolved.

ImagesFromDate: null
ShowMemories: false
ShowFavorites: false
Expand Down
6 changes: 5 additions & 1 deletion docker/example.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
ImmichServerUrl=URL

# Either ApiKey or ApiKeyFile must be specified.
ApiKey=KEY
# ApiKeyFile=/path/to/key

# AuthenticationSecret=
# Interval=10
# TransitionDuration=2
Expand Down Expand Up @@ -41,4 +45,4 @@ ApiKey=KEY
# UnitSystem=imperial
# WeatherLatLong=
# Language=en
# Webhook=
# Webhook=
6 changes: 4 additions & 2 deletions docs/docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Only override settings you intend to change.
Defaults might change between versions, so keeping your config minimal helps future upgrades.
:::

Defaults are below, only one account with `ImmichServerUrl` and `ApiKey` are required.
Defaults are below, only one account with `ImmichServerUrl` and `ApiKey`|`ApiKeyFile` are required.

```yaml
# settings applicable to the web client - when viewing with a browser or webview
Expand Down Expand Up @@ -104,7 +104,9 @@ Accounts:
- # The URL of your Immich server e.g. `http://photos.yourdomain.com` / `http://192.168.0.100:2283`.
ImmichServerUrl: 'REQUIRED' # string, required, no default
# Read more about how to obtain an Immich API key: https://immich.app/docs/features/command-line-interface#obtain-the-api-key
ApiKey: 'REQUIRED' # string, required, no default
# Exactly one of ApiKey or ApiKeyFile must be set.
ApiKey: "super-secret-api-key"
# ApiKeyFile: "/path/to/api.key"
# Show images after date. Overwrites the `ImagesFromDays`-Setting
ImagesFromDate: null # Date
# If this is set, memories are displayed.
Expand Down
Loading